CoompanionApp/server/api.ts

188 lines
6 KiB
TypeScript
Raw Normal View History

2026-02-03 13:48:56 +01:00
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));
},
},
};