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(), // --- Kalenderanbieter (M3) --- // Graph gibt es in beiden Instanzen, Google nur daheim. Welche Werte // tatsaechlich benutzt werden duerfen, entscheidet src/lib/instanz.ts. GRAPH_CLIENT_ID: z.string().trim().default(''), GRAPH_CLIENT_SECRET: z.string().trim().default(''), /// Tenant-ID, oder "organizations" fuer jeden Arbeitsplatz-Tenant. GRAPH_TENANT_ID: z.string().trim().default('organizations'), GOOGLE_CLIENT_ID: z.string().trim().default(''), GOOGLE_CLIENT_SECRET: z.string().trim().default(''), // --- Benachrichtigungen (M5) --- // VAPID-Schluessel fuer Web-Push. Erzeugen mit: // npx web-push generate-vapid-keys VAPID_OEFFENTLICH: z.string().trim().default(''), VAPID_PRIVAT: z.string().trim().default(''), /// Kontaktadresse fuer den Push-Dienst, als mailto:. VAPID_KONTAKT: z.string().trim().default(''), SMTP_HOST: z.string().trim().default(''), SMTP_PORT: z.coerce.number().int().min(1).max(65535).default(587), SMTP_BENUTZER: z.string().trim().default(''), SMTP_PASSWORT: z.string().default(''), SMTP_ABSENDER: z.string().trim().default(''), /// Implizites TLS (Port 465). Sonst wird STARTTLS versucht. SMTP_TLS: jaNein.default(false), DEMO_MODUS: jaNein.default(false), }) export type Umgebung = z.infer & { instanz: Instanz /** OIDC ist nur eingerichtet, wenn alle vier Werte gesetzt sind. */ oidcAktiv: boolean /** Graph laesst sich verbinden, sobald Kennung und Geheimnis stehen. */ graphAktiv: boolean googleAktiv: boolean /** Web-Push ist erst mit vollstaendigem VAPID-Satz moeglich. */ pushAktiv: boolean mailAktiv: 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, ), graphAktiv: Boolean(werte.GRAPH_CLIENT_ID && werte.GRAPH_CLIENT_SECRET), googleAktiv: Boolean(werte.GOOGLE_CLIENT_ID && werte.GOOGLE_CLIENT_SECRET), pushAktiv: Boolean( werte.VAPID_OEFFENTLICH && werte.VAPID_PRIVAT && werte.VAPID_KONTAKT, ), mailAktiv: Boolean(werte.SMTP_HOST && werte.SMTP_ABSENDER), } } /** 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 }