From 23405d68d3a11bf97cd0a597cddb02efce7efd4b Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Tue, 11 Aug 2026 18:53:28 +0200 Subject: [PATCH] feat: mehrere Rollen duerfen herein, nicht nur eine 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 --- server/src/routes/dash.ts | 40 ++++++++++++++++++----- server/src/routes/discord.ts | 13 +++++--- server/src/settings.ts | 29 +++++++++++++--- server/ui/src/api.ts | 5 +-- server/ui/src/components/DiscordKarte.tsx | 36 +++++++++++++------- 5 files changed, 93 insertions(+), 30 deletions(-) diff --git a/server/src/routes/dash.ts b/server/src/routes/dash.ts index a40077a..79bc118 100644 --- a/server/src/routes/dash.ts +++ b/server/src/routes/dash.ts @@ -597,7 +597,7 @@ dashRoutes.get('/settings', (c) => { discord: { clientId: d.clientId, guildId: d.guildId, - roleId: d.roleId, + roleIds: d.roleIds, hatGeheimnis: Boolean(d.clientSecret), bereit: d.bereit, // Die Stelle, an der der erste Anlauf fast immer scheitert. @@ -607,7 +607,10 @@ dashRoutes.get('/settings', (c) => { clientSecret: Boolean(d.clientSecret) && !ausDatenbank(DISCORD_KEYS.clientSecret), guildId: Boolean(d.guildId) && !ausDatenbank(DISCORD_KEYS.guildId), - roleId: Boolean(d.roleId) && !ausDatenbank(DISCORD_KEYS.roleId), + roleIds: + d.roleIds.length > 0 && + !ausDatenbank(DISCORD_KEYS.roleIds) && + !ausDatenbank(DISCORD_KEYS.roleIdAlt), }, }, }) @@ -619,7 +622,7 @@ dashRoutes.post('/settings', async (c) => { clientId?: string clientSecret?: string guildId?: string - roleId?: string + roleIds?: string | string[] } }>(c) if (!body) return c.json(KEIN_JSON, 400) @@ -642,10 +645,31 @@ dashRoutes.post('/settings', async (c) => { if (v && !istId(v)) return c.json({ error: 'Server-ID: 17 bis 20 Ziffern' }, 400) setSetting(DISCORD_KEYS.guildId, v) } - if (d.roleId !== undefined) { - const v = d.roleId.trim() - if (v && !istId(v)) return c.json({ error: 'Rollen-ID: 17 bis 20 Ziffern' }, 400) - setSetting(DISCORD_KEYS.roleId, v) + if (d.roleIds !== undefined) { + // Komma, Semikolon, Leerzeichen, Zeilenumbruch — alles trennt. Wer + // drei IDs untereinander einfuegt, soll sie nicht erst zu einer Zeile + // zusammenschieben muessen. + const liste = (Array.isArray(d.roleIds) ? d.roleIds.join(',') : d.roleIds) + .split(/[\s,;]+/) + .map((t) => t.trim()) + .filter(Boolean) + + // JEDE einzeln pruefen und die schlechte BEIM NAMEN nennen. "Rollen-ID + // ungueltig" bei fuenf Eintraegen ist eine Suchaufgabe, keine Meldung. + for (const eintrag of liste) { + if (!istId(eintrag)) { + return c.json( + { error: `"${eintrag}" ist keine Rollen-ID (17 bis 20 Ziffern)` }, + 400, + ) + } + } + + const eindeutig = [...new Set(liste)] + setSetting(DISCORD_KEYS.roleIds, eindeutig.join(',')) + // Den alten Einzelwert raeumen wir weg, sobald die Liste gepflegt + // wird: zwei Quellen fuer dieselbe Frage laufen sonst auseinander. + setSetting(DISCORD_KEYS.roleIdAlt, '') } // Ein leeres Feld heisst "unveraendert lassen", nicht "loeschen" — sonst // wuerde jedes Speichern das Geheimnis wegwerfen, weil es nie im Formular @@ -664,7 +688,7 @@ dashRoutes.post('/settings', async (c) => { !jetzt.clientId && 'Client-ID', !jetzt.clientSecret && 'Client-Geheimnis', !jetzt.guildId && 'Server-ID', - !jetzt.roleId && 'Rollen-ID', + jetzt.roleIds.length === 0 && 'mindestens eine Rollen-ID', ].filter(Boolean), }) }) diff --git a/server/src/routes/discord.ts b/server/src/routes/discord.ts index 8840743..dafb968 100644 --- a/server/src/routes/discord.ts +++ b/server/src/routes/discord.ts @@ -144,10 +144,15 @@ discordRoutes.get('/discord/callback', async (c) => { } const member = (await memberAntwort.json()) as { roles?: string[] } - if (!Array.isArray(member.roles) || !member.roles.includes(d.roleId)) { - // Bewusst ohne den Namen der Rolle: wer sie nicht hat, muss auch - // nicht erfahren, wie sie heisst. - return zurueck(c, 'Dir fehlt die noetige Rolle auf dem Server.') + // EINE der eingetragenen Rollen reicht. Mehrere Rollen sind der + // Normalfall, sobald mehr als eine Person Zugang haben soll — Admin + // und Fotograf sind selten dieselbe Rolle. + const hatRolle = + Array.isArray(member.roles) && member.roles.some((r) => d.roleIds.includes(r)) + if (!hatRolle) { + // Bewusst ohne die Namen der Rollen: wer keine hat, muss auch nicht + // erfahren, welche es gaebe. + return zurueck(c, 'Dir fehlt eine der noetigen Rollen auf dem Server.') } // -- Konto finden oder anlegen -------------------------------------- diff --git a/server/src/settings.ts b/server/src/settings.ts index b8b2773..66a9184 100644 --- a/server/src/settings.ts +++ b/server/src/settings.ts @@ -50,31 +50,50 @@ export const DISCORD_KEYS = { clientId: 'discord.client_id', clientSecret: 'discord.client_secret', guildId: 'discord.guild_id', - roleId: 'discord.role_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 - roleId: 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 roleId = getSetting(DISCORD_KEYS.roleId, 'DISCORD_ROLE_ID') + const roleIds = rollen() return { clientId, clientSecret, guildId, - roleId, - bereit: Boolean(clientId && clientSecret && guildId && roleId), + roleIds, + bereit: Boolean(clientId && clientSecret && guildId && roleIds.length > 0), } } diff --git a/server/ui/src/api.ts b/server/ui/src/api.ts index 32490e4..a7c89ea 100644 --- a/server/ui/src/api.ts +++ b/server/ui/src/api.ts @@ -154,7 +154,8 @@ export type Einstellungen = { discord: { clientId: string guildId: string - roleId: string + /** Wer EINE davon hat, darf herein. */ + roleIds: string[] /** Nur OB eines hinterlegt ist — das Geheimnis selbst geht nie heraus. */ hatGeheimnis: boolean bereit: boolean @@ -163,7 +164,7 @@ export type Einstellungen = { clientId: boolean clientSecret: boolean guildId: boolean - roleId: boolean + roleIds: boolean } } } diff --git a/server/ui/src/components/DiscordKarte.tsx b/server/ui/src/components/DiscordKarte.tsx index c2c5663..8701472 100644 --- a/server/ui/src/components/DiscordKarte.tsx +++ b/server/ui/src/components/DiscordKarte.tsx @@ -19,7 +19,7 @@ export function DiscordKarte() { const [clientId, setClientId] = useState('') const [geheimnis, setGeheimnis] = useState('') const [guildId, setGuildId] = useState('') - const [roleId, setRoleId] = useState('') + const [rollen, setRollen] = useState('') async function laden() { setLaedt(true) @@ -28,7 +28,7 @@ export function DiscordKarte() { setDaten(e) setClientId(e.discord.clientId) setGuildId(e.discord.guildId) - setRoleId(e.discord.roleId) + setRollen(e.discord.roleIds.join('\n')) } catch (err) { note('fehler', err instanceof Error ? err.message : 'Unbekannt') } finally { @@ -51,7 +51,7 @@ export function DiscordKarte() { // das Geheimnis wegwerfen, weil es nie im Formular steht. ...(geheimnis ? { clientSecret: geheimnis } : {}), guildId, - roleId, + roleIds: rollen, }), (r) => r.bereit @@ -84,7 +84,8 @@ export function DiscordKarte() { rechts={ d.bereit ? ( - eingerichtet + eingerichtet ·{' '} + {d.roleIds.length === 1 ? '1 Rolle' : `${d.roleIds.length} Rollen`} ) : ( @@ -156,13 +157,26 @@ export function DiscordKarte() { hinweis={d.ausDerUmgebung.guildId ? 'kommt derzeit aus der Umgebung' : undefined} onChange={(e) => setGuildId(e.target.value)} /> - setRoleId(e.target.value)} - /> +