diff --git a/prisma/seed.ts b/prisma/seed.ts
index 3297969..c5ae85e 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -23,13 +23,14 @@
* Der Seed ist idempotent: mehrfaches Ausfuehren aendert nichts.
*/
import { PrismaClient } from '@prisma/client'
-import { hash, Algorithm } from '@node-rs/argon2'
+import { hash } from '@node-rs/argon2'
import { createCipheriv, randomBytes } from 'node:crypto'
const db = new PrismaClient()
+// Muss zu ARGON2_OPTIONEN in src/lib/crypto/hash.ts passen.
+// Ohne Angabe benutzt @node-rs/argon2 von sich aus Argon2id.
const ARGON2 = {
- algorithm: Algorithm.Argon2id,
memoryCost: 19_456,
timeCost: 2,
parallelism: 1,
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
index 9dc6f40..b2636a1 100644
--- a/src/app/api/auth/[...nextauth]/route.ts
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -6,4 +6,6 @@
*/
export const runtime = 'nodejs'
-export { GET, POST } from '@/auth'
+import { handlers } from '@/auth'
+
+export const { GET, POST } = handlers
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 4351ff7..f4947a7 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -57,7 +57,18 @@ export default async function RootLayout({ children }: { children: React.ReactNo
return (
-
+ {/*
+ suppressHydrationWarning ist hier noetig: React gibt das
+ nonce-Attribut bewusst nicht an den Browser weiter (es soll nicht
+ im DOM auslesbar sein). Serverseitig steht es im HTML, clientseitig
+ ist es leer - und genau das meldet React sonst bei jedem Seiten-
+ aufruf als Abweichung.
+ */}
+
{children}
diff --git a/src/lib/crypto/feld.ts b/src/lib/crypto/feld.ts
index c5f7ab2..b649a4a 100644
--- a/src/lib/crypto/feld.ts
+++ b/src/lib/crypto/feld.ts
@@ -177,6 +177,22 @@ export const VERSCHLUESSELTE_FELDER = {
export type VerschluesseltesModell = keyof typeof VERSCHLUESSELTE_FELDER
+/** Aus "diagnosenVerschluesselt" wird "diagnosen". */
+type OhneSuffix = F extends `${infer Name}Verschluesselt` ? Name : never
+
+/**
+ * Die Klartextfelder eines Modells als benannte Eigenschaften.
+ *
+ * Ohne diesen Typ waere das Ergebnis von oeffnen() nur ein
+ * Record. Mit noUncheckedIndexedAccess bekaeme
+ * jeder Zugriff darauf zusaetzlich "undefined" - und jede aufrufende
+ * Seite muesste das wegkaesen, obwohl die Felder tatsaechlich immer da
+ * sind.
+ */
+export type Klartextfelder = {
+ [K in OhneSuffix<(typeof VERSCHLUESSELTE_FELDER)[M][number]>]: string | null
+}
+
/**
* Nimmt ein Objekt mit Klartextfeldern (ohne Suffix) und liefert die
* verschluesselten Felder (mit Suffix). Beispiel:
@@ -209,17 +225,22 @@ export function sealen(
* Umschlagfelder bleiben stehen, damit nichts versehentlich zurueck-
* geschrieben wird.
*/
-export function oeffnen>(
- modell: VerschluesseltesModell,
+export function oeffnen>(
+ modell: M,
datensatz: T,
-): T & Record {
- const ergebnis = { ...datensatz } as T & Record
+): T & Klartextfelder {
+ // Zwischenschritt ueber ein einfaches Objekt: ein generischer Typ laesst
+ // sich in TypeScript nicht per Index beschreiben.
+ const ergebnis: Record = { ...datensatz }
+
for (const feld of VERSCHLUESSELTE_FELDER[modell] as readonly string[]) {
- if (!(feld in datensatz)) continue
const klarname = feld.replace(/Verschluesselt$/, '')
- ergebnis[klarname] = entschluesselnWennMoeglich(datensatz[feld] as string | null, feld)
+ ergebnis[klarname] = (feld in datensatz)
+ ? entschluesselnWennMoeglich(datensatz[feld] as string | null, feld)
+ : null
}
- return ergebnis
+
+ return ergebnis as T & Klartextfelder
}
/** Konstantzeit-Vergleich zweier Zeichenketten. */
diff --git a/src/lib/crypto/hash.ts b/src/lib/crypto/hash.ts
index f68e6d2..e3cd609 100644
--- a/src/lib/crypto/hash.ts
+++ b/src/lib/crypto/hash.ts
@@ -1,5 +1,5 @@
import 'server-only'
-import { hash, verify, Algorithm } from '@node-rs/argon2'
+import { hash, verify } from '@node-rs/argon2'
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'
import { env } from '@/lib/env'
@@ -13,8 +13,12 @@ import { env } from '@/lib/env'
* lassen sich also ohne Migration einfuehren (siehe hashVeraltet).
*/
const ARGON2_OPTIONEN = {
- algorithm: Algorithm.Argon2id,
- memoryCost: 19_456, // KiB = 19 MiB
+ // Der Algorithmus wird bewusst nicht gesetzt: @node-rs/argon2 benutzt
+ // von sich aus Argon2id. Die Konstante Algorithm.Argon2id ist ein
+ // "const enum" und laesst sich mit isolatedModules nicht importieren -
+ // eine hart geschriebene 2 waere die Alternative, aber die sagt beim
+ // Lesen nichts.
+ memoryCost: 19_456, // KiB = 19 MiB, OWASP-Empfehlung Stand 2024
timeCost: 2,
parallelism: 1,
} as const
diff --git a/src/middleware.ts b/src/middleware.ts
index 9570614..1d2c941 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -10,16 +10,24 @@ import { NextResponse, type NextRequest } from 'next/server'
* Google, kein CDN, keine Analytics. Was nicht aus dieser Anwendung kommt,
* darf nicht geladen werden - hier liegen Gesundheits- und Finanzdaten.
*
- * 'strict-dynamic' erlaubt es dem Next.js-Loader, weitere eigene Skripte
- * nachzuladen, ohne dass jede einzelne Datei aufgelistet werden muss.
+ * Im Produktivbetrieb gilt 'strict-dynamic': Next.js haengt die Nonce an
+ * seine eigenen Skript-Tags, und diese duerfen dann weitere eigene
+ * Skripte nachladen. Kein Fremdskript kommt durch, auch nicht ueber eine
+ * eingeschleuste