Wer EINE der eingetragenen Rollen hat, kommt ins Dashboard. Mehrere sind der Normalfall, sobald mehr als eine Person Zugang haben soll -- Admin und Fotograf sind selten dieselbe Rolle. Die bisherige einzelne Rolle geht NICHT verloren: der alte Schluessel (discord.role_id) wird weitergelesen, bis die Liste einmal gepflegt wird; dann raeumt das Speichern ihn weg. Zwei Quellen fuer dieselbe Frage laufen sonst auseinander. Nachgemessen: die vorhandene Rolle stand nach dem Umbau unveraendert drin, bereit blieb true. Getrennt wird an Komma, Semikolon, Leerzeichen und Zeilenumbruch -- wer drei IDs untereinander aus Discord kopiert, soll sie nicht erst zu einer Zeile zusammenschieben. Doppelte fallen zusammen. UND DIE SCHLECHTE WIRD BEIM NAMEN GENANNT: "123 ist keine Rollen-ID (17 bis 20 Ziffern)" statt "Rollen-ID ungueltig". Bei fuenf Eintraegen ist die zweite Meldung eine Suchaufgabe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
198 lines
8.7 KiB
TypeScript
198 lines
8.7 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Check, Copy, ExternalLink, Loader2, ShieldCheck, ShieldX } from 'lucide-react'
|
|
import { api, type Einstellungen } from '../api'
|
|
import { run, useStore } from '../store'
|
|
import { Feld, Karte, Knopf, useKopieren } from '../components/Bausteine'
|
|
|
|
/** Die Discord-Anmeldung einrichten — im Panel statt in der .env.
|
|
*
|
|
* Der eigentliche Gewinn steht ganz oben in der Karte: die Redirect-URI zum
|
|
* Kopieren. Sie muss ZEICHEN FÜR ZEICHEN mit dem übereinstimmen, was im
|
|
* Discord-Portal steht, und daran scheitert der erste Anlauf fast immer. */
|
|
export function DiscordKarte() {
|
|
const note = useStore((s) => s.note)
|
|
const [daten, setDaten] = useState<Einstellungen | null>(null)
|
|
const [laedt, setLaedt] = useState(true)
|
|
const [speichert, setSpeichert] = useState(false)
|
|
const { kopiert, kopieren } = useKopieren()
|
|
|
|
const [clientId, setClientId] = useState('')
|
|
const [geheimnis, setGeheimnis] = useState('')
|
|
const [guildId, setGuildId] = useState('')
|
|
const [rollen, setRollen] = useState('')
|
|
|
|
async function laden() {
|
|
setLaedt(true)
|
|
try {
|
|
const e = await api.einstellungen()
|
|
setDaten(e)
|
|
setClientId(e.discord.clientId)
|
|
setGuildId(e.discord.guildId)
|
|
setRollen(e.discord.roleIds.join('\n'))
|
|
} catch (err) {
|
|
note('fehler', err instanceof Error ? err.message : 'Unbekannt')
|
|
} finally {
|
|
setLaedt(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void laden()
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
async function speichern() {
|
|
setSpeichert(true)
|
|
const ergebnis = await run(
|
|
() =>
|
|
api.einstellungenSpeichern({
|
|
clientId,
|
|
// Leer heißt „nicht anfassen" — sonst würde jedes Speichern
|
|
// das Geheimnis wegwerfen, weil es nie im Formular steht.
|
|
...(geheimnis ? { clientSecret: geheimnis } : {}),
|
|
guildId,
|
|
roleIds: rollen,
|
|
}),
|
|
(r) =>
|
|
r.bereit
|
|
? 'Gespeichert. Die Discord-Anmeldung ist eingerichtet.'
|
|
: `Gespeichert. Es fehlt noch: ${r.fehlt.join(', ')}.`,
|
|
)
|
|
setSpeichert(false)
|
|
if (ergebnis) {
|
|
setGeheimnis('')
|
|
void laden()
|
|
}
|
|
}
|
|
|
|
if (laedt) {
|
|
return (
|
|
<Karte titel="Anmeldung über Discord">
|
|
<div className="flex justify-center py-6">
|
|
<Loader2 className="animate-spin text-schrift-leise" />
|
|
</div>
|
|
</Karte>
|
|
)
|
|
}
|
|
if (!daten) return null
|
|
|
|
const d = daten.discord
|
|
|
|
return (
|
|
<Karte
|
|
titel="Anmeldung über Discord"
|
|
rechts={
|
|
d.bereit ? (
|
|
<span className="flex items-center gap-1.5 text-xs text-gut">
|
|
<ShieldCheck size={14} /> eingerichtet ·{' '}
|
|
{d.roleIds.length === 1 ? '1 Rolle' : `${d.roleIds.length} Rollen`}
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-1.5 text-xs text-schrift-leise">
|
|
<ShieldX size={14} /> nicht eingerichtet
|
|
</span>
|
|
)
|
|
}
|
|
>
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<p className="mb-1 text-xs font-medium text-schrift-leise">
|
|
Diese Adresse muss im Discord-Portal unter OAuth2 → Redirects stehen —
|
|
Zeichen für Zeichen:
|
|
</p>
|
|
<div className="flex items-center gap-2">
|
|
<code className="flex-1 overflow-x-auto rounded border border-rand bg-grund px-3 py-1.5 font-mono text-xs">
|
|
{d.redirectUri}
|
|
</code>
|
|
<button
|
|
onClick={async () => {
|
|
if (!(await kopieren(d.redirectUri, 'uri')))
|
|
note('fehler', 'Die Zwischenablage ist gesperrt.')
|
|
}}
|
|
className="shrink-0 rounded border border-rand px-2.5 py-1.5 text-schrift-leise hover:bg-flaeche-hoch hover:text-schrift"
|
|
aria-label="Adresse kopieren"
|
|
>
|
|
{kopiert === 'uri' ? (
|
|
<Check size={14} className="text-gut" />
|
|
) : (
|
|
<Copy size={14} />
|
|
)}
|
|
</button>
|
|
<a
|
|
href="https://discord.com/developers/applications"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="shrink-0 rounded border border-rand px-2.5 py-1.5 text-schrift-leise hover:bg-flaeche-hoch hover:text-schrift"
|
|
aria-label="Discord-Portal öffnen"
|
|
>
|
|
<ExternalLink size={14} />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<Feld
|
|
label="Client-ID"
|
|
value={clientId}
|
|
placeholder="17 bis 20 Ziffern"
|
|
hinweis={d.ausDerUmgebung.clientId ? 'kommt derzeit aus der Umgebung' : undefined}
|
|
onChange={(e) => setClientId(e.target.value)}
|
|
/>
|
|
<Feld
|
|
label="Client-Geheimnis"
|
|
type="password"
|
|
value={geheimnis}
|
|
placeholder={d.hatGeheimnis ? '•••••••• (hinterlegt)' : 'noch keines'}
|
|
hinweis={
|
|
d.ausDerUmgebung.clientSecret
|
|
? 'kommt derzeit aus der Umgebung'
|
|
: 'leer lassen heißt: unverändert. Zum Entfernen ein „-" eintragen.'
|
|
}
|
|
onChange={(e) => setGeheimnis(e.target.value)}
|
|
/>
|
|
<Feld
|
|
label="Server-ID"
|
|
value={guildId}
|
|
placeholder="Rechtsklick auf den Server → ID kopieren"
|
|
hinweis={d.ausDerUmgebung.guildId ? 'kommt derzeit aus der Umgebung' : undefined}
|
|
onChange={(e) => setGuildId(e.target.value)}
|
|
/>
|
|
<label className="block sm:col-span-2">
|
|
<span className="mb-1 block text-xs font-medium text-schrift-leise">
|
|
Rollen-IDs — eine je Zeile
|
|
</span>
|
|
<textarea
|
|
value={rollen}
|
|
onChange={(e) => setRollen(e.target.value)}
|
|
rows={3}
|
|
spellCheck={false}
|
|
placeholder={'143902xxxxxxxxxxxxx\n143903xxxxxxxxxxxxx'}
|
|
className="w-full rounded-md border border-rand bg-grund px-3 py-1.5 font-mono
|
|
text-sm text-schrift placeholder:text-schrift-leise/60
|
|
focus:border-akzent focus:outline-none"
|
|
/>
|
|
<span className="mt-1 block text-xs text-schrift-leise">
|
|
{d.ausDerUmgebung.roleIds
|
|
? 'kommt derzeit aus der Umgebung'
|
|
: 'Wer EINE davon hat, darf herein. Komma, Semikolon oder Zeilenumbruch trennen — kopier sie einfach hinein.'}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<p className="text-xs text-schrift-leise">
|
|
Die IDs bekommst du in Discord über Einstellungen → Erweitert →
|
|
Entwicklermodus, danach Rechtsklick → ID kopieren. Es wird nur{' '}
|
|
<code className="font-mono">identify</code> und{' '}
|
|
<code className="font-mono">guilds.members.read</code> erfragt — kein Bot,
|
|
keine Nachrichten, nichts weiter. Das Passwort bleibt als Notausgang
|
|
bestehen.
|
|
</p>
|
|
|
|
<div>
|
|
<Knopf art="haupt" wartet={speichert} onClick={speichern} kind="Speichern" />
|
|
</div>
|
|
</div>
|
|
</Karte>
|
|
)
|
|
}
|