feat: Discord-Anmeldung -- im Panel einrichtbar, nicht in der .env
Auf Wunsch ueber die Oberflaeche statt ueber Umgebungsvariablen. Der Gewinn ist
nicht nur Bequemlichkeit: das Panel legt die REDIRECT-URI zum Kopieren hin, und
genau daran scheitert ein OAuth-Anlauf fast immer.
settings.ts Schluessel/Wert in SQLite, mit Umgebung als Rueckfall. Wer
schon Stack-Variablen gesetzt hat, behaelt sie; das Panel
uebersteuert. In der Umgebung bleibt, was den Dienst ueberhaupt
erreichbar macht (PUBLIC_URL, PORT, DATA_DIR) -- wer das per
Formular verstellen koennte, sperrt sich mit einem Klick aus.
discord.ts Der Weg in drei Schritten, ohne Bibliothek und OHNE BOT-TOKEN:
die Rolle wird mit dem Merkmal des BENUTZERS geholt
(guilds.members.read). Kein Bot, keine Bot-Rechte, kein zweites
Geheimnis, das ablaufen kann.
DAS GEHEIMNIS GEHT NIE HERAUS -- auch nicht an eine angemeldete Sitzung. Das
Panel bekommt nur "hatGeheimnis: true". Ein Formular, das den Client-Schluessel
im Klartext zurueckschickt, streut ihn in jeden Browser-Cache. Ein leeres Feld
heisst "unveraendert", nicht "loeschen"; zum Entfernen gibt es "-".
JEDER ABBRUCH IST SICHTBAR: kein weisser Bildschirm, sondern zurueck aufs
Formular mit dem Grund im Adressfeld -- "Du bist nicht auf diesem Server",
"Dir fehlt die noetige Rolle", oder bei 4xx vom Tausch der Hinweis auf die
Redirect-URI. Die IDs werden schon beim Speichern geprueft (17-20 Ziffern),
damit ein halber Link nicht erst beim Anmelden auffaellt.
Das Passwort bleibt als Notausgang. Ein ueber Discord angelegtes Konto bekommt
KEIN brauchbares Passwort ("nur-ueber-discord"): verifyPassword scheitert an
allem, was nicht dem scrypt-Format entspricht -- nachgesehen, nicht vermutet.
Dazu der Verlauf, wie gewuenscht: Suche nach Pfad (mit derselben
LIKE-Maskierung wie in der Galerie -- Suche nach "_" findet 0 statt allem),
waehlbare Seitengroesse (25/50/100/200, Standard 50) und mehr Luft zwischen
"Groesse" und "Von wem".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// 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 !== '')
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- Discord
|
||||
|
||||
export const DISCORD_KEYS = {
|
||||
clientId: 'discord.client_id',
|
||||
clientSecret: 'discord.client_secret',
|
||||
guildId: 'discord.guild_id',
|
||||
roleId: 'discord.role_id',
|
||||
} as const
|
||||
|
||||
export type DiscordConfig = {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
guildId: string
|
||||
roleId: 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
|
||||
}
|
||||
|
||||
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 roleId = getSetting(DISCORD_KEYS.roleId, 'DISCORD_ROLE_ID')
|
||||
|
||||
return {
|
||||
clientId,
|
||||
clientSecret,
|
||||
guildId,
|
||||
roleId,
|
||||
bereit: Boolean(clientId && clientSecret && guildId && roleId),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user