chore(api): add explicit type casting for request payloads

This commit is contained in:
jessy-david-dev
2026-04-11 15:20:29 +02:00
parent 7d1b236b46
commit d5a66c2dac
13 changed files with 204 additions and 96 deletions
+20 -5
View File
@@ -16,8 +16,15 @@ export async function GET() {
const games = await prisma.game.findMany({ const games = await prisma.game.findMany({
where: { userId: session.user.id }, where: { userId: session.user.id },
select: { select: {
id: true, mode: true, startArticle: true, targetArticle: true, id: true,
path: true, clicks: true, timeSeconds: true, won: true, playedAt: true, mode: true,
startArticle: true,
targetArticle: true,
path: true,
clicks: true,
timeSeconds: true,
won: true,
playedAt: true,
}, },
orderBy: { playedAt: "desc" }, orderBy: { playedAt: "desc" },
}); });
@@ -25,8 +32,13 @@ export async function GET() {
const dailyResults = await prisma.dailyResult.findMany({ const dailyResults = await prisma.dailyResult.findMany({
where: { userId: session.user.id }, where: { userId: session.user.id },
select: { select: {
id: true, puzzleId: true, path: true, clicks: true, id: true,
timeSeconds: true, won: true, playedAt: true, puzzleId: true,
path: true,
clicks: true,
timeSeconds: true,
won: true,
playedAt: true,
}, },
orderBy: { playedAt: "desc" }, orderBy: { playedAt: "desc" },
}); });
@@ -34,7 +46,10 @@ export async function GET() {
const data = { const data = {
profile: user, profile: user,
games: games.map((g) => ({ ...g, path: JSON.parse(g.path) as string[] })), games: games.map((g) => ({ ...g, path: JSON.parse(g.path) as string[] })),
dailyResults: dailyResults.map((d) => ({ ...d, path: JSON.parse(d.path) as string[] })), dailyResults: dailyResults.map((d) => ({
...d,
path: JSON.parse(d.path) as string[],
})),
exportedAt: new Date().toISOString(), exportedAt: new Date().toISOString(),
}; };
+3 -3
View File
@@ -3,10 +3,10 @@ import { prisma } from "../../../../../lib/prisma";
type Params = { params: Promise<{ id: string }> }; type Params = { params: Promise<{ id: string }> };
// PATCH /api/admin/daily/[id] modifier un puzzle // PATCH /api/admin/daily/[id] - modifier un puzzle
export async function PATCH(req: Request, { params }: Params) { export async function PATCH(req: Request, { params }: Params) {
const { id } = await params; const { id } = await params;
const { startArticle, targetArticle } = await req.json() as { const { startArticle, targetArticle } = (await req.json()) as {
startArticle: string; startArticle: string;
targetArticle: string; targetArticle: string;
}; };
@@ -17,7 +17,7 @@ export async function PATCH(req: Request, { params }: Params) {
return NextResponse.json(puzzle); return NextResponse.json(puzzle);
} }
// DELETE /api/admin/daily/[id] supprimer un puzzle // DELETE /api/admin/daily/[id] - supprimer un puzzle
export async function DELETE(_req: Request, { params }: Params) { export async function DELETE(_req: Request, { params }: Params) {
const { id } = await params; const { id } = await params;
await prisma.dailyPuzzle.delete({ where: { id } }); await prisma.dailyPuzzle.delete({ where: { id } });
+3 -3
View File
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { prisma } from "../../../../lib/prisma"; import { prisma } from "../../../../lib/prisma";
// GET /api/admin/daily liste tous les puzzles // GET /api/admin/daily - liste tous les puzzles
export async function GET() { export async function GET() {
const puzzles = await prisma.dailyPuzzle.findMany({ const puzzles = await prisma.dailyPuzzle.findMany({
orderBy: { date: "desc" }, orderBy: { date: "desc" },
@@ -11,9 +11,9 @@ export async function GET() {
return NextResponse.json(puzzles); return NextResponse.json(puzzles);
} }
// POST /api/admin/daily créer un puzzle pour une date // POST /api/admin/daily - créer un puzzle pour une date
export async function POST(req: Request) { export async function POST(req: Request) {
const { date, startArticle, targetArticle } = await req.json() as { const { date, startArticle, targetArticle } = (await req.json()) as {
date: string; date: string;
startArticle: string; startArticle: string;
targetArticle: string; targetArticle: string;
+9 -2
View File
@@ -10,8 +10,15 @@ export async function GET() {
const since14 = new Date(todayStart); const since14 = new Date(todayStart);
since14.setDate(since14.getDate() - 13); since14.setDate(since14.getDate() - 13);
const [totalUsers, totalGames, todayGames, recentUsers, recentGames, modeStats, activityRaw] = const [
await Promise.all([ totalUsers,
totalGames,
todayGames,
recentUsers,
recentGames,
modeStats,
activityRaw,
] = await Promise.all([
prisma.user.count(), prisma.user.count(),
prisma.game.count(), prisma.game.count(),
prisma.game.count({ where: { playedAt: { gte: todayStart } } }), prisma.game.count({ where: { playedAt: { gte: todayStart } } }),
+3 -3
View File
@@ -3,17 +3,17 @@ import { prisma } from "../../../../../lib/prisma";
type Params = { params: Promise<{ id: string }> }; type Params = { params: Promise<{ id: string }> };
// DELETE /api/admin/users/[id] supprimer un utilisateur // DELETE /api/admin/users/[id] - supprimer un utilisateur
export async function DELETE(_req: Request, { params }: Params) { export async function DELETE(_req: Request, { params }: Params) {
const { id } = await params; const { id } = await params;
await prisma.user.delete({ where: { id } }); await prisma.user.delete({ where: { id } });
return NextResponse.json({ ok: true }); return NextResponse.json({ ok: true });
} }
// PATCH /api/admin/users/[id] ban/unban // PATCH /api/admin/users/[id] - ban/unban
export async function PATCH(req: Request, { params }: Params) { export async function PATCH(req: Request, { params }: Params) {
const { id } = await params; const { id } = await params;
const { banned } = await req.json() as { banned: boolean }; const { banned } = (await req.json()) as { banned: boolean };
const user = await prisma.user.update({ where: { id }, data: { banned } }); const user = await prisma.user.update({ where: { id }, data: { banned } });
return NextResponse.json({ id: user.id, banned: user.banned }); return NextResponse.json({ id: user.id, banned: user.banned });
} }
+4 -2
View File
@@ -13,11 +13,13 @@ export async function GET() {
take: 20, take: 20,
}); });
return NextResponse.json(results.map((r, i) => ({ return NextResponse.json(
results.map((r, i) => ({
rank: i + 1, rank: i + 1,
name: r.user.name, name: r.user.name,
clicks: r.clicks, clicks: r.clicks,
timeSeconds: r.timeSeconds, timeSeconds: r.timeSeconds,
userId: r.userId, userId: r.userId,
}))); })),
);
} }
+39 -9
View File
@@ -13,7 +13,9 @@ async function getOrCreatePuzzle() {
if (existing) return existing; if (existing) return existing;
const { start, target } = await pickTwoArticles(); const { start, target } = await pickTwoArticles();
return prisma.dailyPuzzle.create({ data: { date, startArticle: start, targetArticle: target } }); return prisma.dailyPuzzle.create({
data: { date, startArticle: start, targetArticle: target },
});
} }
// GET - retourne le puzzle du jour + si l'utilisateur a déjà joué // GET - retourne le puzzle du jour + si l'utilisateur a déjà joué
@@ -24,28 +26,49 @@ export async function GET() {
let myResult = null; let myResult = null;
if (session?.user?.id) { if (session?.user?.id) {
myResult = await prisma.dailyResult.findUnique({ myResult = await prisma.dailyResult.findUnique({
where: { puzzleId_userId: { puzzleId: puzzle.id, userId: session.user.id } }, where: {
puzzleId_userId: { puzzleId: puzzle.id, userId: session.user.id },
},
}); });
} }
return NextResponse.json({ return NextResponse.json({
puzzle: { id: puzzle.id, date: puzzle.date, startArticle: puzzle.startArticle, targetArticle: puzzle.targetArticle }, puzzle: {
id: puzzle.id,
date: puzzle.date,
startArticle: puzzle.startArticle,
targetArticle: puzzle.targetArticle,
},
alreadyPlayed: !!myResult, alreadyPlayed: !!myResult,
myResult: myResult ? { clicks: myResult.clicks, timeSeconds: myResult.timeSeconds, won: myResult.won, path: JSON.parse(myResult.path) } : null, myResult: myResult
? {
clicks: myResult.clicks,
timeSeconds: myResult.timeSeconds,
won: myResult.won,
path: JSON.parse(myResult.path),
}
: null,
}); });
} }
// POST - soumettre un résultat // POST - soumettre un résultat
export async function POST(req: Request) { export async function POST(req: Request) {
const session = await auth(); const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Non connecté" }, { status: 401 }); if (!session?.user?.id)
return NextResponse.json({ error: "Non connecté" }, { status: 401 });
const { puzzleId, path, clicks, timeSeconds, won } = await req.json() as { const { puzzleId, path, clicks, timeSeconds, won } = (await req.json()) as {
puzzleId: string; path: string[]; clicks: number; timeSeconds: number; won: boolean; puzzleId: string;
path: string[];
clicks: number;
timeSeconds: number;
won: boolean;
}; };
// Vérifier que le puzzle est bien celui du jour // Vérifier que le puzzle est bien celui du jour
const puzzle = await prisma.dailyPuzzle.findUnique({ where: { id: puzzleId } }); const puzzle = await prisma.dailyPuzzle.findUnique({
where: { id: puzzleId },
});
if (!puzzle || puzzle.date !== todayKey()) { if (!puzzle || puzzle.date !== todayKey()) {
return NextResponse.json({ error: "Puzzle invalide" }, { status: 400 }); return NextResponse.json({ error: "Puzzle invalide" }, { status: 400 });
} }
@@ -53,7 +76,14 @@ export async function POST(req: Request) {
// Upsert - on n'enregistre qu'une fois // Upsert - on n'enregistre qu'une fois
const result = await prisma.dailyResult.upsert({ const result = await prisma.dailyResult.upsert({
where: { puzzleId_userId: { puzzleId, userId: session.user.id } }, where: { puzzleId_userId: { puzzleId, userId: session.user.id } },
create: { puzzleId, userId: session.user.id, path: JSON.stringify(path), clicks, timeSeconds, won }, create: {
puzzleId,
userId: session.user.id,
path: JSON.stringify(path),
clicks,
timeSeconds,
won,
},
update: {}, update: {},
}); });
+2 -2
View File
@@ -10,7 +10,7 @@ export async function POST(req: NextRequest) {
} }
const { mode, startArticle, targetArticle, path, clicks, timeSeconds, won } = const { mode, startArticle, targetArticle, path, clicks, timeSeconds, won } =
await req.json() as { (await req.json()) as {
mode: string; mode: string;
startArticle: string; startArticle: string;
targetArticle: string; targetArticle: string;
@@ -53,6 +53,6 @@ export async function GET() {
games.map((g) => ({ games.map((g) => ({
...g, ...g,
path: JSON.parse(g.path) as string[], path: JSON.parse(g.path) as string[],
})) })),
); );
} }
+3 -1
View File
@@ -23,7 +23,9 @@ export async function GET() {
const wins = all.filter((g) => g.won); const wins = all.filter((g) => g.won);
const wonGames = wins.filter((g) => g.clicks > 0); const wonGames = wins.filter((g) => g.clicks > 0);
const avgClicks = wonGames.length const avgClicks = wonGames.length
? Math.round(wonGames.reduce((s, g) => s + g.clicks, 0) / wonGames.length) ? Math.round(
wonGames.reduce((s, g) => s + g.clicks, 0) / wonGames.length,
)
: null; : null;
const bestTime = wins.length const bestTime = wins.length
? Math.min(...wins.map((g) => g.timeSeconds)) ? Math.min(...wins.map((g) => g.timeSeconds))
+20 -5
View File
@@ -3,15 +3,27 @@ import bcrypt from "bcryptjs";
import { prisma } from "../../../lib/prisma"; import { prisma } from "../../../lib/prisma";
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const { name, email, password } = await req.json() as { name?: string; email?: string; password?: string }; const { name, email, password } = (await req.json()) as {
name?: string;
email?: string;
password?: string;
};
if (!name?.trim() || !email?.trim() || !password || password.length < 6) { if (!name?.trim() || !email?.trim() || !password || password.length < 6) {
return NextResponse.json({ error: "Champs invalides (mot de passe min. 6 caractères)" }, { status: 400 }); return NextResponse.json(
{ error: "Champs invalides (mot de passe min. 6 caractères)" },
{ status: 400 },
);
} }
const existing = await prisma.user.findUnique({ where: { email: email.toLowerCase() } }); const existing = await prisma.user.findUnique({
where: { email: email.toLowerCase() },
});
if (existing) { if (existing) {
return NextResponse.json({ error: "Cet email est déjà utilisé" }, { status: 409 }); return NextResponse.json(
{ error: "Cet email est déjà utilisé" },
{ status: 409 },
);
} }
const hashed = await bcrypt.hash(password, 10); const hashed = await bcrypt.hash(password, 10);
@@ -19,5 +31,8 @@ export async function POST(req: NextRequest) {
data: { name: name.trim(), email: email.toLowerCase(), password: hashed }, data: { name: name.trim(), email: email.toLowerCase(), password: hashed },
}); });
return NextResponse.json({ id: user.id, name: user.name, email: user.email }, { status: 201 }); return NextResponse.json(
{ id: user.id, name: user.name, email: user.email },
{ status: 201 },
);
} }
+32 -11
View File
@@ -28,14 +28,14 @@ const PLAYER_TIMEOUT_MS = 15_000;
function prunePlayers(room: Room) { function prunePlayers(room: Room) {
const now = Date.now(); const now = Date.now();
room.players = room.players.filter( room.players = room.players.filter(
(p) => now - p.lastSeen < PLAYER_TIMEOUT_MS (p) => now - p.lastSeen < PLAYER_TIMEOUT_MS,
); );
} }
// PATCH /api/rooms/[code] // PATCH /api/rooms/[code]
export async function PATCH( export async function PATCH(
request: NextRequest, request: NextRequest,
{ params }: { params: Promise<{ code: string }> } { params }: { params: Promise<{ code: string }> },
) { ) {
const { code } = await params; const { code } = await params;
const rooms = getRooms(); const rooms = getRooms();
@@ -62,14 +62,24 @@ export async function PATCH(
switch (action) { switch (action) {
// Rejoindre // Rejoindre
case "join": { case "join": {
if (!playerName || typeof playerName !== "string" || playerName.trim() === "") { if (
!playerName ||
typeof playerName !== "string" ||
playerName.trim() === ""
) {
return Response.json({ error: "Pseudo invalide" }, { status: 400 }); return Response.json({ error: "Pseudo invalide" }, { status: 400 });
} }
if (room.players.length >= room.maxPlayers) { if (room.players.length >= room.maxPlayers) {
return Response.json({ error: `Salle pleine (${room.maxPlayers} joueurs max)` }, { status: 409 }); return Response.json(
{ error: `Salle pleine (${room.maxPlayers} joueurs max)` },
{ status: 409 },
);
} }
if (room.phase !== "waiting" && room.phase !== "results") { if (room.phase !== "waiting" && room.phase !== "results") {
return Response.json({ error: "Partie en cours, attends la prochaine manche" }, { status: 409 }); return Response.json(
{ error: "Partie en cours, attends la prochaine manche" },
{ status: 409 },
);
} }
const newId = generatePlayerId(); const newId = generatePlayerId();
@@ -99,10 +109,16 @@ export async function PATCH(
case "start": { case "start": {
const host = room.players.find((p) => p.id === playerId); const host = room.players.find((p) => p.id === playerId);
if (!host?.isHost) { if (!host?.isHost) {
return Response.json({ error: "Seul l'hote peut demarrer" }, { status: 403 }); return Response.json(
{ error: "Seul l'hote peut demarrer" },
{ status: 403 },
);
} }
if (room.players.length < 1) { if (room.players.length < 1) {
return Response.json({ error: "Pas assez de joueurs" }, { status: 400 }); return Response.json(
{ error: "Pas assez de joueurs" },
{ status: 400 },
);
} }
if (!startArticle || !targetArticle) { if (!startArticle || !targetArticle) {
return Response.json({ error: "Articles manquants" }, { status: 400 }); return Response.json({ error: "Articles manquants" }, { status: 400 });
@@ -174,7 +190,7 @@ export async function PATCH(
// 1er joueur a gagner = +10 points // 1er joueur a gagner = +10 points
const alreadyWon = room.players.some( const alreadyWon = room.players.some(
(p) => p.hasWon && p.id !== player.id (p) => p.hasWon && p.id !== player.id,
); );
if (!alreadyWon) { if (!alreadyWon) {
player.score += 10; player.score += 10;
@@ -190,7 +206,10 @@ export async function PATCH(
case "nextRound": { case "nextRound": {
const host = room.players.find((p) => p.id === playerId); const host = room.players.find((p) => p.id === playerId);
if (!host?.isHost) { if (!host?.isHost) {
return Response.json({ error: "Seul l'hote peut continuer" }, { status: 403 }); return Response.json(
{ error: "Seul l'hote peut continuer" },
{ status: 403 },
);
} }
room.phase = "waiting"; room.phase = "waiting";
room.roundWinner = null; room.roundWinner = null;
@@ -207,7 +226,10 @@ export async function PATCH(
case "resetGame": { case "resetGame": {
const host = room.players.find((p) => p.id === playerId); const host = room.players.find((p) => p.id === playerId);
if (!host?.isHost) { if (!host?.isHost) {
return Response.json({ error: "Seul l'hote peut reinitialiser" }, { status: 403 }); return Response.json(
{ error: "Seul l'hote peut reinitialiser" },
{ status: 403 },
);
} }
room.phase = "waiting"; room.phase = "waiting";
room.round = 0; room.round = 0;
@@ -226,4 +248,3 @@ export async function PATCH(
return Response.json({ error: "Action inconnue" }, { status: 400 }); return Response.json({ error: "Action inconnue" }, { status: 400 });
} }
} }
+18 -4
View File
@@ -76,14 +76,28 @@ function pruneOldRooms(rooms: Map<string, Room>) {
// Response: { room: Room, playerId: string } // Response: { room: Room, playerId: string }
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const { playerName, maxPlayers, totalRounds } = body as { playerName: string; maxPlayers?: number; totalRounds?: number }; const { playerName, maxPlayers, totalRounds } = body as {
playerName: string;
maxPlayers?: number;
totalRounds?: number;
};
if (!playerName || typeof playerName !== "string" || playerName.trim() === "") { if (
!playerName ||
typeof playerName !== "string" ||
playerName.trim() === ""
) {
return Response.json({ error: "Pseudo invalide" }, { status: 400 }); return Response.json({ error: "Pseudo invalide" }, { status: 400 });
} }
const clampedMax = Math.min(Math.max(typeof maxPlayers === "number" ? Math.floor(maxPlayers) : 16, 2), 16); const clampedMax = Math.min(
const clampedRounds = Math.min(Math.max(typeof totalRounds === "number" ? Math.floor(totalRounds) : 3, 1), 10); Math.max(typeof maxPlayers === "number" ? Math.floor(maxPlayers) : 16, 2),
16,
);
const clampedRounds = Math.min(
Math.max(typeof totalRounds === "number" ? Math.floor(totalRounds) : 3, 1),
10,
);
const rooms = getRooms(); const rooms = getRooms();
pruneOldRooms(rooms); pruneOldRooms(rooms);
+3 -1
View File
@@ -33,7 +33,9 @@ export async function GET(_req: Request, { params }: Params) {
? Math.round(won.reduce((s, g) => s + g.clicks, 0) / won.length) ? Math.round(won.reduce((s, g) => s + g.clicks, 0) / won.length)
: null; : null;
const bestClicks = won.length ? Math.min(...won.map((g) => g.clicks)) : null; const bestClicks = won.length ? Math.min(...won.map((g) => g.clicks)) : null;
const bestTime = won.length ? Math.min(...won.map((g) => g.timeSeconds)) : null; const bestTime = won.length
? Math.min(...won.map((g) => g.timeSeconds))
: null;
const avgTime = won.length const avgTime = won.length
? won.reduce((s, g) => s + g.timeSeconds, 0) / won.length ? won.reduce((s, g) => s + g.timeSeconds, 0) / won.length
: null; : null;