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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bb592ad2bd
commit
ffbca01bdc
@@ -0,0 +1,52 @@
|
||||
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",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user