From fa4c1d8958eea93c27e586f23463968967943682 Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 23 Feb 2026 20:06:22 -0800 Subject: [PATCH 1/3] fix: correct base64 cover image validation for books, shows, manga, music Remove premature URL length check that ran before data URL detection, causing all base64 uploads to be incorrectly rejected. Also fix size calculation to use only the base64 data portion (after the comma) rather than the full data URL string, matching the correct pattern already in game.service.ts. --- api/src/app/services/book.service.ts | 10 +++++----- api/src/app/services/manga.service.ts | 10 +++++----- api/src/app/services/music.service.ts | 10 +++++----- api/src/app/services/show.service.ts | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/api/src/app/services/book.service.ts b/api/src/app/services/book.service.ts index 638d302..5fcf34c 100644 --- a/api/src/app/services/book.service.ts +++ b/api/src/app/services/book.service.ts @@ -36,10 +36,6 @@ export class BookService { 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)) { throw new Error("Rating must be an integer between 0 and 10."); @@ -47,7 +43,11 @@ export class BookService { if (data.coverImage) { if (data.coverImage.startsWith("data:")) { - const sizeInBytes = data.coverImage.length * 0.75; + const base64Data = data.coverImage.split(",")[1]; + if (!base64Data) { + throw new Error("Invalid image data URL format."); + } + const sizeInBytes = base64Data.length * 0.75; if (sizeInBytes > MAX_LENGTHS.DATA_URL) { throw new Error("Cover image must be under 5MB."); } diff --git a/api/src/app/services/manga.service.ts b/api/src/app/services/manga.service.ts index 1fa19b5..40beb10 100644 --- a/api/src/app/services/manga.service.ts +++ b/api/src/app/services/manga.service.ts @@ -33,10 +33,6 @@ export class MangaService { 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)) { throw new Error("Rating must be an integer between 0 and 10."); @@ -45,7 +41,11 @@ export class MangaService { // Validate cover image URL if (data.coverImage) { if (data.coverImage.startsWith("data:")) { - const sizeInBytes = data.coverImage.length * 0.75; + const base64Data = data.coverImage.split(",")[1]; + if (!base64Data) { + throw new Error("Invalid image data URL format."); + } + const sizeInBytes = base64Data.length * 0.75; if (sizeInBytes > MAX_LENGTHS.DATA_URL) { throw new Error("Cover image must be under 5MB."); } diff --git a/api/src/app/services/music.service.ts b/api/src/app/services/music.service.ts index c0480d8..0652c0b 100644 --- a/api/src/app/services/music.service.ts +++ b/api/src/app/services/music.service.ts @@ -33,10 +33,6 @@ export class MusicService { if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) { throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`); } - if (!validateStringLength(data.coverArt, MAX_LENGTHS.URL)) { - throw new Error(`Cover art URL must be ${MAX_LENGTHS.URL} characters or less.`); - } - // Validate rating if (data.rating !== undefined && !validateRating(data.rating)) { throw new Error("Rating must be an integer between 0 and 10."); @@ -45,7 +41,11 @@ export class MusicService { // Validate cover art URL if (data.coverArt) { if (data.coverArt.startsWith("data:")) { - const sizeInBytes = data.coverArt.length * 0.75; + const base64Data = data.coverArt.split(",")[1]; + if (!base64Data) { + throw new Error("Invalid image data URL format."); + } + const sizeInBytes = base64Data.length * 0.75; if (sizeInBytes > MAX_LENGTHS.DATA_URL) { throw new Error("Cover image must be under 5MB."); } diff --git a/api/src/app/services/show.service.ts b/api/src/app/services/show.service.ts index cda0ec9..5d5aed9 100644 --- a/api/src/app/services/show.service.ts +++ b/api/src/app/services/show.service.ts @@ -30,10 +30,6 @@ export class ShowService { 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)) { throw new Error("Rating must be an integer between 0 and 10."); @@ -42,7 +38,11 @@ export class ShowService { // Validate cover image URL if (data.coverImage) { if (data.coverImage.startsWith("data:")) { - const sizeInBytes = data.coverImage.length * 0.75; + const base64Data = data.coverImage.split(",")[1]; + if (!base64Data) { + throw new Error("Invalid image data URL format."); + } + const sizeInBytes = base64Data.length * 0.75; if (sizeInBytes > MAX_LENGTHS.DATA_URL) { throw new Error("Cover image must be under 5MB."); } -- 2.52.0 From b81b77ac2f92f063303b721a534755527468daca Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 23 Feb 2026 20:09:36 -0800 Subject: [PATCH 2/3] fix: omit audit log entries for 401s on /api/auth/me Token expiry probes against /api/auth/me are expected behaviour during the refresh flow and should not generate unauthorized access audit events. --- api/src/app/app.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/app/app.ts b/api/src/app/app.ts index bdaee04..433b8a5 100644 --- a/api/src/app/app.ts +++ b/api/src/app/app.ts @@ -22,8 +22,8 @@ export async function app(fastify: FastifyInstance, opts: AppOptions) { }); } - // Log unauthorized access attempts - if (error.statusCode === 401 || error.statusCode === 403) { + // Log unauthorized access attempts (exclude /api/auth/me as 401s there are expected during token refresh) + if ((error.statusCode === 401 || error.statusCode === 403) && request.url !== '/api/auth/me') { await AuditService.log({ action: AuditAction.unauthorizedAccess, category: AuditCategory.security, -- 2.52.0 From e8610667b57c52ac4160d94622a4e466b70bcc23 Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 23 Feb 2026 20:11:46 -0800 Subject: [PATCH 3/3] fix: silently reload on ChunkLoadError after deployments Stale chunk errors occur when a user has an old version of the app cached after a redeployment. Detect ChunkLoadError before logging so the page reloads automatically without showing a toast or sending noise to the API. --- .../src/app/services/global-error-handler.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/frontend/src/app/services/global-error-handler.service.ts b/apps/frontend/src/app/services/global-error-handler.service.ts index d798e38..7270dea 100644 --- a/apps/frontend/src/app/services/global-error-handler.service.ts +++ b/apps/frontend/src/app/services/global-error-handler.service.ts @@ -12,6 +12,11 @@ export class GlobalErrorHandler implements ErrorHandler { private toast = inject(ToastService); handleError(error: Error): void { + if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) { + window.location.reload(); + return; + } + console.error('Global error caught:', error); // Show user-friendly error message -- 2.52.0