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:
2026-08-11 18:44:45 +02:00
co-authored by Claude Opus 5
parent 0b295a2302
commit dffec7714c
9 changed files with 746 additions and 13 deletions
+28
View File
@@ -91,8 +91,33 @@ CREATE TABLE IF NOT EXISTS events (
actor TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS events_at ON events(at DESC);
-- Einstellungen, die zur Laufzeit aenderbar sein sollen.
--
-- Bewusst NICHT alles: was den Dienst ueberhaupt hochfahren laesst (PUBLIC_URL,
-- Port, Datenordner) bleibt in der Umgebung, damit ein falscher Klick den
-- Dienst nicht unerreichbar macht. Hier liegt nur, was ohne Neustart
-- verstellbar sein soll -- derzeit die Discord-Anmeldung.
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`)
// Nachtraegliche Spalten. SQLite kann kein "ADD COLUMN IF NOT EXISTS", also
// wird nachgesehen — das ist die ganze Wanderung, die dieses Projekt braucht.
{
const spalten = db.prepare('PRAGMA table_info(users)').all() as { name: string }[]
if (!spalten.some((s) => s.name === 'discord_id')) {
db.exec('ALTER TABLE users ADD COLUMN discord_id TEXT')
// Kein UNIQUE in der Spaltendefinition: SQLite erlaubt das bei ADD
// COLUMN nicht. Der Index danach leistet dasselbe.
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS users_discord ON users(discord_id)')
console.log('[db] Spalte users.discord_id angelegt')
}
}
// Den Verlauf aus dem vorhandenen Bestand nachtragen -- einmalig, und nur
// wenn er leer ist.
//
@@ -123,6 +148,9 @@ export type User = {
username: string
password_hash: string
created_at: number
/** Gesetzt, wenn dieses Konto ueber Discord hereinkommt. Ein solches Konto
* hat kein brauchbares Passwort — verifyPassword scheitert daran sauber. */
discord_id: string | null
}
export type Token = {