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 font file was received. Choose a font file before clicking Upload.' }, { status: 400 }, ); } const safeName = sanitizeFontName(file.name); const ext = path.extname(safeName).toLowerCase(); if (!ALLOWED_EXT.has(ext)) { return NextResponse.json( { error: `Font extension "${ext || '(none)'}" is not supported. Allowed formats: ${[...ALLOWED_EXT].join(', ')}.` }, { status: 400 }, ); } if (!safeName || safeName === ext) { return NextResponse.json( { error: 'Invalid font filename. The file name must contain at least one letter, digit, hyphen or underscore before the extension.' }, { status: 400 }, ); } if (file.size > UPLOAD_LIMITS.font) { const limitMB = (UPLOAD_LIMITS.font / (1024 * 1024)).toFixed(0); const actualMB = (file.size / (1024 * 1024)).toFixed(1); return NextResponse.json( { error: `Font is too large: ${actualMB} MB (limit: ${limitMB} MB). Web fonts are typically under 500 KB — convert the file to WOFF2 (e.g. with a font-tools subsetter) before uploading.` }, { 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: 'Could not identify the font content. The file may be corrupted or not actually a font. Try re-exporting from your font tool.' }, { status: 400 }, ); } const allowed = ALLOWED_DETECTED[ext] ?? []; if (!allowed.includes(detected.ext)) { return NextResponse.json( { error: `Font content does not match its extension: the file looks like ${detected.ext.toUpperCase()} but the extension is "${ext}". Rename the file or re-export it in the declared format.` }, { 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: 'Unexpected error while saving the font. The upload was aborted. Check the server logs for details and try again.' }, { 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. Specify which font file to delete.' }, { 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 "${rawName}". Expected a basename ending in ${[...ALLOWED_EXT].join(' / ')}.` }, { 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 "${safeName}" was not found on the server. It may have already been deleted — refresh the page.` }, { status: 404 }, ); } throw err; } return NextResponse.json({ ok: true }); } catch (error) { console.error('Font delete error:', error); return NextResponse.json( { error: 'Unexpected error while deleting the font. Check the server logs and try again.' }, { status: 500 }, ); } }