feat(content): add catalogue review metadata

Add reversible review states, audit history, and super-admin controls for catalogue entities without changing current client visibility.

Stage 30 high-risk production packs for archival review, cap generated content at non-explicit language, and preserve a checksummed pre-change production catalogue snapshot.
This commit is contained in:
jjbrunton committed 2026-08-28 11:32:55 +01:00
1 parent b43804d8fd
commit 94c76535a4
30 files changed
+46906 -128

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
# Production catalogue snapshots
These files are immutable, version-controlled backups of Sauci's
developer-authored catalogue. They deliberately exclude subscriber accounts,
couples, responses, matches, messages, media, subscriptions, and other customer
data.
`production-2026-08-28.json` contains every production row from:
- `categories`
- `topics`
- `question_packs`
- `questions`, including soft-deleted rows and inverse links
- `dare_packs`
- `dares`
- `pack_topics`
The snapshot is archival input, not an automatically executed seed. Restoration
must be implemented as an explicit, reviewed operation that validates the target
environment, schema compatibility, foreign keys, row counts, and checksum before
writing anything.
Verify the checked-in snapshot from this directory with:
```sh
shasum -a 256 -c production-2026-08-28.sha256
```
File diff suppressed because it is too large. Load diff
@@ -0,0 +1 @@
042551fd33c8994209f44b1a269716c82e8a0d69b87434373c16407d6e7d48ff production-2026-08-28.json
@@ -0,0 +1,411 @@
-- Add reversible catalogue review metadata without changing customer visibility.
--
-- IMPORTANT: Current mobile/API queries intentionally do not filter content_status.
-- This migration only prepares and audits the catalogue for a later, separately
-- verified enforcement release.
CREATE TYPE public.content_review_status AS ENUM (
'unreviewed',
'allowed',
'archived'
);
CREATE TYPE public.content_entity_type AS ENUM (
'categories',
'question_packs',
'questions',
'dare_packs',
'dares'
);
ALTER TABLE public.categories
ADD COLUMN content_status public.content_review_status NOT NULL DEFAULT 'unreviewed',
ADD COLUMN content_review_reason TEXT,
ADD COLUMN content_reviewed_at TIMESTAMPTZ,
ADD COLUMN content_reviewed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL;
ALTER TABLE public.question_packs
ADD COLUMN content_status public.content_review_status NOT NULL DEFAULT 'unreviewed',
ADD COLUMN content_review_reason TEXT,
ADD COLUMN content_reviewed_at TIMESTAMPTZ,
ADD COLUMN content_reviewed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL;
ALTER TABLE public.questions
ADD COLUMN content_status public.content_review_status NOT NULL DEFAULT 'unreviewed',
ADD COLUMN content_review_reason TEXT,
ADD COLUMN content_reviewed_at TIMESTAMPTZ,
ADD COLUMN content_reviewed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL;
ALTER TABLE public.dare_packs
ADD COLUMN content_status public.content_review_status NOT NULL DEFAULT 'unreviewed',
ADD COLUMN content_review_reason TEXT,
ADD COLUMN content_reviewed_at TIMESTAMPTZ,
ADD COLUMN content_reviewed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL;
ALTER TABLE public.dares
ADD COLUMN content_status public.content_review_status NOT NULL DEFAULT 'unreviewed',
ADD COLUMN content_review_reason TEXT,
ADD COLUMN content_reviewed_at TIMESTAMPTZ,
ADD COLUMN content_reviewed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL;
COMMENT ON COLUMN public.categories.content_status IS
'Editorial review metadata only. Customer visibility does not yet filter this column.';
COMMENT ON COLUMN public.question_packs.content_status IS
'Editorial review metadata only. Customer visibility does not yet filter this column.';
COMMENT ON COLUMN public.questions.content_status IS
'Editorial review metadata only. Customer visibility does not yet filter this column.';
COMMENT ON COLUMN public.dare_packs.content_status IS
'Editorial review metadata only. Customer visibility does not yet filter this column.';
COMMENT ON COLUMN public.dares.content_status IS
'Editorial review metadata only. Customer visibility does not yet filter this column.';
CREATE INDEX categories_content_status_idx
ON public.categories (content_status, sort_order);
CREATE INDEX question_packs_content_status_idx
ON public.question_packs (content_status, is_public, category_id, sort_order);
CREATE INDEX questions_content_status_idx
ON public.questions (content_status, pack_id, intensity)
WHERE deleted_at IS NULL;
CREATE INDEX dare_packs_content_status_idx
ON public.dare_packs (content_status, is_public, category_id, sort_order);
CREATE INDEX dares_content_status_idx
ON public.dares (content_status, pack_id, intensity);
CREATE TABLE public.content_reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type public.content_entity_type NOT NULL,
entity_id UUID NOT NULL,
previous_status public.content_review_status NOT NULL,
new_status public.content_review_status NOT NULL,
reason TEXT NOT NULL CHECK (length(btrim(reason)) > 0),
changed_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE public.content_reviews IS
'Append-only audit history for reversible catalogue review decisions.';
CREATE INDEX content_reviews_entity_idx
ON public.content_reviews (entity_type, entity_id, created_at DESC);
CREATE INDEX content_reviews_created_at_idx
ON public.content_reviews (created_at DESC);
ALTER TABLE public.content_reviews ENABLE ROW LEVEL SECURITY;
-- Supabase grants broad privileges to API roles through default privileges.
-- Remove them before restoring the one intended authenticated capability.
REVOKE ALL PRIVILEGES ON TABLE public.content_reviews FROM anon, authenticated;
CREATE POLICY "Super admins can view content review history"
ON public.content_reviews
FOR SELECT
TO authenticated
USING (public.is_super_admin());
GRANT SELECT ON public.content_reviews TO authenticated;
GRANT ALL ON public.content_reviews TO service_role;
CREATE OR REPLACE FUNCTION public.prepare_new_content_review()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
BEGIN
-- Every new catalogue row enters the review queue. Callers cannot combine
-- creation and approval or forge reviewer metadata in one INSERT.
NEW.content_status := 'unreviewed';
NEW.content_review_reason := NULL;
NEW.content_reviewed_at := NULL;
NEW.content_reviewed_by := NULL;
RETURN NEW;
END;
$$;
CREATE OR REPLACE FUNCTION public.prepare_content_review_change()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
DECLARE
visible_column TEXT;
visible_content_changed BOOLEAN := FALSE;
explicit_status_change BOOLEAN :=
NEW.content_status IS DISTINCT FROM OLD.content_status;
explicit_reason_change BOOLEAN :=
NEW.content_review_reason IS DISTINCT FROM OLD.content_review_reason;
explicit_reviewer_metadata_change BOOLEAN :=
NEW.content_reviewed_at IS DISTINCT FROM OLD.content_reviewed_at
OR NEW.content_reviewed_by IS DISTINCT FROM OLD.content_reviewed_by;
BEGIN
FOREACH visible_column IN ARRAY TG_ARGV LOOP
IF (to_jsonb(NEW) -> visible_column) IS DISTINCT FROM
(to_jsonb(OLD) -> visible_column) THEN
visible_content_changed := TRUE;
EXIT;
END IF;
END LOOP;
-- An explicit status/reason update is a review decision, including a
-- same-status re-review with a new reason. Reviewer identity and time are
-- always derived here rather than trusted from client input.
IF explicit_status_change OR explicit_reason_change THEN
IF auth.role() = 'anon'
OR (auth.uid() IS NOT NULL AND NOT public.is_super_admin()) THEN
RAISE EXCEPTION 'Only super admins can make catalogue review decisions';
END IF;
IF explicit_status_change AND NOT explicit_reason_change THEN
RAISE EXCEPTION 'A new content_review_reason is required when content_status changes';
END IF;
IF NULLIF(btrim(NEW.content_review_reason), '') IS NULL THEN
RAISE EXCEPTION 'A non-blank content_review_reason is required when content_status changes';
END IF;
IF NEW.content_status = 'unreviewed' THEN
NEW.content_reviewed_at := NULL;
NEW.content_reviewed_by := NULL;
ELSE
NEW.content_reviewed_at := now();
NEW.content_reviewed_by := auth.uid();
END IF;
-- A material content edit without an explicit review decision invalidates
-- the prior decision. This path is available to ordinary content editors.
ELSIF visible_content_changed THEN
NEW.content_status := 'unreviewed';
NEW.content_review_reason := 'Visible content changed; review required';
NEW.content_reviewed_at := NULL;
NEW.content_reviewed_by := NULL;
ELSIF explicit_reviewer_metadata_change THEN
-- Permit only the database-internal ON DELETE SET NULL action from the
-- auth.users foreign key. JWT-backed anon/service calls have an auth role;
-- direct client attempts therefore cannot use this exception.
IF auth.uid() IS NULL
AND auth.role() IS NULL
AND NEW.content_reviewed_by IS NULL
AND NEW.content_reviewed_at IS NOT DISTINCT FROM OLD.content_reviewed_at THEN
NULL;
ELSE
RAISE EXCEPTION 'content_reviewed_at and content_reviewed_by are database-managed';
END IF;
END IF;
RETURN NEW;
END;
$$;
CREATE OR REPLACE FUNCTION public.audit_content_review_change()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
BEGIN
IF NEW.content_status IS DISTINCT FROM OLD.content_status
OR NEW.content_review_reason IS DISTINCT FROM OLD.content_review_reason THEN
INSERT INTO public.content_reviews (
entity_type,
entity_id,
previous_status,
new_status,
reason,
changed_by
) VALUES (
TG_TABLE_NAME::public.content_entity_type,
NEW.id,
OLD.content_status,
NEW.content_status,
NEW.content_review_reason,
auth.uid()
);
END IF;
RETURN NEW;
END;
$$;
-- These functions are trigger-only. Revoking direct execution keeps their
-- SECURITY DEFINER privileges unavailable as a general authenticated API.
REVOKE ALL ON FUNCTION public.prepare_content_review_change() FROM PUBLIC;
REVOKE ALL ON FUNCTION public.audit_content_review_change() FROM PUBLIC;
REVOKE ALL ON FUNCTION public.prepare_new_content_review() FROM PUBLIC;
REVOKE ALL ON FUNCTION public.prepare_content_review_change() FROM anon, authenticated;
REVOKE ALL ON FUNCTION public.audit_content_review_change() FROM anon, authenticated;
REVOKE ALL ON FUNCTION public.prepare_new_content_review() FROM anon, authenticated;
CREATE TRIGGER categories_prepare_new_content_review
BEFORE INSERT ON public.categories
FOR EACH ROW
EXECUTE FUNCTION public.prepare_new_content_review();
CREATE TRIGGER question_packs_prepare_new_content_review
BEFORE INSERT ON public.question_packs
FOR EACH ROW
EXECUTE FUNCTION public.prepare_new_content_review();
CREATE TRIGGER questions_prepare_new_content_review
BEFORE INSERT ON public.questions
FOR EACH ROW
EXECUTE FUNCTION public.prepare_new_content_review();
CREATE TRIGGER dare_packs_prepare_new_content_review
BEFORE INSERT ON public.dare_packs
FOR EACH ROW
EXECUTE FUNCTION public.prepare_new_content_review();
CREATE TRIGGER dares_prepare_new_content_review
BEFORE INSERT ON public.dares
FOR EACH ROW
EXECUTE FUNCTION public.prepare_new_content_review();
CREATE TRIGGER categories_prepare_content_review
BEFORE UPDATE ON public.categories
FOR EACH ROW
EXECUTE FUNCTION public.prepare_content_review_change(
'name', 'description', 'icon', 'color'
);
CREATE TRIGGER categories_audit_content_review
AFTER UPDATE ON public.categories
FOR EACH ROW
EXECUTE FUNCTION public.audit_content_review_change();
CREATE TRIGGER question_packs_prepare_content_review
BEFORE UPDATE ON public.question_packs
FOR EACH ROW
EXECUTE FUNCTION public.prepare_content_review_change(
'name', 'description', 'icon', 'category_id', 'is_explicit'
);
CREATE TRIGGER question_packs_audit_content_review
AFTER UPDATE ON public.question_packs
FOR EACH ROW
EXECUTE FUNCTION public.audit_content_review_change();
CREATE TRIGGER questions_prepare_content_review
BEFORE UPDATE ON public.questions
FOR EACH ROW
EXECUTE FUNCTION public.prepare_content_review_change(
'text',
'partner_text',
'pack_id',
'intensity',
'question_type',
'config',
'required_props',
'allowed_couple_genders',
'target_user_genders',
'inverse_of',
'deleted_at'
);
CREATE TRIGGER questions_audit_content_review
AFTER UPDATE ON public.questions
FOR EACH ROW
EXECUTE FUNCTION public.audit_content_review_change();
CREATE TRIGGER dare_packs_prepare_content_review
BEFORE UPDATE ON public.dare_packs
FOR EACH ROW
EXECUTE FUNCTION public.prepare_content_review_change(
'name', 'description', 'icon', 'category_id', 'is_explicit'
);
CREATE TRIGGER dare_packs_audit_content_review
AFTER UPDATE ON public.dare_packs
FOR EACH ROW
EXECUTE FUNCTION public.audit_content_review_change();
CREATE TRIGGER dares_prepare_content_review
BEFORE UPDATE ON public.dares
FOR EACH ROW
EXECUTE FUNCTION public.prepare_content_review_change(
'text',
'pack_id',
'intensity',
'suggested_duration_hours'
);
CREATE TRIGGER dares_audit_content_review
AFTER UPDATE ON public.dares
FOR EACH ROW
EXECUTE FUNCTION public.audit_content_review_change();
-- Stage the conservative archive set by immutable production IDs. This does not
-- change is_public, deleted_at, RLS, RPC selection, or current application output.
WITH archive_packs(id) AS (
VALUES
('9049a67a-a103-4ea3-8405-87dcaefc57ab'::UUID),
('3c17764d-dd11-4103-8a4f-ce6561fd2a1a'::UUID),
('f0a2e7e5-554c-49c4-aa15-42d83b016349'::UUID),
('d2000000-0000-0000-0000-000000000003'::UUID),
('40838dee-0af4-43c7-9d6c-6228d09f91bc'::UUID),
('d2000000-0000-0000-0000-000000000002'::UUID),
('44295bd5-fda3-4ed6-8aa6-d5c7e811348e'::UUID),
('c7244576-f40d-4cdf-804a-788d2b9c1fce'::UUID),
('7eabdfae-e70d-4be8-9b10-b43fee955312'::UUID),
('374e10a6-6ae9-45c6-bee4-b8be0e617b79'::UUID),
('dd143c55-c0a4-4884-a704-1d4d1325cbc2'::UUID),
('035c47a6-858c-46fb-8ee3-84889399dca0'::UUID),
('8bf628d0-2a83-4a12-a66b-39b0d6ff1e86'::UUID),
('42331f68-60f5-4768-af26-b42c4e96c867'::UUID),
('379d6a44-60a3-4ec9-98cc-eda46547cb23'::UUID),
('e5aefe29-1ada-4fe5-a4ee-25342917f02c'::UUID),
('bed94ed6-c065-4ff9-9d7f-02de6cade971'::UUID),
('eb3cbe67-23d8-4c0e-9c6f-dc1583609a26'::UUID),
('d18f6d4a-4a32-4687-8938-2a53a7d82739'::UUID),
('b2df6b3b-4bb5-4baa-a29c-a3955187511c'::UUID),
('d73e9818-4faa-4276-9a93-7c60cff9cdb1'::UUID),
('7951e8c4-c7d4-4460-a3f5-495ccf5ca0cb'::UUID),
('f0f597b1-889c-4537-beeb-d0f459bb7907'::UUID),
('46288aa4-271c-44cf-bfe2-d25662d3f9f8'::UUID),
('d2000000-0000-0000-0000-000000000001'::UUID),
('07f0da41-8ef7-4a48-8ec9-cef2dabaee73'::UUID),
('42c00d6c-ab1f-42ce-afb0-00839064263b'::UUID),
('8a1e8650-a192-4f2e-86b0-0ec7bf6fb61c'::UUID),
('d5d1b8d2-a868-4150-86bc-da3f8e3e8b00'::UUID),
('19e4ddbd-a4aa-4b26-80bb-53b7187f0eba'::UUID)
)
UPDATE public.question_packs qp
SET
content_status = 'archived',
content_review_reason = 'Conservative archive candidate following the 2026-02-03 store rejection',
content_reviewed_at = now(),
content_reviewed_by = NULL
FROM archive_packs ap
WHERE qp.id = ap.id;
WITH archive_packs(id) AS (
VALUES
('9049a67a-a103-4ea3-8405-87dcaefc57ab'::UUID),
('3c17764d-dd11-4103-8a4f-ce6561fd2a1a'::UUID),
('f0a2e7e5-554c-49c4-aa15-42d83b016349'::UUID),
('d2000000-0000-0000-0000-000000000003'::UUID),
('40838dee-0af4-43c7-9d6c-6228d09f91bc'::UUID),
('d2000000-0000-0000-0000-000000000002'::UUID),
('44295bd5-fda3-4ed6-8aa6-d5c7e811348e'::UUID),
('c7244576-f40d-4cdf-804a-788d2b9c1fce'::UUID),
('7eabdfae-e70d-4be8-9b10-b43fee955312'::UUID),
('374e10a6-6ae9-45c6-bee4-b8be0e617b79'::UUID),
('dd143c55-c0a4-4884-a704-1d4d1325cbc2'::UUID),
('035c47a6-858c-46fb-8ee3-84889399dca0'::UUID),
('8bf628d0-2a83-4a12-a66b-39b0d6ff1e86'::UUID),
('42331f68-60f5-4768-af26-b42c4e96c867'::UUID),
('379d6a44-60a3-4ec9-98cc-eda46547cb23'::UUID),
('e5aefe29-1ada-4fe5-a4ee-25342917f02c'::UUID),
('bed94ed6-c065-4ff9-9d7f-02de6cade971'::UUID),
('eb3cbe67-23d8-4c0e-9c6f-dc1583609a26'::UUID),
('d18f6d4a-4a32-4687-8938-2a53a7d82739'::UUID),
('b2df6b3b-4bb5-4baa-a29c-a3955187511c'::UUID),
('d73e9818-4faa-4276-9a93-7c60cff9cdb1'::UUID),
('7951e8c4-c7d4-4460-a3f5-495ccf5ca0cb'::UUID),
('f0f597b1-889c-4537-beeb-d0f459bb7907'::UUID),
('46288aa4-271c-44cf-bfe2-d25662d3f9f8'::UUID),
('d2000000-0000-0000-0000-000000000001'::UUID),
('07f0da41-8ef7-4a48-8ec9-cef2dabaee73'::UUID),
('42c00d6c-ab1f-42ce-afb0-00839064263b'::UUID),
('8a1e8650-a192-4f2e-86b0-0ec7bf6fb61c'::UUID),
('d5d1b8d2-a868-4150-86bc-da3f8e3e8b00'::UUID),
('19e4ddbd-a4aa-4b26-80bb-53b7187f0eba'::UUID)
)
UPDATE public.questions q
SET
content_status = 'archived',
content_review_reason = 'Archived with its pack pending rewrite',
content_reviewed_at = now(),
content_reviewed_by = NULL
FROM archive_packs ap
WHERE q.pack_id = ap.id;
@@ -0,0 +1,381 @@
import { createClient } from "@supabase/supabase-js";
import { assertEquals, assertMatch } from "std/assert/mod.ts";
import {
adminClient,
createTestUser,
SUPABASE_URL,
} from "./utils.ts";
const anonKey = Deno.env.get("SUPABASE_ANON_KEY");
if (!anonKey) {
throw new Error("SUPABASE_ANON_KEY is required");
}
Deno.test("catalogue review migration stages the complete audited archive set", async () => {
const migrationSql = await Deno.readTextFile(
new URL(
"../migrations/20260827215907_add_catalog_content_review_metadata.sql",
import.meta.url,
),
);
const archiveBlocks = [...migrationSql.matchAll(
/WITH archive_packs\(id\) AS \(\s*VALUES([\s\S]*?)\)\s*UPDATE/g,
)];
assertEquals(archiveBlocks.length, 2);
const extractIds = (block: string) =>
[...block.matchAll(
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g,
)].map(([id]) => id);
const packIds = extractIds(archiveBlocks[0][1]);
const questionPackIds = extractIds(archiveBlocks[1][1]);
assertEquals(packIds.length, 30);
assertEquals(new Set(packIds).size, 30);
assertEquals(questionPackIds, packIds);
for (
const auditedMixedPackId of [
"f0f597b1-889c-4537-beeb-d0f459bb7907", // Body Mapping
"46288aa4-271c-44cf-bfe2-d25662d3f9f8", // Date Night Sparks
"d2000000-0000-0000-0000-000000000001", // First Impressions
"07f0da41-8ef7-4a48-8ec9-cef2dabaee73", // Love Notes
"42c00d6c-ab1f-42ce-afb0-00839064263b", // Romantic Gestures
"8a1e8650-a192-4f2e-86b0-0ec7bf6fb61c", // Sensual Massage
"d5d1b8d2-a868-4150-86bc-da3f8e3e8b00", // Testing the Waters
"19e4ddbd-a4aa-4b26-80bb-53b7187f0eba", // Missing You
]
) {
assertEquals(packIds.includes(auditedMixedPackId), true);
}
});
Deno.test("catalogue review metadata is reversible, audited, and non-super-admin decisions are rejected", async () => {
const suffix = crypto.randomUUID();
const email = `content-review-${suffix}@example.com`;
let packId: string | undefined;
let insertedPackId: string | undefined;
let userId: string | undefined;
try {
const { data: pack, error: packError } = await adminClient
.from("question_packs")
.insert({
name: `Content review test ${suffix}`,
is_public: true,
is_premium: false,
is_explicit: false,
})
.select("id, content_status")
.single();
if (packError) throw packError;
packId = pack.id;
assertEquals(pack.content_status, "unreviewed");
const { data: archived, error: archiveError } = await adminClient
.from("question_packs")
.update({
content_status: "archived",
content_review_reason: "Test archive decision",
})
.eq("id", packId)
.select("content_status, content_review_reason")
.single();
if (archiveError) throw archiveError;
assertEquals(archived.content_status, "archived");
assertEquals(archived.content_review_reason, "Test archive decision");
const { error: blankReasonError } = await adminClient
.from("question_packs")
.update({
content_status: "allowed",
content_review_reason: " ",
})
.eq("id", packId);
if (!blankReasonError) {
throw new Error("Expected blank review reason to be rejected");
}
assertMatch(blankReasonError.message, /non-blank content_review_reason/i);
const user = await createTestUser(email);
userId = user.id;
const userClient = createClient(SUPABASE_URL, anonKey, {
auth: { autoRefreshToken: false, persistSession: false },
});
const { error: signInError } = await userClient.auth.signInWithPassword({
email,
password: "password123",
});
if (signInError) throw signInError;
// The metadata-only migration must not hide content from existing clients.
const { data: stillVisible, error: visibilityError } = await userClient
.from("question_packs")
.select("id, content_status")
.eq("id", packId)
.single();
if (visibilityError) throw visibilityError;
assertEquals(stillVisible.id, packId);
assertEquals(stillVisible.content_status, "archived");
const { error: adminError } = await adminClient.from("admin_users").insert({
user_id: userId,
role: "pack_creator",
});
if (adminError) throw adminError;
const { error: forbiddenDecision } = await userClient
.from("question_packs")
.update({
content_status: "allowed",
content_review_reason: "Pack creator should not approve content",
})
.eq("id", packId);
if (!forbiddenDecision) {
throw new Error("Expected pack creator catalogue decision to be rejected");
}
assertMatch(forbiddenDecision.message, /Only super admins/i);
const { error: promoteError } = await adminClient
.from("admin_users")
.update({ role: "super_admin" })
.eq("user_id", userId);
if (promoteError) throw promoteError;
const { data: allowed, error: allowedError } = await userClient
.from("question_packs")
.update({
content_status: "allowed",
content_review_reason: "Test super admin approval",
})
.eq("id", packId)
.select("content_status, content_reviewed_by")
.single();
if (allowedError) throw allowedError;
assertEquals(allowed.content_status, "allowed");
assertEquals(allowed.content_reviewed_by, userId);
const { data: rereviewed, error: rereviewError } = await userClient
.from("question_packs")
.update({ content_review_reason: "Test approval independently rechecked" })
.eq("id", packId)
.select("content_status, content_review_reason, content_reviewed_by")
.single();
if (rereviewError) throw rereviewError;
assertEquals(rereviewed.content_status, "allowed");
assertEquals(
rereviewed.content_review_reason,
"Test approval independently rechecked",
);
assertEquals(rereviewed.content_reviewed_by, userId);
const { error: demoteError } = await adminClient
.from("admin_users")
.update({ role: "pack_creator" })
.eq("user_id", userId);
if (demoteError) throw demoteError;
const { error: forgedReason } = await userClient
.from("question_packs")
.update({ content_review_reason: "Pack creator forged review reason" })
.eq("id", packId);
if (!forgedReason) {
throw new Error("Expected pack creator review metadata change to be rejected");
}
assertMatch(forgedReason.message, /Only super admins/i);
const { error: forgedReviewer } = await userClient
.from("question_packs")
.update({ content_reviewed_by: null })
.eq("id", packId);
if (!forgedReviewer) {
throw new Error("Expected database-managed reviewer metadata to be rejected");
}
assertMatch(forgedReviewer.message, /database-managed/i);
const { data: guardedInsert, error: guardedInsertError } = await userClient
.from("question_packs")
.insert({
name: `Guarded content review insert ${suffix}`,
is_public: true,
is_premium: false,
is_explicit: false,
content_status: "allowed",
content_review_reason: "Forged approval on insert",
content_reviewed_at: new Date().toISOString(),
content_reviewed_by: userId,
})
.select(
"id, content_status, content_review_reason, content_reviewed_at, content_reviewed_by",
)
.single();
if (guardedInsertError) throw guardedInsertError;
insertedPackId = guardedInsert.id;
assertEquals(guardedInsert.content_status, "unreviewed");
assertEquals(guardedInsert.content_review_reason, null);
assertEquals(guardedInsert.content_reviewed_at, null);
assertEquals(guardedInsert.content_reviewed_by, null);
const { data: edited, error: editError } = await userClient
.from("question_packs")
.update({ name: `Edited content review test ${suffix}` })
.eq("id", packId)
.select("content_status, content_review_reason")
.single();
if (editError) throw editError;
assertEquals(edited.content_status, "unreviewed");
assertEquals(
edited.content_review_reason,
"Visible content changed; review required",
);
const { data: reviews, error: reviewsError } = await adminClient
.from("content_reviews")
.select("previous_status, new_status, reason, changed_by")
.eq("entity_type", "question_packs")
.eq("entity_id", packId)
.order("created_at", { ascending: true });
if (reviewsError) throw reviewsError;
assertEquals(reviews.length, 4);
assertEquals(reviews[0].previous_status, "unreviewed");
assertEquals(reviews[0].new_status, "archived");
assertEquals(reviews[1].previous_status, "archived");
assertEquals(reviews[1].new_status, "allowed");
assertEquals(reviews[1].changed_by, userId);
assertEquals(reviews[2].previous_status, "allowed");
assertEquals(reviews[2].new_status, "allowed");
assertEquals(reviews[2].changed_by, userId);
assertEquals(reviews[3].previous_status, "allowed");
assertEquals(reviews[3].new_status, "unreviewed");
assertEquals(reviews[3].changed_by, userId);
const { data: hiddenReviews, error: hiddenReviewsError } = await userClient
.from("content_reviews")
.select("id")
.eq("entity_type", "question_packs")
.eq("entity_id", packId);
if (hiddenReviewsError) throw hiddenReviewsError;
assertEquals(hiddenReviews.length, 0);
const { error: forgedAuditInsert } = await userClient
.from("content_reviews")
.insert({
entity_type: "question_packs",
entity_id: packId,
previous_status: "unreviewed",
new_status: "allowed",
reason: "Forged audit event",
});
if (!forgedAuditInsert) {
throw new Error("Expected authenticated audit insertion to be rejected");
}
} finally {
if (insertedPackId) {
await adminClient
.from("question_packs")
.delete()
.eq("id", insertedPackId);
}
if (packId) {
await adminClient
.from("content_reviews")
.delete()
.eq("entity_type", "question_packs")
.eq("entity_id", packId);
await adminClient.from("question_packs").delete().eq("id", packId);
}
if (userId) {
await adminClient.from("admin_users").delete().eq("user_id", userId);
await adminClient.auth.admin.deleteUser(userId);
}
}
});
Deno.test("deleting a reviewer preserves the decision and clears its current reviewer reference", async () => {
const suffix = crypto.randomUUID();
const email = `deleted-content-reviewer-${suffix}@example.com`;
let packId: string | undefined;
let userId: string | undefined;
try {
const user = await createTestUser(email);
userId = user.id;
const { error: adminError } = await adminClient.from("admin_users").insert({
user_id: userId,
role: "super_admin",
});
if (adminError) throw adminError;
const reviewerClient = createClient(SUPABASE_URL, anonKey, {
auth: { autoRefreshToken: false, persistSession: false },
});
const { error: signInError } = await reviewerClient.auth.signInWithPassword({
email,
password: "password123",
});
if (signInError) throw signInError;
const { data: pack, error: packError } = await adminClient
.from("question_packs")
.insert({
name: `Reviewer deletion test ${suffix}`,
is_public: true,
is_premium: false,
is_explicit: false,
})
.select("id")
.single();
if (packError) throw packError;
packId = pack.id;
const { error: approveError } = await reviewerClient
.from("question_packs")
.update({
content_status: "allowed",
content_review_reason: "Approval survives reviewer deletion",
})
.eq("id", packId);
if (approveError) throw approveError;
const { error: deleteUserError } = await adminClient.auth.admin.deleteUser(
userId,
);
if (deleteUserError) throw deleteUserError;
userId = undefined;
const { data: afterDeletion, error: afterDeletionError } = await adminClient
.from("question_packs")
.select("content_status, content_review_reason, content_reviewed_by")
.eq("id", packId)
.single();
if (afterDeletionError) throw afterDeletionError;
assertEquals(afterDeletion.content_status, "allowed");
assertEquals(
afterDeletion.content_review_reason,
"Approval survives reviewer deletion",
);
assertEquals(afterDeletion.content_reviewed_by, null);
} finally {
if (packId) {
await adminClient
.from("content_reviews")
.delete()
.eq("entity_type", "question_packs")
.eq("entity_id", packId);
await adminClient.from("question_packs").delete().eq("id", packId);
}
if (userId) {
await adminClient.from("admin_users").delete().eq("user_id", userId);
await adminClient.auth.admin.deleteUser(userId);
}
}
});