Compare commits
3 Commits
dda4b5320a
...
b3f039fbd5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3f039fbd5 | ||
|
|
430bf7a5da | ||
|
|
10bb7299a2 |
17
TECH_DEBT.md
17
TECH_DEBT.md
@@ -77,14 +77,15 @@
|
||||
|
||||
## [TECH-DEBT-008] /admin/responses sin autenticación
|
||||
|
||||
- **Estado:** ABIERTO.
|
||||
- **Descripción:** la vista de control `/admin/responses` no tiene auth. Por diseño
|
||||
explícito, excluye preguntas `is_sensitive=true` (14 preguntas de salud/
|
||||
discapacidad) y `text_long`. Para mostrar esas preguntas en la vista, primero
|
||||
implementar un gate de autenticación para staff (Basic Auth con env var como
|
||||
mínimo viable).
|
||||
- **Impacto actual:** ninguno en producción — la URL no está publicada.
|
||||
- **Bloqueante para:** ampliar visibilidad de datos en la vista de control.
|
||||
- **Estado:** RESUELTO (27 jun 2026, ETAPA_AUTH).
|
||||
- **Descripción:** la vista de control `/admin/responses` no tenía auth. Se implementó
|
||||
login con Supabase Auth (`@supabase/ssr`), `middleware.ts` protegiendo
|
||||
`/admin/:path*` y `/dashboard/:path*` con redirect a `/login` sin sesión, y la página
|
||||
migrada de `service_role` a la sesión del usuario (`anon` key + RLS). Sigue
|
||||
excluyendo preguntas `is_sensitive=true` y `text_long` — ese diseño no cambió.
|
||||
- **Nota:** la vista no distingue roles dentro de staff todavía (todo usuario con fila
|
||||
en `user_roles` ve lo mismo). Diferenciar `platform_admin` de `org_admin` ahí sería
|
||||
una etapa nueva, no parte de este cierre.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -964,7 +964,6 @@ body {
|
||||
/* ── Card destacada (estrella) ── */
|
||||
.db-widget-star {
|
||||
border-color: var(--color-re-teal);
|
||||
background: linear-gradient(135deg, #dff4f5, #ffffff);
|
||||
}
|
||||
.db-widget-star .db-widget-title { color: var(--color-re-teal); }
|
||||
|
||||
@@ -1093,6 +1092,26 @@ body {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.login-error { margin: 0; }
|
||||
.login-password-wrapper { position: relative; }
|
||||
.login-password-wrapper .text-short-input { padding-right: 40px; }
|
||||
.login-password-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.login-password-toggle:hover { color: var(--color-text); }
|
||||
.login-submit {
|
||||
margin-top: 8px;
|
||||
padding: 10px 20px;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation'
|
||||
export default function LoginPage() {
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const supabase = createBrowserClient(
|
||||
@@ -54,15 +55,36 @@ export default function LoginPage() {
|
||||
</div>
|
||||
<div className="login-field">
|
||||
<label htmlFor="password" className="login-label">Contraseña</label>
|
||||
<div className="login-password-wrapper">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
disabled={isPending}
|
||||
className="text-short-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="login-password-toggle"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
aria-label={showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
||||
>
|
||||
{showPassword ? (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<p role="alert" className="field-error login-error">{error}</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ESTADO DEL PROYECTO — Datos País sobre el Cuidado
|
||||
## Documento de continuidad — 25 junio 2026
|
||||
## Documento de continuidad — 27 junio 2026
|
||||
|
||||
> Director (Product Owner): Marcos · Claude (Projects) = arquitecto/copiloto · Claude Code = implementación
|
||||
> Metodología: Maria Mersan · Contacto Fundación: Valeria Arce
|
||||
@@ -7,17 +7,14 @@
|
||||
---
|
||||
|
||||
## 1. Versionado y ramas
|
||||
- **Producción:** 2.1.0 (formulario lineal) — los 3 surveys en `draft`
|
||||
- **Desarrollo:** 3.0.0 (wizard + dashboard nuevo) en `main`, NO desplegado aún
|
||||
- **Producción:** 3.0.0 (wizard + dashboard tres miradas) — tag `v3.0.0` pusheado, los 3 surveys
|
||||
en `live`. Release v3 completado (etapa 4I).
|
||||
- **Preservación:** `v2.1.0-stable` + tag `v2.1.0`
|
||||
- SemVer + CHANGELOG + tags. ADRs en `docs/adr/`.
|
||||
|
||||
⚠️ Los 3 surveys están en `draft` a propósito (el frontend v2 desplegado no entiende seed v3).
|
||||
Se reactivan a `live` al desplegar v3 completo.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sprint 4 — Wizard v3 — Formulario COMPLETO, falta release
|
||||
## 2. Sprint 4 — Wizard v3 — COMPLETO, release hecho
|
||||
|
||||
| Etapa | Estado |
|
||||
|---|---|
|
||||
@@ -28,26 +25,41 @@ Se reactivan a `live` al desplegar v3 completo.
|
||||
| 4D wizard renderer (layout estable) | ✅ |
|
||||
| 4E instancias (emergente del motor) | ✅ |
|
||||
| 4G submit + validación server-side (RPC transaccional) | ✅ verificado vs Postgres real |
|
||||
| 4F autocomplete barrios (UX) | ⏳ pendiente |
|
||||
| 4I validación e2e + tag v3.0.0 + desplegar + reactivar surveys | ⏳ pendiente |
|
||||
| 4F autocomplete barrios (UX) | ✅ |
|
||||
| Widgets Mirada Empresa (frontend) | ✅ |
|
||||
| Tema claro dashboard (marca Re) | ✅ |
|
||||
| 4I validación e2e + tag v3.0.0 + desplegar + reactivar surveys | ✅ |
|
||||
| ETAPA_AUTH — login/middleware/sign-out para `/admin` y `/dashboard` | ✅ (27 jun) |
|
||||
|
||||
El formulario v3 funciona end-to-end: motor, wizard, instancias, submit con validación
|
||||
server-side (RPC `rpc_submit_response`, único camino de escritura, verificado).
|
||||
server-side (RPC `rpc_submit_response`, único camino de escritura, verificado). Dashboard
|
||||
Mirada Empresa con sus 13 widgets, consumiendo las RPCs `rpc_me_*` reales. `/admin/responses`
|
||||
y `/dashboard` ahora requieren sesión autenticada (antes sin auth — cierra TECH-DEBT-008).
|
||||
|
||||
---
|
||||
|
||||
## 3. Dashboard v3 — DECISIÓN MAYOR: rediseño de "tres miradas"
|
||||
## 2.1 Incidente VPS (27 jun) — recuperado
|
||||
|
||||
Una segunda instancia de Dokploy desplegada desde el mismo repo (con `container_name` fijos en
|
||||
docker-compose.yml) colisionó con la producción y vació el esquema `public` de `supabase-db`.
|
||||
**Sin pérdida de datos reales** (solo seed demo). Se restauró: migraciones completas + seed +
|
||||
surveys reactivados a `live` + usuario `platform_admin` creado. Verificado end-to-end: login,
|
||||
sign-out, redirect sin sesión, formulario público (3 links `?s=<survey_id>`) — todo OK.
|
||||
Detalle completo en memoria `project_vps_container_name_incident`. El director ya separó la
|
||||
segunda instancia a un repo Gitea propio sin `container_name` fijos, para que no se repita.
|
||||
|
||||
---
|
||||
|
||||
## 3. Dashboard v3 — "tres miradas"
|
||||
|
||||
**Decisión:** NO migrar las RPCs v2 (referencian códigos inexistentes en v3). En su lugar,
|
||||
dashboard nuevo de tres miradas, fundamentado en marcos reales (OIT 5R, CEPAL, PNCUPA Paraguay).
|
||||
|
||||
### Estado
|
||||
- **Mirada Empresa:** ✅ RPCs implementadas y VERIFICADAS contra 50 respuestas reales.
|
||||
E1-E4 exactos, estrella "Talento en riesgo" con umbral parametrizable, E9 segmentación
|
||||
con k≥5. Migraciones `20260625000001` (fix helper) + `20260625000002` (11 RPCs). Commiteado.
|
||||
Widgets del frontend: PENDIENTE (etapa siguiente).
|
||||
- **Mirada Empresa:** ✅ RPCs + widgets de frontend completos y en producción. E1-E10 + estrella
|
||||
"Talento en riesgo", k≥5 en segmentación. Tema claro aplicado.
|
||||
- **Mirada País y Mirada Humana:** propuesta + mockup listos, implementación DIFERIDA a
|
||||
sprint de dashboard post-deadline.
|
||||
sprint de dashboard post-deadline (sin cambios desde el último estado).
|
||||
|
||||
### Documentos (en docs/dashboard/)
|
||||
- `PROPUESTA_DASHBOARD_v3.md` — las 3 miradas, ~30 métricas, marcos citados honestamente
|
||||
@@ -65,16 +77,25 @@ dashboard nuevo de tres miradas, fundamentado en marcos reales (OIT 5R, CEPAL, P
|
||||
---
|
||||
|
||||
## 4. Deuda técnica y seguridad
|
||||
|
||||
~~TECH-DEBT-008 (`/admin/responses` sin autenticación)~~ — **resuelto** por ETAPA_AUTH (27 jun).
|
||||
|
||||
| ID | Descripción | Prioridad |
|
||||
|---|---|---|
|
||||
| TECH-DEBT-006 | Backup remoto S3 | Antes de datos reales |
|
||||
| TECH-DEBT-009 | Rotar secrets post-CVE | Antes de datos reales |
|
||||
| TECH-DEBT-010 | E9 lee modalidad por JOIN a answers (2.7) en vez de qi_modalidad. Promover si se usa seguido | Baja |
|
||||
| SEC-001 | Next.js — vuln "Cache Key Confusion" vigente en 15.3.8; última mayor es 16.2.9 (breaking) | Media |
|
||||
| TECH-001 | `npm audit`: 4 vulns (1 moderate, 3 high) — `form-data`, `next`, `undici` | Pendiente revisión |
|
||||
| SEC-002 | Sprint compliance (audit log, hashing, cifrado en reposo) | Sprint dedicado |
|
||||
| SEC-003 | Agujero TRUNCATE evade ADR-003 | Antes de datos reales |
|
||||
| SEC-004 | RPC validable por bypass REST (coherencia de flujo en Server Action) | Sprint futuro |
|
||||
| BACKLOG-002 | Fase 2: motor genérico | Post-deadline |
|
||||
|
||||
*(SEC-001/TECH-001 verificados 27 jun vía `npm view next version` / `npm audit --json` —
|
||||
los números en BACKLOG.md/TECH_DEBT.md estaban desactualizados, ej. citaban Next 15.3.3
|
||||
y "2 vulnerabilidades" cuando ya corre 15.3.8 con 4.)*
|
||||
|
||||
---
|
||||
|
||||
## 5. Infraestructura
|
||||
@@ -87,14 +108,19 @@ dashboard nuevo de tres miradas, fundamentado en marcos reales (OIT 5R, CEPAL, P
|
||||
---
|
||||
|
||||
## 6. Próximos pasos
|
||||
1. **Widgets de Mirada Empresa** — el frontend del dashboard que consume las RPCs ya verificadas.
|
||||
La spec (SPEC_MIRADA_EMPRESA.md) define shapes de retorno; el mockup define el aspecto.
|
||||
2. **4F autocomplete barrios** — subir `geo_paraguay_full.json` (8152 barrios) a public/data/.
|
||||
3. **4I release** — validación e2e, tag v3.0.0, desplegar v3, reactivar surveys a live.
|
||||
4. **Sprint de dashboard (post-deadline):** Miradas País y Humana, preguntas faltantes
|
||||
(bienestar, horas no remuneradas, escolarización), decisión qi_modalidad.
|
||||
|
||||
Orden sugerido para go-live: widgets Mirada Empresa → 4F → 4I.
|
||||
**Go-live (widgets → 4F → 4I) está cerrado.** v3.0.0 en producción, surveys `live`, auth para
|
||||
staff funcionando. No queda ninguna etapa de Sprint 4 pendiente — el roadmap pre-escrito se
|
||||
agotó. El próximo paso requiere decisión del director entre:
|
||||
|
||||
1. **Sprint de dashboard (post-deadline):** Miradas País y Humana, preguntas faltantes
|
||||
(bienestar, horas no remuneradas, escolarización), decisión qi_modalidad (TECH-DEBT-010).
|
||||
2. **Sprint de compliance/seguridad (SEC-002):** audit log append-only, hashing de integridad,
|
||||
cifrado en reposo — bloquea escalar a más empresas/datos reales.
|
||||
3. **Mantenimiento de dependencias:** SEC-001 (Next.js) + TECH-001 (`npm audit`, 4 vulns).
|
||||
4. Otro ítem de BACKLOG.md/TECH_DEBT.md no listado arriba.
|
||||
|
||||
Ninguno de estos se decide unilateralmente (Nivel 3 — tocan planificación/seguridad).
|
||||
|
||||
---
|
||||
|
||||
|
||||
190
supabase/migrations/20260627000002_audit_log.sql
Normal file
190
supabase/migrations/20260627000002_audit_log.sql
Normal file
@@ -0,0 +1,190 @@
|
||||
-- ============================================================
|
||||
-- Migration: audit_log append-only — SEC-002 (Sprint 5, compliance)
|
||||
--
|
||||
-- Loggea cambios de configuración crítica:
|
||||
-- - user_roles: altas/bajas/cambios de app_role u org_id (quién es admin)
|
||||
-- - surveys.status: activación/desactivación de encuestas
|
||||
-- - DDL en schema public: toda migración aplicada (CREATE/ALTER/DROP),
|
||||
-- vía event trigger ddl_command_end. Confirmado viable en este Postgres
|
||||
-- self-hosted sin extensiones adicionales: el rol `postgres` tiene
|
||||
-- rolsuper=false pero pudo crear el event trigger sin error
|
||||
-- (diagnóstico ETAPA_AUDIT_LOG_PROMPT.md §4.1, 2026-06-27).
|
||||
--
|
||||
-- NO loggea acceso a datos sensibles (is_sensitive=true): hoy ningún
|
||||
-- camino de código (ni /admin/responses ni las RPCs rpc_me_*) lee esas
|
||||
-- columnas (COMPLIANCE.md §3.2). Deferido a cuando ese código exista —
|
||||
-- ese mismo cambio debe agregar su propio log de acceso, no esta migración.
|
||||
--
|
||||
-- GAP CONOCIDO (SEC-003, aceptado, NO resuelto aquí): un operador con
|
||||
-- acceso directo a `psql -U postgres` en el VPS puede TRUNCATE/DROP esta
|
||||
-- tabla, o saltear los triggers con `SET session_replication_role =
|
||||
-- replica` / `ALTER TABLE ... DISABLE TRIGGER ALL`. El append-only de
|
||||
-- abajo cierra la escritura vía API (anon/authenticated/PostgREST) y
|
||||
-- bloquea UPDATE/DELETE para cualquier caller normal (incluido
|
||||
-- service_role, que bypasa RLS pero no bypasa triggers) — no protege
|
||||
-- contra un operador con shell en la base.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE public.audit_log (
|
||||
id bigserial PRIMARY KEY,
|
||||
occurred_at timestamptz NOT NULL DEFAULT now(),
|
||||
actor_role text,
|
||||
actor_user_id uuid,
|
||||
action text NOT NULL,
|
||||
target_table text NOT NULL,
|
||||
target_id text,
|
||||
detail jsonb
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.audit_log IS
|
||||
'Append-only. INSERT solo vía funciones SECURITY DEFINER de los triggers de esta migración. UPDATE/DELETE bloqueados por trigger audit_log_block_mutation. Gap conocido: no protege contra TRUNCATE/DROP por superusuario directo en el VPS (SEC-003).';
|
||||
|
||||
COMMENT ON COLUMN public.audit_log.actor_role IS
|
||||
'current_app_role() del que disparó el cambio (user_roles/surveys), o session_user para eventos DDL.';
|
||||
|
||||
ALTER TABLE public.audit_log ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Solo platform_admin puede leer. No hay policies de INSERT/UPDATE/DELETE
|
||||
-- para ningún rol vía API — el único camino de escritura son las
|
||||
-- funciones SECURITY DEFINER (corren como propietario de la tabla,
|
||||
-- `postgres`, que por default de Postgres bypasa RLS sobre objetos
|
||||
-- propios sin necesitar FORCE ROW LEVEL SECURITY).
|
||||
CREATE POLICY "audit_log_select_platform_admin"
|
||||
ON public.audit_log FOR SELECT
|
||||
TO authenticated
|
||||
USING (public.current_app_role() = 'platform_admin');
|
||||
|
||||
REVOKE INSERT, UPDATE, DELETE ON public.audit_log FROM PUBLIC, anon, authenticated;
|
||||
GRANT SELECT ON public.audit_log TO authenticated;
|
||||
|
||||
-- ============================================================
|
||||
-- Barrera 2: append-only enforcement a nivel de trigger.
|
||||
-- Se aplica a CUALQUIER caller (incluido service_role) salvo que alguien
|
||||
-- deshabilite triggers con privilegios de superusuario real en el VPS
|
||||
-- (gap documentado arriba, SEC-003).
|
||||
-- ============================================================
|
||||
CREATE OR REPLACE FUNCTION public.audit_log_block_mutation()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'audit_log es append-only: % no permitido', TG_OP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.audit_log_block_mutation() FROM PUBLIC;
|
||||
|
||||
CREATE TRIGGER audit_log_no_update
|
||||
BEFORE UPDATE ON public.audit_log
|
||||
FOR EACH ROW EXECUTE FUNCTION public.audit_log_block_mutation();
|
||||
|
||||
CREATE TRIGGER audit_log_no_delete
|
||||
BEFORE DELETE ON public.audit_log
|
||||
FOR EACH ROW EXECUTE FUNCTION public.audit_log_block_mutation();
|
||||
|
||||
-- ============================================================
|
||||
-- Trigger 1: cambios en user_roles (altas/bajas/cambios de acceso)
|
||||
-- Función disparada automáticamente por el trigger — no requiere GRANT
|
||||
-- EXECUTE a ningún rol (el mecanismo de triggers no pasa por una llamada
|
||||
-- SQL directa del caller).
|
||||
-- ============================================================
|
||||
CREATE OR REPLACE FUNCTION public.log_user_roles_change()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.audit_log (actor_role, actor_user_id, action, target_table, target_id, detail)
|
||||
VALUES (
|
||||
public.current_app_role(),
|
||||
auth.uid(),
|
||||
'user_roles_' || lower(TG_OP),
|
||||
'user_roles',
|
||||
COALESCE(NEW.user_id, OLD.user_id)::text,
|
||||
jsonb_build_object(
|
||||
'old', CASE WHEN TG_OP IN ('UPDATE','DELETE') THEN to_jsonb(OLD) ELSE NULL END,
|
||||
'new', CASE WHEN TG_OP IN ('UPDATE','INSERT') THEN to_jsonb(NEW) ELSE NULL END
|
||||
)
|
||||
);
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.log_user_roles_change() FROM PUBLIC;
|
||||
|
||||
CREATE TRIGGER user_roles_audit
|
||||
AFTER INSERT OR UPDATE OR DELETE ON public.user_roles
|
||||
FOR EACH ROW EXECUTE FUNCTION public.log_user_roles_change();
|
||||
|
||||
-- ============================================================
|
||||
-- Trigger 2: cambios de status en surveys (activación/desactivación)
|
||||
-- ============================================================
|
||||
CREATE OR REPLACE FUNCTION public.log_survey_status_change()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NEW.status IS DISTINCT FROM OLD.status THEN
|
||||
INSERT INTO public.audit_log (actor_role, actor_user_id, action, target_table, target_id, detail)
|
||||
VALUES (
|
||||
public.current_app_role(),
|
||||
auth.uid(),
|
||||
'survey_status_change',
|
||||
'surveys',
|
||||
NEW.id::text,
|
||||
jsonb_build_object('old_status', OLD.status, 'new_status', NEW.status, 'title', NEW.title)
|
||||
);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.log_survey_status_change() FROM PUBLIC;
|
||||
|
||||
CREATE TRIGGER surveys_status_audit
|
||||
AFTER UPDATE ON public.surveys
|
||||
FOR EACH ROW EXECUTE FUNCTION public.log_survey_status_change();
|
||||
|
||||
-- ============================================================
|
||||
-- Trigger 3 (Prioridad 2, confirmado viable — ver §4.1 del prompt de etapa):
|
||||
-- DDL en schema public → loggea toda migración aplicada.
|
||||
-- ============================================================
|
||||
CREATE OR REPLACE FUNCTION public.log_ddl_command()
|
||||
RETURNS event_trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
obj record;
|
||||
BEGIN
|
||||
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() LOOP
|
||||
IF obj.schema_name = 'public' OR obj.schema_name IS NULL THEN
|
||||
INSERT INTO public.audit_log (actor_role, actor_user_id, action, target_table, target_id, detail)
|
||||
VALUES (
|
||||
session_user,
|
||||
NULL,
|
||||
'ddl_' || obj.command_tag,
|
||||
COALESCE(obj.object_identity, obj.schema_name, 'unknown'),
|
||||
obj.objid::text,
|
||||
jsonb_build_object('command_tag', obj.command_tag, 'object_type', obj.object_type)
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.log_ddl_command() FROM PUBLIC;
|
||||
|
||||
CREATE EVENT TRIGGER audit_ddl_public
|
||||
ON ddl_command_end
|
||||
EXECUTE FUNCTION public.log_ddl_command();
|
||||
|
||||
-- ============================================================
|
||||
-- Fin de migración. NO aplicar en el VPS sin aprobación explícita del
|
||||
-- director — ver ETAPA_AUDIT_LOG_PROMPT.md §4.3.
|
||||
-- ============================================================
|
||||
Reference in New Issue
Block a user