Files
planer/src/server/umgebung.ts
T
Florian WildeandClaude Opus 5 24f613d372 feat: Datenmodell und Umgebung fuer Benachrichtigungen
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>
2026-09-11 08:58:59 +02:00

111 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}