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 <noreply@anthropic.com>
This commit is contained in:
@@ -597,7 +597,7 @@ dashRoutes.get('/settings', (c) => {
|
|||||||
discord: {
|
discord: {
|
||||||
clientId: d.clientId,
|
clientId: d.clientId,
|
||||||
guildId: d.guildId,
|
guildId: d.guildId,
|
||||||
roleId: d.roleId,
|
roleIds: d.roleIds,
|
||||||
hatGeheimnis: Boolean(d.clientSecret),
|
hatGeheimnis: Boolean(d.clientSecret),
|
||||||
bereit: d.bereit,
|
bereit: d.bereit,
|
||||||
// Die Stelle, an der der erste Anlauf fast immer scheitert.
|
// Die Stelle, an der der erste Anlauf fast immer scheitert.
|
||||||
@@ -607,7 +607,10 @@ dashRoutes.get('/settings', (c) => {
|
|||||||
clientSecret:
|
clientSecret:
|
||||||
Boolean(d.clientSecret) && !ausDatenbank(DISCORD_KEYS.clientSecret),
|
Boolean(d.clientSecret) && !ausDatenbank(DISCORD_KEYS.clientSecret),
|
||||||
guildId: Boolean(d.guildId) && !ausDatenbank(DISCORD_KEYS.guildId),
|
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
|
clientId?: string
|
||||||
clientSecret?: string
|
clientSecret?: string
|
||||||
guildId?: string
|
guildId?: string
|
||||||
roleId?: string
|
roleIds?: string | string[]
|
||||||
}
|
}
|
||||||
}>(c)
|
}>(c)
|
||||||
if (!body) return c.json(KEIN_JSON, 400)
|
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)
|
if (v && !istId(v)) return c.json({ error: 'Server-ID: 17 bis 20 Ziffern' }, 400)
|
||||||
setSetting(DISCORD_KEYS.guildId, v)
|
setSetting(DISCORD_KEYS.guildId, v)
|
||||||
}
|
}
|
||||||
if (d.roleId !== undefined) {
|
if (d.roleIds !== undefined) {
|
||||||
const v = d.roleId.trim()
|
// Komma, Semikolon, Leerzeichen, Zeilenumbruch — alles trennt. Wer
|
||||||
if (v && !istId(v)) return c.json({ error: 'Rollen-ID: 17 bis 20 Ziffern' }, 400)
|
// drei IDs untereinander einfuegt, soll sie nicht erst zu einer Zeile
|
||||||
setSetting(DISCORD_KEYS.roleId, v)
|
// 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
|
// Ein leeres Feld heisst "unveraendert lassen", nicht "loeschen" — sonst
|
||||||
// wuerde jedes Speichern das Geheimnis wegwerfen, weil es nie im Formular
|
// 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.clientId && 'Client-ID',
|
||||||
!jetzt.clientSecret && 'Client-Geheimnis',
|
!jetzt.clientSecret && 'Client-Geheimnis',
|
||||||
!jetzt.guildId && 'Server-ID',
|
!jetzt.guildId && 'Server-ID',
|
||||||
!jetzt.roleId && 'Rollen-ID',
|
jetzt.roleIds.length === 0 && 'mindestens eine Rollen-ID',
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -144,10 +144,15 @@ discordRoutes.get('/discord/callback', async (c) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const member = (await memberAntwort.json()) as { roles?: string[] }
|
const member = (await memberAntwort.json()) as { roles?: string[] }
|
||||||
if (!Array.isArray(member.roles) || !member.roles.includes(d.roleId)) {
|
// EINE der eingetragenen Rollen reicht. Mehrere Rollen sind der
|
||||||
// Bewusst ohne den Namen der Rolle: wer sie nicht hat, muss auch
|
// Normalfall, sobald mehr als eine Person Zugang haben soll — Admin
|
||||||
// nicht erfahren, wie sie heisst.
|
// und Fotograf sind selten dieselbe Rolle.
|
||||||
return zurueck(c, 'Dir fehlt die noetige Rolle auf dem Server.')
|
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 --------------------------------------
|
// -- Konto finden oder anlegen --------------------------------------
|
||||||
|
|||||||
+24
-5
@@ -50,31 +50,50 @@ export const DISCORD_KEYS = {
|
|||||||
clientId: 'discord.client_id',
|
clientId: 'discord.client_id',
|
||||||
clientSecret: 'discord.client_secret',
|
clientSecret: 'discord.client_secret',
|
||||||
guildId: 'discord.guild_id',
|
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
|
} as const
|
||||||
|
|
||||||
export type DiscordConfig = {
|
export type DiscordConfig = {
|
||||||
clientId: string
|
clientId: string
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
guildId: 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.
|
/** Erst wenn alle vier stehen, hat die Anmeldung ueberhaupt eine Chance.
|
||||||
* Ein halb eingerichteter Weg, der auf halber Strecke abbricht, ist
|
* Ein halb eingerichteter Weg, der auf halber Strecke abbricht, ist
|
||||||
* schlimmer als ein Knopf, den es nicht gibt. */
|
* schlimmer als ein Knopf, den es nicht gibt. */
|
||||||
bereit: boolean
|
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 {
|
export function discordConfig(): DiscordConfig {
|
||||||
const clientId = getSetting(DISCORD_KEYS.clientId, 'DISCORD_CLIENT_ID')
|
const clientId = getSetting(DISCORD_KEYS.clientId, 'DISCORD_CLIENT_ID')
|
||||||
const clientSecret = getSetting(DISCORD_KEYS.clientSecret, 'DISCORD_CLIENT_SECRET')
|
const clientSecret = getSetting(DISCORD_KEYS.clientSecret, 'DISCORD_CLIENT_SECRET')
|
||||||
const guildId = getSetting(DISCORD_KEYS.guildId, 'DISCORD_GUILD_ID')
|
const guildId = getSetting(DISCORD_KEYS.guildId, 'DISCORD_GUILD_ID')
|
||||||
const roleId = getSetting(DISCORD_KEYS.roleId, 'DISCORD_ROLE_ID')
|
const roleIds = rollen()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
guildId,
|
guildId,
|
||||||
roleId,
|
roleIds,
|
||||||
bereit: Boolean(clientId && clientSecret && guildId && roleId),
|
bereit: Boolean(clientId && clientSecret && guildId && roleIds.length > 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ export type Einstellungen = {
|
|||||||
discord: {
|
discord: {
|
||||||
clientId: string
|
clientId: string
|
||||||
guildId: string
|
guildId: string
|
||||||
roleId: string
|
/** Wer EINE davon hat, darf herein. */
|
||||||
|
roleIds: string[]
|
||||||
/** Nur OB eines hinterlegt ist — das Geheimnis selbst geht nie heraus. */
|
/** Nur OB eines hinterlegt ist — das Geheimnis selbst geht nie heraus. */
|
||||||
hatGeheimnis: boolean
|
hatGeheimnis: boolean
|
||||||
bereit: boolean
|
bereit: boolean
|
||||||
@@ -163,7 +164,7 @@ export type Einstellungen = {
|
|||||||
clientId: boolean
|
clientId: boolean
|
||||||
clientSecret: boolean
|
clientSecret: boolean
|
||||||
guildId: boolean
|
guildId: boolean
|
||||||
roleId: boolean
|
roleIds: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function DiscordKarte() {
|
|||||||
const [clientId, setClientId] = useState('')
|
const [clientId, setClientId] = useState('')
|
||||||
const [geheimnis, setGeheimnis] = useState('')
|
const [geheimnis, setGeheimnis] = useState('')
|
||||||
const [guildId, setGuildId] = useState('')
|
const [guildId, setGuildId] = useState('')
|
||||||
const [roleId, setRoleId] = useState('')
|
const [rollen, setRollen] = useState('')
|
||||||
|
|
||||||
async function laden() {
|
async function laden() {
|
||||||
setLaedt(true)
|
setLaedt(true)
|
||||||
@@ -28,7 +28,7 @@ export function DiscordKarte() {
|
|||||||
setDaten(e)
|
setDaten(e)
|
||||||
setClientId(e.discord.clientId)
|
setClientId(e.discord.clientId)
|
||||||
setGuildId(e.discord.guildId)
|
setGuildId(e.discord.guildId)
|
||||||
setRoleId(e.discord.roleId)
|
setRollen(e.discord.roleIds.join('\n'))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
note('fehler', err instanceof Error ? err.message : 'Unbekannt')
|
note('fehler', err instanceof Error ? err.message : 'Unbekannt')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -51,7 +51,7 @@ export function DiscordKarte() {
|
|||||||
// das Geheimnis wegwerfen, weil es nie im Formular steht.
|
// das Geheimnis wegwerfen, weil es nie im Formular steht.
|
||||||
...(geheimnis ? { clientSecret: geheimnis } : {}),
|
...(geheimnis ? { clientSecret: geheimnis } : {}),
|
||||||
guildId,
|
guildId,
|
||||||
roleId,
|
roleIds: rollen,
|
||||||
}),
|
}),
|
||||||
(r) =>
|
(r) =>
|
||||||
r.bereit
|
r.bereit
|
||||||
@@ -84,7 +84,8 @@ export function DiscordKarte() {
|
|||||||
rechts={
|
rechts={
|
||||||
d.bereit ? (
|
d.bereit ? (
|
||||||
<span className="flex items-center gap-1.5 text-xs text-gut">
|
<span className="flex items-center gap-1.5 text-xs text-gut">
|
||||||
<ShieldCheck size={14} /> eingerichtet
|
<ShieldCheck size={14} /> eingerichtet ·{' '}
|
||||||
|
{d.roleIds.length === 1 ? '1 Rolle' : `${d.roleIds.length} Rollen`}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-1.5 text-xs text-schrift-leise">
|
<span className="flex items-center gap-1.5 text-xs text-schrift-leise">
|
||||||
@@ -156,13 +157,26 @@ export function DiscordKarte() {
|
|||||||
hinweis={d.ausDerUmgebung.guildId ? 'kommt derzeit aus der Umgebung' : undefined}
|
hinweis={d.ausDerUmgebung.guildId ? 'kommt derzeit aus der Umgebung' : undefined}
|
||||||
onChange={(e) => setGuildId(e.target.value)}
|
onChange={(e) => setGuildId(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Feld
|
<label className="block sm:col-span-2">
|
||||||
label="Rollen-ID"
|
<span className="mb-1 block text-xs font-medium text-schrift-leise">
|
||||||
value={roleId}
|
Rollen-IDs — eine je Zeile
|
||||||
placeholder="Wer diese Rolle hat, darf herein"
|
</span>
|
||||||
hinweis={d.ausDerUmgebung.roleId ? 'kommt derzeit aus der Umgebung' : undefined}
|
<textarea
|
||||||
onChange={(e) => setRoleId(e.target.value)}
|
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>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-schrift-leise">
|
<p className="text-xs text-schrift-leise">
|
||||||
|
|||||||
Reference in New Issue
Block a user