generated from nhcarrigan/template
fix: base64 uploads, audit log noise, and stale chunk reloads (#69)
## Summary - **Base64 cover image uploads broken for books, shows, manga, and music** — a premature `validateStringLength` check ran before the data URL detection, rejecting all base64 images with a 2,048-char URL limit error. Also fixed the size calculation to extract only the base64 portion after the comma (matching the correct pattern already in `game.service.ts`). - **Audit log flooded with expected 401s on `/api/auth/me`** — these occur during normal token refresh flow and are not genuine security events. Excluded this URL from the global 401/403 audit log handler. - **ChunkLoadError spam after deployments** — when Angular lazy-loaded chunks are missing (stale cache after a redeploy), the global error handler now detects `ChunkLoadError` and silently reloads the page instead of logging the error and sending it to the API/Discord. ## Test plan - [ ] Upload a base64 cover image for a book, show, manga, and music item — should succeed - [ ] Verify `/api/auth/me` 401s no longer appear in the audit log - [ ] Deploy a new build and confirm stale-chunk users are silently reloaded ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #69 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #69.
This commit is contained in:
+2
-2
@@ -22,8 +22,8 @@ export async function app(fastify: FastifyInstance, opts: AppOptions) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log unauthorized access attempts
|
// Log unauthorized access attempts (exclude /api/auth/me as 401s there are expected during token refresh)
|
||||||
if (error.statusCode === 401 || error.statusCode === 403) {
|
if ((error.statusCode === 401 || error.statusCode === 403) && request.url !== '/api/auth/me') {
|
||||||
await AuditService.log({
|
await AuditService.log({
|
||||||
action: AuditAction.unauthorizedAccess,
|
action: AuditAction.unauthorizedAccess,
|
||||||
category: AuditCategory.security,
|
category: AuditCategory.security,
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ export class BookService {
|
|||||||
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
||||||
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
|
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
|
// Validate rating
|
||||||
if (!validateRating(data.rating)) {
|
if (!validateRating(data.rating)) {
|
||||||
throw new Error("Rating must be an integer between 0 and 10.");
|
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) {
|
||||||
if (data.coverImage.startsWith("data:")) {
|
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) {
|
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
|
||||||
throw new Error("Cover image must be under 5MB.");
|
throw new Error("Cover image must be under 5MB.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,6 @@ export class MangaService {
|
|||||||
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
||||||
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
|
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
|
// Validate rating
|
||||||
if (!validateRating(data.rating)) {
|
if (!validateRating(data.rating)) {
|
||||||
throw new Error("Rating must be an integer between 0 and 10.");
|
throw new Error("Rating must be an integer between 0 and 10.");
|
||||||
@@ -45,7 +41,11 @@ export class MangaService {
|
|||||||
// Validate cover image URL
|
// Validate cover image URL
|
||||||
if (data.coverImage) {
|
if (data.coverImage) {
|
||||||
if (data.coverImage.startsWith("data:")) {
|
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) {
|
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
|
||||||
throw new Error("Cover image must be under 5MB.");
|
throw new Error("Cover image must be under 5MB.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,6 @@ export class MusicService {
|
|||||||
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
||||||
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
|
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
|
// Validate rating
|
||||||
if (data.rating !== undefined && !validateRating(data.rating)) {
|
if (data.rating !== undefined && !validateRating(data.rating)) {
|
||||||
throw new Error("Rating must be an integer between 0 and 10.");
|
throw new Error("Rating must be an integer between 0 and 10.");
|
||||||
@@ -45,7 +41,11 @@ export class MusicService {
|
|||||||
// Validate cover art URL
|
// Validate cover art URL
|
||||||
if (data.coverArt) {
|
if (data.coverArt) {
|
||||||
if (data.coverArt.startsWith("data:")) {
|
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) {
|
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
|
||||||
throw new Error("Cover image must be under 5MB.");
|
throw new Error("Cover image must be under 5MB.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ export class ShowService {
|
|||||||
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
||||||
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
|
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
|
// Validate rating
|
||||||
if (!validateRating(data.rating)) {
|
if (!validateRating(data.rating)) {
|
||||||
throw new Error("Rating must be an integer between 0 and 10.");
|
throw new Error("Rating must be an integer between 0 and 10.");
|
||||||
@@ -42,7 +38,11 @@ export class ShowService {
|
|||||||
// Validate cover image URL
|
// Validate cover image URL
|
||||||
if (data.coverImage) {
|
if (data.coverImage) {
|
||||||
if (data.coverImage.startsWith("data:")) {
|
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) {
|
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
|
||||||
throw new Error("Cover image must be under 5MB.");
|
throw new Error("Cover image must be under 5MB.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ export class GlobalErrorHandler implements ErrorHandler {
|
|||||||
private toast = inject(ToastService);
|
private toast = inject(ToastService);
|
||||||
|
|
||||||
handleError(error: Error): void {
|
handleError(error: Error): void {
|
||||||
|
if (error.name === 'ChunkLoadError' || error.message.includes('Loading chunk')) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.error('Global error caught:', error);
|
console.error('Global error caught:', error);
|
||||||
|
|
||||||
// Show user-friendly error message
|
// Show user-friendly error message
|
||||||
|
|||||||
Reference in New Issue
Block a user