// 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 !== '') } // ------------------------------------------------------------ Adressvorlagen // // Eine Vorlage ist der Bauplan einer oeffentlichen Adresse: // // vehicles/{model}.webp -> https://…/f/vehicles/adder.webp // // Wozu speichern? Weil genau diese Zeichenkette an der anderen Seite gebraucht // wird -- im Handy, im Skript, im Fotostudio -- und weil sie sich jemand sonst // aus Ordnername plus Endung zusammenreimt. Einmal falsch zusammengereimt // heisst: das Bild fehlt, und gesucht wird an der falschen Stelle. // // Als JSON in EINER Zeile der Einstellungstabelle. Eine eigene Tabelle fuer // eine Handvoll Zeilen waere eine Wanderung mehr, die niemand braucht. /** Wie der Dienst sich nennt. * * DIESELBE Einstellung, die auch in der Fusszeile der Discord-Meldungen steht * (`D4RKST3R // MEDIA`). Ein zweiter Name fuer die Oberflaeche waere ein * zweiter, der beim naechsten Mal anders lautet als der erste -- und dann * heisst dasselbe Ding an zwei Stellen verschieden. * * Der Standard steht in meldung.ts; hier wird er nur weitergereicht. */ export const MARKE_KEY = 'marke.name' export const VORLAGEN_KEY = 'vorlagen' export type Vorlage = { name: string; muster: string } export function vorlagen(): Vorlage[] { try { const roh = getSetting(VORLAGEN_KEY) if (!roh) return [] const daten = JSON.parse(roh) if (!Array.isArray(daten)) return [] return daten.filter( (v): v is Vorlage => v && typeof v.name === 'string' && typeof v.muster === 'string', ) } catch { // Ein kaputter Eintrag darf die Einstellungsseite nicht mitreissen. return [] } } export function setzeVorlagen(liste: Vorlage[]): void { setSetting(VORLAGEN_KEY, JSON.stringify(liste)) } // ----------------------------------------------------------------- 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', /** Ein ZWEITER lokaler Ablageort, moeglichst auf einer anderen Platte. * * WOZU, wenn es doch schon die Nextcloud gibt: die ist weit weg. Wer * 100 GB zurueckholen muss, laedt sie ueber die Leitung -- das ist die * Sicherung fuer den Fall, dass das Haus abbrennt, nicht fuer den Fall, * dass eine Platte stirbt. Fuer den zweiten will man die Kopie IM Rechner * haben, nur eben nicht auf derselben Platte. * * Der Pfad gilt auf dem HOST, nicht im Container -- ihn prueft deshalb * nicht der Dienst, sondern tools/sichern.ps1. */ zweitziel: 'sicherung.zweitziel', /** 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 zweitziel: string 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, zweitziel: getSetting(SICHERUNG_KEYS.zweitziel), 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), } }