Add account restrictions, appeals, and admin moderation tools

This commit is contained in:
2026-10-13 12:32:37 +05:00
parent 018be5049e
commit cfe5820802
28 changed files with 1936 additions and 125 deletions

View File

@@ -2,7 +2,8 @@ import "server-only";
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { admin, required, user } from "./supabase";
import { admin, required, user, assertAccountActive } from "./supabase";
import { accountControlsApi } from "./account-controls-api";
import {
datasetSchema,
faces,
@@ -64,6 +65,7 @@ async function contributor(request: NextRequest, browserOnly = false) {
owner = row.owner_id;
} else owner = (await user())?.id;
if (!owner) throw new Error("Sign in to continue");
await assertAccountActive(owner);
const { error } = await admin()
.from("profiles")
.upsert({ id: owner }, { onConflict: "id", ignoreDuplicates: true });
@@ -97,7 +99,7 @@ async function limit(key: string, count: number, seconds: number) {
}
async function ownedDataset(request: NextRequest, id: string) {
const owner = await contributor(request);
return required(
const dataset = required(
await admin()
.from("datasets")
.select("*")
@@ -105,6 +107,8 @@ async function ownedDataset(request: NextRequest, id: string) {
.eq("owner_id", owner)
.single(),
);
// The ownership predicate above proves this is a current account's map.
return { ...dataset, owner_id: owner };
}
async function session(request: NextRequest, id: string) {
const record = required(
@@ -119,8 +123,16 @@ async function session(request: NextRequest, id: string) {
export async function handle(request: NextRequest, path: string[]) {
try {
verifyOrigin(request);
if (
(path[0] === "ban" && path[1] === "appeal") ||
(path[0] === "moderation" &&
["accounts", "account-appeals"].includes(path[1]))
)
return await accountControlsApi(request, path, { jsonBody, limit });
if (path[0] === "auth" || path[0] === "profile")
return await accountApi(request, path, { jsonBody, limit });
// Check signed-in identities before any guest-cookie fallback (games/rooms).
if (path[0] !== "health") await user();
if (path[0] === "rooms")
return await multiplayerApi(request, path, { jsonBody, limit });
if (path[0] === "bug-reports")
@@ -733,7 +745,13 @@ export async function handle(request: NextRequest, path: string[]) {
await client
.from("profiles")
.select("id,display_name")
.in("id", [...new Set(rows.map((c) => c.owner_id))]),
.in("id", [
...new Set(
rows
.map((c) => c.owner_id)
.filter((id): id is string => id !== null),
),
]),
)
: [];
return NextResponse.json({
@@ -1066,12 +1084,14 @@ export async function handle(request: NextRequest, path: string[]) {
.is("parent_dataset_id", null)
.in("state", ["review", "published", "unpublished"])
.order("created_at");
if (!isAdministrator(role))
const adminView = request.nextUrl.searchParams.get("view") === "admin";
if (adminView) requireAdministrator(role);
if (!adminView)
mapQuery = mapQuery.or(
"state.eq.review,and(state.eq.published,update_state.eq.review)",
);
const maps = required(await mapQuery);
const reports = isAdministrator(role)
const reports = adminView
? required(
await client
.from("reports")
@@ -1080,7 +1100,7 @@ export async function handle(request: NextRequest, path: string[]) {
.order("created_at"),
)
: [];
const appeals = isAdministrator(role)
const appeals = adminView
? required(
await client
.from("map_appeals")