ML Engine Updates: - Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records - Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence) - Backtest results: +176.71R profit with aggressive_filter strategy Documentation Consolidation: - Created docs/99-analisis/_MAP.md index with 13 new analysis documents - Consolidated inventories: removed duplicates from orchestration/inventarios/ - Updated ML_INVENTORY.yml with BTCUSD metrics and training results - Added execution reports: FASE11-BTCUSD, correction issues, alignment validation Architecture & Integration: - Updated all module documentation with NEXUS v3.4 frontmatter - Fixed _MAP.md indexes across all folders - Updated orchestration plans and traces Files: 229 changed, 5064 insertions(+), 1872 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
20 KiB
20 KiB
| id | title | type | status | rf_parent | epic | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|
| ET-AUTH-003 | Database Schema for Auth | Specification | Done | RF-AUTH-002 | OQI-001 | 1.0 | 2025-12-05 | 2026-01-04 |
ET-AUTH-003: Especificación Técnica - Esquema de Base de Datos
Version: 1.0.0 Fecha: 2025-12-05 Estado: ✅ Implementado Épica: OQI-001
Resumen
Esta especificación detalla el esquema de base de datos para el sistema de autenticación de Trading Platform.
Diagrama ER
┌─────────────────────────────────────────────────────────────────────────────┐
│ AUTHENTICATION SCHEMA │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────┐ ┌─────────────────────┐
│ users │ │ user_profiles │
├─────────────────────┤ ├─────────────────────┤
│ id (PK) │──1:1──│ id (PK) │
│ email │ │ user_id (FK) │
│ password_hash │ │ display_name │
│ phone │ │ bio │
│ first_name │ │ avatar_url │
│ last_name │ │ preferred_language │
│ role │ │ timezone │
│ status │ │ date_format │
│ email_verified │ │ notification_prefs │
│ phone_verified │ │ created_at │
│ two_factor_enabled │ │ updated_at │
│ two_factor_secret │ └─────────────────────┘
│ backup_codes │
│ created_at │ ┌─────────────────────┐
│ updated_at │ │ oauth_accounts │
│ deleted_at │ ├─────────────────────┤
└─────────────────────┘ │ id (PK) │
│ │ user_id (FK) │──┐
│ │ provider │ │
├──────1:N──────────▶│ provider_user_id │ │
│ │ email │ │
│ │ access_token │ │
│ │ refresh_token │ │
│ │ token_expires_at │ │
│ │ raw_profile │ │
│ │ created_at │ │
│ │ updated_at │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ sessions │ │
│ ├─────────────────────┤ │
├──────1:N──────────▶│ id (PK) │ │
│ │ user_id (FK) │──┘
│ │ refresh_token_hash │
│ │ device_info │
│ │ ip_address │
│ │ location │
│ │ user_agent │
│ │ is_active │
│ │ created_at │
│ │ last_activity │
│ │ expires_at │
│ │ revoked_at │
│ │ revoked_reason │
│ └─────────────────────┘
│
│ ┌─────────────────────┐
│ │email_verifications │
│ ├─────────────────────┤
├──────1:N──────────▶│ id (PK) │
│ │ user_id (FK) │
│ │ token_hash │
│ │ expires_at │
│ │ verified_at │
│ │ created_at │
│ └─────────────────────┘
│
│ ┌─────────────────────┐
│ │phone_verifications │
│ ├─────────────────────┤
├──────1:N──────────▶│ id (PK) │
│ │ phone │
│ │ user_id (FK) │
│ │ code_hash │
│ │ channel │
│ │ attempts │
│ │ expires_at │
│ │ verified_at │
│ │ created_at │
│ └─────────────────────┘
│
│ ┌─────────────────────┐
│ │password_reset_tokens│
│ ├─────────────────────┤
└──────1:N──────────▶│ id (PK) │
│ user_id (FK) │
│ token_hash │
│ expires_at │
│ used_at │
│ created_at │
└─────────────────────┘
ENUMs
-- Roles de usuario
CREATE TYPE user_role_enum AS ENUM (
'investor', -- Usuario inversionista básico
'trader', -- Trader activo con más permisos
'student', -- Solo acceso a educación
'admin', -- Administrador
'superadmin' -- Super administrador
);
-- Estados de usuario
CREATE TYPE user_status_enum AS ENUM (
'pending_verification', -- Email no verificado
'active', -- Cuenta activa
'suspended', -- Suspendida temporalmente
'banned', -- Baneada permanentemente
'deleted' -- Eliminada (soft delete)
);
-- Proveedores OAuth
CREATE TYPE auth_provider_enum AS ENUM (
'google',
'facebook',
'twitter',
'apple',
'github'
);
-- Canales de verificación telefónica
CREATE TYPE phone_channel_enum AS ENUM (
'sms',
'whatsapp'
);
-- Eventos de autenticación (audit)
CREATE TYPE auth_event_enum AS ENUM (
'login_success',
'login_failed',
'logout',
'register',
'email_verified',
'phone_verified',
'password_changed',
'password_reset_requested',
'password_reset_completed',
'2fa_enabled',
'2fa_disabled',
'2fa_verified',
'backup_code_used',
'oauth_linked',
'oauth_unlinked',
'session_revoked',
'account_suspended',
'account_reactivated'
);
Tablas
users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Identificadores
email VARCHAR(255) UNIQUE,
phone VARCHAR(20) UNIQUE,
-- Credenciales
password_hash VARCHAR(255),
-- Datos personales
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
-- Permisos
role user_role_enum NOT NULL DEFAULT 'investor',
status user_status_enum NOT NULL DEFAULT 'pending_verification',
-- Verificación
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
email_verified_at TIMESTAMPTZ,
phone_verified_at TIMESTAMPTZ,
-- 2FA
two_factor_enabled BOOLEAN NOT NULL DEFAULT FALSE,
two_factor_secret VARCHAR(255), -- Encriptado
backup_codes JSONB DEFAULT '[]', -- Array de hashes
-- Metadata
last_login_at TIMESTAMPTZ,
login_count INTEGER DEFAULT 0,
failed_login_attempts INTEGER DEFAULT 0,
locked_until TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
-- Constraints
CONSTRAINT email_or_phone_required CHECK (
email IS NOT NULL OR phone IS NOT NULL
),
CONSTRAINT password_required_for_email CHECK (
email IS NULL OR password_hash IS NOT NULL OR
EXISTS (SELECT 1 FROM oauth_accounts WHERE user_id = id)
)
);
-- Índices
CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_phone ON users(phone) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_status ON users(status) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_role ON users(role) WHERE deleted_at IS NULL;
CREATE INDEX idx_users_created_at ON users(created_at);
-- Trigger para updated_at
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
user_profiles
CREATE TABLE user_profiles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
-- Display
display_name VARCHAR(50),
bio TEXT,
avatar_url VARCHAR(500),
-- Preferencias regionales
preferred_language VARCHAR(5) DEFAULT 'es',
timezone VARCHAR(50) DEFAULT 'America/Mexico_City',
date_format VARCHAR(20) DEFAULT 'DD/MM/YYYY',
currency VARCHAR(3) DEFAULT 'MXN',
-- Preferencias de notificación
notification_prefs JSONB DEFAULT '{
"email": {
"marketing": true,
"security": true,
"trading": true,
"education": true
},
"push": {
"trading_signals": true,
"price_alerts": true,
"course_updates": true
},
"sms": {
"security": true,
"trading": false
}
}',
-- Trading preferences
trading_prefs JSONB DEFAULT '{
"default_timeframe": "1h",
"chart_theme": "dark",
"confirmations": true
}',
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_user_profiles_user_id ON user_profiles(user_id);
oauth_accounts
CREATE TABLE oauth_accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Proveedor
provider auth_provider_enum NOT NULL,
provider_user_id VARCHAR(255) NOT NULL,
-- Datos del proveedor
email VARCHAR(255),
-- Tokens (encriptados)
access_token TEXT,
refresh_token TEXT,
token_expires_at TIMESTAMPTZ,
-- Perfil raw del proveedor
raw_profile JSONB,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
UNIQUE(provider, provider_user_id),
UNIQUE(user_id, provider)
);
CREATE INDEX idx_oauth_accounts_user_id ON oauth_accounts(user_id);
CREATE INDEX idx_oauth_accounts_provider ON oauth_accounts(provider);
CREATE INDEX idx_oauth_accounts_email ON oauth_accounts(email);
sessions
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Token
refresh_token_hash VARCHAR(255) NOT NULL,
-- Device info
device_info JSONB NOT NULL DEFAULT '{}',
-- {
-- "type": "desktop",
-- "browser": "Chrome",
-- "browserVersion": "120.0.0",
-- "os": "Windows",
-- "osVersion": "11"
-- }
-- Ubicación
ip_address INET,
location JSONB,
-- {
-- "country": "México",
-- "countryCode": "MX",
-- "region": "CDMX",
-- "city": "Ciudad de México",
-- "timezone": "America/Mexico_City"
-- }
user_agent TEXT,
-- Estado
is_active BOOLEAN NOT NULL DEFAULT TRUE,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_activity TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
-- Revocación
revoked_at TIMESTAMPTZ,
revoked_reason VARCHAR(100)
);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_token_hash ON sessions(refresh_token_hash);
CREATE INDEX idx_sessions_active ON sessions(is_active) WHERE is_active = TRUE;
CREATE INDEX idx_sessions_expires ON sessions(expires_at) WHERE is_active = TRUE;
email_verifications
CREATE TABLE email_verifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Token
token_hash VARCHAR(255) NOT NULL,
-- Estado
expires_at TIMESTAMPTZ NOT NULL,
verified_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_email_verifications_user_id ON email_verifications(user_id);
CREATE INDEX idx_email_verifications_token ON email_verifications(token_hash);
phone_verifications
CREATE TABLE phone_verifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Teléfono (puede ser nuevo, no vinculado aún)
phone VARCHAR(20) NOT NULL,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
-- OTP
code_hash VARCHAR(255) NOT NULL,
channel phone_channel_enum NOT NULL,
-- Intentos
attempts INTEGER NOT NULL DEFAULT 0,
-- Estado
expires_at TIMESTAMPTZ NOT NULL,
verified_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_phone_verifications_phone ON phone_verifications(phone);
CREATE INDEX idx_phone_verifications_user_id ON phone_verifications(user_id);
password_reset_tokens
CREATE TABLE password_reset_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Token
token_hash VARCHAR(255) NOT NULL,
-- Estado
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_password_reset_tokens_user_id ON password_reset_tokens(user_id);
CREATE INDEX idx_password_reset_tokens_token ON password_reset_tokens(token_hash);
auth_logs (Audit)
CREATE TABLE auth_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
-- Evento
event auth_event_enum NOT NULL,
-- Contexto
ip_address INET,
user_agent TEXT,
device_info JSONB,
location JSONB,
-- Detalles adicionales
metadata JSONB DEFAULT '{}',
-- Ejemplo: { "provider": "google", "reason": "invalid_password" }
-- Resultado
success BOOLEAN NOT NULL,
error_message TEXT,
-- Timestamp
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_auth_logs_user_id ON auth_logs(user_id);
CREATE INDEX idx_auth_logs_event ON auth_logs(event);
CREATE INDEX idx_auth_logs_created_at ON auth_logs(created_at);
CREATE INDEX idx_auth_logs_ip ON auth_logs(ip_address);
-- Particionar por fecha para mejor performance
-- (considerar para producción con alto volumen)
Funciones
update_updated_at_column
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
log_auth_event
CREATE OR REPLACE FUNCTION log_auth_event(
p_user_id UUID,
p_event auth_event_enum,
p_ip_address INET,
p_user_agent TEXT,
p_device_info JSONB,
p_location JSONB,
p_success BOOLEAN,
p_metadata JSONB DEFAULT '{}'
)
RETURNS UUID AS $$
DECLARE
v_log_id UUID;
BEGIN
INSERT INTO auth_logs (
user_id, event, ip_address, user_agent,
device_info, location, success, metadata
) VALUES (
p_user_id, p_event, p_ip_address, p_user_agent,
p_device_info, p_location, p_success, p_metadata
) RETURNING id INTO v_log_id;
RETURN v_log_id;
END;
$$ LANGUAGE plpgsql;
cleanup_expired_sessions
CREATE OR REPLACE FUNCTION cleanup_expired_sessions()
RETURNS INTEGER AS $$
DECLARE
v_count INTEGER;
BEGIN
UPDATE sessions
SET is_active = FALSE,
revoked_at = NOW(),
revoked_reason = 'expired'
WHERE is_active = TRUE
AND expires_at < NOW();
GET DIAGNOSTICS v_count = ROW_COUNT;
RETURN v_count;
END;
$$ LANGUAGE plpgsql;
Triggers
Crear perfil al crear usuario
CREATE OR REPLACE FUNCTION create_user_profile()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO user_profiles (user_id)
VALUES (NEW.id);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_create_user_profile
AFTER INSERT ON users
FOR EACH ROW
EXECUTE FUNCTION create_user_profile();
Log de eventos de autenticación
CREATE OR REPLACE FUNCTION log_user_status_change()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.status IS DISTINCT FROM NEW.status THEN
IF NEW.status = 'suspended' THEN
PERFORM log_auth_event(
NEW.id, 'account_suspended', NULL, NULL, NULL, NULL, TRUE
);
ELSIF NEW.status = 'active' AND OLD.status = 'suspended' THEN
PERFORM log_auth_event(
NEW.id, 'account_reactivated', NULL, NULL, NULL, NULL, TRUE
);
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_log_status_change
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION log_user_status_change();
Migraciones
Archivo: 001_create_auth_schema.sql
-- Extensiones requeridas
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ENUMs
-- (copiar definiciones de ENUMs)
-- Tablas
-- (copiar definiciones de tablas en orden)
-- Funciones
-- (copiar funciones)
-- Triggers
-- (copiar triggers)
-- Datos iniciales
INSERT INTO users (
id,
email,
password_hash,
first_name,
last_name,
role,
status,
email_verified
) VALUES (
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'admin@trading.com',
crypt('AdminPassword123!', gen_salt('bf', 12)),
'Admin',
'Trading Platform',
'superadmin',
'active',
TRUE
);
Consideraciones de Performance
Índices Parciales
-- Solo usuarios activos
CREATE INDEX idx_users_active_email
ON users(email)
WHERE status = 'active' AND deleted_at IS NULL;
-- Solo sesiones activas no expiradas
CREATE INDEX idx_sessions_valid
ON sessions(user_id, expires_at)
WHERE is_active = TRUE;
Particionamiento (Producción)
-- Particionar auth_logs por mes
CREATE TABLE auth_logs (
-- ... columnas ...
) PARTITION BY RANGE (created_at);
CREATE TABLE auth_logs_2025_01 PARTITION OF auth_logs
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
-- ... más particiones ...