Files
lernbar/src/app/anhaenge/[id]/route.ts
T
Florian WildeandClaude Opus 5 ffbca01bdc Baustein 5: Praxisaufgaben mit Anhaengen und Abnahme
- Status offen / in Arbeit / zur Abnahme / abgenommen, mit Zeitstempeln
- Notizfeld fuer den Azubi, Dateianhaenge bis 25 MB
- Speicherpfad nur aus selbst erzeugter UUID, nie aus dem Dateinamen
- Auslieferung ueber /anhaenge/<kennung> mit Rechtepruefung bei jedem Abruf,
  CSP sandbox und nosniff; SVG ausdruecklich nicht erlaubt
- Anhaenge haengen an Aufgabe UND Azubi
- Ausbilder hakt Kriterien einzeln ab, Note und Rueckmeldung, Abnahme sperrt
- Zurueckgeben zur Nacharbeit
- "Was auf dich wartet" zeigt offene Abnahmen
- Abgenommene Aufgaben zaehlen im Wochenfortschritt mit

Behoben: Next.js deckelt Serveraktionen ab Werk bei 1 MB - damit waere kein
Handyfoto durchgekommen. bodySizeLimit jetzt 30 MB, Vorabpruefung im Browser,
client_max_body_size im Proxy-Beispiel auf 30m.

Geaendert: Seiten verweigern fremde Daten mit 404 statt mit einem Fehler.
Eigene Seiten fuer "Nicht gefunden" und Stoerfaelle.

24 neue Tests, insgesamt 157.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:18:54 +02:00

53 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { angemeldeterBenutzer } from '@/lib/auth/sitzung';
import { darfAzubiDatenSehen } from '@/lib/auth/regeln';
import { ausbildungVonAzubi } from '@/lib/auth/rechte';
import { anhangLesen, anhangNachId } from '@/lib/repos/anhaenge';
import { istBild } from '@/lib/anhaenge/dateien';
export const dynamic = 'force-dynamic';
/**
* Liefert einen Anhang aus.
*
* Zwei Dinge sind hier wichtig:
* 1. Der Pfad kommt ausschließlich aus der Datenbank. Es gibt keinen Weg,
* über die Adresse eine beliebige Datei anzufordern.
* 2. Die Berechtigung wird bei **jedem** Abruf geprüft. Wer den Anhang
* hochgeladen hat, darf ihn sehen; sein Ausbilder auch, sonst niemand.
*/
export async function GET(_anfrage: Request, kontext: { params: Promise<{ id: string }> }) {
const benutzer = await angemeldeterBenutzer();
if (!benutzer) return new Response('Nicht angemeldet.', { status: 401 });
const { id } = await kontext.params;
const eintrag = anhangNachId(id);
if (!eintrag) return new Response('Nicht gefunden.', { status: 404 });
const bezug = ausbildungVonAzubi(eintrag.benutzerId);
if (!darfAzubiDatenSehen(benutzer, eintrag.benutzerId, bezug)) {
// Bewusst 404 statt 403: Ob es diesen Anhang gibt, geht Fremde nichts an.
return new Response('Nicht gefunden.', { status: 404 });
}
const inhalt = await anhangLesen(eintrag);
if (!inhalt) return new Response('Die Datei fehlt im Speicher.', { status: 410 });
// Bilder und PDF dürfen im Browser aufgehen, alles andere wird geladen.
const anzeigbar = istBild(eintrag.mimeTyp) || eintrag.mimeTyp === 'application/pdf';
const art = anzeigbar ? 'inline' : 'attachment';
return new Response(new Uint8Array(inhalt), {
headers: {
'Content-Type': eintrag.mimeTyp,
'Content-Length': String(inhalt.byteLength),
'Content-Disposition': `${art}; filename="${eintrag.dateiname}"; filename*=UTF-8''${encodeURIComponent(eintrag.dateiname)}`,
// Kein öffentlicher Zwischenspeicher – der Inhalt hängt am angemeldeten Benutzer.
'Cache-Control': 'private, max-age=300',
'X-Content-Type-Options': 'nosniff',
// Schutz davor, dass ein hochgeladenes SVG oder HTML im Kontext der
// Anwendung ausgeführt wird. Anhänge sind Daten, kein Programm.
'Content-Security-Policy': "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox",
},
});
}