Drei Tabellen: die Einstellung je Benutzer, angemeldete Push-Geraete und ein Nachweis je verschickter Meldung. Der Nachweis ist das Gedaechtnis gegen Wiederholungen — ohne ihn kaeme dieselbe Sache bei jedem Durchlauf erneut. Kalenderkonten bekommen einen Zaehler fuer Fehlversuche in Folge, damit ein Abgleichfehler erst nach mehreren Versuchen gemeldet wird. VAPID- und SMTP-Werte sind freiwillig: fehlen sie, laeuft der Planer ohne den jeweiligen Weg weiter und sagt es in den Einstellungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
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<typeof schema> & {
|
||
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
|
||
}
|