generated from nhcarrigan/template
983b78b0e9
## Summary This PR includes multiple feature additions and fixes to improve the library application: ### 🎨 Base64 Image Upload Support - Fixed Fastify body limit (1MB → 10MB) to accommodate base64-encoded images - Corrected base64 size calculation and validation logic - Improved error handling with proper 400 status codes and helpful messages - Removed duplicate validation that was blocking uploads - Users can now upload cover images up to 5MB (decoded size) ### 📝 Reusable Form Components - Created 6 form components: `GameForm`, `BookForm`, `MusicForm`, `ShowForm`, `MangaForm`, `ArtForm` - All forms support both 'add' and 'edit' modes with pre-population - Integrated inline editing into all detail views (edit/delete buttons) - Enhanced admin suggestions workflow with full forms instead of basic modals - Added scroll-to-top when clicking edit in list views for better UX ### 🖼️ Default Cover Image - Added beautiful library reading image as default cover for all media types - Fixed static asset serving to use correct MIME types - Updated all 12 components (6 list views + 6 detail views) to always show images ### 🔒 Tiered Rate Limiting - Unauthenticated users: 100 requests/minute - Authenticated users: 500 requests/minute (5x more lenient) - Admin users: No rate limits (complete bypass via allowList) ### 🎮 Discord Integration - Auto-assign library member role to users in NHCarrigan Discord server - Checks server membership on every login - Only assigns role if user is in server and doesn't have it yet - Graceful error handling without blocking login - Similar pattern to badge refresh flow ### 📚 Documentation - Added comprehensive CLAUDE.md with: - Project structure and tech stack - Development workflow and commands - Database schema documentation - Authentication flow details - Security features - Code style conventions - Common gotchas and solutions ## Test Plan - [x] Base64 image uploads work for cover images up to 5MB - [x] Helpful error messages appear for validation failures - [x] Edit/delete buttons appear on all detail views for admin users - [x] Inline edit forms display and save correctly - [x] Admin suggestions workflow uses full forms for all media types - [x] Scroll-to-top works when editing from list views - [x] Default cover image displays when no cover is provided - [x] Static assets serve with correct MIME types - [x] Rate limiting works correctly for different user types - [x] Discord role assignment works on login - [x] All builds pass without errors - [x] No TypeScript errors ## Related Issues Closes #65 - Base64 image upload issue ✨ This pull request was created with help from Hikari~ 🌸 Reviewed-on: #66 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
96 lines
2.2 KiB
TypeScript
96 lines
2.2 KiB
TypeScript
/**
|
|
* @copyright 2026 NHCarrigan
|
|
* @license Naomi's Public License
|
|
* @author Naomi Carrigan
|
|
*/
|
|
|
|
/**
|
|
* 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+/=\s]+$/.test(url);
|
|
}
|
|
|
|
/**
|
|
* Validates that a URL is safe and points to an allowed protocol.
|
|
* Prevents javascript:, data:, vbscript:, and file: URLs.
|
|
*/
|
|
export function validateUrl(url: string): boolean {
|
|
if (!url) {
|
|
return true; // Empty URLs are acceptable for optional fields
|
|
}
|
|
|
|
// Check for dangerous protocols
|
|
const dangerousProtocols = /^(javascript|data|vbscript|file):/i;
|
|
if (dangerousProtocols.test(url)) {
|
|
return false;
|
|
}
|
|
|
|
// Must be a valid URL format
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
// Only allow http and https protocols
|
|
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates string length is within acceptable bounds.
|
|
*/
|
|
export function validateStringLength(
|
|
value: string | undefined,
|
|
maxLength: number
|
|
): boolean {
|
|
if (!value) {
|
|
return true;
|
|
}
|
|
return value.length <= maxLength;
|
|
}
|
|
|
|
/**
|
|
* Validates that a slug contains only safe characters (alphanumeric, hyphens, underscores).
|
|
*/
|
|
export function validateSlug(slug: string | undefined): boolean {
|
|
if (!slug) {
|
|
return true;
|
|
}
|
|
// Allow alphanumeric, hyphens, underscores only
|
|
const slugPattern = /^[a-z0-9-_]+$/i;
|
|
return slugPattern.test(slug) && slug.length <= 50;
|
|
}
|
|
|
|
/**
|
|
* Validates rating is within acceptable range.
|
|
*/
|
|
export function validateRating(rating: number | undefined): boolean {
|
|
if (rating === undefined) {
|
|
return true;
|
|
}
|
|
return Number.isInteger(rating) && rating >= 0 && rating <= 10;
|
|
}
|
|
|
|
/**
|
|
* Maximum string lengths for various fields.
|
|
*/
|
|
export const MAX_LENGTHS = {
|
|
TITLE: 500,
|
|
AUTHOR: 200,
|
|
DESCRIPTION: 5000,
|
|
BIO: 1000,
|
|
SLUG: 50,
|
|
URL: 2048,
|
|
DISPLAY_NAME: 100,
|
|
USERNAME: 100,
|
|
COMMENT_CONTENT: 10000,
|
|
NOTES: 5000,
|
|
TAGS: 50, // per tag
|
|
ISBN: 50,
|
|
DATA_URL: 5 * 1024 * 1024, // 5MB in bytes (not chars)
|
|
} as const;
|