mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 20:43:17 +00:00
db: migrar infraestructura de BD a Postgres (fase 0-1)
- Fase 0: scripts de aprovisionamiento (db/provision/) para roles, dos bases de datos separadas (panels_platform / panels_product con esquemas iam+core) y ACLs de Redis por modulo, con verificacion automatizada de aislamiento (verify-isolation.sh) y setup local reproducible (dev-local.sh). - Fase 1: changelogs de Liquibase reescritos para Postgres (db/platform, db/iam, db/core reemplazan db/app + los changesets SQLite de platform). Baseline como estado final (no replay literal), tipos traducidos (IDENTITY, TIMESTAMPTZ/DATE, NUMERIC, BOOLEAN, CITEXT), contexts dev vs. schema/catalogos, RLS por tenant_id como defensa en profundidad, uploaded_by/created_by como snapshot desnormalizado (sin FK hacia iam). - Migraciones ya no corren en el arranque de la API: paso explicito de deploy via db/update.sh con credenciales _owner. Verificado end-to-end contra Postgres 16 + Redis local. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
This commit is contained in:
parent
97c66282bf
commit
b87b0205a8
39 changed files with 1471 additions and 691 deletions
77
db/README.md
77
db/README.md
|
|
@ -1,28 +1,73 @@
|
||||||
# Database migrations (Liquibase)
|
# Base de datos (Postgres + Liquibase)
|
||||||
|
|
||||||
Homologa el schema de cada ambiente con la misma cadena de changesets.
|
PANELS usa **Postgres** organizado como monolito modular: dos bases de datos
|
||||||
|
físicamente separadas en el mismo servidor/instancia por ambiente.
|
||||||
|
|
||||||
## Bases
|
| Base de datos | Esquema(s) | Changelog | Qué vive ahí |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `panels_platform` | `public` | `db/platform/` | Control plane SaaS: `tenants`, `platform_users` (identidad de PANELS como operador), `smtp_settings`. Solo lo tocan las rutas `/v1/saas/*`. |
|
||||||
|
| `panels_product` | `iam` | `db/iam/` | Identidad/roles/permisos **de cada tenant** (ej. usuarios de ARCTEC). Aislado de `core` -- sin JOIN/FK cruzado. |
|
||||||
|
| `panels_product` | `core` | `db/core/` | Negocio: empresas, personal, obras, expedientes, presupuesto, nómina, gafetes. |
|
||||||
|
|
||||||
| Archivo | Changelog |
|
No hay FK real entre `panels_platform` y `panels_product` (bases distintas),
|
||||||
|---------|-----------|
|
ni entre los esquemas `iam` y `core` (aislamiento a propósito). `tenant_id`
|
||||||
| `data/app.db` | `db/app/` |
|
es una referencia lógica validada en la capa de aplicación. Ver el plan de
|
||||||
| `data/platform.db` | `db/platform/` |
|
migración para el detalle de la arquitectura y las reglas del monolito
|
||||||
|
modular.
|
||||||
|
|
||||||
## Cómo aplicar
|
## Aprovisionamiento (una vez por ambiente, antes de Liquibase)
|
||||||
|
|
||||||
Requisitos: Java 17+.
|
Ver [`db/provision/README.md`](provision/README.md): crea los 6 roles de
|
||||||
|
Postgres (`panels_{platform,iam,core}_{owner,app}`), las 2 bases de datos,
|
||||||
|
los esquemas `iam`/`core`, la extensión `citext`, y los usuarios ACL de
|
||||||
|
Redis. Incluye `dev-local.sh` para reproducir todo esto en una máquina de
|
||||||
|
desarrollo sin depender de Coolify, y `verify-isolation.sh` para confirmar
|
||||||
|
que el aislamiento realmente se cumple.
|
||||||
|
|
||||||
|
## Aplicar migraciones
|
||||||
|
|
||||||
|
Requiere Java 17+ y las credenciales del rol `_owner` de cada módulo (nunca
|
||||||
|
`_app`, que es solo runtime):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# descarga CLI + JDBC (una vez) y aplica changesets
|
# Local, después de correr db/provision/dev-local.sh:
|
||||||
./db/update.sh all
|
set -a && source .env.dev-local && set +a
|
||||||
# o solo una
|
./db/update.sh all --context-filter=dev # dev: incluye datos de demo
|
||||||
./db/update.sh app
|
./db/update.sh all # staging/producción: solo esquema + catálogos
|
||||||
|
|
||||||
|
# Un solo módulo
|
||||||
|
./db/update.sh core
|
||||||
|
./db/update.sh iam
|
||||||
./db/update.sh platform
|
./db/update.sh platform
|
||||||
```
|
```
|
||||||
|
|
||||||
También: `npm run db:migrate` o al arrancar la API (`runLiquibase()` en Deno).
|
`npm run db:migrate` corre `./db/update.sh all`. **Las migraciones ya NO
|
||||||
|
corren automáticamente al arrancar la API** -- son un paso explícito de
|
||||||
|
deploy (a diferencia del `runLiquibase()` que existía con SQLite).
|
||||||
|
|
||||||
Nuevos cambios de schema → nuevo changeset en `db/app/changesets/` o `db/platform/changesets/` y referenciarlo en el `changelog-master.xml` correspondiente. No añadir migraciones en `api/db.ts`.
|
Contexts de Liquibase:
|
||||||
|
- Sin `context`: changesets de esquema y catálogos requeridos (`risk_levels`,
|
||||||
|
`document_types`, etc.) -- corren en **todos** los ambientes.
|
||||||
|
- `context="dev"`: datos de demostración (tenant/empresas/proyecto ARCTEC) --
|
||||||
|
**nunca** en staging/producción. Siempre pasar `--context-filter`
|
||||||
|
explícito; no confiar en el default de Liquibase.
|
||||||
|
|
||||||
Las herramientas viven en `db/tools/` (gitignored); `bootstrap-tools.sh` las descarga.
|
## Nuevos cambios de schema
|
||||||
|
|
||||||
|
Un changeset nuevo en `db/{platform,iam,core}/changesets/`, referenciado en
|
||||||
|
el `changelog-master.xml` correspondiente. Reglas:
|
||||||
|
|
||||||
|
- Nunca editar un changeset ya aplicado en un ambiente compartido -- crear
|
||||||
|
uno nuevo.
|
||||||
|
- Usar `dbms:postgresql` en vez de `context` para lógica específica de motor.
|
||||||
|
- No añadir migraciones directamente en `api/*.ts`.
|
||||||
|
- Las herramientas (Liquibase + driver JDBC de Postgres) viven en
|
||||||
|
`db/tools/` (gitignored); `bootstrap-tools.sh` las descarga.
|
||||||
|
|
||||||
|
## Bootstrap del primer usuario administrador
|
||||||
|
|
||||||
|
El `password_hash` (PBKDF2) no lo puede generar un changeset SQL. El primer
|
||||||
|
`platform_admin` y el primer `tenant_admin` de un tenant nuevo se crean con
|
||||||
|
`scripts/bootstrap-admin.ts` (ver Fase 4 del plan de migración), no con
|
||||||
|
lógica de seed en el arranque de la API ni con datos hardcodeados en
|
||||||
|
Liquibase.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<databaseChangeLog
|
|
||||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
|
||||||
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
|
||||||
|
|
||||||
<include file="changesets/001-baseline.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/002-tenant-id.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/003-must-change-password.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/004-document-catalog.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/005-payroll-week.sql" relativeToChangelogFile="true"/>
|
|
||||||
</databaseChangeLog>
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:app-001-baseline endDelimiter:; splitStatements:true
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='users'
|
|
||||||
--comment: Baseline schema (skipped/MARK_RAN when legacy schema.sql already applied)
|
|
||||||
|
|
||||||
PRAGMA foreign_keys = ON;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS companies (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
code TEXT NOT NULL UNIQUE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
parent_id INTEGER REFERENCES companies(id),
|
|
||||||
kind TEXT NOT NULL DEFAULT 'sub' CHECK (kind IN ('principal', 'sub')),
|
|
||||||
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'inactivo')),
|
|
||||||
registro_patronal TEXT NOT NULL DEFAULT '',
|
|
||||||
razon_social TEXT NOT NULL DEFAULT '',
|
|
||||||
nombre_comercial TEXT NOT NULL DEFAULT '',
|
|
||||||
rfc TEXT NOT NULL DEFAULT '',
|
|
||||||
regimen_fiscal TEXT NOT NULL DEFAULT '',
|
|
||||||
clase_riesgo TEXT NOT NULL DEFAULT '',
|
|
||||||
domicilio_fiscal TEXT NOT NULL DEFAULT '',
|
|
||||||
codigo_postal TEXT NOT NULL DEFAULT '',
|
|
||||||
ciudad TEXT NOT NULL DEFAULT '',
|
|
||||||
estado TEXT NOT NULL DEFAULT '',
|
|
||||||
telefono TEXT NOT NULL DEFAULT '',
|
|
||||||
email TEXT NOT NULL DEFAULT '',
|
|
||||||
representante_legal TEXT NOT NULL DEFAULT '',
|
|
||||||
giro TEXT NOT NULL DEFAULT '',
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS risk_levels (
|
|
||||||
code TEXT PRIMARY KEY,
|
|
||||||
label TEXT NOT NULL,
|
|
||||||
color TEXT NOT NULL,
|
|
||||||
text_color TEXT NOT NULL DEFAULT '#FFFFFF'
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS badge_themes (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
layout TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS projects (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
code TEXT NOT NULL UNIQUE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
address TEXT NOT NULL DEFAULT '',
|
|
||||||
stage TEXT NOT NULL DEFAULT '',
|
|
||||||
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'pausado', 'concluido', 'cancelado')),
|
|
||||||
theme_id TEXT NOT NULL REFERENCES badge_themes(id),
|
|
||||||
logo_left_path TEXT,
|
|
||||||
logo_right_path TEXT,
|
|
||||||
company_id INTEGER REFERENCES companies(id),
|
|
||||||
contract_amount REAL,
|
|
||||||
start_date TEXT,
|
|
||||||
end_date TEXT,
|
|
||||||
resident_name TEXT NOT NULL DEFAULT '',
|
|
||||||
siroc TEXT NOT NULL DEFAULT '',
|
|
||||||
payroll_tax_pct REAL NOT NULL DEFAULT 4,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS workers (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
first_name TEXT NOT NULL,
|
|
||||||
middle_name TEXT,
|
|
||||||
last_name_p TEXT NOT NULL,
|
|
||||||
last_name_m TEXT NOT NULL,
|
|
||||||
curp TEXT NOT NULL COLLATE NOCASE,
|
|
||||||
rfc TEXT NOT NULL COLLATE NOCASE,
|
|
||||||
nss TEXT NOT NULL,
|
|
||||||
phone TEXT NOT NULL,
|
|
||||||
email TEXT NOT NULL,
|
|
||||||
address TEXT NOT NULL,
|
|
||||||
blood_type TEXT,
|
|
||||||
hire_type TEXT NOT NULL,
|
|
||||||
company_id INTEGER REFERENCES companies(id),
|
|
||||||
position TEXT NOT NULL,
|
|
||||||
risk_code TEXT NOT NULL REFERENCES risk_levels(code),
|
|
||||||
work_type TEXT NOT NULL CHECK (work_type IN ('N', 'D')),
|
|
||||||
daily_wage REAL NOT NULL DEFAULT 0,
|
|
||||||
needs_badge INTEGER NOT NULL DEFAULT 1,
|
|
||||||
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'baja')),
|
|
||||||
pipeline_status TEXT NOT NULL DEFAULT 'incompleto',
|
|
||||||
imss_status TEXT NOT NULL DEFAULT 'sin_alta' CHECK (imss_status IN ('sin_alta', 'alta', 'baja_imss')),
|
|
||||||
imss_company_id INTEGER REFERENCES companies(id),
|
|
||||||
imss_alta_at TEXT,
|
|
||||||
imss_baja_at TEXT,
|
|
||||||
last_rehire_at TEXT,
|
|
||||||
vcard_password_enc TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workers_curp ON workers(curp);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workers_rfc ON workers(rfc);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workers_nss ON workers(nss);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS assignments (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
worker_id INTEGER NOT NULL REFERENCES workers(id) ON DELETE CASCADE,
|
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
active INTEGER NOT NULL DEFAULT 1,
|
|
||||||
start_date TEXT NOT NULL,
|
|
||||||
end_date TEXT,
|
|
||||||
UNIQUE (worker_id, project_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS document_types (
|
|
||||||
code TEXT PRIMARY KEY,
|
|
||||||
label TEXT NOT NULL,
|
|
||||||
required INTEGER NOT NULL DEFAULT 1,
|
|
||||||
validity_mode TEXT NOT NULL DEFAULT 'none' CHECK (validity_mode IN ('none', 'freshness', 'expiry')),
|
|
||||||
freshness_days INTEGER,
|
|
||||||
requires_issued_at INTEGER NOT NULL DEFAULT 0,
|
|
||||||
requires_expires_at INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS documents (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
worker_id INTEGER NOT NULL REFERENCES workers(id) ON DELETE CASCADE,
|
|
||||||
type_code TEXT NOT NULL REFERENCES document_types(code),
|
|
||||||
original_name TEXT NOT NULL,
|
|
||||||
mime TEXT NOT NULL,
|
|
||||||
size_bytes INTEGER NOT NULL,
|
|
||||||
sha256 TEXT NOT NULL,
|
|
||||||
iv TEXT NOT NULL,
|
|
||||||
storage_name TEXT NOT NULL,
|
|
||||||
is_current INTEGER NOT NULL DEFAULT 1,
|
|
||||||
parse_status TEXT NOT NULL DEFAULT 'manual',
|
|
||||||
issued_at TEXT,
|
|
||||||
expires_at TEXT,
|
|
||||||
imss_company_id INTEGER REFERENCES companies(id),
|
|
||||||
imss_alta_at TEXT,
|
|
||||||
uploaded_by INTEGER REFERENCES users(id),
|
|
||||||
uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS project_document_types (
|
|
||||||
code TEXT PRIMARY KEY,
|
|
||||||
label TEXT NOT NULL,
|
|
||||||
required INTEGER NOT NULL DEFAULT 1
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS project_documents (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
type_code TEXT NOT NULL REFERENCES project_document_types(code),
|
|
||||||
original_name TEXT NOT NULL,
|
|
||||||
mime TEXT NOT NULL,
|
|
||||||
size_bytes INTEGER NOT NULL,
|
|
||||||
sha256 TEXT NOT NULL,
|
|
||||||
iv TEXT NOT NULL,
|
|
||||||
storage_name TEXT NOT NULL,
|
|
||||||
is_current INTEGER NOT NULL DEFAULT 1,
|
|
||||||
parse_status TEXT NOT NULL DEFAULT 'manual',
|
|
||||||
uploaded_by INTEGER REFERENCES users(id),
|
|
||||||
uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS badge_jobs (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
||||||
status TEXT NOT NULL DEFAULT 'done',
|
|
||||||
pdf_path TEXT,
|
|
||||||
created_by INTEGER REFERENCES users(id),
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS badge_job_people (
|
|
||||||
job_id INTEGER NOT NULL REFERENCES badge_jobs(id) ON DELETE CASCADE,
|
|
||||||
worker_id INTEGER NOT NULL REFERENCES workers(id),
|
|
||||||
delivered INTEGER NOT NULL DEFAULT 0,
|
|
||||||
delivered_at TEXT,
|
|
||||||
PRIMARY KEY (job_id, worker_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS loans (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
worker_id INTEGER NOT NULL REFERENCES workers(id) ON DELETE CASCADE,
|
|
||||||
amount REAL NOT NULL,
|
|
||||||
balance REAL NOT NULL,
|
|
||||||
weekly_payment REAL NOT NULL,
|
|
||||||
note TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS attendance (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
worker_id INTEGER NOT NULL REFERENCES workers(id) ON DELETE CASCADE,
|
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
work_date TEXT NOT NULL,
|
|
||||||
present INTEGER NOT NULL DEFAULT 1,
|
|
||||||
UNIQUE (worker_id, project_id, work_date)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS payroll_periods (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
||||||
week_start TEXT NOT NULL,
|
|
||||||
week_end TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'draft',
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS payroll_lines (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
period_id INTEGER NOT NULL REFERENCES payroll_periods(id) ON DELETE CASCADE,
|
|
||||||
worker_id INTEGER NOT NULL REFERENCES workers(id),
|
|
||||||
days REAL NOT NULL DEFAULT 0,
|
|
||||||
daily_wage REAL NOT NULL,
|
|
||||||
gross REAL NOT NULL,
|
|
||||||
discounts REAL NOT NULL DEFAULT 0,
|
|
||||||
loan_payment REAL NOT NULL DEFAULT 0,
|
|
||||||
net REAL NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS budget_chapters (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
parent_id INTEGER REFERENCES budget_chapters(id) ON DELETE SET NULL,
|
|
||||||
code TEXT NOT NULL DEFAULT '',
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
wbs TEXT NOT NULL DEFAULT '',
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS budget_items (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
||||||
chapter_id INTEGER REFERENCES budget_chapters(id) ON DELETE SET NULL,
|
|
||||||
code TEXT NOT NULL DEFAULT '',
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
unit TEXT NOT NULL DEFAULT '',
|
|
||||||
quantity REAL NOT NULL DEFAULT 0,
|
|
||||||
unit_price REAL NOT NULL DEFAULT 0,
|
|
||||||
amount REAL NOT NULL DEFAULT 0,
|
|
||||||
wbs TEXT NOT NULL DEFAULT '',
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_code ON projects(code);
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:app-002a-users-tenant endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('users') WHERE name='tenant_id'
|
|
||||||
ALTER TABLE users ADD COLUMN tenant_id INTEGER;
|
|
||||||
|
|
||||||
--changeset panel:app-002b-users-role endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('users') WHERE name='role'
|
|
||||||
ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user';
|
|
||||||
|
|
||||||
--changeset panel:app-002c-users-company endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('users') WHERE name='company_id'
|
|
||||||
ALTER TABLE users ADD COLUMN company_id INTEGER REFERENCES companies(id);
|
|
||||||
|
|
||||||
--changeset panel:app-002d-companies-tenant endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('companies') WHERE name='tenant_id'
|
|
||||||
ALTER TABLE companies ADD COLUMN tenant_id INTEGER;
|
|
||||||
|
|
||||||
--changeset panel:app-002e-workers-tenant endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('workers') WHERE name='tenant_id'
|
|
||||||
ALTER TABLE workers ADD COLUMN tenant_id INTEGER;
|
|
||||||
|
|
||||||
--changeset panel:app-002f-projects-tenant endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('projects') WHERE name='tenant_id'
|
|
||||||
ALTER TABLE projects ADD COLUMN tenant_id INTEGER;
|
|
||||||
|
|
||||||
--changeset panel:app-002g-backfill-tenant endDelimiter:; splitStatements:true
|
|
||||||
UPDATE companies SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|
||||||
UPDATE workers SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|
||||||
UPDATE projects SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|
||||||
UPDATE users SET tenant_id = 1 WHERE tenant_id IS NULL;
|
|
||||||
UPDATE users SET role = 'tenant_admin' WHERE COALESCE(role, 'user') = 'user';
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_companies_tenant ON companies(tenant_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_workers_tenant ON workers(tenant_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_projects_tenant ON projects(tenant_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id);
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:app-003a-must-change-password endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('users') WHERE name='must_change_password'
|
|
||||||
ALTER TABLE users ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
--changeset panel:app-003b-user-email endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('users') WHERE name='email'
|
|
||||||
ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT '';
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:app-004a-doc-type-category endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('document_types') WHERE name='category'
|
|
||||||
ALTER TABLE document_types ADD COLUMN category TEXT NOT NULL DEFAULT 'identidad';
|
|
||||||
|
|
||||||
--changeset panel:app-004b-project-doc-category endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('project_document_types') WHERE name='category'
|
|
||||||
ALTER TABLE project_document_types ADD COLUMN category TEXT NOT NULL DEFAULT 'contrato';
|
|
||||||
|
|
||||||
--changeset panel:app-004c-company-document-types endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='company_document_types'
|
|
||||||
CREATE TABLE company_document_types (
|
|
||||||
code TEXT PRIMARY KEY,
|
|
||||||
label TEXT NOT NULL,
|
|
||||||
required INTEGER NOT NULL DEFAULT 0,
|
|
||||||
category TEXT NOT NULL DEFAULT 'otro'
|
|
||||||
);
|
|
||||||
|
|
||||||
--changeset panel:app-004d-company-documents endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='company_documents'
|
|
||||||
CREATE TABLE company_documents (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
company_id INTEGER NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
||||||
type_code TEXT NOT NULL REFERENCES company_document_types(code),
|
|
||||||
original_name TEXT NOT NULL,
|
|
||||||
mime TEXT NOT NULL,
|
|
||||||
size_bytes INTEGER NOT NULL,
|
|
||||||
sha256 TEXT NOT NULL,
|
|
||||||
iv TEXT NOT NULL,
|
|
||||||
storage_name TEXT NOT NULL,
|
|
||||||
is_current INTEGER NOT NULL DEFAULT 1,
|
|
||||||
parse_status TEXT NOT NULL DEFAULT 'manual',
|
|
||||||
uploaded_by INTEGER REFERENCES users(id),
|
|
||||||
uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:app-005a-payroll-settings endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='payroll_settings'
|
|
||||||
CREATE TABLE payroll_settings (
|
|
||||||
tenant_id INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
loan_commission_enabled INTEGER NOT NULL DEFAULT 1,
|
|
||||||
loan_commission_pct REAL NOT NULL DEFAULT 10,
|
|
||||||
loan_small_max REAL NOT NULL DEFAULT 500
|
|
||||||
);
|
|
||||||
|
|
||||||
--changeset panel:app-005b-payroll-weeks endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='payroll_weeks'
|
|
||||||
CREATE TABLE payroll_weeks (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
week_start TEXT NOT NULL,
|
|
||||||
week_end TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'assembled', 'paid')),
|
|
||||||
required_net REAL NOT NULL DEFAULT 0,
|
|
||||||
payable_net REAL NOT NULL DEFAULT 0,
|
|
||||||
assembled_at TEXT,
|
|
||||||
paid_at TEXT,
|
|
||||||
tenant_id INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
CREATE UNIQUE INDEX idx_payroll_weeks_tenant_start ON payroll_weeks(tenant_id, week_start);
|
|
||||||
|
|
||||||
--changeset panel:app-005c-payroll-sheets endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='payroll_sheets'
|
|
||||||
CREATE TABLE payroll_sheets (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
week_id INTEGER NOT NULL REFERENCES payroll_weeks(id) ON DELETE CASCADE,
|
|
||||||
kind TEXT NOT NULL CHECK (kind IN ('obra', 'destajo', 'admin')),
|
|
||||||
project_id INTEGER REFERENCES projects(id)
|
|
||||||
);
|
|
||||||
CREATE UNIQUE INDEX idx_payroll_sheets_week_kind_project ON payroll_sheets(week_id, kind, IFNULL(project_id, 0));
|
|
||||||
|
|
||||||
--changeset panel:app-005d-payroll-week-lines endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='payroll_week_lines'
|
|
||||||
CREATE TABLE payroll_week_lines (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
sheet_id INTEGER NOT NULL REFERENCES payroll_sheets(id) ON DELETE CASCADE,
|
|
||||||
worker_id INTEGER NOT NULL REFERENCES workers(id),
|
|
||||||
destajo_cut_line_id INTEGER,
|
|
||||||
project_id INTEGER REFERENCES projects(id),
|
|
||||||
days REAL NOT NULL DEFAULT 0,
|
|
||||||
daily_wage REAL NOT NULL DEFAULT 0,
|
|
||||||
qty_planned REAL NOT NULL DEFAULT 0,
|
|
||||||
qty_actual REAL NOT NULL DEFAULT 0,
|
|
||||||
qty_extra REAL NOT NULL DEFAULT 0,
|
|
||||||
unit_price REAL NOT NULL DEFAULT 0,
|
|
||||||
unit_code TEXT,
|
|
||||||
concepto TEXT,
|
|
||||||
amount REAL NOT NULL DEFAULT 0,
|
|
||||||
gross REAL NOT NULL DEFAULT 0,
|
|
||||||
discounts REAL NOT NULL DEFAULT 0,
|
|
||||||
loan_id INTEGER REFERENCES loans(id),
|
|
||||||
loan_discount REAL NOT NULL DEFAULT 0,
|
|
||||||
loan_label TEXT,
|
|
||||||
required_net REAL NOT NULL DEFAULT 0,
|
|
||||||
payable_net REAL NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
--changeset panel:app-005e-destajo-units endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='destajo_units'
|
|
||||||
CREATE TABLE destajo_units (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
tenant_id INTEGER NOT NULL DEFAULT 0,
|
|
||||||
code TEXT NOT NULL,
|
|
||||||
label TEXT NOT NULL,
|
|
||||||
UNIQUE (tenant_id, code)
|
|
||||||
);
|
|
||||||
|
|
||||||
--changeset panel:app-005f-destajo-periods endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='destajo_periods'
|
|
||||||
CREATE TABLE destajo_periods (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
period_start TEXT NOT NULL,
|
|
||||||
period_end TEXT NOT NULL,
|
|
||||||
week_id INTEGER NOT NULL REFERENCES payroll_weeks(id) ON DELETE CASCADE,
|
|
||||||
tenant_id INTEGER NOT NULL DEFAULT 0,
|
|
||||||
UNIQUE (tenant_id, period_end)
|
|
||||||
);
|
|
||||||
|
|
||||||
--changeset panel:app-005g-destajo-jobs endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='destajo_jobs'
|
|
||||||
CREATE TABLE destajo_jobs (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
||||||
worker_id INTEGER NOT NULL REFERENCES workers(id),
|
|
||||||
concepto TEXT NOT NULL,
|
|
||||||
unit_code TEXT NOT NULL,
|
|
||||||
qty_total_estimated REAL NOT NULL,
|
|
||||||
unit_price REAL NOT NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'done')),
|
|
||||||
tenant_id INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
--changeset panel:app-005h-destajo-cut-lines endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='destajo_cut_lines'
|
|
||||||
CREATE TABLE destajo_cut_lines (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
period_id INTEGER NOT NULL REFERENCES destajo_periods(id) ON DELETE CASCADE,
|
|
||||||
job_id INTEGER NOT NULL REFERENCES destajo_jobs(id) ON DELETE CASCADE,
|
|
||||||
qty_planned REAL NOT NULL DEFAULT 0,
|
|
||||||
qty_actual REAL NOT NULL DEFAULT 0,
|
|
||||||
qty_extra REAL NOT NULL DEFAULT 0,
|
|
||||||
UNIQUE (period_id, job_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
--changeset panel:app-005i-loan-payments endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='loan_payments'
|
|
||||||
CREATE TABLE loan_payments (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
loan_id INTEGER NOT NULL REFERENCES loans(id) ON DELETE CASCADE,
|
|
||||||
week_id INTEGER NOT NULL REFERENCES payroll_weeks(id),
|
|
||||||
amount REAL NOT NULL,
|
|
||||||
installment_n INTEGER NOT NULL DEFAULT 1,
|
|
||||||
label TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
--changeset panel:app-005j-loans-delivered endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('loans') WHERE name='delivered'
|
|
||||||
ALTER TABLE loans ADD COLUMN delivered REAL NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
--changeset panel:app-005k-loans-commission-pct endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('loans') WHERE name='commission_pct'
|
|
||||||
ALTER TABLE loans ADD COLUMN commission_pct REAL NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
--changeset panel:app-005l-loans-commission-amount endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('loans') WHERE name='commission_amount'
|
|
||||||
ALTER TABLE loans ADD COLUMN commission_amount REAL NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
--changeset panel:app-005m-loans-plan endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('loans') WHERE name='plan'
|
|
||||||
ALTER TABLE loans ADD COLUMN plan TEXT NOT NULL DEFAULT 'single';
|
|
||||||
|
|
||||||
--changeset panel:app-005n-loans-installments endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('loans') WHERE name='installments_n'
|
|
||||||
ALTER TABLE loans ADD COLUMN installments_n INTEGER NOT NULL DEFAULT 1;
|
|
||||||
|
|
||||||
--changeset panel:app-005o-loans-first-due endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('loans') WHERE name='first_due'
|
|
||||||
ALTER TABLE loans ADD COLUMN first_due TEXT;
|
|
||||||
|
|
||||||
--changeset panel:app-005p-loans-migrate-delivered endDelimiter:;
|
|
||||||
UPDATE loans SET delivered = amount WHERE delivered = 0 AND amount > 0;
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
changeLogFile=changelog-master.xml
|
|
||||||
driver=org.sqlite.JDBC
|
|
||||||
url=jdbc:sqlite:../../data/app.db
|
|
||||||
classpath=../tools/sqlite-jdbc.jar
|
|
||||||
|
|
@ -13,10 +13,10 @@ if [[ ! -x "$TOOLS/liquibase/liquibase" ]]; then
|
||||||
rm -f liquibase.zip
|
rm -f liquibase.zip
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "$TOOLS/sqlite-jdbc.jar" ]]; then
|
if [[ ! -f "$TOOLS/postgresql-jdbc.jar" ]]; then
|
||||||
echo "Downloading sqlite-jdbc..."
|
echo "Downloading PostgreSQL JDBC driver..."
|
||||||
curl -fsSL -o sqlite-jdbc.jar \
|
curl -fsSL -o postgresql-jdbc.jar \
|
||||||
"https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.46.1.3/sqlite-jdbc-3.46.1.3.jar"
|
"https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.4/postgresql-42.7.4.jar"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Liquibase tools ready."
|
echo "Liquibase tools ready."
|
||||||
|
|
|
||||||
16
db/core/changelog-master.xml
Normal file
16
db/core/changelog-master.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
|
||||||
|
|
||||||
|
<!-- panels_product, esquema core: negocio (empresas, personal, obras,
|
||||||
|
expedientes, presupuesto, nómina, gafetes). -->
|
||||||
|
<include file="changesets/001-baseline.sql" relativeToChangelogFile="true"/>
|
||||||
|
<include file="changesets/002-tenant-settings.sql" relativeToChangelogFile="true"/>
|
||||||
|
<include file="changesets/003-seed-catalogs.sql" relativeToChangelogFile="true"/>
|
||||||
|
<include file="changesets/004-seed-dev-data.sql" relativeToChangelogFile="true"/>
|
||||||
|
<include file="changesets/005-rls.sql" relativeToChangelogFile="true"/>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
482
db/core/changesets/001-baseline.sql
Normal file
482
db/core/changesets/001-baseline.sql
Normal file
|
|
@ -0,0 +1,482 @@
|
||||||
|
--liquibase formatted sql
|
||||||
|
-- PANELS · panels_product.core (Postgres) · baseline
|
||||||
|
--
|
||||||
|
-- Negocio: empresas, personal, obras, expedientes, presupuesto, nómina,
|
||||||
|
-- gafetes. Sin FK hacia iam (documents.uploaded_by / badge_jobs.created_by
|
||||||
|
-- son snapshot desnormalizado, no referencia viva) ni hacia
|
||||||
|
-- panels_platform.tenants (bases distintas -- tenant_id se valida en la
|
||||||
|
-- capa de aplicación, igual que hoy entre app.db/platform.db).
|
||||||
|
--
|
||||||
|
-- Requiere que la extensión citext ya esté instalada en la base de datos
|
||||||
|
-- (ver db/provision/04-product-database.sql, corre como superuser).
|
||||||
|
|
||||||
|
--changeset panel:core-001a-companies endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='companies'
|
||||||
|
CREATE TABLE core.companies (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
code TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
parent_id INTEGER REFERENCES core.companies(id),
|
||||||
|
kind TEXT NOT NULL DEFAULT 'sub' CHECK (kind IN ('principal', 'sub')),
|
||||||
|
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'inactivo')),
|
||||||
|
tenant_id INTEGER,
|
||||||
|
registro_patronal TEXT NOT NULL DEFAULT '',
|
||||||
|
razon_social TEXT NOT NULL DEFAULT '',
|
||||||
|
nombre_comercial TEXT NOT NULL DEFAULT '',
|
||||||
|
rfc TEXT NOT NULL DEFAULT '',
|
||||||
|
regimen_fiscal TEXT NOT NULL DEFAULT '',
|
||||||
|
clase_riesgo TEXT NOT NULL DEFAULT '',
|
||||||
|
domicilio_fiscal TEXT NOT NULL DEFAULT '',
|
||||||
|
codigo_postal TEXT NOT NULL DEFAULT '',
|
||||||
|
ciudad TEXT NOT NULL DEFAULT '',
|
||||||
|
estado TEXT NOT NULL DEFAULT '',
|
||||||
|
telefono TEXT NOT NULL DEFAULT '',
|
||||||
|
email TEXT NOT NULL DEFAULT '',
|
||||||
|
representante_legal TEXT NOT NULL DEFAULT '',
|
||||||
|
giro TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_core_companies_tenant ON core.companies(tenant_id);
|
||||||
|
|
||||||
|
--changeset panel:core-001b-risk-levels endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='risk_levels'
|
||||||
|
CREATE TABLE core.risk_levels (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
color TEXT NOT NULL,
|
||||||
|
text_color TEXT NOT NULL DEFAULT '#FFFFFF'
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001c-badge-themes endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='badge_themes'
|
||||||
|
CREATE TABLE core.badge_themes (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
layout TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001d-projects endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='projects'
|
||||||
|
CREATE TABLE core.projects (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
code TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
address TEXT NOT NULL DEFAULT '',
|
||||||
|
stage TEXT NOT NULL DEFAULT '',
|
||||||
|
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'pausado', 'concluido', 'cancelado')),
|
||||||
|
theme_id TEXT NOT NULL REFERENCES core.badge_themes(id),
|
||||||
|
logo_left_path TEXT,
|
||||||
|
logo_right_path TEXT,
|
||||||
|
company_id INTEGER REFERENCES core.companies(id),
|
||||||
|
tenant_id INTEGER,
|
||||||
|
contract_amount NUMERIC(14,2),
|
||||||
|
start_date DATE,
|
||||||
|
end_date DATE,
|
||||||
|
resident_name TEXT NOT NULL DEFAULT '',
|
||||||
|
siroc TEXT NOT NULL DEFAULT '',
|
||||||
|
payroll_tax_pct NUMERIC(5,2) NOT NULL DEFAULT 4,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_core_projects_tenant ON core.projects(tenant_id);
|
||||||
|
|
||||||
|
--changeset panel:core-001e-workers endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='workers'
|
||||||
|
CREATE TABLE core.workers (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
first_name TEXT NOT NULL,
|
||||||
|
middle_name TEXT,
|
||||||
|
last_name_p TEXT NOT NULL,
|
||||||
|
last_name_m TEXT NOT NULL,
|
||||||
|
curp CITEXT NOT NULL UNIQUE,
|
||||||
|
rfc CITEXT NOT NULL UNIQUE,
|
||||||
|
nss TEXT NOT NULL UNIQUE,
|
||||||
|
phone TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
blood_type TEXT,
|
||||||
|
hire_type TEXT NOT NULL,
|
||||||
|
company_id INTEGER REFERENCES core.companies(id),
|
||||||
|
tenant_id INTEGER,
|
||||||
|
position TEXT NOT NULL,
|
||||||
|
risk_code TEXT NOT NULL REFERENCES core.risk_levels(code),
|
||||||
|
work_type TEXT NOT NULL CHECK (work_type IN ('N', 'D')),
|
||||||
|
daily_wage NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
needs_badge BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'baja')),
|
||||||
|
pipeline_status TEXT NOT NULL DEFAULT 'incompleto',
|
||||||
|
imss_status TEXT NOT NULL DEFAULT 'sin_alta' CHECK (imss_status IN ('sin_alta', 'alta', 'baja_imss')),
|
||||||
|
imss_company_id INTEGER REFERENCES core.companies(id),
|
||||||
|
imss_alta_at DATE,
|
||||||
|
imss_baja_at DATE,
|
||||||
|
last_rehire_at DATE,
|
||||||
|
vcard_password_enc TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_core_workers_tenant ON core.workers(tenant_id);
|
||||||
|
|
||||||
|
--changeset panel:core-001f-assignments endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='assignments'
|
||||||
|
CREATE TABLE core.assignments (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
worker_id INTEGER NOT NULL REFERENCES core.workers(id) ON DELETE CASCADE,
|
||||||
|
project_id INTEGER NOT NULL REFERENCES core.projects(id) ON DELETE CASCADE,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
start_date DATE NOT NULL,
|
||||||
|
end_date DATE,
|
||||||
|
UNIQUE (worker_id, project_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001g-document-types endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='document_types'
|
||||||
|
CREATE TABLE core.document_types (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
required BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
validity_mode TEXT NOT NULL DEFAULT 'none' CHECK (validity_mode IN ('none', 'freshness', 'expiry')),
|
||||||
|
freshness_days INTEGER,
|
||||||
|
requires_issued_at BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
requires_expires_at BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
category TEXT NOT NULL DEFAULT 'identidad'
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001h-documents endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='documents'
|
||||||
|
--comment: uploaded_by_id/uploaded_by_name son snapshot -- sin FK hacia
|
||||||
|
-- iam.users (esquemas aislados a propósito).
|
||||||
|
CREATE TABLE core.documents (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
worker_id INTEGER NOT NULL REFERENCES core.workers(id) ON DELETE CASCADE,
|
||||||
|
type_code TEXT NOT NULL REFERENCES core.document_types(code),
|
||||||
|
original_name TEXT NOT NULL,
|
||||||
|
mime TEXT NOT NULL,
|
||||||
|
size_bytes BIGINT NOT NULL,
|
||||||
|
sha256 TEXT NOT NULL,
|
||||||
|
iv TEXT NOT NULL,
|
||||||
|
storage_name TEXT NOT NULL,
|
||||||
|
is_current BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
parse_status TEXT NOT NULL DEFAULT 'manual',
|
||||||
|
issued_at DATE,
|
||||||
|
expires_at DATE,
|
||||||
|
imss_company_id INTEGER REFERENCES core.companies(id),
|
||||||
|
imss_alta_at DATE,
|
||||||
|
uploaded_by_id INTEGER,
|
||||||
|
uploaded_by_name TEXT NOT NULL DEFAULT '',
|
||||||
|
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001i-project-document-types endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='project_document_types'
|
||||||
|
CREATE TABLE core.project_document_types (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
required BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
category TEXT NOT NULL DEFAULT 'contrato'
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001j-project-documents endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='project_documents'
|
||||||
|
CREATE TABLE core.project_documents (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
project_id INTEGER NOT NULL REFERENCES core.projects(id) ON DELETE CASCADE,
|
||||||
|
type_code TEXT NOT NULL REFERENCES core.project_document_types(code),
|
||||||
|
original_name TEXT NOT NULL,
|
||||||
|
mime TEXT NOT NULL,
|
||||||
|
size_bytes BIGINT NOT NULL,
|
||||||
|
sha256 TEXT NOT NULL,
|
||||||
|
iv TEXT NOT NULL,
|
||||||
|
storage_name TEXT NOT NULL,
|
||||||
|
is_current BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
parse_status TEXT NOT NULL DEFAULT 'manual',
|
||||||
|
uploaded_by_id INTEGER,
|
||||||
|
uploaded_by_name TEXT NOT NULL DEFAULT '',
|
||||||
|
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001k-company-document-types endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='company_document_types'
|
||||||
|
CREATE TABLE core.company_document_types (
|
||||||
|
code TEXT PRIMARY KEY,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
required BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
category TEXT NOT NULL DEFAULT 'otro'
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001l-company-documents endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='company_documents'
|
||||||
|
CREATE TABLE core.company_documents (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
company_id INTEGER NOT NULL REFERENCES core.companies(id) ON DELETE CASCADE,
|
||||||
|
type_code TEXT NOT NULL REFERENCES core.company_document_types(code),
|
||||||
|
original_name TEXT NOT NULL,
|
||||||
|
mime TEXT NOT NULL,
|
||||||
|
size_bytes BIGINT NOT NULL,
|
||||||
|
sha256 TEXT NOT NULL,
|
||||||
|
iv TEXT NOT NULL,
|
||||||
|
storage_name TEXT NOT NULL,
|
||||||
|
is_current BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
parse_status TEXT NOT NULL DEFAULT 'manual',
|
||||||
|
uploaded_by_id INTEGER,
|
||||||
|
uploaded_by_name TEXT NOT NULL DEFAULT '',
|
||||||
|
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001m-badge-jobs endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='badge_jobs'
|
||||||
|
CREATE TABLE core.badge_jobs (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
project_id INTEGER NOT NULL REFERENCES core.projects(id),
|
||||||
|
status TEXT NOT NULL DEFAULT 'done',
|
||||||
|
pdf_path TEXT,
|
||||||
|
created_by_id INTEGER,
|
||||||
|
created_by_name TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001n-badge-job-people endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='badge_job_people'
|
||||||
|
CREATE TABLE core.badge_job_people (
|
||||||
|
job_id INTEGER NOT NULL REFERENCES core.badge_jobs(id) ON DELETE CASCADE,
|
||||||
|
worker_id INTEGER NOT NULL REFERENCES core.workers(id),
|
||||||
|
delivered BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
delivered_at TIMESTAMPTZ,
|
||||||
|
PRIMARY KEY (job_id, worker_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001o-loans endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='loans'
|
||||||
|
CREATE TABLE core.loans (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
worker_id INTEGER NOT NULL REFERENCES core.workers(id) ON DELETE CASCADE,
|
||||||
|
amount NUMERIC(12,2) NOT NULL,
|
||||||
|
delivered NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
balance NUMERIC(12,2) NOT NULL,
|
||||||
|
weekly_payment NUMERIC(12,2) NOT NULL,
|
||||||
|
note TEXT,
|
||||||
|
commission_pct NUMERIC(5,2) NOT NULL DEFAULT 0,
|
||||||
|
commission_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
plan TEXT NOT NULL DEFAULT 'single',
|
||||||
|
installments_n INTEGER NOT NULL DEFAULT 1,
|
||||||
|
first_due DATE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001p-attendance endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='attendance'
|
||||||
|
CREATE TABLE core.attendance (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
worker_id INTEGER NOT NULL REFERENCES core.workers(id) ON DELETE CASCADE,
|
||||||
|
project_id INTEGER NOT NULL REFERENCES core.projects(id) ON DELETE CASCADE,
|
||||||
|
work_date DATE NOT NULL,
|
||||||
|
present BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
UNIQUE (worker_id, project_id, work_date)
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001q-payroll-periods endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='payroll_periods'
|
||||||
|
--comment: Nómina simple por proyecto (legacy, sigue en uso via payroll_http.ts).
|
||||||
|
CREATE TABLE core.payroll_periods (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
project_id INTEGER NOT NULL REFERENCES core.projects(id),
|
||||||
|
week_start DATE NOT NULL,
|
||||||
|
week_end DATE NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'draft',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001r-payroll-lines endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='payroll_lines'
|
||||||
|
CREATE TABLE core.payroll_lines (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
period_id INTEGER NOT NULL REFERENCES core.payroll_periods(id) ON DELETE CASCADE,
|
||||||
|
worker_id INTEGER NOT NULL REFERENCES core.workers(id),
|
||||||
|
days NUMERIC(5,2) NOT NULL DEFAULT 0,
|
||||||
|
daily_wage NUMERIC(12,2) NOT NULL,
|
||||||
|
gross NUMERIC(12,2) NOT NULL,
|
||||||
|
discounts NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
loan_payment NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
net NUMERIC(12,2) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001s-payroll-settings endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='payroll_settings'
|
||||||
|
CREATE TABLE core.payroll_settings (
|
||||||
|
tenant_id INTEGER PRIMARY KEY,
|
||||||
|
loan_commission_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
loan_commission_pct NUMERIC(5,2) NOT NULL DEFAULT 10,
|
||||||
|
loan_small_max NUMERIC(12,2) NOT NULL DEFAULT 500
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001t-payroll-weeks endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='payroll_weeks'
|
||||||
|
CREATE TABLE core.payroll_weeks (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
week_start DATE NOT NULL,
|
||||||
|
week_end DATE NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'assembled', 'paid')),
|
||||||
|
required_net NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||||
|
payable_net NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||||
|
assembled_at TIMESTAMPTZ,
|
||||||
|
paid_at TIMESTAMPTZ,
|
||||||
|
tenant_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (tenant_id, week_start)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_core_payroll_weeks_tenant ON core.payroll_weeks(tenant_id);
|
||||||
|
|
||||||
|
--changeset panel:core-001u-payroll-sheets endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='payroll_sheets'
|
||||||
|
CREATE TABLE core.payroll_sheets (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
week_id INTEGER NOT NULL REFERENCES core.payroll_weeks(id) ON DELETE CASCADE,
|
||||||
|
kind TEXT NOT NULL CHECK (kind IN ('obra', 'destajo', 'admin')),
|
||||||
|
project_id INTEGER REFERENCES core.projects(id)
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX idx_core_payroll_sheets_week_kind_project
|
||||||
|
ON core.payroll_sheets(week_id, kind, COALESCE(project_id, 0));
|
||||||
|
|
||||||
|
--changeset panel:core-001v-payroll-week-lines endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='payroll_week_lines'
|
||||||
|
CREATE TABLE core.payroll_week_lines (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
sheet_id INTEGER NOT NULL REFERENCES core.payroll_sheets(id) ON DELETE CASCADE,
|
||||||
|
worker_id INTEGER NOT NULL REFERENCES core.workers(id),
|
||||||
|
destajo_cut_line_id INTEGER,
|
||||||
|
project_id INTEGER REFERENCES core.projects(id),
|
||||||
|
days NUMERIC(5,2) NOT NULL DEFAULT 0,
|
||||||
|
daily_wage NUMERIC(12,2) NOT NULL DEFAULT 0,
|
||||||
|
qty_planned NUMERIC(14,4) NOT NULL DEFAULT 0,
|
||||||
|
qty_actual NUMERIC(14,4) NOT NULL DEFAULT 0,
|
||||||
|
qty_extra NUMERIC(14,4) NOT NULL DEFAULT 0,
|
||||||
|
unit_price NUMERIC(14,4) NOT NULL DEFAULT 0,
|
||||||
|
unit_code TEXT,
|
||||||
|
concepto TEXT,
|
||||||
|
amount NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||||
|
gross NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||||
|
discounts NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||||
|
loan_id INTEGER REFERENCES core.loans(id),
|
||||||
|
loan_discount NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||||
|
loan_label TEXT,
|
||||||
|
required_net NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||||
|
payable_net NUMERIC(14,2) NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001w-destajo-units endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='destajo_units'
|
||||||
|
CREATE TABLE core.destajo_units (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
tenant_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
UNIQUE (tenant_id, code)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_core_destajo_units_tenant ON core.destajo_units(tenant_id);
|
||||||
|
|
||||||
|
--changeset panel:core-001x-destajo-periods endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='destajo_periods'
|
||||||
|
CREATE TABLE core.destajo_periods (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
period_start DATE NOT NULL,
|
||||||
|
period_end DATE NOT NULL,
|
||||||
|
week_id INTEGER NOT NULL REFERENCES core.payroll_weeks(id) ON DELETE CASCADE,
|
||||||
|
tenant_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE (tenant_id, period_end)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_core_destajo_periods_tenant ON core.destajo_periods(tenant_id);
|
||||||
|
|
||||||
|
--changeset panel:core-001y-destajo-jobs endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='destajo_jobs'
|
||||||
|
CREATE TABLE core.destajo_jobs (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
project_id INTEGER NOT NULL REFERENCES core.projects(id),
|
||||||
|
worker_id INTEGER NOT NULL REFERENCES core.workers(id),
|
||||||
|
concepto TEXT NOT NULL,
|
||||||
|
unit_code TEXT NOT NULL,
|
||||||
|
qty_total_estimated NUMERIC(14,4) NOT NULL,
|
||||||
|
unit_price NUMERIC(14,4) NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'done')),
|
||||||
|
tenant_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_core_destajo_jobs_tenant ON core.destajo_jobs(tenant_id);
|
||||||
|
|
||||||
|
--changeset panel:core-001z-destajo-cut-lines endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='destajo_cut_lines'
|
||||||
|
CREATE TABLE core.destajo_cut_lines (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
period_id INTEGER NOT NULL REFERENCES core.destajo_periods(id) ON DELETE CASCADE,
|
||||||
|
job_id INTEGER NOT NULL REFERENCES core.destajo_jobs(id) ON DELETE CASCADE,
|
||||||
|
qty_planned NUMERIC(14,4) NOT NULL DEFAULT 0,
|
||||||
|
qty_actual NUMERIC(14,4) NOT NULL DEFAULT 0,
|
||||||
|
qty_extra NUMERIC(14,4) NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE (period_id, job_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001aa-loan-payments endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='loan_payments'
|
||||||
|
CREATE TABLE core.loan_payments (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
loan_id INTEGER NOT NULL REFERENCES core.loans(id) ON DELETE CASCADE,
|
||||||
|
week_id INTEGER NOT NULL REFERENCES core.payroll_weeks(id),
|
||||||
|
amount NUMERIC(12,2) NOT NULL,
|
||||||
|
installment_n INTEGER NOT NULL DEFAULT 1,
|
||||||
|
label TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001ab-budget-chapters endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='budget_chapters'
|
||||||
|
CREATE TABLE core.budget_chapters (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
project_id INTEGER NOT NULL REFERENCES core.projects(id) ON DELETE CASCADE,
|
||||||
|
parent_id INTEGER REFERENCES core.budget_chapters(id) ON DELETE SET NULL,
|
||||||
|
code TEXT NOT NULL DEFAULT '',
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
wbs TEXT NOT NULL DEFAULT '',
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:core-001ac-budget-items endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='budget_items'
|
||||||
|
CREATE TABLE core.budget_items (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
project_id INTEGER NOT NULL REFERENCES core.projects(id) ON DELETE CASCADE,
|
||||||
|
chapter_id INTEGER REFERENCES core.budget_chapters(id) ON DELETE SET NULL,
|
||||||
|
code TEXT NOT NULL DEFAULT '',
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
unit TEXT NOT NULL DEFAULT '',
|
||||||
|
quantity NUMERIC(14,4) NOT NULL DEFAULT 0,
|
||||||
|
unit_price NUMERIC(14,4) NOT NULL DEFAULT 0,
|
||||||
|
amount NUMERIC(14,2) NOT NULL DEFAULT 0,
|
||||||
|
wbs TEXT NOT NULL DEFAULT '',
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
17
db/core/changesets/002-tenant-settings.sql
Normal file
17
db/core/changesets/002-tenant-settings.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
--liquibase formatted sql
|
||||||
|
-- PANELS · panels_product.core (Postgres) · Fase 4d
|
||||||
|
--
|
||||||
|
-- Zona horaria configurable por tenant. Reemplaza el PAYROLL_TZ hardcodeado
|
||||||
|
-- de api/payroll.ts ("America/Cancun"). Se separa de core.payroll_settings
|
||||||
|
-- a propósito: la zona horaria la usan también document_validity.ts y
|
||||||
|
-- cualquier validación de fecha del sistema, no solo nómina.
|
||||||
|
|
||||||
|
--changeset panel:core-002a-tenant-settings endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='core' AND table_name='tenant_settings'
|
||||||
|
CREATE TABLE core.tenant_settings (
|
||||||
|
tenant_id INTEGER PRIMARY KEY,
|
||||||
|
timezone TEXT NOT NULL DEFAULT 'America/Mexico_City',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
73
db/core/changesets/003-seed-catalogs.sql
Normal file
73
db/core/changesets/003-seed-catalogs.sql
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
--liquibase formatted sql
|
||||||
|
-- PANELS · panels_product.core (Postgres) · catálogos requeridos
|
||||||
|
--
|
||||||
|
-- A diferencia de los datos de demo (changeset siguiente, context:dev),
|
||||||
|
-- esto NO es data de ejemplo: la app asume que estos catálogos existen en
|
||||||
|
-- cualquier ambiente (referencian risk_code/theme_id/type_code desde
|
||||||
|
-- workers/projects/documents). Por eso corre sin context, en todos lados.
|
||||||
|
|
||||||
|
--changeset panel:core-003a-risk-levels endDelimiter:; splitStatements:true
|
||||||
|
INSERT INTO core.risk_levels (code, label, color, text_color) VALUES
|
||||||
|
('rojo', 'Rojo', '#A20000', '#FFFFFF'),
|
||||||
|
('amarillo', 'Amarillo', '#EEEE3C', '#000000'),
|
||||||
|
('azul', 'Azul', '#001485', '#FFFFFF'),
|
||||||
|
('verde', 'Verde', '#008514', '#FFFFFF'),
|
||||||
|
('negro', 'Negro', '#000000', '#FFFFFF'),
|
||||||
|
('naranja', 'Naranja', '#FF5733', '#FFFFFF')
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
|
--changeset panel:core-003b-badge-themes endDelimiter:; splitStatements:true
|
||||||
|
INSERT INTO core.badge_themes (id, name, layout) VALUES
|
||||||
|
('arctec-dos-logos-fold', 'Arctec dos logos (doblez carta)', 'letter-landscape-4-fold')
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
--changeset panel:core-003c-document-types endDelimiter:; splitStatements:true
|
||||||
|
INSERT INTO core.document_types (code, label, required, category, validity_mode, freshness_days, requires_issued_at, requires_expires_at) VALUES
|
||||||
|
('foto', 'Foto', true, 'identidad', 'none', NULL, false, false),
|
||||||
|
('ine', 'INE', true, 'identidad', 'expiry', NULL, false, true),
|
||||||
|
('curp', 'Constancia CURP', true, 'identidad', 'freshness', 90, true, false),
|
||||||
|
('nss', 'Constancia NSS', true, 'identidad', 'freshness', 90, true, false),
|
||||||
|
('rfc', 'Constancia RFC', true, 'identidad', 'freshness', 90, true, false),
|
||||||
|
('domicilio', 'Comprobante de domicilio', true, 'identidad', 'freshness', 90, true, false),
|
||||||
|
('alta_imss', 'Alta IMSS', false, 'imss', 'none', NULL, false, false),
|
||||||
|
('baja_imss', 'Baja IMSS', false, 'imss', 'none', NULL, false, false),
|
||||||
|
('contrato', 'Contrato laboral', false, 'laboral', 'none', NULL, false, false),
|
||||||
|
('aviso_privacidad', 'Aviso de privacidad', false, 'laboral', 'none', NULL, false, false),
|
||||||
|
('dc3_nom031', 'DC-3 NOM-031 (construcción)', false, 'sst', 'expiry', NULL, false, false),
|
||||||
|
('epp', 'Acuse de entrega de EPP', false, 'sst', 'none', NULL, false, false),
|
||||||
|
('medico', 'Certificado médico', false, 'sst', 'expiry', NULL, false, false),
|
||||||
|
('dc3_altura', 'DC-3 trabajo en alturas', false, 'oficio', 'expiry', NULL, false, false),
|
||||||
|
('dc3_soldadura', 'DC-3 soldadura', false, 'oficio', 'expiry', NULL, false, false),
|
||||||
|
('dc3_montacargas', 'DC-3 montacargas', false, 'oficio', 'expiry', NULL, false, false),
|
||||||
|
('licencia', 'Licencia de conducir', false, 'oficio', 'expiry', NULL, false, false),
|
||||||
|
('cedula', 'Cédula profesional', false, 'oficio', 'none', NULL, false, false),
|
||||||
|
('otro', 'Otro', false, 'otro', 'none', NULL, false, false)
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
|
--changeset panel:core-003d-project-document-types endDelimiter:; splitStatements:true
|
||||||
|
INSERT INTO core.project_document_types (code, label, required, category) VALUES
|
||||||
|
('contrato', 'Contrato', true, 'contrato'),
|
||||||
|
('presupuesto', 'Presupuesto', true, 'contrato'),
|
||||||
|
('planos', 'Planos', false, 'tecnico'),
|
||||||
|
('licencia_construccion', 'Licencia de construcción', false, 'permisos'),
|
||||||
|
('uso_suelo', 'Uso de suelo', false, 'permisos'),
|
||||||
|
('proteccion_civil', 'Anuencia de Protección Civil', false, 'permisos'),
|
||||||
|
('mia', 'MIA / resolutivo ambiental', false, 'ambiental'),
|
||||||
|
('siroc_acuse', 'Acuse SIROC', false, 'imss'),
|
||||||
|
('programa_sst', 'Programa de seguridad', false, 'sst'),
|
||||||
|
('seguro_obra', 'Seguro de obra', false, 'contrato'),
|
||||||
|
('fianza', 'Fianza', false, 'contrato'),
|
||||||
|
('otro', 'Otro', false, 'otro')
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
|
--changeset panel:core-003e-company-document-types endDelimiter:; splitStatements:true
|
||||||
|
INSERT INTO core.company_document_types (code, label, required, category) VALUES
|
||||||
|
('repse_federal', 'REPSE federal (STPS)', false, 'repse'),
|
||||||
|
('repse_estatal', 'REPSE estatal (SATQ)', false, 'repse'),
|
||||||
|
('opinion_sat', 'Opinión de cumplimiento SAT (32-D)', false, 'fiscal'),
|
||||||
|
('opinion_imss', 'Opinión de cumplimiento IMSS', false, 'fiscal'),
|
||||||
|
('opinion_infonavit', 'Opinión de cumplimiento INFONAVIT', false, 'fiscal'),
|
||||||
|
('acta_constitutiva', 'Acta constitutiva', false, 'legal'),
|
||||||
|
('poder_notarial', 'Poder notarial', false, 'legal'),
|
||||||
|
('otro', 'Otro', false, 'otro')
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
31
db/core/changesets/004-seed-dev-data.sql
Normal file
31
db/core/changesets/004-seed-dev-data.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
--liquibase formatted sql
|
||||||
|
-- PANELS · panels_product.core (Postgres) · datos de demostración
|
||||||
|
--
|
||||||
|
-- Solo para dev/pruebas -- nunca staging/producción. tenant_id=1 asume el
|
||||||
|
-- mismo id fijo que db/platform/changesets/001-baseline.sql (changeset
|
||||||
|
-- platform-001d) le da a ARCT2608; no hay FK real entre bases que lo
|
||||||
|
-- garantice, así que si cambia uno hay que cambiar el otro.
|
||||||
|
|
||||||
|
--changeset panel:core-004a-seed-companies context:dev endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM core.companies WHERE code = 'ARCT2608'
|
||||||
|
INSERT INTO core.companies (code, name, parent_id, kind, tenant_id, nombre_comercial, razon_social)
|
||||||
|
VALUES ('ARCT2608', 'ARCTEC', NULL, 'principal', 1, 'ARCTEC', 'ARCTEC');
|
||||||
|
|
||||||
|
INSERT INTO core.companies (code, name, parent_id, kind, tenant_id, nombre_comercial, razon_social)
|
||||||
|
SELECT 'FISICA', 'FISICA', id, 'sub', 1, 'FISICA', 'FISICA' FROM core.companies WHERE code = 'ARCT2608';
|
||||||
|
|
||||||
|
INSERT INTO core.companies (code, name, parent_id, kind, tenant_id, nombre_comercial, razon_social)
|
||||||
|
SELECT 'ARCOTEC', 'ARCOTEC', id, 'sub', 1, 'ARCOTEC', 'ARCOTEC' FROM core.companies WHERE code = 'ARCT2608';
|
||||||
|
|
||||||
|
--changeset panel:core-004b-seed-project context:dev endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM core.projects WHERE code = 'PRY-0001'
|
||||||
|
INSERT INTO core.projects (code, name, address, theme_id, company_id, tenant_id)
|
||||||
|
SELECT 'PRY-0001', 'ZENDALA CANCUN', '', 'arctec-dos-logos-fold', id, 1
|
||||||
|
FROM core.companies WHERE code = 'ARCT2608';
|
||||||
|
|
||||||
|
--changeset panel:core-004c-seed-tenant-settings context:dev endDelimiter:;
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM core.tenant_settings WHERE tenant_id = 1
|
||||||
|
INSERT INTO core.tenant_settings (tenant_id, timezone) VALUES (1, 'America/Cancun');
|
||||||
71
db/core/changesets/005-rls.sql
Normal file
71
db/core/changesets/005-rls.sql
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
--liquibase formatted sql
|
||||||
|
-- PANELS · panels_product.core (Postgres) · Row Level Security (Fase 4b)
|
||||||
|
--
|
||||||
|
-- Mismo mecanismo que iam (ver db/iam/changesets/002-rls.sql): el rol de
|
||||||
|
-- runtime (panels_core_app) no es dueño de las tablas, así que
|
||||||
|
-- ENABLE ROW LEVEL SECURITY ya lo restringe sin afectar a panels_core_owner
|
||||||
|
-- (que sí necesita ver todos los tenants para migrar/hacer backfills).
|
||||||
|
--
|
||||||
|
-- Alcance: se aplica a las tablas con columna tenant_id directa --
|
||||||
|
-- companies, workers, projects, payroll_weeks, destajo_units/periods/jobs.
|
||||||
|
-- Tablas hijas sin tenant_id propio (assignments, documents, budget_items,
|
||||||
|
-- loans, attendance, badge_jobs, payroll_week_lines, etc.) dependen del
|
||||||
|
-- scoping corregido en la capa de aplicación (Fase 3, cierre del IDOR) via
|
||||||
|
-- su FK a projects/workers -- extender RLS a esas tablas con políticas por
|
||||||
|
-- subquery es un endurecimiento futuro válido, no bloqueante para esta fase.
|
||||||
|
|
||||||
|
--changeset panel:core-005a-rls-companies endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pg_policies WHERE schemaname='core' AND tablename='companies' AND policyname='tenant_isolation'
|
||||||
|
ALTER TABLE core.companies ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY tenant_isolation ON core.companies
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
||||||
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
||||||
|
|
||||||
|
--changeset panel:core-005b-rls-workers endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pg_policies WHERE schemaname='core' AND tablename='workers' AND policyname='tenant_isolation'
|
||||||
|
ALTER TABLE core.workers ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY tenant_isolation ON core.workers
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
||||||
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
||||||
|
|
||||||
|
--changeset panel:core-005c-rls-projects endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pg_policies WHERE schemaname='core' AND tablename='projects' AND policyname='tenant_isolation'
|
||||||
|
ALTER TABLE core.projects ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY tenant_isolation ON core.projects
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
||||||
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
||||||
|
|
||||||
|
--changeset panel:core-005d-rls-payroll-weeks endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pg_policies WHERE schemaname='core' AND tablename='payroll_weeks' AND policyname='tenant_isolation'
|
||||||
|
ALTER TABLE core.payroll_weeks ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY tenant_isolation ON core.payroll_weeks
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
||||||
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
||||||
|
|
||||||
|
--changeset panel:core-005e-rls-destajo-units endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pg_policies WHERE schemaname='core' AND tablename='destajo_units' AND policyname='tenant_isolation'
|
||||||
|
ALTER TABLE core.destajo_units ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY tenant_isolation ON core.destajo_units
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
||||||
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
||||||
|
|
||||||
|
--changeset panel:core-005f-rls-destajo-periods endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pg_policies WHERE schemaname='core' AND tablename='destajo_periods' AND policyname='tenant_isolation'
|
||||||
|
ALTER TABLE core.destajo_periods ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY tenant_isolation ON core.destajo_periods
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
||||||
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
||||||
|
|
||||||
|
--changeset panel:core-005g-rls-destajo-jobs endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pg_policies WHERE schemaname='core' AND tablename='destajo_jobs' AND policyname='tenant_isolation'
|
||||||
|
ALTER TABLE core.destajo_jobs ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY tenant_isolation ON core.destajo_jobs
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
||||||
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
||||||
9
db/core/liquibase.properties
Normal file
9
db/core/liquibase.properties
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# PANELS · Liquibase · panels_product, esquema core (Postgres)
|
||||||
|
#
|
||||||
|
# url/username/password se pasan por variable de entorno o -D en la línea
|
||||||
|
# de comandos (ver db/update.sh); no se commitean credenciales aquí.
|
||||||
|
changelogFile=changelog-master.xml
|
||||||
|
liquibaseSchemaName=core
|
||||||
|
defaultSchemaName=core
|
||||||
|
driver=org.postgresql.Driver
|
||||||
|
classpath=../tools/postgresql-jdbc.jar
|
||||||
12
db/iam/changelog-master.xml
Normal file
12
db/iam/changelog-master.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
|
||||||
|
|
||||||
|
<!-- panels_product, esquema iam: identidad/roles/permisos del tenant. -->
|
||||||
|
<include file="changesets/001-baseline.sql" relativeToChangelogFile="true"/>
|
||||||
|
<include file="changesets/002-rls.sql" relativeToChangelogFile="true"/>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
92
db/iam/changesets/001-baseline.sql
Normal file
92
db/iam/changesets/001-baseline.sql
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
--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.
|
||||||
28
db/iam/changesets/002-rls.sql
Normal file
28
db/iam/changesets/002-rls.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
--liquibase formatted sql
|
||||||
|
-- PANELS · panels_product.iam (Postgres) · Row Level Security (Fase 4b)
|
||||||
|
--
|
||||||
|
-- Defensa en profundidad contra el bug de IDOR cross-tenant encontrado en
|
||||||
|
-- la revisión de seguridad: aunque un handler olvide el WHERE tenant_id=?,
|
||||||
|
-- Postgres ya no devuelve/permite escribir filas de otro tenant.
|
||||||
|
--
|
||||||
|
-- Cómo se activa: el rol de runtime (panels_iam_app) NO es dueño de la
|
||||||
|
-- tabla (panels_iam_owner sí lo es), así que ENABLE ROW LEVEL SECURITY ya
|
||||||
|
-- restringe automáticamente a panels_iam_app sin necesitar FORCE (FORCE
|
||||||
|
-- haría que la política también aplique al dueño, lo cual rompería las
|
||||||
|
-- migraciones/backfills que sí necesitan ver todos los tenants).
|
||||||
|
--
|
||||||
|
-- El valor de comparación viene de un parámetro de sesión que la API fija
|
||||||
|
-- al inicio de cada transacción: SET LOCAL app.tenant_id = '<id>'. Si el
|
||||||
|
-- código olvida fijarlo, current_setting(...) devuelve NULL y la
|
||||||
|
-- comparación "tenant_id = NULL" es siempre falsa -- es decir, "fail
|
||||||
|
-- closed": sin tenant_id explícito no se ve ni se escribe ninguna fila,
|
||||||
|
-- en vez de exponer todo por defecto.
|
||||||
|
|
||||||
|
--changeset panel:iam-002a-rls-users endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pg_policies WHERE schemaname='iam' AND tablename='users' AND policyname='tenant_isolation'
|
||||||
|
ALTER TABLE iam.users ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY tenant_isolation ON iam.users
|
||||||
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
||||||
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
||||||
11
db/iam/liquibase.properties
Normal file
11
db/iam/liquibase.properties
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# PANELS · Liquibase · panels_product, esquema iam (Postgres)
|
||||||
|
#
|
||||||
|
# url/username/password se pasan por variable de entorno o -D en la línea
|
||||||
|
# de comandos (ver db/update.sh); no se commitean credenciales aquí.
|
||||||
|
# liquibaseSchemaName controla dónde vive el DATABASECHANGELOG de este
|
||||||
|
# módulo -- separado del de `core`, aunque compartan la misma base de datos.
|
||||||
|
changelogFile=changelog-master.xml
|
||||||
|
liquibaseSchemaName=iam
|
||||||
|
defaultSchemaName=iam
|
||||||
|
driver=org.postgresql.Driver
|
||||||
|
classpath=../tools/postgresql-jdbc.jar
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<databaseChangeLog
|
<databaseChangeLog
|
||||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.20.xsd">
|
||||||
|
|
||||||
|
<!-- panels_platform: control plane SaaS. Un solo esquema (public). -->
|
||||||
|
<include file="changesets/001-baseline.sql" relativeToChangelogFile="true"/>
|
||||||
|
|
||||||
<include file="changesets/001-init.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/002-seed-arctec-tenant.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/003-tenant-license.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/004-tenant-status-expand.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/005-contact-access.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/006-tenant-code-date.sql" relativeToChangelogFile="true"/>
|
|
||||||
<include file="changesets/007-smtp-settings.sql" relativeToChangelogFile="true"/>
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
||||||
66
db/platform/changesets/001-baseline.sql
Normal file
66
db/platform/changesets/001-baseline.sql
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
--liquibase formatted sql
|
||||||
|
-- PANELS · panels_platform (Postgres) · baseline
|
||||||
|
--
|
||||||
|
-- Control plane SaaS: tenants, operadores de PANELS (platform_users) y
|
||||||
|
-- configuración SMTP. Base de datos físicamente separada de panels_product
|
||||||
|
-- (ver plan de migración) -- nada aquí se referencia desde iam/core con FK.
|
||||||
|
--
|
||||||
|
-- Este baseline representa el ESTADO FINAL equivalente a los changesets
|
||||||
|
-- SQLite 001..007 de la implementación anterior, no un replay literal
|
||||||
|
-- (varios de esos changesets eran parches específicos de SQLite, p. ej.
|
||||||
|
-- el rebuild de tabla en 004-tenant-status-expand.sql para ampliar un CHECK).
|
||||||
|
|
||||||
|
--changeset panel:platform-001a-tenants endDelimiter:;
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='tenants'
|
||||||
|
CREATE TABLE tenants (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
code TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'suspendido', 'cancelado')),
|
||||||
|
plan TEXT NOT NULL DEFAULT 'trial',
|
||||||
|
valid_until DATE,
|
||||||
|
notes TEXT NOT NULL DEFAULT '',
|
||||||
|
contact_email TEXT NOT NULL DEFAULT '',
|
||||||
|
contact_name TEXT NOT NULL DEFAULT '',
|
||||||
|
access_sent_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:platform-001b-platform-users endDelimiter:;
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='platform_users'
|
||||||
|
CREATE TABLE platform_users (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'inactivo')),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
--changeset panel:platform-001c-smtp-settings endDelimiter:; splitStatements:true
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='smtp_settings'
|
||||||
|
CREATE TABLE smtp_settings (
|
||||||
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
|
host TEXT NOT NULL DEFAULT '',
|
||||||
|
port INTEGER NOT NULL DEFAULT 587,
|
||||||
|
username TEXT NOT NULL DEFAULT '',
|
||||||
|
password TEXT NOT NULL DEFAULT '',
|
||||||
|
from_address TEXT NOT NULL DEFAULT '',
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO smtp_settings (id, host, port, username, password, from_address, enabled)
|
||||||
|
VALUES (1, '', 587, '', '', '', false);
|
||||||
|
|
||||||
|
--changeset panel:platform-001d-seed-arctec-tenant context:dev endDelimiter:; splitStatements:true
|
||||||
|
--comment: Datos de demostración -- nunca en staging/producción. id=1 fijo a
|
||||||
|
-- propósito: los changesets de dev-seed de core/iam asumen este mismo id
|
||||||
|
-- para tenant_id (no hay FK real entre bases, así que se sincroniza a mano).
|
||||||
|
--preconditions onFail:MARK_RAN
|
||||||
|
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM tenants WHERE code = 'ARCT2608'
|
||||||
|
INSERT INTO tenants (id, code, name, status) OVERRIDING SYSTEM VALUE VALUES (1, 'ARCT2608', 'ARCTEC', 'activo');
|
||||||
|
SELECT setval(pg_get_serial_sequence('tenants', 'id'), (SELECT MAX(id) FROM tenants));
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:platform-001-init endDelimiter:; splitStatements:true
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tenants (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
code TEXT NOT NULL UNIQUE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'inactivo')),
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS platform_users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'inactivo')),
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:platform-002-seed-arctec endDelimiter:; splitStatements:true
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM tenants WHERE code = 'ARCTEC'
|
|
||||||
|
|
||||||
INSERT INTO tenants (id, code, name, status) VALUES (1, 'ARCTEC', 'ARCTEC', 'activo');
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:platform-003a-plan endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('tenants') WHERE name='plan'
|
|
||||||
ALTER TABLE tenants ADD COLUMN plan TEXT NOT NULL DEFAULT 'trial';
|
|
||||||
|
|
||||||
--changeset panel:platform-003b-valid-until endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('tenants') WHERE name='valid_until'
|
|
||||||
ALTER TABLE tenants ADD COLUMN valid_until TEXT;
|
|
||||||
|
|
||||||
--changeset panel:platform-003c-notes endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('tenants') WHERE name='notes'
|
|
||||||
ALTER TABLE tenants ADD COLUMN notes TEXT NOT NULL DEFAULT '';
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:platform-004-status-expand endDelimiter:; splitStatements:true
|
|
||||||
--comment: Expand tenant status to activo|suspendido|cancelado (SQLite rebuild)
|
|
||||||
|
|
||||||
CREATE TABLE tenants_new (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
code TEXT NOT NULL UNIQUE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'activo' CHECK (status IN ('activo', 'suspendido', 'cancelado')),
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
||||||
plan TEXT NOT NULL DEFAULT 'trial',
|
|
||||||
valid_until TEXT,
|
|
||||||
notes TEXT NOT NULL DEFAULT ''
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tenants_new (id, code, name, status, created_at, plan, valid_until, notes)
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
code,
|
|
||||||
name,
|
|
||||||
CASE WHEN status = 'inactivo' THEN 'suspendido' ELSE status END,
|
|
||||||
created_at,
|
|
||||||
COALESCE(plan, 'trial'),
|
|
||||||
valid_until,
|
|
||||||
COALESCE(notes, '')
|
|
||||||
FROM tenants;
|
|
||||||
|
|
||||||
DROP TABLE tenants;
|
|
||||||
ALTER TABLE tenants_new RENAME TO tenants;
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:platform-005a-contact-email endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('tenants') WHERE name='contact_email'
|
|
||||||
ALTER TABLE tenants ADD COLUMN contact_email TEXT NOT NULL DEFAULT '';
|
|
||||||
|
|
||||||
--changeset panel:platform-005b-contact-name endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('tenants') WHERE name='contact_name'
|
|
||||||
ALTER TABLE tenants ADD COLUMN contact_name TEXT NOT NULL DEFAULT '';
|
|
||||||
|
|
||||||
--changeset panel:platform-005c-access-sent-at endDelimiter:;
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM pragma_table_info('tenants') WHERE name='access_sent_at'
|
|
||||||
ALTER TABLE tenants ADD COLUMN access_sent_at TEXT;
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:platform-006-tenant-code-date endDelimiter:; splitStatements:true
|
|
||||||
--preconditions onFail:MARK_RAN
|
|
||||||
--precondition-sql-check expectedResult:1 SELECT COUNT(*) FROM tenants WHERE code = 'ARCTEC'
|
|
||||||
|
|
||||||
UPDATE tenants SET code = 'ARCT2608' WHERE code = 'ARCTEC';
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
--liquibase formatted sql
|
|
||||||
|
|
||||||
--changeset panel:platform-007-smtp-settings endDelimiter:; splitStatements:true
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS smtp_settings (
|
|
||||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
||||||
host TEXT NOT NULL DEFAULT '',
|
|
||||||
port INTEGER NOT NULL DEFAULT 587,
|
|
||||||
username TEXT NOT NULL DEFAULT '',
|
|
||||||
password TEXT NOT NULL DEFAULT '',
|
|
||||||
from_address TEXT NOT NULL DEFAULT '',
|
|
||||||
enabled INTEGER NOT NULL DEFAULT 0,
|
|
||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO smtp_settings (id, host, port, username, password, from_address, enabled)
|
|
||||||
SELECT 1, '', 587, '', '', '', 0
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM smtp_settings WHERE id = 1);
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
changeLogFile=changelog-master.xml
|
# PANELS · Liquibase · panels_platform (Postgres)
|
||||||
driver=org.sqlite.JDBC
|
#
|
||||||
url=jdbc:sqlite:../../data/platform.db
|
# url/username/password se pasan por variable de entorno o -D en la línea
|
||||||
classpath=../tools/sqlite-jdbc.jar
|
# de comandos (ver db/update.sh); no se commitean credenciales aquí.
|
||||||
|
changelogFile=changelog-master.xml
|
||||||
|
liquibaseSchemaName=public
|
||||||
|
driver=org.postgresql.Driver
|
||||||
|
classpath=../tools/postgresql-jdbc.jar
|
||||||
|
|
|
||||||
51
db/provision/01-roles.sql
Normal file
51
db/provision/01-roles.sql
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
-- PANELS · Postgres · Fase 0 (aprovisionamiento de roles)
|
||||||
|
--
|
||||||
|
-- Crea los 6 roles cluster-wide usados por PANELS: dos por módulo
|
||||||
|
-- (platform, iam, core), un rol "_owner" para Liquibase/migraciones y un
|
||||||
|
-- rol "_app" para runtime con privilegios acotados a su esquema/base.
|
||||||
|
--
|
||||||
|
-- Idempotente vía el idiom \gexec (solo emite el CREATE ROLE si todavía
|
||||||
|
-- no existe). Nota: la interpolación de variables de psql (:'var') NO
|
||||||
|
-- funciona dentro de bloques DO $$...$$ (dollar-quoting), por eso este
|
||||||
|
-- script arma el DDL con format(...)+\gexec en vez de un DO block.
|
||||||
|
--
|
||||||
|
-- Uso (contra un Postgres nuevo, como superuser/admin de Coolify). Los
|
||||||
|
-- valores se pasan SIN comillas propias; :'var' ya produce un literal SQL
|
||||||
|
-- correctamente escapado:
|
||||||
|
-- psql "$SUPERUSER_URL" \
|
||||||
|
-- -v platform_owner_pw="CAMBIA-ESTA-CLAVE-1" \
|
||||||
|
-- -v platform_app_pw="CAMBIA-ESTA-CLAVE-2" \
|
||||||
|
-- -v iam_owner_pw="CAMBIA-ESTA-CLAVE-3" \
|
||||||
|
-- -v iam_app_pw="CAMBIA-ESTA-CLAVE-4" \
|
||||||
|
-- -v core_owner_pw="CAMBIA-ESTA-CLAVE-5" \
|
||||||
|
-- -v core_app_pw="CAMBIA-ESTA-CLAVE-6" \
|
||||||
|
-- -f 01-roles.sql
|
||||||
|
--
|
||||||
|
-- Genera claves fuertes por ambiente con: openssl rand -hex 24
|
||||||
|
|
||||||
|
SELECT format('CREATE ROLE panels_platform_owner LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE', :'platform_owner_pw')
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'panels_platform_owner')\gexec
|
||||||
|
|
||||||
|
SELECT format('CREATE ROLE panels_platform_app LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE', :'platform_app_pw')
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'panels_platform_app')\gexec
|
||||||
|
|
||||||
|
SELECT format('CREATE ROLE panels_iam_owner LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE', :'iam_owner_pw')
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'panels_iam_owner')\gexec
|
||||||
|
|
||||||
|
SELECT format('CREATE ROLE panels_iam_app LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE', :'iam_app_pw')
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'panels_iam_app')\gexec
|
||||||
|
|
||||||
|
SELECT format('CREATE ROLE panels_core_owner LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE', :'core_owner_pw')
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'panels_core_owner')\gexec
|
||||||
|
|
||||||
|
SELECT format('CREATE ROLE panels_core_app LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE', :'core_app_pw')
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'panels_core_app')\gexec
|
||||||
|
|
||||||
|
-- Si el rol ya existía de una corrida anterior, refresca su password al
|
||||||
|
-- valor actual (para poder rotar credenciales corriendo este script de nuevo).
|
||||||
|
ALTER ROLE panels_platform_owner PASSWORD :'platform_owner_pw';
|
||||||
|
ALTER ROLE panels_platform_app PASSWORD :'platform_app_pw';
|
||||||
|
ALTER ROLE panels_iam_owner PASSWORD :'iam_owner_pw';
|
||||||
|
ALTER ROLE panels_iam_app PASSWORD :'iam_app_pw';
|
||||||
|
ALTER ROLE panels_core_owner PASSWORD :'core_owner_pw';
|
||||||
|
ALTER ROLE panels_core_app PASSWORD :'core_app_pw';
|
||||||
33
db/provision/02-databases.sql
Normal file
33
db/provision/02-databases.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
-- PANELS · Postgres · Fase 0 (aprovisionamiento de bases)
|
||||||
|
--
|
||||||
|
-- Crea las dos bases de datos físicamente separadas del monolito modular:
|
||||||
|
-- panels_platform -> control plane SaaS (tenants, platform_users, smtp_settings)
|
||||||
|
-- panels_product -> producto que usan los tenants (esquemas iam/core dentro)
|
||||||
|
--
|
||||||
|
-- Requiere haber corrido antes 01-roles.sql. Idempotente vía el idiom
|
||||||
|
-- \gexec (solo emite el CREATE DATABASE si todavía no existe).
|
||||||
|
--
|
||||||
|
-- Uso: psql "$SUPERUSER_URL" -f 02-databases.sql
|
||||||
|
|
||||||
|
SELECT 'CREATE DATABASE panels_platform OWNER panels_platform_owner'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'panels_platform')\gexec
|
||||||
|
|
||||||
|
SELECT 'CREATE DATABASE panels_product OWNER panels_iam_owner'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'panels_product')\gexec
|
||||||
|
|
||||||
|
-- panels_core_owner también necesita poder crear su esquema dentro de panels_product,
|
||||||
|
-- aunque el owner nominal de la base sea panels_iam_owner.
|
||||||
|
GRANT CREATE ON DATABASE panels_product TO panels_core_owner;
|
||||||
|
REVOKE CREATE ON DATABASE panels_product FROM PUBLIC;
|
||||||
|
REVOKE CREATE ON DATABASE panels_platform FROM PUBLIC;
|
||||||
|
|
||||||
|
-- Postgres otorga CONNECT a PUBLIC por defecto en toda base nueva: sin este
|
||||||
|
-- REVOKE, panels_platform_app podría autenticarse contra panels_product (o
|
||||||
|
-- viceversa) aunque no tenga privilegios dentro -- justo el cruce que este
|
||||||
|
-- diseño busca evitar. Se revoca de PUBLIC y se otorga solo a los roles que
|
||||||
|
-- corresponden a cada base.
|
||||||
|
REVOKE CONNECT ON DATABASE panels_platform FROM PUBLIC;
|
||||||
|
GRANT CONNECT ON DATABASE panels_platform TO panels_platform_owner, panels_platform_app;
|
||||||
|
|
||||||
|
REVOKE CONNECT ON DATABASE panels_product FROM PUBLIC;
|
||||||
|
GRANT CONNECT ON DATABASE panels_product TO panels_iam_owner, panels_iam_app, panels_core_owner, panels_core_app;
|
||||||
25
db/provision/03-platform-database.sql
Normal file
25
db/provision/03-platform-database.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
-- PANELS · Postgres · Fase 0 (privilegios dentro de panels_platform)
|
||||||
|
--
|
||||||
|
-- Correr conectado A LA BASE panels_platform (no a "postgres"), como
|
||||||
|
-- superuser/admin: psql "$SUPERUSER_URL/panels_platform" -f 03-platform-database.sql
|
||||||
|
--
|
||||||
|
-- panels_platform es de un solo esquema (public) porque es un control
|
||||||
|
-- plane simple (tenants, platform_users, smtp_settings) usado solo por
|
||||||
|
-- las rutas /v1/saas/*. No se subdivide en más esquemas.
|
||||||
|
|
||||||
|
REVOKE ALL ON SCHEMA public FROM PUBLIC;
|
||||||
|
|
||||||
|
GRANT USAGE, CREATE ON SCHEMA public TO panels_platform_owner;
|
||||||
|
GRANT USAGE ON SCHEMA public TO panels_platform_app;
|
||||||
|
|
||||||
|
-- Privilegios por defecto para tablas/secuencias que cree panels_platform_owner
|
||||||
|
-- (vía Liquibase) a partir de ahora: panels_platform_app solo puede hacer DML,
|
||||||
|
-- nunca DDL.
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE panels_platform_owner IN SCHEMA public
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO panels_platform_app;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE panels_platform_owner IN SCHEMA public
|
||||||
|
GRANT USAGE, SELECT ON SEQUENCES TO panels_platform_app;
|
||||||
|
|
||||||
|
-- Objetos que ya existan (si esto corre después de la primera migración de Liquibase)
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO panels_platform_app;
|
||||||
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO panels_platform_app;
|
||||||
54
db/provision/04-product-database.sql
Normal file
54
db/provision/04-product-database.sql
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
-- PANELS · Postgres · Fase 0 (esquemas y privilegios dentro de panels_product)
|
||||||
|
--
|
||||||
|
-- Correr conectado A LA BASE panels_product, como superuser/admin:
|
||||||
|
-- psql "$SUPERUSER_URL/panels_product" -f 04-product-database.sql
|
||||||
|
--
|
||||||
|
-- Crea los esquemas iam/core, cada uno con su propio dueño (_owner, usado
|
||||||
|
-- por Liquibase) y su propio rol de runtime (_app, solo DML). Se revoca
|
||||||
|
-- explícitamente cualquier acceso cruzado entre iam y core: son dos
|
||||||
|
-- módulos del monolito modular que NO deben poder leerse entre sí por SQL.
|
||||||
|
|
||||||
|
REVOKE ALL ON SCHEMA public FROM PUBLIC;
|
||||||
|
|
||||||
|
CREATE SCHEMA IF NOT EXISTS iam AUTHORIZATION panels_iam_owner;
|
||||||
|
CREATE SCHEMA IF NOT EXISTS core AUTHORIZATION panels_core_owner;
|
||||||
|
|
||||||
|
-- citext (comparación case-insensitive real para curp/rfc, reemplaza el
|
||||||
|
-- COLLATE NOCASE de SQLite) se instala DENTRO del esquema core (no en
|
||||||
|
-- public, que queda sin USAGE para nadie salvo el propio superuser) --
|
||||||
|
-- así el tipo es resoluble por panels_core_owner/panels_core_app con su
|
||||||
|
-- search_path=core, sin tener que abrirles el esquema public.
|
||||||
|
CREATE EXTENSION IF NOT EXISTS citext SCHEMA core;
|
||||||
|
|
||||||
|
-- ===== iam =====
|
||||||
|
GRANT USAGE ON SCHEMA iam TO panels_iam_app;
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE panels_iam_owner IN SCHEMA iam
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO panels_iam_app;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE panels_iam_owner IN SCHEMA iam
|
||||||
|
GRANT USAGE, SELECT ON SEQUENCES TO panels_iam_app;
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA iam TO panels_iam_app;
|
||||||
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA iam TO panels_iam_app;
|
||||||
|
|
||||||
|
ALTER ROLE panels_iam_owner IN DATABASE panels_product SET search_path = iam;
|
||||||
|
ALTER ROLE panels_iam_app IN DATABASE panels_product SET search_path = iam;
|
||||||
|
|
||||||
|
-- ===== core =====
|
||||||
|
GRANT USAGE ON SCHEMA core TO panels_core_app;
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE panels_core_owner IN SCHEMA core
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO panels_core_app;
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE panels_core_owner IN SCHEMA core
|
||||||
|
GRANT USAGE, SELECT ON SEQUENCES TO panels_core_app;
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA core TO panels_core_app;
|
||||||
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA core TO panels_core_app;
|
||||||
|
|
||||||
|
ALTER ROLE panels_core_owner IN DATABASE panels_product SET search_path = core;
|
||||||
|
ALTER ROLE panels_core_app IN DATABASE panels_product SET search_path = core;
|
||||||
|
|
||||||
|
-- ===== Aislamiento cruzado explícito (defensa en profundidad) =====
|
||||||
|
-- Nadie del lado "core" puede tocar "iam" y viceversa, ni por accidente.
|
||||||
|
REVOKE ALL ON SCHEMA iam FROM panels_core_app, panels_core_owner, PUBLIC;
|
||||||
|
REVOKE ALL ON SCHEMA core FROM panels_iam_app, panels_iam_owner, PUBLIC;
|
||||||
64
db/provision/05-redis-acl.sh
Executable file
64
db/provision/05-redis-acl.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# PANELS · Redis · Fase 0 (usuarios ACL por módulo)
|
||||||
|
#
|
||||||
|
# Crea dos usuarios ACL (Redis 6+) con acceso restringido por patrón de
|
||||||
|
# llave: panels_iam_redis (~iam:*) y panels_core_redis (~core:*). Un
|
||||||
|
# prefijo de llave por sí solo NO aísla nada — cualquier cliente con la
|
||||||
|
# misma contraseña puede escanear todo el keyspace; el aislamiento real
|
||||||
|
# lo da el ACL server-side, no la convención de nombres.
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# REDIS_ADMIN_URL="redis://:admin-password@host:6379" \
|
||||||
|
# IAM_REDIS_PASSWORD="$(openssl rand -base64 32)" \
|
||||||
|
# CORE_REDIS_PASSWORD="$(openssl rand -base64 32)" \
|
||||||
|
# ./05-redis-acl.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${REDIS_ADMIN_URL:?Define REDIS_ADMIN_URL (redis://[:password@]host:port)}"
|
||||||
|
: "${IAM_REDIS_PASSWORD:?Define IAM_REDIS_PASSWORD}"
|
||||||
|
: "${CORE_REDIS_PASSWORD:?Define CORE_REDIS_PASSWORD}"
|
||||||
|
|
||||||
|
# Categorías necesarias para sesiones (hash/string + TTL) y cache
|
||||||
|
# (string/hash/set): keyspace cubre DEL/EXPIRE/EXISTS/TTL/SCAN (el patrón de
|
||||||
|
# llave sigue acotando el alcance real). Sin @scripting (no se usa Lua/EVAL,
|
||||||
|
# menor superficie de ataque) ni @admin/@dangerous.
|
||||||
|
ACL_COMMANDS="-@all +@read +@write +@keyspace +@string +@hash +@set +@sortedset +@connection"
|
||||||
|
|
||||||
|
# Importante: "reset" debe ir PRIMERO (deja al usuario en blanco); si va
|
||||||
|
# después de "on >password" borra la contraseña que se acaba de fijar.
|
||||||
|
echo "Creando usuario panels_iam_redis (~iam:*)..."
|
||||||
|
redis-cli -u "$REDIS_ADMIN_URL" ACL SETUSER panels_iam_redis \
|
||||||
|
reset \
|
||||||
|
on ">${IAM_REDIS_PASSWORD}" \
|
||||||
|
'~iam:*' '&iam:*' \
|
||||||
|
$ACL_COMMANDS
|
||||||
|
|
||||||
|
echo "Creando usuario panels_core_redis (~core:*)..."
|
||||||
|
redis-cli -u "$REDIS_ADMIN_URL" ACL SETUSER panels_core_redis \
|
||||||
|
reset \
|
||||||
|
on ">${CORE_REDIS_PASSWORD}" \
|
||||||
|
'~core:*' '&core:*' \
|
||||||
|
$ACL_COMMANDS
|
||||||
|
|
||||||
|
redis-cli -u "$REDIS_ADMIN_URL" ACL SAVE >/dev/null 2>&1 || {
|
||||||
|
echo "Aviso: ACL SAVE falló (normal si no hay aclfile configurado; revisar persistencia de ACLs por separado)."
|
||||||
|
}
|
||||||
|
|
||||||
|
HOST_PORT=$(echo "$REDIS_ADMIN_URL" | sed -E 's#^redis://##; s#^[^@]*@##')
|
||||||
|
|
||||||
|
echo "Verificando aislamiento (se espera NOPERM al cruzar de modulo)..."
|
||||||
|
IAM_PROBE=$(redis-cli -u "redis://panels_iam_redis:${IAM_REDIS_PASSWORD}@${HOST_PORT}" GET core:probe 2>&1 || true)
|
||||||
|
if echo "$IAM_PROBE" | grep -qi "NOPERM"; then
|
||||||
|
echo "OK: panels_iam_redis no puede leer llaves core:*"
|
||||||
|
else
|
||||||
|
echo "ADVERTENCIA: no se confirmó NOPERM al probar panels_iam_redis -> core:*. Salida: $IAM_PROBE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
CORE_PROBE=$(redis-cli -u "redis://panels_core_redis:${CORE_REDIS_PASSWORD}@${HOST_PORT}" GET iam:probe 2>&1 || true)
|
||||||
|
if echo "$CORE_PROBE" | grep -qi "NOPERM"; then
|
||||||
|
echo "OK: panels_core_redis no puede leer llaves iam:*"
|
||||||
|
else
|
||||||
|
echo "ADVERTENCIA: no se confirmó NOPERM al probar panels_core_redis -> iam:*. Salida: $CORE_PROBE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Listo. Guarda IAM_REDIS_PASSWORD/CORE_REDIS_PASSWORD como secrets (REDIS_URL_IAM/REDIS_URL_CORE)."
|
||||||
80
db/provision/README.md
Normal file
80
db/provision/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Aprovisionamiento de infraestructura (Fase 0)
|
||||||
|
|
||||||
|
Scripts para dejar Postgres y Redis listos, en cualquier ambiente (dev local,
|
||||||
|
staging o producción en Coolify), antes de correr Liquibase o levantar la API.
|
||||||
|
Ver el plan de migración para el diseño completo: dos bases de datos
|
||||||
|
(`panels_platform`, `panels_product`) y, dentro de `panels_product`, dos
|
||||||
|
esquemas (`iam`, `core`).
|
||||||
|
|
||||||
|
## Orden de ejecución (Postgres)
|
||||||
|
|
||||||
|
Contra el servidor Postgres del ambiente (uno por dev/staging/producción),
|
||||||
|
como usuario superusuario/admin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SUPERUSER_URL="postgresql://postgres:adminpass@HOST:5432/postgres"
|
||||||
|
|
||||||
|
# 1. Roles cluster-wide (genera una clave distinta por rol y por ambiente:
|
||||||
|
# openssl rand -hex 24). Pasar los valores SIN comillas propias -- el
|
||||||
|
# script usa :'var' internamente para escaparlos como literal SQL.
|
||||||
|
psql "$SUPERUSER_URL" \
|
||||||
|
-v platform_owner_pw="..." -v platform_app_pw="..." \
|
||||||
|
-v iam_owner_pw="..." -v iam_app_pw="..." \
|
||||||
|
-v core_owner_pw="..." -v core_app_pw="..." \
|
||||||
|
-f 01-roles.sql
|
||||||
|
|
||||||
|
# 2. Bases de datos (panels_platform, panels_product)
|
||||||
|
psql "$SUPERUSER_URL" -f 02-databases.sql
|
||||||
|
|
||||||
|
# 3. Privilegios dentro de panels_platform
|
||||||
|
psql "${SUPERUSER_URL%/postgres}/panels_platform" -f 03-platform-database.sql
|
||||||
|
|
||||||
|
# 4. Esquemas + privilegios dentro de panels_product
|
||||||
|
psql "${SUPERUSER_URL%/postgres}/panels_product" -f 04-product-database.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Las conexiones que usará la API (secrets por ambiente):
|
||||||
|
|
||||||
|
| Secret | Apunta a | Usuario |
|
||||||
|
|---|---|---|
|
||||||
|
| `DATABASE_URL_PLATFORM` | `panels_platform` | `panels_platform_app` |
|
||||||
|
| `DATABASE_URL_IAM` | `panels_product` (search_path=iam) | `panels_iam_app` |
|
||||||
|
| `DATABASE_URL_CORE` | `panels_product` (search_path=core) | `panels_core_app` |
|
||||||
|
|
||||||
|
Liquibase (paso de deploy, no en runtime) usa los roles `_owner`:
|
||||||
|
`DATABASE_URL_PLATFORM_OWNER`, `DATABASE_URL_IAM_OWNER`, `DATABASE_URL_CORE_OWNER`.
|
||||||
|
|
||||||
|
## Redis
|
||||||
|
|
||||||
|
```bash
|
||||||
|
REDIS_ADMIN_URL="redis://:adminpass@HOST:6379" \
|
||||||
|
IAM_REDIS_PASSWORD="$(openssl rand -base64 32)" \
|
||||||
|
CORE_REDIS_PASSWORD="$(openssl rand -base64 32)" \
|
||||||
|
./05-redis-acl.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Secrets resultantes: `REDIS_URL_IAM` (`redis://panels_iam_redis:<pw>@HOST:6379`),
|
||||||
|
`REDIS_URL_CORE` (`redis://panels_core_redis:<pw>@HOST:6379`).
|
||||||
|
|
||||||
|
El script verifica automáticamente que cruzar de módulo devuelva `NOPERM`
|
||||||
|
(ver "Riesgos y mitigaciones" del plan: un prefijo de llave sin ACL detrás
|
||||||
|
no aísla nada).
|
||||||
|
|
||||||
|
## Qué falta hacer manualmente en Coolify (staging/producción)
|
||||||
|
|
||||||
|
Estos scripts asumen que ya existe un servidor Postgres y un servidor Redis
|
||||||
|
accesibles (el recurso gestionado de Coolify por ambiente). El agente no
|
||||||
|
tiene acceso a la cuenta de Coolify del usuario, así que:
|
||||||
|
|
||||||
|
1. Crear el recurso Postgres gestionado en Coolify para el ambiente.
|
||||||
|
2. Crear el recurso Redis gestionado en Coolify para el ambiente.
|
||||||
|
3. Correr estos scripts contra ambos usando las credenciales admin que da Coolify.
|
||||||
|
4. Cargar los secrets resultantes (`DATABASE_URL_*`, `REDIS_URL_*`) en la
|
||||||
|
configuración del servicio `api` de ese ambiente.
|
||||||
|
|
||||||
|
## Desarrollo local
|
||||||
|
|
||||||
|
`db/provision/dev-local.sh` reproduce estos mismos pasos contra un Postgres
|
||||||
|
y Redis instalados en la máquina de desarrollo (sin Coolify), generando
|
||||||
|
`.env.dev-local` con las URLs resultantes — útil para correr la API y los
|
||||||
|
tests sin depender de infraestructura externa.
|
||||||
62
db/provision/dev-local.sh
Executable file
62
db/provision/dev-local.sh
Executable file
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# PANELS · Fase 0 · aprovisiona Postgres + Redis LOCALES (dev), reproduciendo
|
||||||
|
# la misma topología de roles/esquemas/ACLs que se usará en Coolify.
|
||||||
|
#
|
||||||
|
# Requiere: un Postgres corriendo en localhost:5432 accesible como `postgres`
|
||||||
|
# vía socket unix (auth peer, el default de un `apt install postgresql`), y
|
||||||
|
# un Redis en localhost:6379.
|
||||||
|
#
|
||||||
|
# Uso: ./db/provision/dev-local.sh
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
PG_SUPERUSER_CMD="sudo -u postgres psql -v ON_ERROR_STOP=1"
|
||||||
|
PGHOST=127.0.0.1
|
||||||
|
PGPORT=5432
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
gen_pw() { openssl rand -hex 24; }
|
||||||
|
|
||||||
|
PLATFORM_OWNER_PW=$(gen_pw); PLATFORM_APP_PW=$(gen_pw)
|
||||||
|
IAM_OWNER_PW=$(gen_pw); IAM_APP_PW=$(gen_pw)
|
||||||
|
CORE_OWNER_PW=$(gen_pw); CORE_APP_PW=$(gen_pw)
|
||||||
|
IAM_REDIS_PW=$(gen_pw); CORE_REDIS_PW=$(gen_pw)
|
||||||
|
|
||||||
|
echo "== 1/5: roles =="
|
||||||
|
$PG_SUPERUSER_CMD \
|
||||||
|
-v platform_owner_pw="${PLATFORM_OWNER_PW}" -v platform_app_pw="${PLATFORM_APP_PW}" \
|
||||||
|
-v iam_owner_pw="${IAM_OWNER_PW}" -v iam_app_pw="${IAM_APP_PW}" \
|
||||||
|
-v core_owner_pw="${CORE_OWNER_PW}" -v core_app_pw="${CORE_APP_PW}" \
|
||||||
|
-f 01-roles.sql
|
||||||
|
|
||||||
|
echo "== 2/5: bases de datos =="
|
||||||
|
$PG_SUPERUSER_CMD -f 02-databases.sql
|
||||||
|
|
||||||
|
echo "== 3/5: privilegios panels_platform =="
|
||||||
|
$PG_SUPERUSER_CMD -d panels_platform -f 03-platform-database.sql
|
||||||
|
|
||||||
|
echo "== 4/5: esquemas + privilegios panels_product =="
|
||||||
|
$PG_SUPERUSER_CMD -d panels_product -f 04-product-database.sql
|
||||||
|
|
||||||
|
echo "== 5/5: Redis ACLs =="
|
||||||
|
REDIS_ADMIN_URL="redis://${REDIS_HOST}:${REDIS_PORT}" \
|
||||||
|
IAM_REDIS_PASSWORD="$IAM_REDIS_PW" \
|
||||||
|
CORE_REDIS_PASSWORD="$CORE_REDIS_PW" \
|
||||||
|
./05-redis-acl.sh
|
||||||
|
|
||||||
|
ENV_FILE="../../.env.dev-local"
|
||||||
|
cat > "$ENV_FILE" <<EOF
|
||||||
|
# Generado por db/provision/dev-local.sh — solo para desarrollo local. No commitear.
|
||||||
|
DATABASE_URL_PLATFORM=postgresql://panels_platform_app:${PLATFORM_APP_PW}@${PGHOST}:${PGPORT}/panels_platform
|
||||||
|
DATABASE_URL_PLATFORM_OWNER=postgresql://panels_platform_owner:${PLATFORM_OWNER_PW}@${PGHOST}:${PGPORT}/panels_platform
|
||||||
|
DATABASE_URL_IAM=postgresql://panels_iam_app:${IAM_APP_PW}@${PGHOST}:${PGPORT}/panels_product
|
||||||
|
DATABASE_URL_IAM_OWNER=postgresql://panels_iam_owner:${IAM_OWNER_PW}@${PGHOST}:${PGPORT}/panels_product
|
||||||
|
DATABASE_URL_CORE=postgresql://panels_core_app:${CORE_APP_PW}@${PGHOST}:${PGPORT}/panels_product
|
||||||
|
DATABASE_URL_CORE_OWNER=postgresql://panels_core_owner:${CORE_OWNER_PW}@${PGHOST}:${PGPORT}/panels_product
|
||||||
|
REDIS_URL_IAM=redis://panels_iam_redis:${IAM_REDIS_PW}@${REDIS_HOST}:${REDIS_PORT}
|
||||||
|
REDIS_URL_CORE=redis://panels_core_redis:${CORE_REDIS_PW}@${REDIS_HOST}:${REDIS_PORT}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Listo. Credenciales de desarrollo local escritas en $(cd "$(dirname "$ENV_FILE")" && pwd)/$(basename "$ENV_FILE")"
|
||||||
67
db/provision/verify-isolation.sh
Executable file
67
db/provision/verify-isolation.sh
Executable file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# PANELS · Fase 0 · verifica que el aislamiento entre módulos realmente
|
||||||
|
# se cumple (no solo que las credenciales existan). Pensado para correr
|
||||||
|
# después de 01-04 (Postgres) y 05 (Redis), en cualquier ambiente.
|
||||||
|
#
|
||||||
|
# Uso: source el .env con DATABASE_URL_*/REDIS_URL_* y correr este script.
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
check() {
|
||||||
|
local desc="$1"; local expect_fail="$2"; shift 2
|
||||||
|
if "$@" >/tmp/verify-out.$$ 2>&1; then
|
||||||
|
ok=1
|
||||||
|
else
|
||||||
|
ok=0
|
||||||
|
fi
|
||||||
|
if [[ "$expect_fail" == "yes" && "$ok" == "0" ]] || [[ "$expect_fail" == "no" && "$ok" == "1" ]]; then
|
||||||
|
echo "OK - $desc"
|
||||||
|
else
|
||||||
|
echo "FAIL - $desc"
|
||||||
|
cat /tmp/verify-out.$$
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
rm -f /tmp/verify-out.$$
|
||||||
|
}
|
||||||
|
|
||||||
|
: "${DATABASE_URL_PLATFORM:?}"; : "${DATABASE_URL_IAM:?}"; : "${DATABASE_URL_CORE:?}"
|
||||||
|
: "${REDIS_URL_IAM:?}"; : "${REDIS_URL_CORE:?}"
|
||||||
|
|
||||||
|
extract_pw() { echo "$1" | sed -E 's#[a-zA-Z]+://[^:]+:([^@]+)@.*#\1#'; }
|
||||||
|
PLATFORM_HOST=$(echo "$DATABASE_URL_PLATFORM" | sed -E 's#.*@([^/]+)/.*#\1#')
|
||||||
|
|
||||||
|
echo "== Postgres: DML normal funciona =="
|
||||||
|
check "iam_app puede hacer SELECT en su esquema" no psql "$DATABASE_URL_IAM" -c "SELECT 1;"
|
||||||
|
check "core_app puede hacer SELECT en su esquema" no psql "$DATABASE_URL_CORE" -c "SELECT 1;"
|
||||||
|
|
||||||
|
echo "== Postgres: aislamiento cruzado =="
|
||||||
|
check "iam_app NO puede leer schema core" yes psql "$DATABASE_URL_IAM" -c "SELECT * FROM core.pg_tables LIMIT 1;"
|
||||||
|
check "core_app NO puede leer schema iam" yes psql "$DATABASE_URL_CORE" -c "SELECT 1 FROM iam.pg_am LIMIT 1;"
|
||||||
|
check "iam_app NO puede hacer DDL (solo _owner puede)" yes psql "$DATABASE_URL_IAM" -c "CREATE TABLE iam.probe_$$(id int);"
|
||||||
|
|
||||||
|
PLATFORM_APP_PW=$(extract_pw "$DATABASE_URL_PLATFORM")
|
||||||
|
IAM_APP_PW=$(extract_pw "$DATABASE_URL_IAM")
|
||||||
|
check "platform_app NO puede CONNECT a panels_product" yes \
|
||||||
|
psql "postgresql://panels_platform_app:${PLATFORM_APP_PW}@${PLATFORM_HOST%/*}/panels_product" -c "SELECT 1;"
|
||||||
|
check "iam_app NO puede CONNECT a panels_platform" yes \
|
||||||
|
psql "postgresql://panels_iam_app:${IAM_APP_PW}@${PLATFORM_HOST%/*}/panels_platform" -c "SELECT 1;"
|
||||||
|
|
||||||
|
echo "== Redis: ACL por módulo =="
|
||||||
|
IAM_REDIS_HOST=$(echo "$REDIS_URL_IAM" | sed -E 's#redis://[^@]+@##')
|
||||||
|
IAM_REDIS_PW=$(extract_pw "$REDIS_URL_IAM")
|
||||||
|
CORE_REDIS_PW=$(extract_pw "$REDIS_URL_CORE")
|
||||||
|
check "panels_iam_redis puede escribir iam:*" no \
|
||||||
|
redis-cli -u "redis://panels_iam_redis:${IAM_REDIS_PW}@${IAM_REDIS_HOST}" SET iam:verify:probe 1
|
||||||
|
check "panels_iam_redis NO puede leer core:* (NOPERM)" no \
|
||||||
|
bash -c "redis-cli -u 'redis://panels_iam_redis:${IAM_REDIS_PW}@${IAM_REDIS_HOST}' GET core:probe 2>&1 | grep -q NOPERM"
|
||||||
|
check "panels_core_redis NO puede leer iam:* (NOPERM)" no \
|
||||||
|
bash -c "redis-cli -u 'redis://panels_core_redis:${CORE_REDIS_PW}@${IAM_REDIS_HOST}' GET iam:probe 2>&1 | grep -q NOPERM"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [[ "$fail" == "0" ]]; then
|
||||||
|
echo "Todas las verificaciones de aislamiento pasaron."
|
||||||
|
else
|
||||||
|
echo "Hay verificaciones fallidas -- revisar antes de continuar."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
60
db/update.sh
60
db/update.sh
|
|
@ -1,38 +1,72 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# PANELS · aplica los changelogs de Liquibase contra Postgres.
|
||||||
|
#
|
||||||
|
# Requiere las credenciales del rol "_owner" de cada módulo (nunca el rol
|
||||||
|
# "_app" -- ese es solo para runtime, sin privilegios de DDL). Se leen de
|
||||||
|
# variables de entorno con el mismo nombre que usan los secrets de la app:
|
||||||
|
# DATABASE_URL_PLATFORM_OWNER (jdbc: panels_platform)
|
||||||
|
# DATABASE_URL_IAM_OWNER (jdbc: panels_product, schema iam)
|
||||||
|
# DATABASE_URL_CORE_OWNER (jdbc: panels_product, schema core)
|
||||||
|
#
|
||||||
|
# Formato esperado: postgresql://usuario:password@host:puerto/basededatos
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# set -a && source .env.dev-local && set +a && ./db/update.sh all
|
||||||
|
# ./db/update.sh core --context-filter=dev
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
"$ROOT/db/bootstrap-tools.sh"
|
"$ROOT/db/bootstrap-tools.sh"
|
||||||
LIQUIBASE="$ROOT/db/tools/liquibase/liquibase"
|
LIQUIBASE="$ROOT/db/tools/liquibase/liquibase"
|
||||||
SQLITE_JDBC="$ROOT/db/tools/sqlite-jdbc.jar"
|
PG_JDBC="$ROOT/db/tools/postgresql-jdbc.jar"
|
||||||
|
|
||||||
mkdir -p "$ROOT/data"
|
|
||||||
|
|
||||||
TARGET="${1:-all}"
|
TARGET="${1:-all}"
|
||||||
|
shift || true
|
||||||
|
EXTRA_ARGS=("$@")
|
||||||
|
|
||||||
|
# postgresql://user:pass@host:port/db -> jdbc:postgresql://host:port/db + user/pass
|
||||||
|
parse_url() {
|
||||||
|
local url="$1"
|
||||||
|
local rest="${url#postgresql://}"
|
||||||
|
local userinfo="${rest%%@*}"
|
||||||
|
local hostpart="${rest#*@}"
|
||||||
|
PARSED_USER="${userinfo%%:*}"
|
||||||
|
PARSED_PASS="${userinfo#*:}"
|
||||||
|
PARSED_JDBC="jdbc:postgresql://${hostpart}"
|
||||||
|
}
|
||||||
|
|
||||||
run_one() {
|
run_one() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local dir="$ROOT/db/$name"
|
local var="DATABASE_URL_${name^^}_OWNER"
|
||||||
local db="$ROOT/data/${name}.db"
|
local url="${!var:-}"
|
||||||
echo "==> Liquibase update: $name ($db)"
|
if [[ -z "$url" ]]; then
|
||||||
|
echo "Falta la variable de entorno $var (credenciales del rol _owner de $name)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
parse_url "$url"
|
||||||
|
echo "==> Liquibase update: $name ($PARSED_JDBC)"
|
||||||
(
|
(
|
||||||
cd "$dir"
|
cd "$ROOT/db/$name"
|
||||||
"$LIQUIBASE" \
|
"$LIQUIBASE" \
|
||||||
--defaultsFile=liquibase.properties \
|
--defaultsFile=liquibase.properties \
|
||||||
--classpath="$SQLITE_JDBC" \
|
--classpath="$PG_JDBC" \
|
||||||
--url="jdbc:sqlite:$db" \
|
--url="$PARSED_JDBC" \
|
||||||
update
|
--username="$PARSED_USER" \
|
||||||
|
--password="$PARSED_PASS" \
|
||||||
|
update "${EXTRA_ARGS[@]}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$TARGET" in
|
case "$TARGET" in
|
||||||
app) run_one app ;;
|
|
||||||
platform) run_one platform ;;
|
platform) run_one platform ;;
|
||||||
|
iam) run_one iam ;;
|
||||||
|
core) run_one core ;;
|
||||||
all)
|
all)
|
||||||
run_one app
|
|
||||||
run_one platform
|
run_one platform
|
||||||
|
run_one iam
|
||||||
|
run_one core
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 [all|app|platform]" >&2
|
echo "Usage: $0 [all|platform|iam|core] [-- liquibase-args...]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue