Ein Zustand je Quelle haelt fest, wie weit gelesen wurde und ob es klappt. Der Merker ist bewusst ein freier Text: Zammad braucht darin einen Zeitstempel, Telegram den Zaehler der zuletzt geholten Nachricht. Dazu die gespiegelte Belegung ohne Titel, das Adressbuch der erlaubten Telegram-Chats und der Einmalcode, mit dem sich ein Chat anmeldet. Welche Integration eine Instanz darf, entscheidet weiterhin INSTANZ. Ein Zammad-Token in der Instanz daheim bleibt wirkungslos — das steht in den Faehigkeiten, nicht in einer Einstellung, die sich verstellen liesse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
5.6 KiB
TypeScript
149 lines
5.6 KiB
TypeScript
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<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
|
||
/** 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
|
||
}
|