-- An audit trail for privileged actions (H17). -- -- What existed before: nothing. `grep -i audit` across `handlers/host.rs` and `handlers/admin.rs` -- returned no hits. Individual actions logged a `tracing::info!` line, but config changes, gallery -- release and event lock/unlock logged nothing at all — and the "audit trail" as a whole was a -- 30 MB rotating Docker log that the runbook's own retention settings will discard. -- -- Why it matters here specifically: a host is a promoted GUEST, and `reset_pin` overwrites another -- guest's credential and returns the new PIN in the clear. So a host can take over any guest's -- account and post as them, and nothing in the record showed it happened (only /recover FAILURES -- were logged). At a wedding the people involved know each other; the point is not catching a -- villain, it is being able to answer "what happened to my photo?" the next morning without -- guessing. -- -- Deliberately append-only in practice: no UPDATE or DELETE path is written for it anywhere. Small -- (a few hundred rows for a real event), so no partitioning or retention job. CREATE TABLE host_action_audit ( id BIGSERIAL PRIMARY KEY, event_id UUID NOT NULL REFERENCES event(id) ON DELETE CASCADE, -- The privileged caller. NOT a FK with ON DELETE CASCADE: the record must survive the actor's -- account being removed, which is exactly when it is most likely to be wanted. actor_id UUID, actor_name TEXT, actor_role TEXT NOT NULL, -- Short stable slug: 'ban_user', 'unban_user', 'reset_pin', 'delete_upload', -- 'delete_comment', 'release_gallery', 'lock_uploads', 'unlock_uploads', 'patch_config', -- 'promote_user', 'demote_user', 'delete_user'. action TEXT NOT NULL, -- The guest or object acted upon, when there is one. target_id UUID, target_name TEXT, -- Free-form context: the config key and its old/new value, the caption that was removed, etc. -- Never credentials — a reset PIN must not be recoverable from this table. detail JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- The only query shape this needs: "what happened at this event, newest first". CREATE INDEX host_action_audit_event_created_idx ON host_action_audit (event_id, created_at DESC);