Files
joshuaandClaude Opus 5.5 825eb14f2e Phase 13: questions, sounds and videos on the scoreboard
A game can now carry a list of items the host puts on the scoreboard one at
a time: a question (shown above the standings, with an optional picture and
an answer to reveal), a sound (played through the scoreboard's speakers) or
a video such as a karaoke track (takes over the screen).

- Media is a YouTube link, an https link to a file, or an upload to the
  private game-media bucket. The scoreboard API signs a URL only for the
  item that is on screen.
- What is on screen is state on the event, like the timer, so the ping
  still carries no data. An unrevealed answer and a sound's or video's name
  never reach the scoreboard payload.
- Control room: click an item to show it, or "Random" to have one picked
  (prefers items not played yet). Show/hide answer, pause, take off screen;
  shortcuts Q / A / X.
- Duplicating a game or event copies its items and files; deleting removes
  the files.

Needs supabase/migrations/0006_screen_items.sql applied before deploying.
database.types.ts was edited by hand to match; re-run gen:types after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:54:08 +02:00

50 lines
1.9 KiB
JavaScript

// Proves the anon key has no table access (CLAUDE.md rule 2).
// Run with: npm run check:rls
import { createClient } from "@supabase/supabase-js";
const TABLES = ["events", "teams", "games", "criteria", "judges", "scores", "adjustments", "buzzes", "event_cohosts", "game_items"];
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!url || !anonKey || !serviceKey) {
console.error("Missing Supabase env vars. Fill in .env.local first.");
process.exit(1);
}
const options = { auth: { persistSession: false, autoRefreshToken: false } };
const anon = createClient(url, anonKey, options);
const service = createClient(url, serviceKey, options);
let failed = false;
const fail = (message) => {
failed = true;
console.error(` FAIL ${message}`);
};
for (const table of TABLES) {
console.log(table);
// Not a HEAD request: those report no error when the table is missing.
const real = await service.from(table).select("id", { count: "exact" }).limit(1);
if (real.error) {
fail(`table not reachable with the service key (${real.error.code}). Has the migration been applied?`);
continue;
}
const read = await anon.from(table).select("id").limit(1);
if (read.data?.length) fail("anon can read rows");
else if (real.count === 0) console.log(" ok anon reads nothing (table is empty, so this is a weak check)");
else console.log(` ok anon reads nothing (${real.count} rows exist)`);
const write = await anon.from(table).insert({}).select("id");
if (!write.error) {
fail("anon inserted a row");
await service.from(table).delete().in("id", write.data.map((row) => row.id));
} else {
console.log(` ok anon insert denied (${write.error.code})`);
}
}
console.log(failed ? "\nRLS check FAILED" : "\nRLS check passed: anon has no table access.");
process.exit(failed ? 1 : 0);