mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat(feedback): add user feedback/review system with admin management and hardened image upload validation
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
export const ALLOWED_IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] as const;
|
||||
export type AllowedImageMimeType = (typeof ALLOWED_IMAGE_MIME_TYPES)[number];
|
||||
|
||||
const EXT_BY_MIME: Record<AllowedImageMimeType, string> = {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/webp': 'webp',
|
||||
'image/gif': 'gif',
|
||||
};
|
||||
|
||||
export function normalizeImageMime(value: string): string {
|
||||
if (value === 'image/jpg' || value === 'image/pjpeg') return 'image/jpeg';
|
||||
return value;
|
||||
}
|
||||
|
||||
export function isAllowedImageType(value: string): value is AllowedImageMimeType {
|
||||
return (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export function detectImageMime(buffer: Uint8Array): AllowedImageMimeType | null {
|
||||
// JPEG
|
||||
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xd8) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
// PNG
|
||||
if (
|
||||
buffer.length >= 8
|
||||
&& buffer[0] === 0x89
|
||||
&& buffer[1] === 0x50
|
||||
&& buffer[2] === 0x4e
|
||||
&& buffer[3] === 0x47
|
||||
&& buffer[4] === 0x0d
|
||||
&& buffer[5] === 0x0a
|
||||
&& buffer[6] === 0x1a
|
||||
&& buffer[7] === 0x0a
|
||||
) {
|
||||
return 'image/png';
|
||||
}
|
||||
// GIF87a/GIF89a
|
||||
if (
|
||||
buffer.length >= 6
|
||||
&& buffer[0] === 0x47
|
||||
&& buffer[1] === 0x49
|
||||
&& buffer[2] === 0x46
|
||||
&& buffer[3] === 0x38
|
||||
&& (buffer[4] === 0x37 || buffer[4] === 0x39)
|
||||
&& buffer[5] === 0x61
|
||||
) {
|
||||
return 'image/gif';
|
||||
}
|
||||
// WEBP: "RIFF"...."WEBP"
|
||||
if (
|
||||
buffer.length >= 12
|
||||
&& buffer[0] === 0x52
|
||||
&& buffer[1] === 0x49
|
||||
&& buffer[2] === 0x46
|
||||
&& buffer[3] === 0x46
|
||||
&& buffer[8] === 0x57
|
||||
&& buffer[9] === 0x45
|
||||
&& buffer[10] === 0x42
|
||||
&& buffer[11] === 0x50
|
||||
) {
|
||||
return 'image/webp';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getImageExtension(mime: AllowedImageMimeType): string {
|
||||
return EXT_BY_MIME[mime];
|
||||
}
|
||||
|
||||
export function firstBytesHex(buffer: Uint8Array, length = 16): string {
|
||||
return Array.from(buffer.slice(0, length))
|
||||
.map((byte) => byte.toString(16).padStart(2, '0'))
|
||||
.join(' ');
|
||||
}
|
||||
@@ -22,7 +22,10 @@ export const RATE_LIMIT_CONFIGS: Record<string, RateLimitConfig> = {
|
||||
|
||||
// Content creation — moderate limits
|
||||
comment: { windowMs: 60 * 1000, maxRequests: 15 }, // 15 per minute
|
||||
'image-upload': { windowMs: 60 * 1000, maxRequests: 20 }, // 20 per minute
|
||||
'voice-upload': { windowMs: 60 * 1000, maxRequests: 10 }, // 10 per minute
|
||||
'feedback-submit': { windowMs: 60 * 1000, maxRequests: 8 }, // 8 per minute
|
||||
'feedback-upload': { windowMs: 60 * 1000, maxRequests: 20 }, // 20 per minute
|
||||
'create-project': { windowMs: 60 * 60 * 1000, maxRequests: 20 }, // 20 per hour
|
||||
'create-video': { windowMs: 60 * 1000, maxRequests: 10 }, // 10 per minute
|
||||
'create-version': { windowMs: 60 * 1000, maxRequests: 10 }, // 10 per minute
|
||||
|
||||
Reference in New Issue
Block a user