Harden gameplay timing and map loading

This commit is contained in:
2026-10-05 15:15:35 +05:00
parent 8b31891776
commit dcc3779262
36 changed files with 1050 additions and 205 deletions

View File

@@ -11,7 +11,8 @@ import {
panoramaPath,
viewpointSchema,
} from "../shared/protocol";
import { mediaUrl } from "./catalog";
import { mediaUrl, mediaUrls } from "./catalog";
import { ServiceError, checkServiceError } from "./service-error";
import { requestOrigin, verifyOrigin } from "./request-origin";
import { accountApi } from "./account-api";
const hash = (value: string) =>
@@ -107,7 +108,7 @@ export async function handle(request: NextRequest, path: string[]) {
return await accountApi(request, path, { jsonBody, limit });
const client = admin(),
method = request.method;
if (path[0] === "health") {
if (path[0] === "health" && path.length === 1 && method === "GET") {
const { error } = await client
.from("catalog_servers")
.select("id")
@@ -117,7 +118,7 @@ export async function handle(request: NextRequest, path: string[]) {
{ status: error ? 503 : 200 },
);
}
if (method === "GET" && path[0] === "catalog") {
if (method === "GET" && path[0] === "catalog" && path.length === 1) {
const rows = required(
await client.from("datasets").select("*").eq("state", "published"),
);
@@ -153,14 +154,16 @@ export async function handle(request: NextRequest, path: string[]) {
response.cookies.set("mg_guest", secret, {
httpOnly: true,
sameSite: "lax",
secure: new URL(request.url).protocol === "https:",
secure: requestOrigin(request, process.env.MINEGEO_URL).startsWith(
"https:",
),
path: "/",
maxAge: 60 * 60 * 24 * 30,
});
return response;
}
const s = await session(request, path[1]);
if (method === "GET") {
if (method === "GET" && path.length === 2) {
const revision = required(
await client
.from("revisions")
@@ -176,6 +179,18 @@ export async function handle(request: NextRequest, path: string[]) {
.eq("session_id", s.id)
.order("round_number"),
);
const signed = await mediaUrls([
`${revision.asset_root}/map.webp`,
...rounds.flatMap((r) =>
faces.map((f) =>
panoramaPath(
revision.asset_root,
viewpointSchema.parse(r.viewpoint),
`${f}.webp`,
),
),
),
]);
return NextResponse.json(
{
id: s.id,
@@ -183,31 +198,22 @@ export async function handle(request: NextRequest, path: string[]) {
round: s.current_round,
bounds: manifest.bounds,
name: manifest.name,
map: await mediaUrl(`${revision.asset_root}/map.webp`),
rounds: await Promise.all(
rounds.map(async (r) => ({
number: r.round_number,
deadline: r.deadline,
result: r.result,
faces: await Promise.all(
faces.map((f) =>
mediaUrl(
panoramaPath(
revision.asset_root,
viewpointSchema.parse(r.viewpoint),
`${f}.webp`,
),
),
),
),
})),
),
map: signed[0],
rounds: rounds.map((r, index) => ({
number: r.round_number,
deadline: r.deadline,
result: r.result,
faces: signed.slice(
1 + index * faces.length,
1 + (index + 1) * faces.length,
),
})),
serverNow: Date.now(),
},
{ headers: { "Cache-Control": "no-store" } },
);
}
if (method === "POST" && path[2] === "activate") {
if (method === "POST" && path[2] === "activate" && path.length === 3) {
const body = z
.object({ round: z.number().int().min(0).max(4) })
.parse(await jsonBody(request));
@@ -220,7 +226,7 @@ export async function handle(request: NextRequest, path: string[]) {
);
return NextResponse.json({ deadline, serverNow: Date.now() });
}
if (method === "POST" && path[2] === "guess") {
if (method === "POST" && path[2] === "guess" && path.length === 3) {
const body = z
.object({
round: z.number().int().min(0).max(4),
@@ -272,7 +278,7 @@ export async function handle(request: NextRequest, path: string[]) {
p_dataset: d.id,
p_owner: owner,
});
if (serverCheck.error) throw new Error(serverCheck.error.message);
checkServiceError(serverCheck.error);
const input = required(
await client
.from("dataset_inputs")
@@ -371,7 +377,7 @@ export async function handle(request: NextRequest, path: string[]) {
.select("state,progress,error,updated_at,lease_until")
.eq("dataset_id", d.id)
.maybeSingle();
if (jobResult.error) throw new Error(jobResult.error.message);
checkServiceError(jobResult.error);
return NextResponse.json({
...d,
upload_jobs: jobResult.data,
@@ -420,7 +426,7 @@ export async function handle(request: NextRequest, path: string[]) {
p_owner: d.owner_id,
p_address: body.contributionServerAddress ?? null,
});
if (error) throw new Error(error.message);
checkServiceError(error);
return NextResponse.json({ ok: true });
}
if (method === "PATCH") {
@@ -438,7 +444,7 @@ export async function handle(request: NextRequest, path: string[]) {
p_description: body.description,
p_ids: body.selectedIds,
});
if (error) throw new Error(error.message);
checkServiceError(error);
return NextResponse.json({ ok: true });
}
if (method === "POST" && path[2] === "submit") {
@@ -446,7 +452,7 @@ export async function handle(request: NextRequest, path: string[]) {
p_dataset: d.id,
p_owner: d.owner_id,
});
if (error) throw new Error(error.message);
checkServiceError(error);
return NextResponse.json({ ok: true });
}
if (method === "POST" && path[2] === "cancel") {
@@ -455,7 +461,7 @@ export async function handle(request: NextRequest, path: string[]) {
p_dataset: d.id,
p_owner: d.owner_id,
});
if (error) throw new Error(error.message);
checkServiceError(error);
return NextResponse.json({ ok: true });
}
if (!["uploading", "processing"].includes(d.state))
@@ -467,7 +473,7 @@ export async function handle(request: NextRequest, path: string[]) {
.in("state", ["queued", "running"])
.select("id")
.maybeSingle();
if (cancelled.error) throw new Error(cancelled.error.message);
checkServiceError(cancelled.error);
required(
await client
.from("datasets")
@@ -571,7 +577,7 @@ export async function handle(request: NextRequest, path: string[]) {
p_decision: body.decision,
p_reason: body.reason ?? undefined,
});
if (error) throw new Error(error.message);
checkServiceError(error);
return NextResponse.json({ ok: true });
}
}
@@ -657,7 +663,7 @@ export async function handle(request: NextRequest, path: string[]) {
p_token: token,
p_hash: hash(token),
});
if (error) throw new Error(error.message);
checkServiceError(error);
return NextResponse.json({ ok: true });
}
if (method === "POST" && path[1] === "poll") {
@@ -781,7 +787,7 @@ export async function handle(request: NextRequest, path: string[]) {
p_decision: b.decision,
p_reason: b.reason ?? null,
});
if (error) throw new Error(error.message);
checkServiceError(error);
return NextResponse.json({ ok: true });
}
}
@@ -824,16 +830,25 @@ export async function handle(request: NextRequest, path: string[]) {
const message =
error instanceof z.ZodError
? "Invalid request: " + error.issues.map((i) => i.message).join("; ")
: error instanceof Error
? error.message
: "Request failed";
const status = message.includes("Sign in")
? 401
: message.includes("permission") || message.includes("origin")
? 403
: message.includes("Too many")
? 429
: 400;
: error instanceof SyntaxError
? "Invalid JSON request"
: error instanceof Error
? error.message
: "Request failed";
const status =
path[0] === "health"
? 503
: error instanceof ServiceError
? error.status
: message.includes("Sign in")
? 401
: message.includes("permission") || message.includes("origin")
? 403
: message.includes("Too many")
? 429
: message.includes("not found")
? 404
: 400;
return NextResponse.json({ error: message }, { status });
}
}