Die Zugangsdaten der Sicherung standen in einer Textdatei auf der Platte. Jetzt
stehen sie im Panel, zusammen mit der Discord-Einrichtung, die vom Konto
dorthin umgezogen ist.
DER EIGENTLICHE GEWINN IST NICHT DIE BEQUEMLICHKEIT. Eine Textdatei sagt
niemandem, ob die Sicherung heute Nacht gelaufen ist -- und eine Sicherung, die
still aufhoert zu laufen, ist der eigentliche Schaden, nicht die eine Nacht, in
der sie fehlt. Das Skript schreibt sein Ergebnis deshalb nach jedem Lauf in die
Datenbank, und die Karte zeigt es als Erstes: wann, ob erfolgreich, wie viele
Dateien, wie gross, und ob es in der Nextcloud gelandet ist oder nur lokal.
Nachgemessen: {"dateien":228,"groesse":12063419,"hoch":false,"ok":true}.
DAS PASSWORT GEHT NICHT UEBER DIE API. Das Skript laeuft auf dem Wirt, nicht im
Dienst -- es liest die Werte per `docker exec` DIREKT aus media.db. Ueber HTTP
geht das Passwort nur beim Eintragen im Browser, und das ueber TLS. Zurueck gibt
die API nur "hatPasswort: true", wie beim Discord-Geheimnis.
nextcloud.txt neben den Sicherungen gilt weiter und hat VORRANG: wer sie
angelegt hat, soll nicht suchen muessen, warum sie ploetzlich ignoriert wird.
Zwei Fussangeln beim Melden, beide beim Bauen aufgefallen:
- Der JSON-Text geht ueber die STANDARDEINGABE in den Container und nicht als
Argument -- Anfuehrungszeichen ueberleben den Weg durch zwei Shells nicht.
- Auch dieses Hier-Dokument braucht das -replace "`r": dieselbe CRLF-Falle wie
bei der Pruefung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
5.8 KiB
TypeScript
148 lines
5.8 KiB
TypeScript
// 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),
|
|
}
|
|
}
|