This commit was merged in pull request #59.
This commit is contained in:
@@ -15,10 +15,12 @@ import {
|
||||
LaufMitAdKontoVerknuepfen,
|
||||
LaufZurueckholen,
|
||||
} from "@/components/laeufe/lauf-aktionen";
|
||||
import { SchrittAnhaenge, type SchrittAnhang } from "@/components/laeufe/schritt-anhaenge";
|
||||
import { SchrittWert } from "@/components/laeufe/schritt-wert";
|
||||
import { SchrittZustandWaehlen, ZustandKennzeichen } from "@/components/laeufe/schritt-zustand";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { anhaengeDesLaufs } from "@/server/services/anhaenge";
|
||||
import { protokollDesLaufs } from "@/server/queries/protokoll";
|
||||
import {
|
||||
kommentareDesLaufs,
|
||||
@@ -82,12 +84,14 @@ function Schritt({
|
||||
lesend,
|
||||
ausfuehrlich,
|
||||
kommentare,
|
||||
anhaenge,
|
||||
}: {
|
||||
schritt: AbarbeitungsSchritt;
|
||||
laufId: string;
|
||||
lesend: boolean;
|
||||
ausfuehrlich: boolean;
|
||||
kommentare: readonly KommentarZeile[];
|
||||
anhaenge: readonly SchrittAnhang[];
|
||||
}) {
|
||||
const vomTisch =
|
||||
schritt.zustand === SchrittZustand.ERLEDIGT || schritt.zustand === SchrittZustand.NICHT_ZUTREFFEND;
|
||||
@@ -197,6 +201,22 @@ function Schritt({
|
||||
</ul>
|
||||
) : null}
|
||||
|
||||
{/*
|
||||
* Anhaenge (#032). Immer sichtbar, wenn es welche gibt — auch an einem
|
||||
* archivierten Lauf, nur dann ohne Knoepfe. Ein Foto vom Typenschild
|
||||
* ist Teil des Belegs.
|
||||
*/}
|
||||
{anhaenge.length > 0 || (!lesend && schritt.ergebnisId) ? (
|
||||
<div className="mt-3">
|
||||
<SchrittAnhaenge
|
||||
laufId={laufId}
|
||||
vorlagenSchrittId={schritt.schrittId}
|
||||
anhaenge={anhaenge}
|
||||
lesend={lesend || !schritt.ergebnisId}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{lesend || !schritt.ergebnisId ? null : (
|
||||
<details className="mt-2">
|
||||
<summary className="flex min-h-11 cursor-pointer list-none items-center gap-2 text-xs text-nebentext hover:text-text md:min-h-9">
|
||||
@@ -220,12 +240,14 @@ function Abschnitt({
|
||||
lesend,
|
||||
ausfuehrlich,
|
||||
kommentare,
|
||||
anhaenge,
|
||||
}: {
|
||||
abschnitt: AbarbeitungsAbschnitt;
|
||||
laufId: string;
|
||||
lesend: boolean;
|
||||
ausfuehrlich: boolean;
|
||||
kommentare: readonly KommentarZeile[];
|
||||
anhaenge: readonly (SchrittAnhang & { readonly schrittergebnisId: string })[];
|
||||
}) {
|
||||
const erledigt = abschnitt.schritte.filter(
|
||||
(schritt) =>
|
||||
@@ -263,6 +285,7 @@ function Abschnitt({
|
||||
lesend={lesend}
|
||||
ausfuehrlich={ausfuehrlich}
|
||||
kommentare={kommentare.filter((kommentar) => kommentar.schrittergebnisId === schritt.ergebnisId)}
|
||||
anhaenge={anhaenge.filter((anhang) => anhang.schrittergebnisId === schritt.ergebnisId)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
@@ -297,6 +320,9 @@ export default async function LaufSeite({
|
||||
// Die Sicht hat `laufHolen` oben schon angewandt; hier geht es nur noch um die
|
||||
// Einträge dieses Laufs (#023).
|
||||
const laufProtokoll = await protokollDesLaufs(id);
|
||||
// Die Sicht hat `laufHolen` oben schon angewandt; hier nur die Metadaten der
|
||||
// Anhaenge dieses Laufs (#032).
|
||||
const anhaenge = await anhaengeDesLaufs(id);
|
||||
/*
|
||||
* Die Personenliste nur fuer den Onboarding-Fall holen — sonst stuende in
|
||||
* jedem Seitenaufbau eine Abfrage, die niemand braucht.
|
||||
@@ -394,6 +420,7 @@ export default async function LaufSeite({
|
||||
lesend={lesend}
|
||||
ausfuehrlich={ausfuehrlich}
|
||||
kommentare={kommentare}
|
||||
anhaenge={anhaenge}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { AnhangFehler, anhangLesen } from "@/server/services/anhaenge";
|
||||
import { MandantFehler } from "@/server/mandant";
|
||||
import { RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { istBild } from "@/lib/anhaenge";
|
||||
|
||||
/**
|
||||
* Einen Anhang ausliefern (#032).
|
||||
*
|
||||
* **Route Handler prüfen selbst.** Die Hülle unter `src/app/(app)/layout.tsx`
|
||||
* deckt sie nicht ab, und der Proxy sieht nur, **ob** ein Cookie da ist, nie was
|
||||
* drinsteht (CLAUDE.md). Hier zählt das doppelt: eine Adresse, die eine Datei
|
||||
* ohne Prüfung ausliefert, umgeht alles andere.
|
||||
*
|
||||
* `Content-Disposition: inline` nur für Bilder. Ein PDF wird **heruntergeladen**
|
||||
* statt im Browser gerendert: ein PDF kann Skripte enthalten, und ein Anhang,
|
||||
* den jemand aus einem fremden Lauf öffnet, soll nichts ausführen.
|
||||
*/
|
||||
export async function GET(
|
||||
_anfrage: Request,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
): Promise<Response> {
|
||||
let benutzer: { id: string; rolle: Parameters<typeof anhangLesen>[1]["rolle"] };
|
||||
try {
|
||||
const zugang = await rechtErforderlich("lesen");
|
||||
benutzer = { id: zugang.benutzer.id, rolle: zugang.benutzer.rolle };
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof RechteFehler) {
|
||||
return Response.json({ fehler: "Nicht angemeldet." }, { status: 401 });
|
||||
}
|
||||
throw fehler;
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
try {
|
||||
const datei = await anhangLesen(id, benutzer);
|
||||
// Der Dateiname steckt in einer Kopfzeile. `dateinameSaeubern` hat
|
||||
// Steuerzeichen und Anführungszeichen schon beim Anlegen entfernt.
|
||||
const anzeige = istBild(datei.medientyp) ? "inline" : "attachment";
|
||||
return new Response(new Uint8Array(datei.inhalt), {
|
||||
headers: {
|
||||
"Content-Type": datei.medientyp,
|
||||
"Content-Length": String(datei.groesse),
|
||||
"Content-Disposition": `${anzeige}; filename="${datei.dateiname}"`,
|
||||
// Privat: ein Anhang gehört einem Kunden, kein Zwischenspeicher darf ihn
|
||||
// an den nächsten Betrachter weitergeben.
|
||||
"Cache-Control": "private, max-age=0, must-revalidate",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
});
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof MandantFehler) {
|
||||
// Nicht vorhanden und nicht sichtbar sind dieselbe Antwort – die
|
||||
// Unterscheidung wäre schon eine Auskunft über fremde Kunden.
|
||||
return Response.json({ fehler: "Diesen Anhang gibt es nicht." }, { status: 404 });
|
||||
}
|
||||
if (fehler instanceof AnhangFehler) {
|
||||
return Response.json({ fehler: fehler.message }, { status: 404 });
|
||||
}
|
||||
throw fehler;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user