mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
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:
1 parent
94c76535a4
commit
c0650ed5b7
10 files changed
+924
-57
No files matched your search
@@ -185,23 +185,34 @@ Deno.serve(async (req) => {
|
||||
|
||||
const { data: safePacks, error: safePacksError } = await supabase
|
||||
.from("question_packs")
|
||||
.select("id")
|
||||
.select("id, category_id, category:categories(content_status)")
|
||||
.lte("avg_intensity", 2)
|
||||
.eq("is_public", true);
|
||||
.eq("is_public", true)
|
||||
.eq("content_status", "allowed");
|
||||
|
||||
if (safePacksError) {
|
||||
console.error("Failed to fetch safe packs:", safePacksError);
|
||||
} else if (safePacks?.length) {
|
||||
const { error: insertError } = await supabase
|
||||
.from("couple_packs")
|
||||
.insert(safePacks.map((pack: { id: string }) => ({
|
||||
couple_id: couple.id,
|
||||
pack_id: pack.id,
|
||||
enabled: true,
|
||||
})));
|
||||
} else {
|
||||
const eligibleSafePacks = (safePacks ?? []).filter((pack) => {
|
||||
const category = Array.isArray(pack.category)
|
||||
? pack.category[0]
|
||||
: pack.category;
|
||||
return pack.category_id === null
|
||||
|| category?.content_status === "allowed";
|
||||
});
|
||||
|
||||
if (insertError) {
|
||||
console.error("Failed to enable safe packs:", insertError);
|
||||
if (eligibleSafePacks.length) {
|
||||
const { error: insertError } = await supabase
|
||||
.from("couple_packs")
|
||||
.insert(eligibleSafePacks.map((pack: { id: string }) => ({
|
||||
couple_id: couple.id,
|
||||
pack_id: pack.id,
|
||||
enabled: true,
|
||||
})));
|
||||
|
||||
if (insertError) {
|
||||
console.error("Failed to enable safe packs:", insertError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,8 +224,8 @@ Deno.serve(async (req) => {
|
||||
}),
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (method === "DELETE") {
|
||||
// Get user's current couple
|
||||
|
||||
@@ -61,8 +61,9 @@ Deno.serve(async (req) => {
|
||||
// Get all packs due for release
|
||||
const { data: duePacks, error: fetchError } = await supabase
|
||||
.from("question_packs")
|
||||
.select("id, name, description")
|
||||
.select("id, name, description, category_id, category:categories(content_status)")
|
||||
.eq("is_public", false)
|
||||
.eq("content_status", "allowed")
|
||||
.eq("release_notified", false)
|
||||
.not("scheduled_release_at", "is", null)
|
||||
.lte("scheduled_release_at", new Date().toISOString());
|
||||
@@ -75,14 +76,22 @@ Deno.serve(async (req) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!duePacks || duePacks.length === 0) {
|
||||
const eligibleDuePacks = (duePacks ?? []).filter((pack) => {
|
||||
const category = Array.isArray(pack.category)
|
||||
? pack.category[0]
|
||||
: pack.category;
|
||||
return pack.category_id === null
|
||||
|| category?.content_status === "allowed";
|
||||
});
|
||||
|
||||
if (eligibleDuePacks.length === 0) {
|
||||
return new Response(
|
||||
JSON.stringify({ released: 0, message: "No packs due for release" }),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
|
||||
const packIds = duePacks.map(p => p.id);
|
||||
const packIds = eligibleDuePacks.map(p => p.id);
|
||||
|
||||
// Make packs public
|
||||
const { error: updateError } = await supabase
|
||||
@@ -133,7 +142,7 @@ Deno.serve(async (req) => {
|
||||
}
|
||||
|
||||
// Send notification for each released pack
|
||||
for (const pack of duePacks) {
|
||||
for (const pack of eligibleDuePacks) {
|
||||
messages.push({
|
||||
to: user.push_token!,
|
||||
title: "New pack available",
|
||||
@@ -150,12 +159,12 @@ Deno.serve(async (req) => {
|
||||
await sendExpoPushNotifications(messages);
|
||||
}
|
||||
|
||||
console.log(`Released ${duePacks.length} packs, sent ${messages.length} notifications`);
|
||||
console.log(`Released ${eligibleDuePacks.length} packs, sent ${messages.length} notifications`);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
released: duePacks.length,
|
||||
packNames: duePacks.map(p => p.name),
|
||||
released: eligibleDuePacks.length,
|
||||
packNames: eligibleDuePacks.map(p => p.name),
|
||||
notificationsSent: messages.length,
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/json" } }
|
||||
|
||||
@@ -96,11 +96,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" } }
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user