Adds /api/v1/admin/users list / DELETE / PATCH guarded by RequireAdmin, plus the audit-log substrate every future destructive admin endpoint will reuse. Safety properties: - Cannot self-delete or self-demote (409 conflict, message calls out "yourself" so the UI can render an explanation). - Cannot remove the last admin via either DELETE or demote. The check takes pg_advisory_xact_lock(ADMIN_INVARIANT_LOCK_KEY) and re-counts admins inside the same tx, closing the parallel-demote race that a bare "if count > 1" check would let through. The HTTP-serial path to this guard is structurally unreachable (the actor would have to be the lone admin demoting themselves, which the self-guard fires on first); the parallel race test exercises it via repo calls. Audit log (admin_audit table) records the action inside the same tx as the action itself, so a rolled-back action never leaves an orphan audit row. actor_user_id is ON DELETE SET NULL so the log outlives a later-deleted admin. target_id is not a FK because future audit kinds will target non-user rows.
21 lines
952 B
SQL
21 lines
952 B
SQL
-- Admin audit log. Written from inside the same transaction as the action
|
|
-- it records, so a failed COMMIT also rolls back the audit row — the log
|
|
-- never claims an action happened that didn't.
|
|
--
|
|
-- `actor_user_id` is ON DELETE SET NULL so audit rows outlive a deleted
|
|
-- admin (the answer to "who promoted Bob to admin?" survives even after
|
|
-- Alice's account is removed). `target_id` is intentionally not a FK
|
|
-- because future audit kinds may target non-user rows (manga, source,
|
|
-- etc.) and a single typed FK can't express that.
|
|
CREATE TABLE admin_audit (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
actor_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
action text NOT NULL,
|
|
target_kind text NOT NULL,
|
|
target_id uuid,
|
|
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX admin_audit_at_idx ON admin_audit (at DESC);
|