157 lines
4.5 KiB
PL/PgSQL
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;
|