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:
co-authored by
Claude Opus 5
parent
748d032f70
commit
e2b00f4a57
@@ -0,0 +1,46 @@
|
||||
import 'server-only'
|
||||
import { notFound } from 'next/navigation'
|
||||
import {
|
||||
type Faehigkeit,
|
||||
type Instanz,
|
||||
istErlaubt,
|
||||
kalenderAnbieterVon,
|
||||
} from '@/lib/instanz'
|
||||
import { umgebung } from '@/server/umgebung'
|
||||
|
||||
/** Welche Instanz laeuft hier? */
|
||||
export function aktuelleInstanz(): Instanz {
|
||||
return umgebung().instanz
|
||||
}
|
||||
|
||||
/** Ist die Faehigkeit in dieser Instanz erlaubt? */
|
||||
export function kann(faehigkeit: Faehigkeit): boolean {
|
||||
return istErlaubt(aktuelleInstanz(), faehigkeit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Riegel fuer Seiten und Route Handler: was diese Instanz nicht darf,
|
||||
* existiert hier auch nicht. Kein Hinweis, kein 403 – schlicht 404.
|
||||
*/
|
||||
export function erzwingeFaehigkeit(faehigkeit: Faehigkeit): void {
|
||||
if (!kann(faehigkeit)) {
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Riegel fuer Server Actions und alles ausserhalb des Renderns, wo notFound()
|
||||
* nicht greift.
|
||||
*/
|
||||
export function pruefeFaehigkeit(faehigkeit: Faehigkeit): void {
|
||||
if (!kann(faehigkeit)) {
|
||||
throw new Error(
|
||||
`Die Faehigkeit "${faehigkeit}" ist in der Instanz "${aktuelleInstanz()}" nicht erlaubt.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Kalenderanbieter, die in dieser Instanz verbunden werden duerfen. */
|
||||
export function erlaubteKalenderAnbieter(): readonly ('GRAPH' | 'GOOGLE')[] {
|
||||
return kalenderAnbieterVon(aktuelleInstanz())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user