Das Cookie erhielt im Produktionsbau immer Secure und wurde ueber http vom Browser verworfen – nach der Anmeldung landete jeder Klick wieder auf der Anmeldeseite. Massgeblich ist jetzt das Schema in APP_URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
76 lines
2.4 KiB
TypeScript
76 lines
2.4 KiB
TypeScript
import { SignJWT, jwtVerify } from "jose";
|
||
|
||
/**
|
||
* Sitzungen als signiertes Token im Cookie – kein Serverzustand nötig.
|
||
* Der Schlüssel kommt aus `APP_SECRET`; ohne ihn startet die Anmeldung nicht.
|
||
*/
|
||
|
||
export const SITZUNGS_COOKIE = "pl_sitzung";
|
||
export const SITZUNGSDAUER_STUNDEN = 12;
|
||
|
||
export interface Sitzung {
|
||
benutzerId: string;
|
||
email: string;
|
||
name: string;
|
||
/** Woher die Anmeldung stammt – für das Audit-Log und die Anzeige. */
|
||
herkunft: "passwort" | "oidc";
|
||
}
|
||
|
||
function schluessel(): Uint8Array {
|
||
const geheimnis = process.env.APP_SECRET;
|
||
if (!geheimnis || geheimnis.length < 32) {
|
||
throw new Error(
|
||
"APP_SECRET fehlt oder ist zu kurz (mindestens 32 Zeichen). Beispiel: openssl rand -base64 48",
|
||
);
|
||
}
|
||
return new TextEncoder().encode(geheimnis);
|
||
}
|
||
|
||
export async function sitzungErzeugen(sitzung: Sitzung): Promise<string> {
|
||
return new SignJWT({ ...sitzung })
|
||
.setProtectedHeader({ alg: "HS256" })
|
||
.setSubject(sitzung.benutzerId)
|
||
.setIssuedAt()
|
||
.setIssuer("proxledger")
|
||
.setExpirationTime(`${SITZUNGSDAUER_STUNDEN}h`)
|
||
.sign(schluessel());
|
||
}
|
||
|
||
/** Prüft das Token; bei ungültiger Signatur oder Ablauf kommt null zurück. */
|
||
export async function sitzungLesen(token: string | undefined): Promise<Sitzung | null> {
|
||
if (!token) return null;
|
||
try {
|
||
const { payload } = await jwtVerify(token, schluessel(), { issuer: "proxledger" });
|
||
if (typeof payload.benutzerId !== "string" || typeof payload.email !== "string") return null;
|
||
return {
|
||
benutzerId: payload.benutzerId,
|
||
email: payload.email,
|
||
name: typeof payload.name === "string" ? payload.name : payload.email,
|
||
herkunft: payload.herkunft === "oidc" ? "oidc" : "passwort",
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* `Secure` richtet sich nach der tatsächlichen Adresse, nicht nach NODE_ENV:
|
||
* Ein produktiv gebautes Dashboard, das über http:// läuft (etwa im LAN oder beim lokalen Prüfen),
|
||
* würde sonst ein Cookie setzen, das der Browser verwirft – die Anmeldung liefe dann ins Leere.
|
||
*/
|
||
export function ueberHttps(): boolean {
|
||
const adresse = process.env.APP_URL;
|
||
if (adresse) return adresse.startsWith("https://");
|
||
return process.env.NODE_ENV === "production";
|
||
}
|
||
|
||
export function cookieOptionen() {
|
||
return {
|
||
httpOnly: true,
|
||
sameSite: "lax" as const,
|
||
secure: ueberHttps(),
|
||
path: "/",
|
||
maxAge: SITZUNGSDAUER_STUNDEN * 3600,
|
||
};
|
||
}
|