/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { FastifyPluginAsync } from "fastify"; import { Game, CreateGameDto, UpdateGameDto, Comment, CreateCommentDto, AuditAction, AuditCategory, AchievementCategory } from "@library/shared-types"; import { GameService } from "../../services/game.service"; import { CommentService } from "../../services/comment.service"; import { AuditService } from "../../services/audit.service"; import { AchievementService } from "../../services/achievement.service"; import { adminGuard } from "../../middleware/admin-guard"; import { bannedGuard } from "../../middleware/banned-guard"; const gamesRoutes: FastifyPluginAsync = async (app) => { const gameService = new GameService(); const commentService = new CommentService(); // Get all games (public route) app.get<{ Reply: Game[] }>("/", async () => { return gameService.getAllGames(); }); // Get all games in a series (public route) app.get<{ Params: { seriesName: string }; Reply: Game[] }>( "/series/:seriesName", async (request) => { const { seriesName } = request.params; return gameService.getGamesBySeries(seriesName); } ); // Get single game (public route) app.get<{ Params: { id: string }; Reply: Game | null }>( "/:id", async (request) => { const { id } = request.params; return gameService.getGameById(id); } ); // Create game (protected admin route) app.post<{ Body: CreateGameDto; Reply: Game | { error: string } }>( "/", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, async (request, reply) => { try { const game = await gameService.createGame(request.body); await AuditService.logFromRequest(request, { action: AuditAction.entryCreate, category: AuditCategory.content, resourceType: "game", resourceId: game.id, details: `Created game: ${game.title}`, }); return game; } catch (error) { if (error instanceof Error) { return reply.code(400).send({ error: error.message }); } throw error; } } ); // Update game (protected admin route) app.put<{ Params: { id: string }; Body: UpdateGameDto; Reply: Game | null | { error: string }; }>( "/:id", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, async (request, reply) => { try { const { id } = request.params; const game = await gameService.updateGame(id, request.body); if (game) { await AuditService.logFromRequest(request, { action: AuditAction.entryUpdate, category: AuditCategory.content, resourceType: "game", resourceId: id, details: `Updated game: ${game.title}`, }); } return game; } catch (error) { if (error instanceof Error) { return reply.code(400).send({ error: error.message }); } throw error; } } ); // Delete game (protected admin route) app.delete<{ Params: { id: string }; Reply: { success: boolean } }>( "/:id", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, async (request) => { const { id } = request.params; await gameService.deleteGame(id); await AuditService.logFromRequest(request, { action: AuditAction.entryDelete, category: AuditCategory.content, resourceType: "game", resourceId: id, details: `Deleted game with ID: ${id}`, }); return { success: true }; } ); // Get comments for a game (public route) app.get<{ Params: { id: string }; Reply: Comment[] }>( "/:id/comments", async (request) => { const { id } = request.params; return commentService.getCommentsForGame(id); } ); // Add comment to a game (authenticated users) app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>( "/:id/comments", { preValidation: [app.authenticate, bannedGuard], preHandler: [app.csrfProtection], }, async (request) => { const { id } = request.params; const userId = request.user.id; const comment = await commentService.createCommentForGame(id, userId, request.body); await AuditService.logFromRequest(request, { action: AuditAction.commentCreate, category: AuditCategory.content, resourceType: "game", resourceId: id, details: `Added comment to game`, }); // Check for comment achievements const achievementService = new AchievementService(); await achievementService.checkAchievements( userId, AchievementCategory.Comment, request ); return comment; } ); // Update comment (owner or admin) app.put<{ Params: { id: string; commentId: string }; Body: CreateCommentDto; Reply: Comment | { error: string } }>( "/:id/comments/:commentId", { preValidation: [app.authenticate], preHandler: [app.csrfProtection], }, async (request, reply) => { const { id, commentId } = request.params; const userId = request.user.id; const isAdmin = request.user.isAdmin; const verification = await commentService.verifyCommentOwnership(commentId, "game", id); if (!verification.exists) { return reply.code(404).send({ error: "Comment not found" }); } if (verification.comment?.userId !== userId && !isAdmin) { return reply.code(403).send({ error: "You can only edit your own comments" }); } const comment = await commentService.updateComment(commentId, request.body.content); await AuditService.logFromRequest(request, { action: AuditAction.commentUpdate, category: AuditCategory.content, resourceType: "game", resourceId: id, details: `Updated comment ${commentId} on game`, }); return comment; } ); // Delete comment (owner or admin) app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } | { error: string } }>( "/:id/comments/:commentId", { preValidation: [app.authenticate], preHandler: [app.csrfProtection], }, async (request, reply) => { const { id, commentId } = request.params; const userId = request.user.id; const isAdmin = request.user.isAdmin; const verification = await commentService.verifyCommentOwnership(commentId, "game", id); if (!verification.exists) { return reply.code(404).send({ error: "Comment not found" }); } if (verification.comment?.userId !== userId && !isAdmin) { return reply.code(403).send({ error: "You can only delete your own comments" }); } await commentService.deleteComment(commentId); await AuditService.logFromRequest(request, { action: AuditAction.commentDelete, category: isAdmin && verification.comment?.userId !== userId ? AuditCategory.admin : AuditCategory.content, resourceType: "game", resourceId: id, details: `Deleted comment ${commentId} from game`, }); return { success: true }; } ); }; export default gamesRoutes;