-- ============================================================================ -- 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;