-- ============================================================================ -- SCHEMA: auth -- TABLE: sessions -- DESCRIPTION: Gestion de sesiones de usuario con tracking de dispositivos -- VERSION: 1.0.0 -- CREATED: 2026-01-10 -- ============================================================================ -- Crear schema si no existe CREATE SCHEMA IF NOT EXISTS auth; -- Enum para estado de sesion DO $$ BEGIN CREATE TYPE auth.session_status AS ENUM ( 'active', -- Sesion activa 'expired', -- Expirada por tiempo 'revoked' -- Revocada manualmente (logout) ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Enum para tipo de dispositivo DO $$ BEGIN CREATE TYPE auth.device_type AS ENUM ( 'desktop', 'mobile', 'tablet', 'unknown' ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Tabla de Sesiones CREATE TABLE IF NOT EXISTS auth.sessions ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, -- Token hash (para validacion) token_hash VARCHAR(255) NOT NULL, -- Informacion del dispositivo device_type auth.device_type DEFAULT 'unknown', device_name VARCHAR(255), browser VARCHAR(100), browser_version VARCHAR(50), os VARCHAR(100), os_version VARCHAR(50), -- Ubicacion ip_address INET, user_agent TEXT, -- Estado status auth.session_status NOT NULL DEFAULT 'active', -- Timestamps last_active_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); COMMENT ON TABLE auth.sessions IS 'Sesiones activas de usuarios con informacion de dispositivo para seguridad'; COMMENT ON COLUMN auth.sessions.token_hash IS 'Hash SHA256 del refresh token para validacion sin almacenar el token'; -- Indices CREATE INDEX IF NOT EXISTS idx_sessions_user ON auth.sessions(user_id); CREATE INDEX IF NOT EXISTS idx_sessions_tenant ON auth.sessions(tenant_id); CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON auth.sessions(token_hash); CREATE INDEX IF NOT EXISTS idx_sessions_status ON auth.sessions(status) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS idx_sessions_expires ON auth.sessions(expires_at) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS idx_sessions_last_active ON auth.sessions(last_active_at DESC); -- Funcion para limpiar sesiones expiradas CREATE OR REPLACE FUNCTION auth.cleanup_expired_sessions() RETURNS INTEGER AS $$ DECLARE deleted_count INTEGER; BEGIN UPDATE auth.sessions SET status = 'expired' WHERE status = 'active' AND expires_at < NOW(); GET DIAGNOSTICS deleted_count = ROW_COUNT; -- Eliminar sesiones muy antiguas (> 90 dias) DELETE FROM auth.sessions WHERE (status = 'expired' OR status = 'revoked') AND created_at < NOW() - INTERVAL '90 days'; RETURN deleted_count; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION auth.cleanup_expired_sessions() IS 'Limpia sesiones expiradas y elimina registros muy antiguos. Ejecutar periodicamente.'; -- Funcion para revocar todas las sesiones de un usuario CREATE OR REPLACE FUNCTION auth.revoke_all_user_sessions( p_user_id UUID, p_except_session_id UUID DEFAULT NULL ) RETURNS INTEGER AS $$ DECLARE revoked_count INTEGER; BEGIN UPDATE auth.sessions SET status = 'revoked', revoked_at = NOW() WHERE user_id = p_user_id AND status = 'active' AND (p_except_session_id IS NULL OR id != p_except_session_id); GET DIAGNOSTICS revoked_count = ROW_COUNT; RETURN revoked_count; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION auth.revoke_all_user_sessions IS 'Revoca todas las sesiones activas de un usuario, opcionalmente excepto una sesion especifica'; -- RLS Policy para multi-tenancy ALTER TABLE auth.sessions ENABLE ROW LEVEL SECURITY; CREATE POLICY sessions_tenant_isolation ON auth.sessions FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Grants GRANT SELECT, INSERT, UPDATE, DELETE ON auth.sessions TO trading_app; GRANT SELECT ON auth.sessions TO trading_readonly;