feat: add server modules (audit, upload, email, content, tracking)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
||||
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
||||
|
||||
const MAGIC_BYTES: Record<string, number[]> = {
|
||||
'image/jpeg': [0xff, 0xd8, 0xff],
|
||||
'image/png': [0x89, 0x50, 0x4e, 0x47],
|
||||
'image/gif': [0x47, 0x49, 0x46],
|
||||
'image/webp': [0x52, 0x49, 0x46, 0x46]
|
||||
};
|
||||
|
||||
function validateMagicBytes(buffer: Buffer, mimeType: string): boolean {
|
||||
const expected = MAGIC_BYTES[mimeType];
|
||||
if (!expected) return false;
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
if (buffer[i] !== expected[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function saveUpload(
|
||||
file: File,
|
||||
subdir = 'general'
|
||||
): Promise<{ url: string; path: string }> {
|
||||
if (file.size > MAX_SIZE) {
|
||||
throw new Error('File too large (max 10MB)');
|
||||
}
|
||||
|
||||
if (!ALLOWED_TYPES.includes(file.type)) {
|
||||
throw new Error('File type not allowed');
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
if (!validateMagicBytes(buffer, file.type)) {
|
||||
throw new Error('File content does not match declared type');
|
||||
}
|
||||
|
||||
const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg';
|
||||
const filename = `${crypto.randomUUID()}.${ext}`;
|
||||
const dir = path.join(UPLOAD_DIR, subdir);
|
||||
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
|
||||
const filePath = path.join(dir, filename);
|
||||
await fs.writeFile(filePath, buffer);
|
||||
|
||||
return {
|
||||
url: `/uploads/${subdir}/${filename}`,
|
||||
path: filePath
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteUpload(url: string): Promise<void> {
|
||||
if (!url.startsWith('/uploads/')) return;
|
||||
const filePath = path.join(UPLOAD_DIR, url.replace('/uploads/', ''));
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch {
|
||||
// File already gone
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user