188 lines
6 KiB
TypeScript
188 lines
6 KiB
TypeScript
|
|
import type { SessionSnapshot } from "../shared/types";
|
||
|
|
import type { BunRequest } from "bun";
|
||
|
|
import { applySnapshot, joinSession, snapshotSession } from "./domain";
|
||
|
|
import {
|
||
|
|
createSession,
|
||
|
|
createTestPreview,
|
||
|
|
createTestSession,
|
||
|
|
getSession,
|
||
|
|
getSessionByCode,
|
||
|
|
isTestSessionCode,
|
||
|
|
} from "./store";
|
||
|
|
import { broadcastSessionState, startTestSimulation } from "./websocket";
|
||
|
|
|
||
|
|
function jsonResponse(data: unknown, status = 200): Response {
|
||
|
|
return Response.json(data, { status });
|
||
|
|
}
|
||
|
|
|
||
|
|
async function readJson<T>(req: Request): Promise<T> {
|
||
|
|
return (await req.json()) as T;
|
||
|
|
}
|
||
|
|
|
||
|
|
function previewSession(session: SessionSnapshot) {
|
||
|
|
return {
|
||
|
|
sessionId: session.id,
|
||
|
|
code: session.code,
|
||
|
|
status: session.status,
|
||
|
|
players: session.players.map((player) => ({
|
||
|
|
id: player.id,
|
||
|
|
name: player.name,
|
||
|
|
role: player.role,
|
||
|
|
isDummy: player.isDummy,
|
||
|
|
connected: player.connected,
|
||
|
|
})),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function readBankerId(req: Request): string | null {
|
||
|
|
const url = new URL(req.url);
|
||
|
|
return url.searchParams.get("bankerId");
|
||
|
|
}
|
||
|
|
|
||
|
|
function isSnapshotCandidate(value: unknown): value is SessionSnapshot {
|
||
|
|
if (!value || typeof value !== "object") return false;
|
||
|
|
const snapshot = value as SessionSnapshot;
|
||
|
|
return (
|
||
|
|
typeof snapshot.id === "string" &&
|
||
|
|
typeof snapshot.code === "string" &&
|
||
|
|
typeof snapshot.status === "string" &&
|
||
|
|
typeof snapshot.createdAt === "number" &&
|
||
|
|
typeof snapshot.bankerId === "string" &&
|
||
|
|
typeof snapshot.blackoutActive === "boolean" &&
|
||
|
|
Array.isArray(snapshot.players) &&
|
||
|
|
Array.isArray(snapshot.transactions) &&
|
||
|
|
Array.isArray(snapshot.chats) &&
|
||
|
|
Array.isArray(snapshot.groups) &&
|
||
|
|
Array.isArray(snapshot.takeoverRequests)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export const apiRoutes = {
|
||
|
|
"/api/health": {
|
||
|
|
GET() {
|
||
|
|
return jsonResponse({ ok: true });
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"/api/session": {
|
||
|
|
async POST(req: BunRequest) {
|
||
|
|
let body: { bankerName?: string };
|
||
|
|
try {
|
||
|
|
body = await readJson<{ bankerName?: string }>(req);
|
||
|
|
} catch {
|
||
|
|
return jsonResponse({ message: "Invalid request body" }, 400);
|
||
|
|
}
|
||
|
|
const bankerName = body.bankerName?.trim() || "Banker";
|
||
|
|
const { session, banker } = createSession(bankerName);
|
||
|
|
return jsonResponse({
|
||
|
|
sessionId: session.id,
|
||
|
|
sessionCode: session.code,
|
||
|
|
playerId: banker.id,
|
||
|
|
role: banker.role,
|
||
|
|
status: session.status,
|
||
|
|
});
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"/api/session/:code/join": {
|
||
|
|
async POST(req: BunRequest) {
|
||
|
|
const code = req.params.code;
|
||
|
|
let body: { name?: string; playerId?: string };
|
||
|
|
try {
|
||
|
|
body = await readJson<{ name?: string; playerId?: string }>(req);
|
||
|
|
} catch {
|
||
|
|
return jsonResponse({ message: "Invalid request body" }, 400);
|
||
|
|
}
|
||
|
|
if (isTestSessionCode(code)) {
|
||
|
|
const session = createTestSession();
|
||
|
|
const player = joinSession(session, body.name ?? "Player", body.playerId);
|
||
|
|
startTestSimulation(session.id);
|
||
|
|
return jsonResponse({
|
||
|
|
sessionId: session.id,
|
||
|
|
sessionCode: session.code,
|
||
|
|
playerId: player.id,
|
||
|
|
role: player.role,
|
||
|
|
status: session.status,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const session = getSessionByCode(code) ?? getSession(code);
|
||
|
|
if (!session) {
|
||
|
|
return jsonResponse({ message: "Session not found" }, 404);
|
||
|
|
}
|
||
|
|
if (session.status === "ended") {
|
||
|
|
return jsonResponse({ message: "Session has ended" }, 400);
|
||
|
|
}
|
||
|
|
const player = joinSession(session, body.name ?? "Player", body.playerId);
|
||
|
|
return jsonResponse({
|
||
|
|
sessionId: session.id,
|
||
|
|
sessionCode: session.code,
|
||
|
|
playerId: player.id,
|
||
|
|
role: player.role,
|
||
|
|
status: session.status,
|
||
|
|
});
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"/api/session/:id": {
|
||
|
|
GET(req: BunRequest) {
|
||
|
|
const session = getSession(req.params.id);
|
||
|
|
if (!session) {
|
||
|
|
return jsonResponse({ message: "Session not found" }, 404);
|
||
|
|
}
|
||
|
|
const snapshot: SessionSnapshot = snapshotSession(session);
|
||
|
|
return jsonResponse(snapshot);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"/api/session/:id/state": {
|
||
|
|
GET(req: BunRequest) {
|
||
|
|
const session = getSession(req.params.id) ?? getSessionByCode(req.params.id);
|
||
|
|
if (!session) {
|
||
|
|
return jsonResponse({ message: "Session not found" }, 404);
|
||
|
|
}
|
||
|
|
const bankerId = readBankerId(req);
|
||
|
|
if (!bankerId || bankerId !== session.bankerId) {
|
||
|
|
return jsonResponse({ message: "Not authorized" }, 403);
|
||
|
|
}
|
||
|
|
const snapshot: SessionSnapshot = snapshotSession(session);
|
||
|
|
return jsonResponse(snapshot);
|
||
|
|
},
|
||
|
|
async POST(req: BunRequest) {
|
||
|
|
const session = getSession(req.params.id) ?? getSessionByCode(req.params.id);
|
||
|
|
if (!session) {
|
||
|
|
return jsonResponse({ message: "Session not found" }, 404);
|
||
|
|
}
|
||
|
|
let body: Record<string, unknown>;
|
||
|
|
try {
|
||
|
|
body = await readJson<Record<string, unknown>>(req);
|
||
|
|
} catch {
|
||
|
|
return jsonResponse({ message: "Invalid request body" }, 400);
|
||
|
|
}
|
||
|
|
const bankerId = (typeof body.bankerId === "string" ? body.bankerId : null) ?? readBankerId(req);
|
||
|
|
if (!bankerId || bankerId !== session.bankerId) {
|
||
|
|
return jsonResponse({ message: "Not authorized" }, 403);
|
||
|
|
}
|
||
|
|
|
||
|
|
const candidate = (body.state ?? body) as unknown;
|
||
|
|
if (!isSnapshotCandidate(candidate)) {
|
||
|
|
return jsonResponse({ message: "Invalid session snapshot" }, 400);
|
||
|
|
}
|
||
|
|
|
||
|
|
applySnapshot(session, candidate);
|
||
|
|
broadcastSessionState(session.id);
|
||
|
|
return jsonResponse({ ok: true });
|
||
|
|
},
|
||
|
|
},
|
||
|
|
"/api/session/:code/info": {
|
||
|
|
GET(req: BunRequest) {
|
||
|
|
const code = req.params.code;
|
||
|
|
if (isTestSessionCode(code)) {
|
||
|
|
const snapshot = createTestPreview();
|
||
|
|
return jsonResponse(previewSession(snapshot));
|
||
|
|
}
|
||
|
|
const session = getSessionByCode(code) ?? getSession(code);
|
||
|
|
if (!session) {
|
||
|
|
return jsonResponse({ message: "Session not found" }, 404);
|
||
|
|
}
|
||
|
|
const snapshot: SessionSnapshot = snapshotSession(session);
|
||
|
|
return jsonResponse(previewSession(snapshot));
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|