import 'server-only' import { z } from 'zod' import { INSTANZEN, istErlaubt, 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), // --- Integrationen (M6) --- // Zammad, nur in der Instanz "firma". Der Token gehoert einem Benutzer und // sieht genau das, was dieser Benutzer sehen darf. ZAMMAD_URL: z.string().trim().default(''), ZAMMAD_TOKEN: z.string().trim().default(''), /// Suchausdruck in Zammads Syntax. Was hier passt, wird zur Aufgabe. ZAMMAD_ABFRAGE: z .string() .trim() .default('owner_id:me AND (state.name:new OR state.name:open)'), // Telegram, nur in der Instanz "daheim". TELEGRAM_TOKEN: z.string().trim().default(''), // Spiegel: firma sendet, daheim empfaengt. Dasselbe Geheimnis auf beiden // Seiten; ohne gueltige Signatur nimmt der Empfaenger nichts an. SPIEGEL_GEHEIMNIS: z.string().trim().default(''), /// Nur auf der sendenden Seite: wohin geschickt wird. SPIEGEL_ZIEL: z.string().trim().default(''), 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 /** Zammad braucht Adresse und Token – und die Instanz muss es erlauben. */ zammadAktiv: boolean telegramAktiv: boolean /** Senden geht nur mit Ziel, Empfangen schon mit dem Geheimnis allein. */ spiegelSendenAktiv: boolean spiegelEmpfangenAktiv: 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 Instanz entscheidet mit: eine Integration, die hier nicht erlaubt // ist, laesst sich auch mit vollstaendiger Konfiguration nicht anschalten. zammadAktiv: istErlaubt(werte.INSTANZ, 'integration:zammad') && Boolean(werte.ZAMMAD_URL && werte.ZAMMAD_TOKEN), telegramAktiv: istErlaubt(werte.INSTANZ, 'integration:telegram') && Boolean(werte.TELEGRAM_TOKEN), spiegelSendenAktiv: istErlaubt(werte.INSTANZ, 'spiegel:senden') && Boolean(werte.SPIEGEL_GEHEIMNIS && werte.SPIEGEL_ZIEL), spiegelEmpfangenAktiv: istErlaubt(werte.INSTANZ, 'spiegel:empfangen') && Boolean(werte.SPIEGEL_GEHEIMNIS), } } /** 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 }