Abmelden fuehrte im Docker-Betrieb auf 0.0.0.0:3000 - eine Adresse, die kein Browser aufrufen kann. Ursache war hausgemacht: ENV HOSTNAME=0.0.0.0 aus #031 reparierte die Zustandspruefung und lenkte zugleich alle Weiterleitungen aus Server Actions dorthin, weil Next sie serverseitig absolut macht. Vorher stand dort der Containername - derselbe Fehler, nur unauffaelliger. Neuer Helfer eigenerPfad() baut die Adresse aus Einstellung, APP_URL oder den Kopfzeilen der Anfrage. Betroffen waren Abmelden, die Sitzungspruefung und das Loeschen eines Kunden. Zusaetzlich uebergeht oeffentlicheBasis() eine als Lausch-Adresse eingetragene APP_URL, statt sie zu uebernehmen. Gegen einen Docker-Stapel geprueft: Mit APP_URL=http://0.0.0.0:3000 leitet die Anwendung jetzt auf den tatsaechlich aufgerufenen Namen. Dazu #032: Das ct-Skript setzt die Herkunft der App-Skripte selbst, und die Speichervorgabe steht auf 4 GB. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100 lines
4.0 KiB
TypeScript
100 lines
4.0 KiB
TypeScript
import { cookies } from "next/headers";
|
||
import { redirect } from "next/navigation";
|
||
import { cache } from "react";
|
||
import type { Rolle } from "@/generated/prisma/enums";
|
||
import { eigenerPfad } from "@/lib/adresse";
|
||
import { prisma } from "@/lib/db";
|
||
import { type GeleseneSitzung, SITZUNGS_COOKIE, sitzungLesen } from "@/lib/sitzung";
|
||
import { darf, type Recht } from "./rollen";
|
||
|
||
/**
|
||
* Verlangt eine gültige Sitzung und prüft sie gegen die Datenbank.
|
||
*
|
||
* `proxy.ts` hält Unangemeldete von den Seiten fern, aber Server Actions sind eigene
|
||
* Endpunkte: Jede Aktion prüft deshalb selbst, statt sich auf eine einzige Schicht zu
|
||
* verlassen (OWASP A01 – Broken Access Control).
|
||
*
|
||
* Rolle und Kundenbindung kommen **aus der Datenbank**, nicht aus dem Token: Eine geänderte
|
||
* Rolle wirkt damit sofort und nicht erst nach Ablauf der Sitzung.
|
||
*/
|
||
|
||
export interface AngemeldeterBenutzer extends GeleseneSitzung {
|
||
rolle: Rolle;
|
||
/** Nur bei der Rolle KUNDE gesetzt: der Kunde, dessen Daten sichtbar sind. */
|
||
kundeId: string | null;
|
||
}
|
||
|
||
/** Reines Urteil über eine gelesene Sitzung – ohne Next.js, damit es sich prüfen lässt. */
|
||
export function sitzungBewerten(
|
||
sitzung: GeleseneSitzung | null,
|
||
benutzer: { aktiv: boolean; sitzungenUngueltigAb: Date | null } | null,
|
||
): { gueltig: true } | { gueltig: false; grund: string } {
|
||
if (!sitzung) return { gueltig: false, grund: "Bitte anmelden." };
|
||
if (!benutzer) return { gueltig: false, grund: "Dieses Konto gibt es nicht mehr." };
|
||
if (!benutzer.aktiv) return { gueltig: false, grund: "Dieses Konto ist gesperrt." };
|
||
if (benutzer.sitzungenUngueltigAb && sitzung.ausgestelltAm <= benutzer.sitzungenUngueltigAb) {
|
||
return { gueltig: false, grund: "Die Sitzung wurde beendet. Bitte neu anmelden." };
|
||
}
|
||
return { gueltig: true };
|
||
}
|
||
|
||
/**
|
||
* Liest die aktuelle Anmeldung, ohne weiterzuleiten – für Stellen, die auch ohne Sitzung
|
||
* funktionieren müssen (Worker, Skripte, Audit-Log). `cache` sorgt dafür, dass eine Anfrage
|
||
* die Datenbank nur einmal dafür befragt.
|
||
*/
|
||
export const angemeldet = cache(async (): Promise<AngemeldeterBenutzer | null> => {
|
||
let sitzung: GeleseneSitzung | null = null;
|
||
try {
|
||
sitzung = await sitzungLesen((await cookies()).get(SITZUNGS_COOKIE)?.value);
|
||
} catch {
|
||
// Außerhalb einer Anfrage gibt es keine Cookies – das ist kein Fehler.
|
||
return null;
|
||
}
|
||
if (!sitzung) return null;
|
||
|
||
const benutzer = await prisma.benutzer.findUnique({
|
||
where: { id: sitzung.benutzerId },
|
||
select: { aktiv: true, sitzungenUngueltigAb: true, rolle: true, kundeId: true },
|
||
});
|
||
|
||
if (!sitzungBewerten(sitzung, benutzer).gueltig || !benutzer) return null;
|
||
|
||
return { ...sitzung, rolle: benutzer.rolle, kundeId: benutzer.kundeId };
|
||
});
|
||
|
||
/**
|
||
* Verlangt eine Anmeldung und – falls angegeben – ein bestimmtes Recht.
|
||
* Ohne Anmeldung geht es zur Anmeldeseite, bei fehlendem Recht auf die Startseite.
|
||
*/
|
||
export async function sitzungErforderlich(recht?: Recht): Promise<AngemeldeterBenutzer> {
|
||
const benutzer = await angemeldet();
|
||
if (!benutzer) redirect(await eigenerPfad("/login?fehler=Bitte+anmelden."));
|
||
|
||
if (recht && !darf(benutzer.rolle, recht)) {
|
||
redirect(await eigenerPfad("/?fehler=Für+diesen+Bereich+fehlt+die+Berechtigung."));
|
||
}
|
||
|
||
return benutzer;
|
||
}
|
||
|
||
/**
|
||
* Auf welchen Kunden die Sicht eingeschränkt ist – `null` heißt: alles sichtbar.
|
||
*
|
||
* Die Abfragen rufen das selbst auf, statt sich eine Einschränkung von der Seite reichen zu lassen.
|
||
* So kann keine Seite vergessen, sie weiterzugeben – der häufigste Weg, wie solche Sichten lecken.
|
||
*/
|
||
export async function sichtbarerKunde(): Promise<string | null> {
|
||
const benutzer = await angemeldet();
|
||
if (!benutzer || benutzer.rolle !== "KUNDE") return null;
|
||
|
||
// Ein Kundenkonto ohne Kundenbindung darf nichts sehen – nicht alles.
|
||
return benutzer.kundeId ?? "kein-kunde-zugeordnet";
|
||
}
|
||
|
||
/** Dürfen Einkaufspreise und Marge angezeigt werden? */
|
||
export async function darfEinkaufSehen(): Promise<boolean> {
|
||
const benutzer = await angemeldet();
|
||
return benutzer?.rolle !== "KUNDE";
|
||
}
|