Files
betreuungs-cockpit/src/middleware.ts
T
Florian Wilde 1234a57fa6 fix: Typisierung von oeffnen(), CSP im Entwicklungsmodus, Nonce-Abweichung
Drei Fehler, die sich beim Durchsehen der Typpruefung und der
Browserkonsole gezeigt haben.

1. oeffnen() lieferte Record<string, string | null>. Zusammen mit
   noUncheckedIndexedAccess wurde daraus bei jedem Zugriff
   string | null | undefined - und jede aufrufende Seite haette das
   wegkaesen muessen, obwohl die Felder immer da sind. Jetzt leitet ein
   Mapped Type die Klartextnamen aus VERSCHLUESSELTE_FELDER ab:
   oeffnen('Betreute', zeile).diagnosen ist sauber string | null.
   Das hat auf einen Schlag 17 Folgefehler in fremden Modulen erledigt.

2. Die CSP blockierte im Entwicklungsmodus die eigenen Skripte.
   'strict-dynamic' schaltet die Freigabe nach Herkunft ab, also auch
   'self' - und Hot-Reload und Fehleranzeige laden ohne Nonce. Man sah
   eine tote Seite statt der Fehlermeldung. Im Produktivbetrieb bleibt
   es bei Nonce plus 'strict-dynamic', im Entwicklungsmodus gilt die
   lockere Fassung.

3. Das Theme-Skript im Layout meldete bei jedem Seitenaufruf eine
   Hydrations-Abweichung: React gibt das nonce-Attribut absichtlich
   nicht an den Browser weiter, serverseitig steht es aber im HTML.
   suppressHydrationWarning am Skript selbst - das html-Element allein
   deckt das nicht ab.

Nebenbei: Algorithm.Argon2id ist ein const enum und laesst sich mit
isolatedModules nicht importieren. Die Angabe entfaellt, @node-rs/argon2
benutzt von sich aus Argon2id.

Die Anmeldeseite laedt jetzt ohne einen einzigen Konsolenfehler.

Etappe: 8
Phase: Haertung
2026-08-22 06:06:20 +02:00

105 lines
3.7 KiB
TypeScript

import { NextResponse, type NextRequest } from 'next/server'
/**
* Content-Security-Policy mit Nonce.
*
* Warum hier und nicht in next.config? Weil die Nonce pro Anfrage neu sein
* muss. Statische Header koennen das nicht.
*
* Die Richtlinie ist bewusst eng: keine fremden Quellen, keine Fonts von
* Google, kein CDN, keine Analytics. Was nicht aus dieser Anwendung kommt,
* darf nicht geladen werden - hier liegen Gesundheits- und Finanzdaten.
*
* 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 <script src>-Zeile.
*
* Im Entwicklungsmodus geht das NICHT: 'strict-dynamic' schaltet die
* Freigabe nach Herkunft ab, also auch 'self'. Der Hot-Reload und die
* Fehleranzeige von Next.js laden aber Skripte ohne Nonce
* (webpack.js, react-refresh.js, das Fehler-Overlay des Pages-Routers).
* Die wuerden alle blockiert - man saehe eine tote Seite statt der
* Fehlermeldung. Deshalb hier bewusst die lockere Fassung.
*
* Weitere bewusste Zugestaendnisse:
* - style-src erlaubt 'unsafe-inline'. Tailwind selbst braucht das nicht,
* aber Next.js und Radix setzen Inline-Styles fuer Positionierung. Das
* Risiko ist bei Styles deutlich kleiner als bei Skripten.
* - img-src erlaubt data: - QR-Codes und Belegvorschauen werden als
* Data-URI eingebettet, statt sie ueber einen Fremddienst zu holen.
*/
/** Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen. */
const OEFFENTLICHE_PFADE = [
'/anmelden',
'/ersteinrichtung',
'/datenschutz',
'/impressum',
'/api/auth',
]
function nonceErzeugen(): string {
const bytes = new Uint8Array(16)
crypto.getRandomValues(bytes)
return btoa(String.fromCharCode(...bytes))
}
export function middleware(anfrage: NextRequest) {
const nonce = nonceErzeugen()
const istEntwicklung = process.env.NODE_ENV === 'development'
const richtlinie = [
`default-src 'self'`,
istEntwicklung
? `script-src 'self' 'unsafe-inline' 'unsafe-eval'`
: `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' blob: data:`,
`font-src 'self'`,
// Uploads und Server Actions gehen an die eigene Herkunft. Im
// Entwicklungsmodus zusaetzlich der WebSocket des Hot-Reloads.
`connect-src 'self'${istEntwicklung ? ' ws: wss:' : ''}`,
`media-src 'self' blob:`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
`frame-src 'self' blob:`,
`worker-src 'self' blob:`,
`manifest-src 'self'`,
...(istEntwicklung ? [] : ['upgrade-insecure-requests']),
].join('; ')
const kopf = new Headers(anfrage.headers)
kopf.set('x-nonce', nonce)
const antwort = NextResponse.next({ request: { headers: kopf } })
antwort.headers.set('Content-Security-Policy', richtlinie)
// Verhindert, dass ein Bericht oder Beleg im Verlauf des Browsers
// zwischengespeichert wird.
if (!OEFFENTLICHE_PFADE.some((pfad) => anfrage.nextUrl.pathname.startsWith(pfad))) {
antwort.headers.set('Cache-Control', 'no-store, must-revalidate')
}
return antwort
}
export const config = {
matcher: [
/*
* Alles ausser statischen Dateien und dem Favicon. Der negative
* Lookahead haelt die Middleware von Auslieferungspfaden fern, wo sie
* nur Rechenzeit kosten wuerde.
*/
{
source: '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|svg|webp|ico|woff2?)$).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}