--liquibase formatted sql -- PANELS · panels_product.iam (Postgres) · baseline -- -- Identidad y RBAC DE CADA TENANT (ej. empleados de ARCTEC que usan -- web-panel). No confundir con panels_platform.platform_users (identidad -- de PANELS como operador SaaS) -- son dos sistemas de identidad distintos -- que no comparten tabla ni base de datos. -- -- Este esquema NO tiene FK hacia `core` (ver reglas del monolito modular -- en el plan): documents.uploaded_by/badge_jobs.created_by pasan a -- snapshot desnormalizado en `core`, y users.company_id es una referencia -- lógica a core.companies(id) sin FK real. --changeset panel:iam-001a-roles endDelimiter:; splitStatements:true --preconditions onFail:MARK_RAN --precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='iam' AND table_name='roles' CREATE TABLE iam.roles ( code TEXT PRIMARY KEY, label TEXT NOT NULL ); INSERT INTO iam.roles (code, label) VALUES ('tenant_admin', 'Administrador del tenant'), ('user', 'Usuario operativo'); --changeset panel:iam-001b-permissions endDelimiter:; splitStatements:true --preconditions onFail:MARK_RAN --precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='iam' AND table_name='permissions' --comment: Catálogo base de permisos. El código de la app hoy sigue gateando -- por role_code (igual que el string "role" anterior); esta tabla es la -- base para pasar a chequeos granulares por permiso de forma incremental, -- sin bloquear la migración a Postgres en ese rediseño más grande. CREATE TABLE iam.permissions ( code TEXT PRIMARY KEY, label TEXT NOT NULL ); INSERT INTO iam.permissions (code, label) VALUES ('manage_workers', 'Alta, edición y expediente de personal'), ('manage_projects', 'Alta y edición de obras'), ('manage_companies', 'Alta y edición de empresas'), ('manage_budget', 'Presupuesto de obra'), ('manage_payroll', 'Nómina y préstamos'), ('manage_documents', 'Documentos de personal/obra/empresa'), ('manage_users', 'Alta y edición de usuarios del tenant'), ('manage_settings', 'Configuración del tenant (zona horaria, etc.)'), ('view_reports', 'Reportes y tableros'); --changeset panel:iam-001c-role-permissions endDelimiter:; splitStatements:true --preconditions onFail:MARK_RAN --precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='iam' AND table_name='role_permissions' CREATE TABLE iam.role_permissions ( role_code TEXT NOT NULL REFERENCES iam.roles(code) ON DELETE CASCADE, permission_code TEXT NOT NULL REFERENCES iam.permissions(code) ON DELETE CASCADE, PRIMARY KEY (role_code, permission_code) ); INSERT INTO iam.role_permissions (role_code, permission_code) SELECT 'tenant_admin', code FROM iam.permissions; INSERT INTO iam.role_permissions (role_code, permission_code) VALUES ('user', 'manage_workers'), ('user', 'manage_documents'), ('user', 'view_reports'); --changeset panel:iam-001d-users endDelimiter:; splitStatements:true --preconditions onFail:MARK_RAN --precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='iam' AND table_name='users' CREATE TABLE iam.users ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT NOT NULL, -- Referencia lógica a core.companies(id) -- SIN FK real (iam y core están -- aislados a propósito; ver reglas del monolito modular). company_id INTEGER, -- Referencia lógica a panels_platform.tenants(id) -- SIN FK real (bases -- de datos distintas, Postgres no permite FK entre databases). tenant_id INTEGER, role_code TEXT NOT NULL DEFAULT 'user' REFERENCES iam.roles(code), must_change_password BOOLEAN NOT NULL DEFAULT false, email TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_iam_users_tenant ON iam.users(tenant_id); -- Nota: el primer usuario (platform_admin y el tenant_admin de cada tenant -- nuevo) NO se siembra aquí. Un password_hash real requiere PBKDF2 (mismo -- algoritmo que api/crypto.ts), que Liquibase/SQL no puede calcular. Ese -- bootstrap es un comando explícito y one-shot: scripts/bootstrap-admin.ts -- (Fase 4), no lógica de seed en el arranque de la app ni en un changeset.