// Einstellungen, die zur Laufzeit verstellbar sind. // // WAS HIER LIEGT UND WAS NICHT. In der Umgebung bleibt, was den Dienst // ueberhaupt erreichbar macht: PUBLIC_URL, PORT, DATA_DIR, ADMIN_PASSWORD. Wer // die per Formular verstellen koennte, koennte sich mit einem Klick // aussperren, und die Reparatur waere ein Eingriff in die Datenbank. // // Hier liegt der Rest — derzeit die Discord-Anmeldung. Der Gewinn ist nicht // nur Bequemlichkeit: das Panel kann die Redirect-URI gleich zum Kopieren // hinlegen, und die ist die Stelle, an der ein OAuth-Anlauf am haeufigsten // scheitert. // // DAS GEHEIMNIS LIEGT DAMIT IN media.db, und die steckt in jeder Sicherung // unter C:\backup. Die .env ist dort nicht drin. Beides liegt unverschluesselt // auf derselben Platte, der Unterschied ist die Zahl der Kopien — wer den // Client-Schluessel zurueckziehen will, tut das im Discord-Portal, nicht hier. import { db, now } from './db.js' const lesen = db.prepare('SELECT value FROM settings WHERE key = ?') const schreiben = db.prepare( `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`, ) /** Ein Wert aus der Datenbank — oder, wenn dort nichts steht, aus der * Umgebung. Die Reihenfolge ist Absicht: wer schon Stack-Variablen gesetzt * hat, behaelt sie, und wer das Panel benutzt, uebersteuert sie. */ export function getSetting(key: string, envName?: string): string { const zeile = lesen.get(key) as { value: string } | undefined if (zeile && zeile.value !== '') return zeile.value return (envName ? (process.env[envName] ?? '') : '').trim() } export function setSetting(key: string, value: string): void { schreiben.run(key, value, now()) } /** Ob ein Wert aus der Datenbank kommt (true) oder aus der Umgebung (false). * Die Oberflaeche sagt das dazu — sonst wundert sich jemand, warum ein * geleertes Feld weiterhin wirkt. */ export function ausDatenbank(key: string): boolean { const zeile = lesen.get(key) as { value: string } | undefined return Boolean(zeile && zeile.value !== '') } // ----------------------------------------------------------------- Sicherung // // Die Werte fuer die Sicherung stehen hier, obwohl das Skript sie braucht und // nicht der Dienst. Der Grund ist die Rueckmeldung: eine Textdatei auf der // Platte sagt niemandem, ob die Sicherung heute Nacht gelaufen ist. Ein // Eintrag in der Datenbank kann das Panel anzeigen. // // tools/sichern.ps1 liest sie ueber `docker exec` DIREKT aus media.db und // nicht ueber die API: so verlaesst das Passwort den Rechner nie -- ausser // beim Eintragen im Browser, und das geht ueber TLS. export const SICHERUNG_KEYS = { basis: 'sicherung.basis', benutzer: 'sicherung.benutzer', passwort: 'sicherung.passwort', ordner: 'sicherung.ordner', behalten: 'sicherung.behalten', /** Schreibt das Skript nach jedem Lauf. JSON, damit das Panel es anzeigen * kann, ohne dass hier eine weitere Tabelle noetig waere. */ letzte: 'sicherung.letzte', } as const export type SicherungConfig = { basis: string benutzer: string ordner: string behalten: number hatPasswort: boolean bereit: boolean } export function sicherungConfig(): SicherungConfig { const basis = getSetting(SICHERUNG_KEYS.basis) const benutzer = getSetting(SICHERUNG_KEYS.benutzer) const passwort = getSetting(SICHERUNG_KEYS.passwort) const ordner = getSetting(SICHERUNG_KEYS.ordner) || 'Sicherungen/d4rk_media' const behalten = Number(getSetting(SICHERUNG_KEYS.behalten)) || 14 return { basis, benutzer, ordner, behalten, hatPasswort: Boolean(passwort), bereit: Boolean(basis && benutzer && passwort), } } // ------------------------------------------------------------------- Discord export const DISCORD_KEYS = { clientId: 'discord.client_id', clientSecret: 'discord.client_secret', guildId: 'discord.guild_id', /** MEHRERE Rollen, mit Komma getrennt. Der alte Schluessel mit genau einer * Rolle wird weiter gelesen (siehe rollen()) — wer schon eine eingetragen * hatte, soll sie nicht verlieren, nur weil hier eine Liste daraus wurde. */ roleIds: 'discord.role_ids', roleIdAlt: 'discord.role_id', } as const export type DiscordConfig = { clientId: string clientSecret: string guildId: string /** Wer EINE davon hat, darf herein. Leere Liste heisst: nicht eingerichtet. */ roleIds: string[] /** Erst wenn alle vier stehen, hat die Anmeldung ueberhaupt eine Chance. * Ein halb eingerichteter Weg, der auf halber Strecke abbricht, ist * schlimmer als ein Knopf, den es nicht gibt. */ bereit: boolean } /** Die Rollen, aus welcher Quelle sie auch kommen. * * Reihenfolge: die neue Liste, dann der alte Einzelwert, dann die Umgebung. * Getrennt wird an Komma, Semikolon, Leerzeichen und Zeilenumbruch — wer eine * Liste aus Discord kopiert, soll sie nicht erst von Hand aufraeumen. */ export function rollen(): string[] { const roh = getSetting(DISCORD_KEYS.roleIds) || getSetting(DISCORD_KEYS.roleIdAlt) || (process.env.DISCORD_ROLE_IDS ?? process.env.DISCORD_ROLE_ID ?? '') return [...new Set(roh.split(/[\s,;]+/).map((t) => t.trim()).filter(Boolean))] } export function discordConfig(): DiscordConfig { const clientId = getSetting(DISCORD_KEYS.clientId, 'DISCORD_CLIENT_ID') const clientSecret = getSetting(DISCORD_KEYS.clientSecret, 'DISCORD_CLIENT_SECRET') const guildId = getSetting(DISCORD_KEYS.guildId, 'DISCORD_GUILD_ID') const roleIds = rollen() return { clientId, clientSecret, guildId, roleIds, bereit: Boolean(clientId && clientSecret && guildId && roleIds.length > 0), } }