You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

118 lines
4.2 KiB

  1. import { NextResponse } from 'next/server';
  2. import { writeFile, mkdir, unlink } from 'fs/promises';
  3. import path from 'path';
  4. import { fromBuffer as fileTypeFromBuffer } from 'file-type';
  5. import { UPLOAD_LIMITS } from '@/lib/config';
  6. export const dynamic = 'force-dynamic';
  7. export const maxDuration = 60;
  8. const FONTS_DIR = path.join(process.cwd(), 'data', 'fonts');
  9. const ALLOWED_EXT = new Set(['.woff2', '.woff', '.ttf', '.otf']);
  10. // Magic-bytes: ttf e otf condividono il container SFNT, quindi accettiamo entrambi
  11. // per entrambe le estensioni. woff/woff2 hanno header dedicati.
  12. const ALLOWED_DETECTED: Record<string, string[]> = {
  13. '.woff2': ['woff2'],
  14. '.woff': ['woff'],
  15. '.ttf': ['ttf', 'otf'],
  16. '.otf': ['otf', 'ttf'],
  17. };
  18. // Sanitizza il nome del file: solo basename, solo caratteri sicuri, niente path traversal.
  19. // Per i font conviene preservare il nome originale (la logica di lookup italic/bold in
  20. // app/layout.tsx si basa sui pattern `Name-Italic.woff2`, `Name-Bold.woff2`, ecc.).
  21. function sanitizeFontName(rawName: string): string {
  22. const base = path.basename(rawName);
  23. // Mantieni lettere, cifre, underscore, hyphen, punto. Tutto il resto → underscore.
  24. const sanitized = base.replace(/[^a-zA-Z0-9_\-.]/g, '_');
  25. // Niente percorsi nascosti / nomi vuoti
  26. if (sanitized.startsWith('.') || sanitized.length === 0) return '';
  27. return sanitized;
  28. }
  29. // POST — upload font
  30. export async function POST(request: Request) {
  31. try {
  32. const formData = await request.formData();
  33. const file = formData.get('file') as File | null;
  34. if (!file) {
  35. return NextResponse.json({ error: 'No file received.' }, { status: 400 });
  36. }
  37. const safeName = sanitizeFontName(file.name);
  38. const ext = path.extname(safeName).toLowerCase();
  39. if (!ALLOWED_EXT.has(ext)) {
  40. return NextResponse.json(
  41. { error: `Unsupported font extension. Allowed: ${[...ALLOWED_EXT].join(', ')}` },
  42. { status: 400 },
  43. );
  44. }
  45. if (!safeName || safeName === ext) {
  46. return NextResponse.json({ error: 'Invalid font filename.' }, { status: 400 });
  47. }
  48. if (file.size > UPLOAD_LIMITS.font) {
  49. const mb = (UPLOAD_LIMITS.font / (1024 * 1024)).toFixed(0);
  50. return NextResponse.json(
  51. { error: `Font too large (max ${mb} MB).` },
  52. { status: 413 },
  53. );
  54. }
  55. const buffer = Buffer.from(await file.arrayBuffer());
  56. // Magic-bytes: rifiuta se il contenuto non è davvero un font del tipo dichiarato.
  57. const detected = await fileTypeFromBuffer(buffer);
  58. if (!detected) {
  59. return NextResponse.json(
  60. { error: 'Font content not recognized (unknown format).' },
  61. { status: 400 },
  62. );
  63. }
  64. const allowed = ALLOWED_DETECTED[ext] ?? [];
  65. if (!allowed.includes(detected.ext)) {
  66. return NextResponse.json(
  67. { error: `Font content does not match extension (${ext} declared, detected ${detected.ext}).` },
  68. { status: 400 },
  69. );
  70. }
  71. await mkdir(FONTS_DIR, { recursive: true });
  72. await writeFile(path.join(FONTS_DIR, safeName), buffer);
  73. return NextResponse.json({ ok: true, name: safeName }, { status: 201 });
  74. } catch (error) {
  75. console.error('Font upload error:', error);
  76. return NextResponse.json({ error: 'Failed to upload font.' }, { status: 500 });
  77. }
  78. }
  79. // DELETE — rimuove un font (query ?name=<filename>)
  80. export async function DELETE(request: Request) {
  81. try {
  82. const { searchParams } = new URL(request.url);
  83. const rawName = searchParams.get('name');
  84. if (!rawName) {
  85. return NextResponse.json({ error: 'Missing name parameter.' }, { status: 400 });
  86. }
  87. const safeName = sanitizeFontName(rawName);
  88. const ext = path.extname(safeName).toLowerCase();
  89. if (!ALLOWED_EXT.has(ext) || !safeName) {
  90. return NextResponse.json({ error: 'Invalid font name.' }, { status: 400 });
  91. }
  92. try {
  93. await unlink(path.join(FONTS_DIR, safeName));
  94. } catch (err) {
  95. const e = err as NodeJS.ErrnoException;
  96. if (e.code === 'ENOENT') {
  97. return NextResponse.json({ error: 'Font not found.' }, { status: 404 });
  98. }
  99. throw err;
  100. }
  101. return NextResponse.json({ ok: true });
  102. } catch (error) {
  103. console.error('Font delete error:', error);
  104. return NextResponse.json({ error: 'Failed to delete font.' }, { status: 500 });
  105. }
  106. }