feat(admin): preview response media

Show who-likely choices in human terms and provide protected photo and audio previews in activity and user response views. Fetch media by response ID through an authenticated Edge Function that enforces the view_responses permission.
This commit is contained in:
jjbrunton committed 2026-08-27 22:02:06 +01:00
1 parent 7545289dd3
commit 650c81856f
8 files changed
+385 -3

No files matched your search

@@ -0,0 +1,144 @@
// @ts-nocheck
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0?target=deno";
import { canViewResponseMedia, getResponseMediaDescriptor } from "./helpers.ts";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
if (req.method !== "POST") {
return json({ error: "Method not allowed" }, 405);
}
try {
const supabaseAdmin = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
const userId = await getUserIdFromRequest(supabaseAdmin, req);
await assertResponseAccess(supabaseAdmin, userId);
const body = await req.json().catch(() => ({}));
const responseId = typeof body?.responseId === "string"
? body.responseId
: null;
if (!responseId) {
return json({ error: "Missing responseId" }, 400);
}
const { data: response, error: responseError } = await supabaseAdmin
.from("responses")
.select("id, response_data")
.eq("id", responseId)
.maybeSingle();
if (responseError || !response) {
return json({ error: "Response not found" }, 404);
}
const media = getResponseMediaDescriptor(response.response_data);
if (!media) {
return json({ error: "Response has no supported media" }, 400);
}
const { data: mediaBlob, error: downloadError } = await supabaseAdmin
.storage
.from("response-media")
.download(media.path);
if (downloadError || !mediaBlob) {
console.error(
"admin-response-media download failed",
downloadError?.message,
);
return json({ error: "Failed to load response media" }, 500);
}
const fallbackType = media.type === "photo" ? "image/jpeg" : "audio/mp4";
return new Response(mediaBlob, {
status: 200,
headers: {
...corsHeaders,
"Content-Type": mediaBlob.type || fallbackType,
"Cache-Control": "private, no-store",
"X-Content-Type-Options": "nosniff",
},
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
if (
message.includes("authorization header") ||
message.startsWith("Invalid token")
) {
return json({ error: "Unauthorized" }, 401);
}
if (message === "Access denied") {
return json({ error: "Forbidden" }, 403);
}
console.error("admin-response-media error", error);
return json({ error: "Internal server error" }, 500);
}
});
function json(body: unknown, status: number) {
return new Response(JSON.stringify(body), {
status,
headers: {
...corsHeaders,
"Content-Type": "application/json",
"Cache-Control": "no-store",
},
});
}
async function getUserIdFromRequest(
supabaseAdmin: ReturnType<typeof createClient>,
req: Request,
) {
const authHeader = req.headers.get("Authorization");
if (!authHeader) {
throw new Error("Missing authorization header");
}
const { data, error } = await supabaseAdmin.auth.getUser(
authHeader.replace("Bearer ", ""),
);
if (error || !data.user) {
throw new Error(`Invalid token: ${error?.message || "No user"}`);
}
return data.user.id;
}
async function assertResponseAccess(
supabaseAdmin: ReturnType<typeof createClient>,
userId: string,
) {
const { data, error } = await supabaseAdmin
.from("admin_users")
.select("role, permissions")
.eq("user_id", userId)
.maybeSingle();
if (error || !data) {
throw new Error("Access denied");
}
if (!canViewResponseMedia(data.role, data.permissions)) {
throw new Error("Access denied");
}
}