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

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 ...

Referencias