Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apps/web/app/api/admin/analytics/overview/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { prisma, ServerStatus } from "@parcel-society/db";
import { requireAdminAuth } from "../../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../../lib/api/responses";

export async function GET(request: Request) {
try {
await requireAdminAuth(request);
const [serversByStatus, playerCount, decisionCount, contractCount, treasuryAggregate] = await Promise.all([
prisma.server.groupBy({ by: ["status"], _count: { _all: true } }),
prisma.player.count(),
prisma.decision.count(),
prisma.contract.count(),
prisma.server.aggregate({ _sum: { treasury: true }, where: { status: { not: ServerStatus.ARCHIVED } } }),
]);
return apiOk({
serversByStatus,
playerCount,
decisionCount,
contractCount,
activeTreasury: treasuryAggregate._sum.treasury ?? 0,
});
} catch (error) {
return handleApiError(error);
}
}
15 changes: 15 additions & 0 deletions apps/web/app/api/admin/servers/[serverId]/archive/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { prisma, ServerStatus } from "@parcel-society/db";
import { requireAdminAuth } from "../../../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../../../lib/api/responses";
import { serverIdParamsSchema } from "../../../../../../lib/api/schemas";

export async function POST(request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
await requireAdminAuth(request);
const { serverId } = serverIdParamsSchema.parse(await context.params);
const server = await prisma.server.update({ where: { id: serverId }, data: { status: ServerStatus.ARCHIVED } });
return apiOk({ server });
} catch (error) {
return handleApiError(error);
}
}
52 changes: 52 additions & 0 deletions apps/web/app/api/admin/servers/[serverId]/export/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { prisma } from "@parcel-society/db";
import { requireAdminAuth } from "../../../../../../lib/api/auth";
import { handleApiError } from "../../../../../../lib/api/responses";
import { serverIdParamsSchema } from "../../../../../../lib/api/schemas";

const csvEscape = (value: unknown): string => {
if (value === null || value === undefined) return "";
const text = value instanceof Date ? value.toISOString() : typeof value === "object" ? JSON.stringify(value) : String(value);
return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
};

const tableToCsv = (rows: object[]): string => {
const headers = [...new Set(rows.flatMap((row) => Object.keys(row)))];
return [
headers.join(","),
...rows.map((row) =>
headers
.map((header) => csvEscape((row as Record<string, unknown>)[header]))
.join(","),
),
].join("\n");
};

export async function GET(request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
await requireAdminAuth(request);
const { serverId } = serverIdParamsSchema.parse(await context.params);
const [players, parcels, decisions, contracts, serverEvents, treasuryTransactions, roundOutcomes] = await Promise.all([
prisma.player.findMany({ where: { serverId }, orderBy: { createdAt: "asc" } }),
prisma.parcel.findMany({ where: { serverId }, orderBy: [{ y: "asc" }, { x: "asc" }] }),
prisma.decision.findMany({ where: { serverId }, orderBy: [{ roundNumber: "asc" }, { createdAt: "asc" }] }),
prisma.contract.findMany({ where: { serverId }, orderBy: [{ roundNumber: "asc" }, { createdAt: "asc" }] }),
prisma.serverEvent.findMany({ where: { serverId }, orderBy: [{ roundNumber: "asc" }, { createdAt: "asc" }] }),
prisma.treasuryTransaction.findMany({ where: { serverId }, orderBy: [{ roundNumber: "asc" }, { createdAt: "asc" }] }),
prisma.playerRoundState.findMany({ where: { serverId }, orderBy: [{ roundNumber: "asc" }, { playerId: "asc" }] }),
]);

const files = {
players: tableToCsv(players),
parcels: tableToCsv(parcels),
decisions: tableToCsv(decisions),
contracts: tableToCsv(contracts),
server_events: tableToCsv(serverEvents),
treasury_transactions: tableToCsv(treasuryTransactions),
round_outcomes: tableToCsv(roundOutcomes),
};

return Response.json({ ok: true, data: { files } });
} catch (error) {
return handleApiError(error);
}
}
16 changes: 16 additions & 0 deletions apps/web/app/api/admin/servers/[serverId]/generate-map/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { requireAdminAuth } from "../../../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../../../lib/api/responses";
import { generateMapSchema, serverIdParamsSchema } from "../../../../../../lib/api/schemas";
import { createServerMap } from "../../../../../../lib/services/game";

