185 lines
5.5 KiB
PL/PgSQL
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;
|