Member-Bereich: Profilseite, Rollen-Selfservice, Web-Voting, Member-Gate

Der Discord-Login ist jetzt für alle Member nützlich:
- /profil: Rang + XP-Fortschritt, Discord-Rollen als Chips, Playtester-
  Badge, eigener Alpha-Key mit Kopier-Button
- Rollen-Selfservice: veröffentlichte Rollen-Menüs direkt im Browser
  togglen (gleiche Exklusiv-Logik wie die Discord-Buttons; nur Rollen
  aus aktiven Menüs erlaubt)
- Wunsch-Voting im Web: auf /roadmap Wünsche einreichen (postet wie
  /wunsch in den Voting-Kanal) und upvoten (wish_votes-Tabelle,
  ein Vote pro Member, unabhängig von Discord-👍)
- Member-Gate: Login nur für Mitglieder der konfigurierten Guild;
  Abgewiesene sehen einen Banner mit Discord-Invite-Link (neue
  Settings member_gate_enabled + discord_invite_url im System-Tab)
- Membership-Check mit 5-Minuten-Cache, Owner immer erlaubt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 21:52:59 +02:00
co-authored by Claude Fable 5
parent 3bfa559f2e
commit aa12dff7ab
11 changed files with 551 additions and 15 deletions
+18 -2
View File
@@ -1,7 +1,7 @@
// Discord-OAuth2-Login: /auth/login → Discord → /auth/callback → signiertes Session-Cookie
import crypto from 'node:crypto';
import { config } from '../config.js';
import { publicUrl } from '../runtime-settings.js';
import { publicUrl, discordGuildId, memberGateEnabled } from '../runtime-settings.js';
const DISCORD_API = 'https://discord.com/api/v10';
const SESSION_COOKIE = 'd4rkbot_session';
@@ -28,7 +28,18 @@ export function isAdmin(user) {
return user?.id === config.adminDiscordId;
}
export function registerAuthRoutes(app) {
/** Member-Gate: ist die User-ID Mitglied der konfigurierten Guild? (Owner immer) */
export async function isGuildMember(client, userId) {
if (userId === config.adminDiscordId) return true;
const guildId = discordGuildId();
if (!guildId) return true; // ohne Guild-Konfig kein Gate
const guild = client.guilds.cache.get(guildId) ?? await client.guilds.fetch(guildId).catch(() => null);
if (!guild) return true; // Bot nicht auf der Guild → Gate nicht erzwingbar
const member = await guild.members.fetch(userId).catch(() => null);
return Boolean(member);
}
export function registerAuthRoutes(app, client) {
// Login: mit CSRF-State zu Discord weiterleiten
app.get('/auth/login', async (request, reply) => {
const state = crypto.randomBytes(16).toString('hex');
@@ -81,6 +92,11 @@ export function registerAuthRoutes(app) {
}
const me = await userRes.json();
// Member-Gate: nur Mitglieder des Discord-Servers bekommen eine Session
if (memberGateEnabled() && !(await isGuildMember(client, me.id))) {
return reply.clearCookie(STATE_COOKIE, { path: '/auth' }).redirect('/?gate=1');
}
// Nur das Nötigste in die Session — keine Tokens speichern
const session = {
id: me.id,