trading-platform/docs/02-definicion-modulos/OQI-001-fundamentos-auth/especificaciones/ET-AUTH-003-database.md
rckrdmrd c1b5081208 feat(ml): Complete FASE 11 - BTCUSD update and comprehensive documentation alignment
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>
2026-01-07 09:31:29 -06:00

703 lines
20 KiB
Markdown

---
id: "ET-AUTH-003"
title: "Database Schema for Auth"
type: "Specification"
status: "Done"
rf_parent: "RF-AUTH-002"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "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](../_MAP.md)
---
## 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
```sql
-- 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
```sql
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
```sql
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
```sql
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
```sql
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
```sql
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
```sql
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
```sql
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)
```sql
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
```sql
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
```
### log_auth_event
```sql
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
```sql
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
```sql
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
```sql
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`
```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
```sql
-- 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)
```sql
-- 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 ...
```
---
## Referencias
- [PostgreSQL UUID](https://www.postgresql.org/docs/current/uuid-ossp.html)
- [PostgreSQL JSONB](https://www.postgresql.org/docs/current/datatype-json.html)
- [PostgreSQL Partitioning](https://www.postgresql.org/docs/current/ddl-partitioning.html)