feat(content): enforce reviewed catalogue

Hide archived and unreviewed catalogue content from existing clients through RLS and guarded recommendation RPCs. Reject stale writes, preserve historical rows, and seed reviewed defaults when the cutover removes a couple's full active selection.
This commit is contained in:
jjbrunton committed 2026-08-28 13:15:33 +01:00
1 parent 94c76535a4
commit c0650ed5b7
10 files changed
+924 -57

No files matched your search

@@ -125,11 +125,31 @@ Deno.serve(async (req) => {
// Fetch the question to get its type
const { data: question, error: questionError } = await supabase
.from("questions")
.select("id, question_type")
.select(`
id,
question_type,
content_status,
pack:question_packs!inner(
content_status,
category_id,
category:categories(content_status)
)
`)
.eq("id", question_id)
.single();
if (questionError || !question) {
const questionPack = Array.isArray(question?.pack)
? question.pack[0]
: question?.pack;
const questionCategory = Array.isArray(questionPack?.category)
? questionPack.category[0]
: questionPack?.category;
const questionIsAllowed = question?.content_status === "allowed"
&& questionPack?.content_status === "allowed"
&& (questionPack.category_id === null
|| questionCategory?.content_status === "allowed");
if (questionError || !question || !questionIsAllowed) {
return new Response(
JSON.stringify({ error: "Question not found" }),
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } }
@@ -142,7 +162,7 @@ Deno.serve(async (req) => {
.select("answer, response_data")
.eq("user_id", user.id)
.eq("question_id", question_id)
.maybeSingle();
.maybeSingle();
if (currentResponseError) {
return new Response(
@@ -157,14 +177,14 @@ Deno.serve(async (req) => {
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
// If answer hasn't changed and no response_data updates, return early
if (currentResponse.answer === new_answer && typeof response_data === "undefined") {
return new Response(
JSON.stringify({ success: true, message: "Answer unchanged" } as UpdateResponseResult),
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
);
}
}
// Check if a match exists for this question
const { data: existingMatch } = await supabase
@@ -174,7 +194,7 @@ Deno.serve(async (req) => {
.eq("question_id", question_id)
.maybeSingle();
// Get partner's response
// Get partner's response
const { data: partnerResponse } = await supabase
.from("responses")
.select("answer, response_data, user_id")
@@ -196,7 +216,7 @@ Deno.serve(async (req) => {
[user.id]: normalizedResponseData ?? null,
[partnerResponse.user_id]: partnerResponse.response_data ?? null,
}
: null;
: null;
// Case 1: Changing to "no" AND match exists - requires confirmation
if (new_answer === "no" && existingMatch) {
@@ -247,7 +267,7 @@ Deno.serve(async (req) => {
result.match_deleted = true;
}
// Case 2: Changing FROM "no" to yes/maybe - may create a new match
if (currentResponse.answer === "no" && new_answer !== "no" && partnerResponse) {
if (matchType) {
@@ -309,7 +329,7 @@ Deno.serve(async (req) => {
.from("responses")
.update(responseUpdate)
.eq("user_id", user.id)
.eq("question_id", question_id);
.eq("question_id", question_id);
if (updateError) {
return new Response(