Phase C: die letzten sechs Typen — PDF, Bild, Feedback und Datei-Upload
Damit sind alle 15 QR-Typen gebaut. PDF und Bild brauchen eine hochgeladene Datei. Die liegt in der Datenbank, nicht auf der Platte: damit bleibt es bei einem Volumen, das gesichert werden muss — ein zweites wird beim Sichern zu leicht vergessen. Geprueft wird der Dateiinhalt, nicht die Endung; eine Textdatei namens harmlos.pdf kommt nicht durch. SVG ist bewusst nicht erlaubt, weil es Skripte enthalten darf und von derselben Adresse wie die Anwendung ausgeliefert wuerde. Ausgeliefert wird mit nosniff und einer CSP-Sandbox, damit auch ein PDF mit Skripten nichts anrichtet. Dateien haengen am Code: wird er geloescht, geht sie mit. Eine ausgetauschte oder nie gespeicherte Datei wird nach 24 Stunden weggeraeumt. Eine fremde Datei laesst sich nicht an den eigenen Code haengen — sonst koennte man sie einem anderen Code entziehen. Feedback sammelt Note und Kommentar, ohne Name, IP oder Geraetekennung. Die Auswertung mit Durchschnitt, Verteilung und Kommentaren ist aus der Codeliste erreichbar — ein Feedback-Code ohne Auswertung waere nur die halbe Sache.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Context = { params: Promise<{ id: string }> };
|
||||
|
||||
/**
|
||||
* Liefert eine hochgeladene Datei aus.
|
||||
*
|
||||
* Bewusst ohne Anmeldung: Die Landingpages sind öffentlich, und wer einen
|
||||
* Code scannt, soll das hinterlegte PDF sehen können. Der Schutz liegt in der
|
||||
* nicht erratbaren Id — dieselbe Überlegung wie beim Slug einer Kurz-URL.
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
context: Context,
|
||||
): Promise<Response> {
|
||||
const { id } = await context.params;
|
||||
|
||||
const upload = await prisma.upload.findUnique({ where: { id } });
|
||||
if (!upload) {
|
||||
return new Response("Datei nicht gefunden", {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
// `?download` erzwingt das Speichern statt der Anzeige.
|
||||
const alsDownload = request.nextUrl.searchParams.has("download");
|
||||
|
||||
return new Response(new Uint8Array(upload.data), {
|
||||
headers: {
|
||||
"Content-Type": upload.mimeType,
|
||||
"Content-Length": String(upload.size),
|
||||
// Der Typ steht fest — der Browser soll nicht raten und daraus
|
||||
// womöglich etwas Ausführbares machen.
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
// Ein PDF darf Skripte enthalten. Ausgeliefert von derselben Adresse
|
||||
// wie die Anwendung könnte es damit auf angemeldete Sitzungen
|
||||
// zugreifen; die Sandbox nimmt ihm diese Möglichkeit.
|
||||
"Content-Security-Policy": "sandbox; default-src 'none'",
|
||||
"Content-Disposition": `${alsDownload ? "attachment" : "inline"}; filename*=UTF-8''${encodeURIComponent(upload.filename)}`,
|
||||
// Der Inhalt einer Id ändert sich nie — die Id wechselt mit der Datei.
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user