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,186 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { erfordereBenutzer, erfordereBewertungsrecht, erfordereZugriffAufAzubi } from '@/lib/auth/rechte';
|
||||
import { inhalte } from '@/lib/inhalte';
|
||||
import { anhangAblegen, anhangLoeschen, anhangNachId } from '@/lib/repos/anhaenge';
|
||||
import {
|
||||
abnehmen,
|
||||
bewertungSetzen,
|
||||
notizSetzen,
|
||||
statusSetzen,
|
||||
standVon,
|
||||
zurueckgeben,
|
||||
} from '@/lib/repos/aufgaben';
|
||||
import { aufgabenStatus, type AufgabenStatus } from '@/lib/db/schema';
|
||||
|
||||
export type Rueckmeldung = { fehler?: string; erfolg?: string };
|
||||
|
||||
function aktualisiere(aufgabeId: string) {
|
||||
revalidatePath('/aufgaben');
|
||||
revalidatePath(`/aufgaben/${aufgabeId}`);
|
||||
revalidatePath('/');
|
||||
}
|
||||
|
||||
function aufgabeGibtEs(aufgabeId: string): boolean {
|
||||
return inhalte().aufgabeNach.has(aufgabeId);
|
||||
}
|
||||
|
||||
// --- Der Azubi an seiner eigenen Aufgabe -----------------------------------
|
||||
|
||||
export async function eigenenStatusSetzen(formular: FormData): Promise<void> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
if (benutzer.rolle !== 'azubi') return;
|
||||
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
const status = String(formular.get('status') ?? '') as AufgabenStatus;
|
||||
|
||||
if (!aufgabeGibtEs(aufgabeId)) return;
|
||||
if (!aufgabenStatus.includes(status)) return;
|
||||
// Abnehmen darf nur der Ausbilder.
|
||||
if (status === 'abgenommen') return;
|
||||
|
||||
statusSetzen(benutzer.id, aufgabeId, status);
|
||||
aktualisiere(aufgabeId);
|
||||
}
|
||||
|
||||
export async function eigeneNotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
if (benutzer.rolle !== 'azubi') return { fehler: 'Nur der Azubi schreibt seine eigenen Notizen.' };
|
||||
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
const notiz = String(formular.get('notiz') ?? '');
|
||||
|
||||
if (!aufgabeGibtEs(aufgabeId)) return { fehler: 'Diese Aufgabe gibt es nicht.' };
|
||||
if (notiz.length > 8000) return { fehler: 'Die Notiz ist zu lang – höchstens 8000 Zeichen.' };
|
||||
|
||||
notizSetzen(benutzer.id, aufgabeId, notiz);
|
||||
aktualisiere(aufgabeId);
|
||||
return { erfolg: 'Notiz gespeichert.' };
|
||||
}
|
||||
|
||||
export async function anhangHochladen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
if (benutzer.rolle !== 'azubi') return { fehler: 'Anhänge lädt der Azubi hoch.' };
|
||||
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
if (!aufgabeGibtEs(aufgabeId)) return { fehler: 'Diese Aufgabe gibt es nicht.' };
|
||||
|
||||
const stand = standVon(benutzer.id, aufgabeId);
|
||||
if (stand?.status === 'abgenommen') {
|
||||
return { fehler: 'Die Aufgabe ist abgenommen – sie lässt sich nicht mehr ändern.' };
|
||||
}
|
||||
|
||||
const dateien = formular.getAll('datei').filter((d): d is File => d instanceof File && d.size > 0);
|
||||
if (dateien.length === 0) return { fehler: 'Keine Datei ausgewählt.' };
|
||||
|
||||
const beschriftung = String(formular.get('beschriftung') ?? '');
|
||||
const fehler: string[] = [];
|
||||
let angelegt = 0;
|
||||
|
||||
for (const datei of dateien) {
|
||||
const ergebnis = await anhangAblegen({
|
||||
bezugTyp: 'aufgabe',
|
||||
// Der Bezug ist Aufgabe **und** Azubi – sonst sähe ein zweiter Azubi
|
||||
// die Anhänge des ersten an derselben Aufgabe.
|
||||
bezugId: `${aufgabeId}::${benutzer.id}`,
|
||||
benutzerId: benutzer.id,
|
||||
datei,
|
||||
beschriftung: dateien.length === 1 ? beschriftung : undefined,
|
||||
});
|
||||
|
||||
if ('fehler' in ergebnis) {
|
||||
fehler.push(`${datei.name}: ${ergebnis.fehler}`);
|
||||
} else {
|
||||
angelegt += 1;
|
||||
}
|
||||
}
|
||||
|
||||
aktualisiere(aufgabeId);
|
||||
|
||||
if (angelegt === 0) return { fehler: fehler.join(' · ') };
|
||||
if (fehler.length > 0) {
|
||||
return { fehler: `${angelegt} übernommen, aber: ${fehler.join(' · ')}` };
|
||||
}
|
||||
return { erfolg: `${angelegt} ${angelegt === 1 ? 'Anhang' : 'Anhänge'} hinzugefügt.` };
|
||||
}
|
||||
|
||||
export async function anhangEntfernen(formular: FormData): Promise<void> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
const anhangId = String(formular.get('anhangId') ?? '');
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
|
||||
const eintrag = anhangNachId(anhangId);
|
||||
if (!eintrag) return;
|
||||
|
||||
// Nur der eigene Anhang, und nur solange die Aufgabe nicht abgenommen ist.
|
||||
if (eintrag.benutzerId !== benutzer.id) return;
|
||||
const stand = standVon(benutzer.id, aufgabeId);
|
||||
if (stand?.status === 'abgenommen') return;
|
||||
|
||||
await anhangLoeschen(anhangId);
|
||||
aktualisiere(aufgabeId);
|
||||
}
|
||||
|
||||
// --- Der Ausbilder an der Aufgabe seines Azubis ----------------------------
|
||||
|
||||
export async function bewerten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
|
||||
erfordereBewertungsrecht(ausbilder, azubiId);
|
||||
|
||||
const aufgabe = inhalte().aufgabeNach.get(aufgabeId);
|
||||
if (!aufgabe) return { fehler: 'Diese Aufgabe gibt es nicht.' };
|
||||
|
||||
const kriterien = formular
|
||||
.getAll('kriterium')
|
||||
.map((wert) => Number(wert))
|
||||
.filter((n) => Number.isInteger(n) && n >= 0 && n < aufgabe.kriterien.length);
|
||||
|
||||
const bewertung = String(formular.get('bewertung') ?? '').trim();
|
||||
const rueckmeldung = String(formular.get('rueckmeldung') ?? '').trim();
|
||||
|
||||
if (bewertung.length > 60) return { fehler: 'Die Bewertung ist zu lang – höchstens 60 Zeichen.' };
|
||||
if (rueckmeldung.length > 4000) return { fehler: 'Die Rückmeldung ist zu lang – höchstens 4000 Zeichen.' };
|
||||
|
||||
bewertungSetzen(azubiId, aufgabeId, {
|
||||
kriterien,
|
||||
bewertung: bewertung || null,
|
||||
rueckmeldung: rueckmeldung || null,
|
||||
});
|
||||
|
||||
aktualisiere(aufgabeId);
|
||||
return { erfolg: `Bewertung gespeichert – ${kriterien.length} von ${aufgabe.kriterien.length} Kriterien erfüllt.` };
|
||||
}
|
||||
|
||||
export async function aufgabeAbnehmen(formular: FormData): Promise<void> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
|
||||
erfordereBewertungsrecht(ausbilder, azubiId);
|
||||
if (!aufgabeGibtEs(aufgabeId)) return;
|
||||
|
||||
abnehmen(azubiId, aufgabeId, ausbilder.id);
|
||||
aktualisiere(aufgabeId);
|
||||
}
|
||||
|
||||
export async function aufgabeZurueckgeben(formular: FormData): Promise<void> {
|
||||
const ausbilder = await erfordereBenutzer();
|
||||
const azubiId = String(formular.get('azubiId') ?? '');
|
||||
const aufgabeId = String(formular.get('aufgabeId') ?? '');
|
||||
|
||||
erfordereBewertungsrecht(ausbilder, azubiId);
|
||||
if (!aufgabeGibtEs(aufgabeId)) return;
|
||||
|
||||
zurueckgeben(azubiId, aufgabeId);
|
||||
aktualisiere(aufgabeId);
|
||||
}
|
||||
|
||||
/** Wird von der Aufgabenseite gebraucht, um fremde Daten überhaupt lesen zu dürfen. */
|
||||
export async function pruefeLesezugriff(azubiId: string): Promise<void> {
|
||||
const benutzer = await erfordereBenutzer();
|
||||
erfordereZugriffAufAzubi(benutzer, azubiId);
|
||||
}
|
||||
Reference in New Issue
Block a user