diff --git a/api/src/app/routes/games/index.ts b/api/src/app/routes/games/index.ts index eef98a0..8516e92 100644 --- a/api/src/app/routes/games/index.ts +++ b/api/src/app/routes/games/index.ts @@ -41,22 +41,29 @@ const gamesRoutes: FastifyPluginAsync = async (app) => { ); // Create game (protected admin route) - app.post<{ Body: CreateGameDto; Reply: Game }>( + app.post<{ Body: CreateGameDto; Reply: Game | { error: string } }>( "/", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, - async (request) => { - 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; + 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; + } } ); @@ -64,26 +71,33 @@ const gamesRoutes: FastifyPluginAsync = async (app) => { app.put<{ Params: { id: string }; Body: UpdateGameDto; - Reply: Game | null; + Reply: Game | null | { error: string }; }>( "/:id", { preValidation: [app.authenticate, adminGuard], preHandler: [app.csrfProtection], }, - async (request) => { - 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}`, - }); + 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; } - return game; } ); diff --git a/api/src/app/services/game.service.ts b/api/src/app/services/game.service.ts index bc15287..25b82a6 100644 --- a/api/src/app/services/game.service.ts +++ b/api/src/app/services/game.service.ts @@ -33,9 +33,6 @@ export class GameService { if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) { throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`); } - if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) { - throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`); - } // Validate rating if (!validateRating(data.rating)) { @@ -45,8 +42,14 @@ export class GameService { // Validate cover image URL if (data.coverImage) { if (data.coverImage.startsWith("data:")) { - const sizeInBytes = data.coverImage.length * 0.75; - if (sizeInBytes > MAX_LENGTHS.DATA_URL) { + // Extract just the base64 data (after the comma) + const base64Data = data.coverImage.split(",")[1]; + if (!base64Data) { + throw new Error("Invalid image data URL format."); + } + // Calculate decoded size: base64 is ~4/3 larger than original, so multiply by 0.75 + const decodedSizeInBytes = base64Data.length * 0.75; + if (decodedSizeInBytes > MAX_LENGTHS.DATA_URL) { throw new Error("Cover image must be under 5MB."); } if (!validateDataUrl(data.coverImage)) { diff --git a/api/src/app/utils/validation.ts b/api/src/app/utils/validation.ts index 05792b4..f69ebeb 100644 --- a/api/src/app/utils/validation.ts +++ b/api/src/app/utils/validation.ts @@ -6,9 +6,10 @@ /** * Validates that a URL is a proper base64 data string. + * Allows whitespace in the base64 data which may occur during transmission. */ export function validateDataUrl(url: string): boolean { - return /^data:image\/(jpeg|png|gif|webp|svg\+xml);base64,[A-Za-z0-9+/=]+$/.test(url); + return /^data:image\/(jpeg|png|gif|webp|svg\+xml);base64,[A-Za-z0-9+/=\s]+$/.test(url); } /** diff --git a/api/src/main.ts b/api/src/main.ts index 3270acd..e94b868 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -40,7 +40,7 @@ process.on('SIGINT', () => { // Instantiate Fastify with some config const server = Fastify({ logger: true, - bodyLimit: 1048576, // 1MB max body size + bodyLimit: 10485760, // 10MB max body size (to accommodate base64-encoded images) }); // Register your application as a normal plugin.