Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft, Lernzielkontrollen und Feedbackgespraech lassen sich unter Verwaltung -> Module fuer die ganze Lern-Bar ab- und einschalten. Wochenplan, Handouts, Glossar, Auswertung und Verwaltung bleiben immer an. Gesperrt wird an drei Stellen, nicht im Layout (Next rendert Layouts beim Seitenwechsel darunter nicht neu): Seiten mit erfordereModul, Routen fuer PDF, Anhaenge und Lernstand mit modulAn, alle 37 Serveraktionen der Module mit pruefeModul. Die Anmeldepruefung kommt jeweils zuerst, damit ein Unangemeldeter nicht am 404 abliest, was abgeschaltet ist. Ein abgeschaltetes Modul traegt nirgends bei: Wochenplan, Fortschritt, Was-wartet, Mein Stand, Ergebnisverlauf, Rahmenplanabdeckung, Vorschlaege fuers Berichtsheft, Karten im Handout, Kurzbefehle im Manifest. Die Lernstand-Schnittstelle antwortet mit 410, das Geraet verwirft daraufhin seine Offline-Warteschlange. Abschalten loescht nichts. Gespeichert in modul_einstellung (Migration 0010); fehlt die Zeile, ist das Modul an, eine bestehende Datenbank verhaelt sich also wie vorher. 12 Tests fuer die Regeln, Abschnitt A8 im Abnahmelauf, Anleitung Kapitel 9 mit Bildschirmfoto, README, E43. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
2.5 KiB
TypeScript
55 lines
2.5 KiB
TypeScript
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';
|
||
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
|
||
|
||
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 });
|
||
if (!modulAn('aufgaben')) return modulGesperrt();
|
||
|
||
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",
|
||
},
|
||
});
|
||
}
|