feat(instanz): Instanzsteuerung mit serverseitigem Riegel

INSTANZ entscheidet, welche Kalenderanbieter, Postfächer und
Integrationen es gibt. Was eine Instanz nicht darf, liefert dort 404
statt eines Hinweises. Ein falsch geschriebener Wert bricht ab, statt
still auf einen Standard zu fallen.

Die Konfiguration wird mit zod geprüft; Fehlermeldungen nennen den
Variablennamen, nie den Wert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-10 12:41:29 +02:00
co-authored by Claude Opus 5
parent 748d032f70
commit e2b00f4a57
4 changed files with 304 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
import 'server-only'
import { z } from 'zod'
import { INSTANZEN, type Instanz } from '@/lib/instanz'
/**
* Jede Einstellung von aussen wird geprueft, bevor sie benutzt wird. Fehlt
* etwas Pflichtiges, faellt das beim ersten Zugriff auf – mit einer Meldung,
* die den Variablennamen nennt, nie deren Wert.
*/
const jaNein = z
.enum(['true', 'false', '1', '0', 'ja', 'nein'])
.transform((w) => w === 'true' || w === '1' || w === 'ja')
const schema = z.object({
INSTANZ: z.enum(INSTANZEN),
APP_URL: z.url().transform((u) => u.replace(/\/+$/, '')),
DATABASE_URL: z.string().min(1),
AUTH_SECRET: z.string().min(16, 'AUTH_SECRET ist zu kurz, mindestens 16 Zeichen.'),
AUTH_TRUST_HOST: jaNein.default(false),
ANMELDUNG_PASSWORT: jaNein.default(false),
OIDC_NAME: z.string().trim().default(''),
OIDC_ISSUER: z.string().trim().default(''),
OIDC_CLIENT_ID: z.string().trim().default(''),
OIDC_CLIENT_SECRET: z.string().trim().default(''),
WEBAUTHN_RP_ID: z.string().min(1),
WEBAUTHN_RP_NAME: z.string().min(1).default('Planer'),
WEBAUTHN_ORIGIN: z.url(),
DEMO_MODUS: jaNein.default(false),
})
export type Umgebung = z.infer<typeof schema> & {
instanz: Instanz
/** OIDC ist nur eingerichtet, wenn alle vier Werte gesetzt sind. */
oidcAktiv: boolean
}
let zwischengespeichert: Umgebung | null = null
function lesen(): Umgebung {
const ergebnis = schema.safeParse(process.env)
if (!ergebnis.success) {
const felder = ergebnis.error.issues
.map((f) => `${f.path.join('.')}: ${f.message}`)
.join('\n ')
throw new Error(`Die Konfiguration ist unvollstaendig:\n ${felder}`)
}
const werte = ergebnis.data
return {
...werte,
instanz: werte.INSTANZ,
oidcAktiv: Boolean(
werte.OIDC_ISSUER && werte.OIDC_CLIENT_ID && werte.OIDC_CLIENT_SECRET,
),
}
}
/** Die geprueften Einstellungen. Erst beim ersten Aufruf ausgewertet. */
export function umgebung(): Umgebung {
zwischengespeichert ??= lesen()
return zwischengespeichert
}
/** Nur fuer Tests: erzwingt beim naechsten Aufruf ein erneutes Einlesen. */
export function umgebungVergessen(): void {
zwischengespeichert = null
}