export async function POST(request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
await requireAdminAuth(request);
const { serverId } = serverIdParamsSchema.parse(await context.params);
const body = generateMapSchema.parse(await request.json().catch(() => ({})));
const result = await createServerMap({ serverId, width: body.width, height: body.height });
return apiOk(result);
} catch (error) {
return handleApiError(error);
}
}
14 changes: 14 additions & 0 deletions apps/web/app/api/admin/servers/[serverId]/resolve-round/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { requireAdminAuth } from "../../../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../../../lib/api/responses";
import { serverIdParamsSchema } from "../../../../../../lib/api/schemas";
import { resolveActiveRound } from "../../../../../../lib/services/game";

export async function POST(request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
await requireAdminAuth(request);
const { serverId } = serverIdParamsSchema.parse(await context.params);
return apiOk({ summary: await resolveActiveRound(serverId) });
} catch (error) {
return handleApiError(error);
}
}
37 changes: 37 additions & 0 deletions apps/web/app/api/admin/servers/[serverId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { prisma } from "@parcel-society/db";
import { requireAdminAuth } from "../../../../../lib/api/auth";
import { ApiException, apiOk, handleApiError } from "../../../../../lib/api/responses";
import { serverIdParamsSchema, updateServerSchema } from "../../../../../lib/api/schemas";

export async function GET(request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
await requireAdminAuth(request);
const { serverId } = serverIdParamsSchema.parse(await context.params);
const server = await prisma.server.findUnique({
where: { id: serverId },
include: {
seasons: true,
rounds: true,
parcels: true,
players: true,
_count: { select: { decisions: true, contracts: true, events: true } },
},
});
if (!server) throw new ApiException(404, "SERVER_NOT_FOUND", "Server was not found.");
return apiOk({ server });
} catch (error) {
return handleApiError(error);
}
}

export async function PATCH(request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
await requireAdminAuth(request);
const { serverId } = serverIdParamsSchema.parse(await context.params);
const body = updateServerSchema.parse(await request.json());
const server = await prisma.server.update({ where: { id: serverId }, data: body });
return apiOk({ server });
} catch (error) {
return handleApiError(error);
}
}
14 changes: 14 additions & 0 deletions apps/web/app/api/admin/servers/[serverId]/start/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { requireAdminAuth } from "../../../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../../../lib/api/responses";
import { serverIdParamsSchema } from "../../../../../../lib/api/schemas";
import { startServer } from "../../../../../../lib/services/game";

export async function POST(request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
await requireAdminAuth(request);
const { serverId } = serverIdParamsSchema.parse(await context.params);
return apiOk(await startServer(serverId));
} catch (error) {
return handleApiError(error);
}
}
35 changes: 35 additions & 0 deletions apps/web/app/api/admin/servers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { randomUUID } from "node:crypto";
import { prisma, ServerStatus } from "@parcel-society/db";
import { requireAdminAuth } from "../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../lib/api/responses";
import { createServerSchema } from "../../../../lib/api/schemas";

export async function GET(request: Request) {
try {
await requireAdminAuth(request);
const servers = await prisma.server.findMany({
include: { _count: { select: { players: true, parcels: true, decisions: true } } },
orderBy: { createdAt: "desc" },
});
return apiOk({ servers });
} catch (error) {
return handleApiError(error);
}
}

export async function POST(request: Request) {
try {
await requireAdminAuth(request);
const body = createServerSchema.parse(await request.json());
const server = await prisma.server.create({
data: {
...body,
randomSeed: body.randomSeed ?? randomUUID(),
status: ServerStatus.DRAFT,
},
});
return apiOk({ server }, { status: 201 });
} catch (error) {
return handleApiError(error);
}
}
15 changes: 15 additions & 0 deletions apps/web/app/api/join-server/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { joinServerSchema } from "../../../lib/api/schemas";
import { apiOk, handleApiError } from "../../../lib/api/responses";
import { applyAuthCookie, getParticipantAuth } from "../../../lib/api/auth";
import { joinWaitingServer } from "../../../lib/services/game";

export async function POST(request: Request) {
try {
const auth = await getParticipantAuth();
const body = joinServerSchema.parse(await request.json());
const player = await joinWaitingServer({ userId: auth.user.id, serverId: body.serverId });
return applyAuthCookie(apiOk({ user: auth.user, player }), auth);
} catch (error) {
return handleApiError(error);
}
}
16 changes: 16 additions & 0 deletions apps/web/app/api/me/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { prisma } from "@parcel-society/db";
import { applyAuthCookie, getParticipantAuth } from "../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../lib/api/responses";

