Adds GET /api/me/export streaming a deterministic .zip bundle of the caller's RLS-visible projection (per design §2.3): projects, deadlines, appointments, parties, notes, documents (metadata), audit events, approval requests, checklist instances + personal sidecars (me row, caldav config without ciphertext, views, pins, card layouts, paliadin turns) + reference data (proceeding_types, event_types, deadline_rules, courts, countries, holidays …) + restricted users_referenced sheet. Bundle shape: paliad-export.xlsx + paliad-export.json + per-sheet CSVs (UTF-8 BOM, RFC 4180) + README.txt + __meta.json. Outer zip is byte-deterministic — sorted file list, fixed Modified time on every entry, sorted JSON keys. Two runs at same row-state → identical bytes. ExportService.WritePersonal owns the SQL recipe + column discovery + PII deny-regex (?i)secret|token|password|api[_-]?key|private[_-]?key + per-sheet DropColumns belt-and-braces (e.g. user_caldav_config .password_encrypted explicitly dropped on top of the regex). Audit row written to paliad.system_audit_log before the run, patched with row_counts + file_size_bytes after. Migration 102 creates paliad.system_audit_log (generic event_type + actor_id/email + scope + scope_root + metadata jsonb). Idempotent CREATE TABLE IF NOT EXISTS + indexes; RLS enabled with self-read + admin-read policies. AuditService.ListEntries gains a 6th UNION branch so the new table surfaces on /admin/audit-log. excelize/v2 added to go.mod for xlsx generation. Pure-function tests pin formatCellValue value-coercion, PII regex, CSV quoting + BOM + umlaut survival, JSON shape, meta key order stability, filename slugify, and byte-determinism of the bundle assembly. Design: docs/design-paliad-data-export-2026-05-19.md §7 Slice 1.
80 lines
3.8 KiB
SQL
80 lines
3.8 KiB
SQL
-- t-paliad-214 Slice 1 — create paliad.system_audit_log as the 6th source
|
|
-- in the AuditService.ListEntries union. Captures org-wide / scope-spanning
|
|
-- actions that don't naturally belong on any single project_events row.
|
|
--
|
|
-- Design: docs/design-paliad-data-export-2026-05-19.md §4.
|
|
--
|
|
-- Initial use case is data-export auditing (every export run writes one row,
|
|
-- before the artifact is generated, then is patched with row_counts +
|
|
-- file_size_bytes on completion). The table is intentionally generic
|
|
-- (`event_type` + `metadata jsonb`) so future org-wide actions can land here
|
|
-- without a new table per concept.
|
|
--
|
|
-- Idempotent: CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
|
|
-- audit_reason set_config required by the mig 079 trigger pattern when
|
|
-- migrations touch the database — universal convention even for pure-DDL
|
|
-- migrations.
|
|
|
|
SELECT set_config(
|
|
'paliad.audit_reason',
|
|
'mig 102: add paliad.system_audit_log for org-wide / scope-spanning audit events (t-paliad-214 Slice 1 — data-export audit chain)',
|
|
true);
|
|
|
|
CREATE TABLE IF NOT EXISTS paliad.system_audit_log (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
event_type text NOT NULL,
|
|
actor_id uuid REFERENCES paliad.users(id) ON DELETE SET NULL,
|
|
-- actor_email is captured at write time so the audit row survives a
|
|
-- subsequent user-deletion (FK above sets NULL, but the historical
|
|
-- identity stays readable).
|
|
actor_email text NOT NULL,
|
|
scope text NOT NULL CHECK (scope IN ('org', 'project', 'personal')),
|
|
-- scope_root is the project_id for scope='project'; NULL otherwise.
|
|
-- Not a hard FK because we want the audit row to outlive a project
|
|
-- deletion. Resolution happens at read time.
|
|
scope_root uuid,
|
|
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Indexes mirror the read patterns:
|
|
-- - actor lookup ("show me what I've exported"): actor_id + created_at desc
|
|
-- - scope rollup ("how much org-wide activity in the last 30 days"): event_type + created_at desc
|
|
CREATE INDEX IF NOT EXISTS system_audit_log_actor_id_created_at_idx
|
|
ON paliad.system_audit_log (actor_id, created_at DESC);
|
|
|
|
CREATE INDEX IF NOT EXISTS system_audit_log_event_type_created_at_idx
|
|
ON paliad.system_audit_log (event_type, created_at DESC);
|
|
|
|
-- RLS: every authenticated user can SELECT their own rows (actor_id = auth.uid());
|
|
-- global_admins see everything. INSERT / UPDATE happen via the Go service path
|
|
-- under the migration-runner role (no end-user write surface) so no INSERT
|
|
-- policy is needed for end users.
|
|
ALTER TABLE paliad.system_audit_log ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS system_audit_log_select_self ON paliad.system_audit_log;
|
|
CREATE POLICY system_audit_log_select_self ON paliad.system_audit_log
|
|
FOR SELECT
|
|
USING (actor_id = auth.uid());
|
|
|
|
DROP POLICY IF EXISTS system_audit_log_select_admin ON paliad.system_audit_log;
|
|
CREATE POLICY system_audit_log_select_admin ON paliad.system_audit_log
|
|
FOR SELECT
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM paliad.users u
|
|
WHERE u.id = auth.uid()
|
|
AND u.global_role = 'global_admin'
|
|
)
|
|
);
|
|
|
|
COMMENT ON TABLE paliad.system_audit_log IS
|
|
'Org-wide / scope-spanning audit events. 6th source of AuditService union. Generic event_type + metadata jsonb. Initial users: data-export audit chain (t-paliad-214). Audit rows persist forever; artifact retention is separate.';
|
|
|
|
COMMENT ON COLUMN paliad.system_audit_log.actor_email IS
|
|
'Captured at write time so the audit row survives user deletion (actor_id FK uses ON DELETE SET NULL).';
|
|
|
|
COMMENT ON COLUMN paliad.system_audit_log.scope_root IS
|
|
'project_id for scope=project; NULL otherwise. Not a hard FK so audit survives project deletion.';
|