trading-platform-database-v2/ddl/schemas/auth/tables/002_tokens.sql
rckrdmrd e520268348 Migración desde trading-platform/apps/database - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:32:52 -06:00

185 lines
5.5 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: auth
-- TABLE: tokens
-- DESCRIPTION: Tokens de refresh, reset password, verificacion email, etc.
-- VERSION: 1.0.0
-- CREATED: 2026-01-10
-- ============================================================================
-- Enum para tipo de token
DO $$ BEGIN
CREATE TYPE auth.token_type AS ENUM (
'refresh', -- Refresh token para JWT
'password_reset', -- Reset de password
'email_verify', -- Verificacion de email
'phone_verify', -- Verificacion de telefono
'api_key', -- API key para integraciones
'invitation' -- Invitacion a unirse a tenant
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Enum para estado de token
DO $$ BEGIN
CREATE TYPE auth.token_status AS ENUM (
'active', -- Token activo y usable
'used', -- Ya fue usado (one-time tokens)
'expired', -- Expirado por tiempo
'revoked' -- Revocado manualmente
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tabla de Tokens
CREATE TABLE IF NOT EXISTS auth.tokens (
-- Identificadores
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users.users(id) ON DELETE CASCADE, -- NULL para invitations pre-user
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
-- Token info
token_type auth.token_type NOT NULL,
token_hash VARCHAR(255) NOT NULL, -- Hash del token
-- Estado
status auth.token_status NOT NULL DEFAULT 'active',
-- Metadata (flexible por tipo)
metadata JSONB DEFAULT '{}'::JSONB,
-- Para invitations: { "email": "...", "role_id": "..." }
-- Para api_key: { "name": "...", "scopes": [...] }
-- Timestamps
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE auth.tokens IS
'Tokens para diversos propositos: refresh, reset, verificacion, API keys, invitaciones';
COMMENT ON COLUMN auth.tokens.metadata IS
'Metadata especifica por tipo de token (email para invitations, scopes para API keys, etc.)';
-- Indices
CREATE INDEX IF NOT EXISTS idx_tokens_user
ON auth.tokens(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_tokens_tenant
ON auth.tokens(tenant_id);
CREATE INDEX IF NOT EXISTS idx_tokens_hash
ON auth.tokens(token_hash);
CREATE INDEX IF NOT EXISTS idx_tokens_type_status
ON auth.tokens(token_type, status) WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_tokens_expires
ON auth.tokens(expires_at) WHERE status = 'active';
-- Indice para buscar invitations por email
CREATE INDEX IF NOT EXISTS idx_tokens_invitation_email
ON auth.tokens((metadata->>'email'))
WHERE token_type = 'invitation' AND status = 'active';
-- Funcion para crear token de reset de password
CREATE OR REPLACE FUNCTION auth.create_password_reset_token(
p_user_id UUID,
p_tenant_id UUID,
p_token_hash VARCHAR(255),
p_expires_in INTERVAL DEFAULT '1 hour'
)
RETURNS UUID AS $$
DECLARE
v_token_id UUID;
BEGIN
-- Revocar tokens anteriores de reset
UPDATE auth.tokens
SET status = 'revoked', revoked_at = NOW()
WHERE user_id = p_user_id
AND token_type = 'password_reset'
AND status = 'active';
-- Crear nuevo token
INSERT INTO auth.tokens (user_id, tenant_id, token_type, token_hash, expires_at)
VALUES (p_user_id, p_tenant_id, 'password_reset', p_token_hash, NOW() + p_expires_in)
RETURNING id INTO v_token_id;
RETURN v_token_id;
END;
$$ LANGUAGE plpgsql;
-- Funcion para validar y usar token
CREATE OR REPLACE FUNCTION auth.use_token(
p_token_hash VARCHAR(255),
p_token_type auth.token_type
)
RETURNS TABLE (
user_id UUID,
tenant_id UUID,
metadata JSONB,
is_valid BOOLEAN
) AS $$
DECLARE
v_token auth.tokens%ROWTYPE;
BEGIN
-- Buscar token activo
SELECT * INTO v_token
FROM auth.tokens t
WHERE t.token_hash = p_token_hash
AND t.token_type = p_token_type
AND t.status = 'active'
AND t.expires_at > NOW();
IF NOT FOUND THEN
RETURN QUERY SELECT NULL::UUID, NULL::UUID, NULL::JSONB, FALSE;
RETURN;
END IF;
-- Marcar como usado (solo para one-time tokens)
IF p_token_type IN ('password_reset', 'email_verify', 'phone_verify', 'invitation') THEN
UPDATE auth.tokens
SET status = 'used', used_at = NOW()
WHERE id = v_token.id;
END IF;
RETURN QUERY SELECT v_token.user_id, v_token.tenant_id, v_token.metadata, TRUE;
END;
$$ LANGUAGE plpgsql;
-- Funcion para limpiar tokens expirados
CREATE OR REPLACE FUNCTION auth.cleanup_expired_tokens()
RETURNS INTEGER AS $$
DECLARE
updated_count INTEGER;
BEGIN
UPDATE auth.tokens
SET status = 'expired'
WHERE status = 'active'
AND expires_at < NOW();
GET DIAGNOSTICS updated_count = ROW_COUNT;
-- Eliminar tokens muy antiguos (> 30 dias)
DELETE FROM auth.tokens
WHERE status IN ('used', 'expired', 'revoked')
AND created_at < NOW() - INTERVAL '30 days';
RETURN updated_count;
END;
$$ LANGUAGE plpgsql;
-- RLS Policy para multi-tenancy
ALTER TABLE auth.tokens ENABLE ROW LEVEL SECURITY;
CREATE POLICY tokens_tenant_isolation ON auth.tokens
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE, DELETE ON auth.tokens TO trading_app;
GRANT SELECT ON auth.tokens TO trading_readonly;