import { NextResponse } from 'next/server'; import { writeFile, mkdir, unlink } from 'fs/promises'; import path from 'path'; import { fromBuffer as fileTypeFromBuffer } from 'file-type'; import { UPLOAD_LIMITS } from '@/lib/config'; export const dynamic = 'force-dynamic'; export const maxDuration = 60; const FONTS_DIR = path.join(process.cwd(), 'data', 'fonts'); const ALLOWED_EXT = new Set(['.woff2', '.woff', '.ttf', '.otf']); // Magic-bytes: ttf e otf condividono il container SFNT, quindi accettiamo entrambi // per entrambe le estensioni. woff/woff2 hanno header dedicati. const ALLOWED_DETECTED: Record = { '.woff2': ['woff2'], '.woff': ['woff'], '.ttf': ['ttf', 'otf'], '.otf': ['otf', 'ttf'], }; // Sanitizza il nome del file: solo basename, solo caratteri sicuri, niente path traversal. // Per i font conviene preservare il nome originale (la logica di lookup italic/bold in // app/layout.tsx si basa sui pattern `Name-Italic.woff2`, `Name-Bold.woff2`, ecc.). function sanitizeFontName(rawName: string): string { const base = path.basename(rawName); // Mantieni lettere, cifre, underscore, hyphen, punto. Tutto il resto โ†’ underscore. const sanitized = base.replace(/[^a-zA-Z0-9_\-.]/g, '_'); // Niente percorsi nascosti / nomi vuoti if (sanitized.startsWith('.') || sanitized.length === 0) return ''; return sanitized; } // POST โ€” upload font export async function POST(request: Request) { try { const formData = await request.formData(); const file = formData.get('file') as File | null; if (!file) { return NextResponse.json({ error: 'No file received.' }, { status: 400 }); } const safeName = sanitizeFontName(file.name); const ext = path.extname(safeName).toLowerCase(); if (!ALLOWED_EXT.has(ext)) { return NextResponse.json( { error: `Unsupported font extension. Allowed: ${[...ALLOWED_EXT].join(', ')}` }, { status: 400 }, ); } if (!safeName || safeName === ext) { return NextResponse.json({ error: 'Invalid font filename.' }, { status: 400 }); } if (file.size > UPLOAD_LIMITS.font) { const mb = (UPLOAD_LIMITS.font / (1024 * 1024)).toFixed(0); return NextResponse.json( { error: `Font too large (max ${mb} MB).` }, { status: 413 }, ); } const buffer = Buffer.from(await file.arrayBuffer()); // Magic-bytes: rifiuta se il contenuto non รจ davvero un font del tipo dichiarato. const detected = await fileTypeFromBuffer(buffer); if (!detected) { return NextResponse.json( { error: 'Font content not recognized (unknown format).' }, { status: 400 }, ); } const allowed = ALLOWED_DETECTED[ext] ?? []; if (!allowed.includes(detected.ext)) { return NextResponse.json( { error: `Font content does not match extension (${ext} declared, detected ${detected.ext}).` }, { status: 400 }, ); } await mkdir(FONTS_DIR, { recursive: true }); await writeFile(path.join(FONTS_DIR, safeName), buffer); return NextResponse.json({ ok: true, name: safeName }, { status: 201 }); } catch (error) { console.error('Font upload error:', error); return NextResponse.json({ error: 'Failed to upload font.' }, { status: 500 }); } } // DELETE โ€” rimuove un font (query ?name=) export async function DELETE(request: Request) { try { const { searchParams } = new URL(request.url); const rawName = searchParams.get('name'); if (!rawName) { return NextResponse.json({ error: 'Missing name parameter.' }, { status: 400 }); } const safeName = sanitizeFontName(rawName); const ext = path.extname(safeName).toLowerCase(); if (!ALLOWED_EXT.has(ext) || !safeName) { return NextResponse.json({ error: 'Invalid font name.' }, { status: 400 }); } try { await unlink(path.join(FONTS_DIR, safeName)); } catch (err) { const e = err as NodeJS.ErrnoException; if (e.code === 'ENOENT') { return NextResponse.json({ error: 'Font not found.' }, { status: 404 }); } throw err; } return NextResponse.json({ ok: true }); } catch (error) { console.error('Font delete error:', error); return NextResponse.json({ error: 'Failed to delete font.' }, { status: 500 }); } }