trading-platform-database-v2/ddl/schemas/users/tables/001_users.sql
rckrdmrd e520268348 Migración desde trading-platform/apps/database - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:32:52 -06:00

157 lines
4.5 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: users
-- TABLE: users
-- DESCRIPTION: Sistema de usuarios con autenticacion y perfiles
-- VERSION: 1.0.0
-- CREATED: 2026-01-10
-- ============================================================================
-- Crear schema si no existe
CREATE SCHEMA IF NOT EXISTS users;
-- Enum para estado de usuario
DO $$ BEGIN
CREATE TYPE users.user_status AS ENUM (
'pending', -- Pendiente de verificacion de email
'active', -- Activo y verificado
'suspended', -- Suspendido temporalmente
'banned', -- Baneado permanentemente
'deleted' -- Eliminado (soft delete)
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tabla principal de Usuarios
CREATE TABLE IF NOT EXISTS users.users (
-- Identificadores
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
-- Credenciales
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
-- Perfil
first_name VARCHAR(100),
last_name VARCHAR(100),
display_name VARCHAR(100),
avatar_url TEXT,
phone VARCHAR(20),
-- Estado
status users.user_status NOT NULL DEFAULT 'pending',
is_owner BOOLEAN NOT NULL DEFAULT FALSE, -- Owner del tenant
-- Verificaciones
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
email_verified_at TIMESTAMPTZ,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified_at TIMESTAMPTZ,
-- MFA
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
mfa_secret VARCHAR(255), -- TOTP secret (encrypted)
mfa_backup_codes JSONB, -- Array de backup codes hasheados
-- Seguridad
password_changed_at TIMESTAMPTZ DEFAULT NOW(),
failed_login_attempts INTEGER NOT NULL DEFAULT 0,
locked_until TIMESTAMPTZ,
last_login_at TIMESTAMPTZ,
last_login_ip INET,
-- Preferencias (JSONB flexible)
preferences JSONB NOT NULL DEFAULT '{
"theme": "dark",
"language": "es",
"notifications": {
"email": true,
"push": true,
"sms": false
}
}'::JSONB,
-- Metadata adicional
metadata JSONB DEFAULT '{}'::JSONB,
-- Soft delete
deleted_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT users_unique_email_per_tenant UNIQUE (tenant_id, email)
);
COMMENT ON TABLE users.users IS
'Usuarios de la plataforma, aislados por tenant';
COMMENT ON COLUMN users.users.is_owner IS
'Indica si es el owner del tenant (tiene todos los permisos)';
COMMENT ON COLUMN users.users.password_hash IS
'Hash bcrypt del password (cost 10)';
-- Indices
CREATE INDEX IF NOT EXISTS idx_users_tenant
ON users.users(tenant_id);
CREATE INDEX IF NOT EXISTS idx_users_email
ON users.users(email);
CREATE INDEX IF NOT EXISTS idx_users_status
ON users.users(status) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_users_owner
ON users.users(tenant_id, is_owner) WHERE is_owner = TRUE;
CREATE INDEX IF NOT EXISTS idx_users_last_login
ON users.users(last_login_at DESC);
-- Trigger para updated_at
CREATE OR REPLACE FUNCTION users.update_user_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS user_updated_at ON users.users;
CREATE TRIGGER user_updated_at
BEFORE UPDATE ON users.users
FOR EACH ROW
EXECUTE FUNCTION users.update_user_timestamp();
-- Funcion para limpiar failed_login_attempts despues de login exitoso
CREATE OR REPLACE FUNCTION users.clear_failed_logins()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.last_login_at IS DISTINCT FROM OLD.last_login_at THEN
NEW.failed_login_attempts := 0;
NEW.locked_until := NULL;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS user_clear_failed_logins ON users.users;
CREATE TRIGGER user_clear_failed_logins
BEFORE UPDATE ON users.users
FOR EACH ROW
EXECUTE FUNCTION users.clear_failed_logins();
-- RLS Policy para multi-tenancy
ALTER TABLE users.users ENABLE ROW LEVEL SECURITY;
CREATE POLICY users_tenant_isolation ON users.users
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE ON users.users TO trading_app;
GRANT SELECT ON users.users TO trading_readonly;