export async function GET() {
try {
const auth = await getParticipantAuth();
const players = await prisma.player.findMany({
where: { userId: auth.user.id },
include: { server: true, parcel: true },
});
return applyAuthCookie(apiOk({ user: auth.user, players }), auth);
} catch (error) {
return handleApiError(error);
}
}
24 changes: 24 additions & 0 deletions apps/web/app/api/servers/[serverId]/decisions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ActionType } from "@parcel-society/db";
import { applyAuthCookie, getParticipantAuth } from "../../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../../lib/api/responses";
import { serverIdParamsSchema, submitDecisionsSchema } from "../../../../../lib/api/schemas";
import { submitPlayerDecisions } from "../../../../../lib/services/game";

export async function POST(request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
const { serverId } = serverIdParamsSchema.parse(await context.params);
const auth = await getParticipantAuth();
const body = submitDecisionsSchema.parse(await request.json());
const result = await submitPlayerDecisions({
userId: auth.user.id,
serverId,
decisions: body.decisions.map((decision) => ({
...decision,
type: decision.type as ActionType,
})),
});
return applyAuthCookie(apiOk(result), auth);
} catch (error) {
return handleApiError(error);
}
}
20 changes: 20 additions & 0 deletions apps/web/app/api/servers/[serverId]/exit/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { prisma } from "@parcel-society/db";
import { assertParticipantOnServer, applyAuthCookie, getParticipantAuth } from "../../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../../lib/api/responses";
import { serverIdParamsSchema } from "../../../../../lib/api/schemas";

export async function POST(_request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
const { serverId } = serverIdParamsSchema.parse(await context.params);
const auth = await getParticipantAuth();
const player = await assertParticipantOnServer(auth.user.id, serverId);
const server = await prisma.server.findUnique({ where: { id: serverId } });
const updated = await prisma.player.update({
where: { id: player.id },
data: { exited: true, roundExited: server?.currentRound ?? player.roundExited },
});
return applyAuthCookie(apiOk({ player: updated }), auth);
} catch (error) {
return handleApiError(error);
}
}
19 changes: 19 additions & 0 deletions apps/web/app/api/servers/[serverId]/round-summary/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { prisma } from "@parcel-society/db";
import { assertParticipantOnServer, applyAuthCookie, getParticipantAuth } from "../../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../../lib/api/responses";
import { serverIdParamsSchema } from "../../../../../lib/api/schemas";

export async function GET(_request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
const { serverId } = serverIdParamsSchema.parse(await context.params);
const auth = await getParticipantAuth();
const player = await assertParticipantOnServer(auth.user.id, serverId);
const latest = await prisma.playerRoundState.findFirst({
where: { serverId, playerId: player.id },
orderBy: { roundNumber: "desc" },
});
return applyAuthCookie(apiOk({ summary: latest?.state ?? null }), auth);
} catch (error) {
return handleApiError(error);
}
}
23 changes: 23 additions & 0 deletions apps/web/app/api/servers/[serverId]/state/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { prisma } from "@parcel-society/db";
import { assertParticipantOnServer, applyAuthCookie, getParticipantAuth } from "../../../../../lib/api/auth";
import { handleApiError, apiOk } from "../../../../../lib/api/responses";
import { serverIdParamsSchema } from "../../../../../lib/api/schemas";

export async function GET(_request: Request, context: { params: Promise<{ serverId: string }> }) {
try {
const { serverId } = serverIdParamsSchema.parse(await context.params);
const auth = await getParticipantAuth();
const player = await assertParticipantOnServer(auth.user.id, serverId);
const server = await prisma.server.findUnique({
where: { id: serverId },
include: {
parcels: true,
rounds: { orderBy: { roundNumber: "desc" }, take: 1 },
players: { select: { id: true, parcelId: true, wealth: true, exited: true } },
},
});
return applyAuthCookie(apiOk({ server, player }), auth);
} catch (error) {
return handleApiError(error);
}
}
17 changes: 17 additions & 0 deletions apps/web/app/api/servers/available/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { prisma, ServerStatus } from "@parcel-society/db";
import { applyAuthCookie, getParticipantAuth } from "../../../../lib/api/auth";
import { apiOk, handleApiError } from "../../../../lib/api/responses";

export async function GET() {
try {
const auth = await getParticipantAuth();
const servers = await prisma.server.findMany({
where: { status: ServerStatus.WAITING },
include: { _count: { select: { players: true, parcels: true } } },
orderBy: { createdAt: "desc" },
});
return applyAuthCookie(apiOk({ servers }), auth);
} catch (error) {
return handleApiError(error);
}
}
Loading