Add multi-guild Discord OAuth support
- Users can now login via Bacanaks OR Piccadilly Discord server - Highest role from all servers is used (superadmin > moderator > user) - Lazy initialization fixes env loading timing issue - Updated documentation with implementation details and troubleshooting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
23
auth.js
23
auth.js
@@ -2,7 +2,7 @@ import { Router } from 'express';
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { db, VALID_ROLES, initDiscordUsers } from '../db/init.js';
|
import { db, VALID_ROLES, initDiscordUsers } from '../db/init.js';
|
||||||
import { authenticateToken, requireRole } from '../middleware/auth.js';
|
import { authenticateToken, requireRole } from '../middleware/auth.js';
|
||||||
import { getDiscordAuthUrl, exchangeCode, getDiscordUser, getGuildMember, getUserRole } from '../services/discord.js';
|
import { getDiscordAuthUrl, exchangeCode, getDiscordUser, getGuildMember, getGuildMemberships, getUserRole, getUserRoleFromMemberships } from '../services/discord.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -38,15 +38,18 @@ router.get('/discord/callback', async (req, res) => {
|
|||||||
// Get Discord user info
|
// Get Discord user info
|
||||||
const discordUser = await getDiscordUser(tokenData.access_token);
|
const discordUser = await getDiscordUser(tokenData.access_token);
|
||||||
|
|
||||||
// Check if user is in the guild
|
// Check if user is in any of the configured guilds
|
||||||
const member = await getGuildMember(discordUser.id);
|
const memberships = await getGuildMemberships(discordUser.id);
|
||||||
|
|
||||||
if (!member) {
|
if (!memberships) {
|
||||||
return res.redirect(`${frontendUrl}/login?error=not_in_guild`);
|
return res.redirect(`${frontendUrl}/login?error=not_in_guild`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine role based on Discord roles
|
// Determine role based on Discord roles (highest role from all servers)
|
||||||
const role = getUserRole(member.roles);
|
const role = getUserRoleFromMemberships(memberships);
|
||||||
|
|
||||||
|
// Use first membership for display name
|
||||||
|
const member = memberships[0].member;
|
||||||
|
|
||||||
// Get display name (nickname or username)
|
// Get display name (nickname or username)
|
||||||
const displayName = member.nick || discordUser.global_name || discordUser.username;
|
const displayName = member.nick || discordUser.global_name || discordUser.username;
|
||||||
@@ -126,13 +129,13 @@ router.post('/refresh-role', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const member = await getGuildMember(req.user.discordId);
|
const memberships = await getGuildMemberships(req.user.discordId);
|
||||||
|
|
||||||
if (!member) {
|
if (!memberships) {
|
||||||
return res.status(403).json({ error: 'No longer in guild' });
|
return res.status(403).json({ error: 'No longer in any guild' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRole = getUserRole(member.roles);
|
const newRole = getUserRoleFromMemberships(memberships);
|
||||||
|
|
||||||
db.prepare('UPDATE discord_users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE discord_id = ?')
|
db.prepare('UPDATE discord_users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE discord_id = ?')
|
||||||
.run(newRole, req.user.discordId);
|
.run(newRole, req.user.discordId);
|
||||||
|
|||||||
97
discord.js
97
discord.js
@@ -1,6 +1,29 @@
|
|||||||
// Discord OAuth2 Service
|
// Discord OAuth2 Service
|
||||||
const DISCORD_API = 'https://discord.com/api/v10';
|
const DISCORD_API = 'https://discord.com/api/v10';
|
||||||
|
|
||||||
|
// Lazy initialization - wird erst bei Verwendung geladen (nach dotenv)
|
||||||
|
let _guildConfigs = null;
|
||||||
|
|
||||||
|
function getGuildConfigs() {
|
||||||
|
if (_guildConfigs === null) {
|
||||||
|
_guildConfigs = [
|
||||||
|
{
|
||||||
|
name: 'Bacanaks',
|
||||||
|
guildId: process.env.DISCORD_GUILD_ID_1,
|
||||||
|
adminRoleId: process.env.DISCORD_ADMIN_ROLE_ID_1,
|
||||||
|
modRoleId: process.env.DISCORD_MOD_ROLE_ID_1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Piccadilly',
|
||||||
|
guildId: process.env.DISCORD_GUILD_ID_2,
|
||||||
|
adminRoleId: process.env.DISCORD_ADMIN_ROLE_ID_2,
|
||||||
|
modRoleId: process.env.DISCORD_MOD_ROLE_ID_2
|
||||||
|
}
|
||||||
|
].filter(config => config.guildId);
|
||||||
|
}
|
||||||
|
return _guildConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDiscordAuthUrl() {
|
export function getDiscordAuthUrl() {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: process.env.DISCORD_CLIENT_ID,
|
client_id: process.env.DISCORD_CLIENT_ID,
|
||||||
@@ -48,9 +71,10 @@ export async function getDiscordUser(accessToken) {
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGuildMember(userId) {
|
// Prüft einen einzelnen Server
|
||||||
|
async function fetchGuildMember(guildId, userId) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${DISCORD_API}/guilds/${process.env.DISCORD_GUILD_ID}/members/${userId}`,
|
`${DISCORD_API}/guilds/${guildId}/members/${userId}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`
|
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`
|
||||||
@@ -60,17 +84,78 @@ export async function getGuildMember(userId) {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return null; // User not in guild
|
return null;
|
||||||
}
|
}
|
||||||
throw new Error('Failed to get guild member');
|
throw new Error(`Failed to get guild member from ${guildId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prüft alle konfigurierten Server und gibt Memberships zurück
|
||||||
|
export async function getGuildMemberships(userId) {
|
||||||
|
const configs = getGuildConfigs();
|
||||||
|
const memberships = [];
|
||||||
|
|
||||||
|
for (const config of configs) {
|
||||||
|
try {
|
||||||
|
const member = await fetchGuildMember(config.guildId, userId);
|
||||||
|
if (member) {
|
||||||
|
memberships.push({
|
||||||
|
config,
|
||||||
|
member,
|
||||||
|
roles: member.roles || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Discord] Failed to check membership for guild ${config.name}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberships.length > 0 ? memberships : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy-Funktion für Kompatibilität
|
||||||
|
export async function getGuildMember(userId) {
|
||||||
|
const memberships = await getGuildMemberships(userId);
|
||||||
|
if (!memberships || memberships.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return memberships[0].member;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollen-Priorität: superadmin > moderator > user
|
||||||
|
const ROLE_PRIORITY = { superadmin: 3, moderator: 2, user: 1 };
|
||||||
|
|
||||||
|
// Bestimmt die höchste Rolle aus allen Server-Memberships
|
||||||
|
export function getUserRoleFromMemberships(memberships) {
|
||||||
|
if (!memberships || memberships.length === 0) {
|
||||||
|
return 'user';
|
||||||
|
}
|
||||||
|
|
||||||
|
let highestRole = 'user';
|
||||||
|
|
||||||
|
for (const { config, roles } of memberships) {
|
||||||
|
let role = 'user';
|
||||||
|
|
||||||
|
if (roles.includes(config.adminRoleId)) {
|
||||||
|
role = 'superadmin';
|
||||||
|
} else if (roles.includes(config.modRoleId)) {
|
||||||
|
role = 'moderator';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ROLE_PRIORITY[role] > ROLE_PRIORITY[highestRole]) {
|
||||||
|
highestRole = role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return highestRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy-Funktion für Kompatibilität
|
||||||
export function getUserRole(memberRoles) {
|
export function getUserRole(memberRoles) {
|
||||||
const adminRoleId = process.env.DISCORD_ADMIN_ROLE_ID;
|
const adminRoleId = process.env.DISCORD_ADMIN_ROLE_ID || process.env.DISCORD_ADMIN_ROLE_ID_1;
|
||||||
const modRoleId = process.env.DISCORD_MOD_ROLE_ID;
|
const modRoleId = process.env.DISCORD_MOD_ROLE_ID || process.env.DISCORD_MOD_ROLE_ID_1;
|
||||||
|
|
||||||
if (memberRoles.includes(adminRoleId)) {
|
if (memberRoles.includes(adminRoleId)) {
|
||||||
return 'superadmin';
|
return 'superadmin';
|
||||||
|
|||||||
@@ -49,13 +49,15 @@ Wenn der Bot einem Server beitritt, erstellt er automatisch folgende Struktur:
|
|||||||
|
|
||||||
| Channel | @everyone | Bot |
|
| Channel | @everyone | Bot |
|
||||||
|---------|-----------|-----|
|
|---------|-----------|-----|
|
||||||
| Kategorie | Lesen | Schreiben |
|
| Kategorie | Lesen | ViewChannel + Schreiben |
|
||||||
| info | Lesen | Schreiben |
|
| info | Lesen (kein Schreiben) | ViewChannel + Schreiben |
|
||||||
| status | Lesen | Schreiben |
|
| status | Lesen (kein Schreiben) | ViewChannel + Schreiben |
|
||||||
| alerts | Lesen | Schreiben |
|
| alerts | Lesen (kein Schreiben) | ViewChannel + Schreiben |
|
||||||
| updates | Lesen | Schreiben |
|
| updates | Lesen (kein Schreiben) | ViewChannel + Schreiben |
|
||||||
| diskussion | Lesen + Schreiben | Schreiben |
|
| diskussion | Lesen + Schreiben | ViewChannel + Schreiben |
|
||||||
| requests | Lesen + Threads erstellen | Schreiben |
|
| requests | Lesen + Threads erstellen | ViewChannel + Schreiben |
|
||||||
|
|
||||||
|
**Wichtig**: Der Bot braucht explizit `ViewChannel` Permission für jeden Channel, auch wenn @everyone den Channel sehen kann. Ohne `ViewChannel` kann der Bot nicht in den Channel schreiben ("Missing Access" Fehler).
|
||||||
|
|
||||||
## Datenbank
|
## Datenbank
|
||||||
|
|
||||||
@@ -80,6 +82,8 @@ CREATE TABLE guild_settings (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Die Datenbank liegt in `backend/db/users.sqlite`.
|
||||||
|
|
||||||
### DB-Funktionen
|
### DB-Funktionen
|
||||||
|
|
||||||
In `backend/db/init.js`:
|
In `backend/db/init.js`:
|
||||||
@@ -137,12 +141,43 @@ Jeder Server bekommt ein eigenes Embed mit:
|
|||||||
DISCORD_CLIENT_ID=1458251194806833306
|
DISCORD_CLIENT_ID=1458251194806833306
|
||||||
DISCORD_CLIENT_SECRET=xxx
|
DISCORD_CLIENT_SECRET=xxx
|
||||||
DISCORD_BOT_TOKEN=xxx
|
DISCORD_BOT_TOKEN=xxx
|
||||||
DISCORD_GUILD_ID=729865854329815051 # Haupt-Server für Login
|
|
||||||
DISCORD_ADMIN_ROLE_ID=1024693717434650736
|
# Multi-Server OAuth Login
|
||||||
DISCORD_MOD_ROLE_ID=1024693170958766141
|
# User muss nur in EINEM der Server Mitglied sein
|
||||||
|
# Rollen werden pro Server geprüft, höchste Berechtigung zählt
|
||||||
|
|
||||||
|
# Server 1: Bacanaks
|
||||||
|
DISCORD_GUILD_ID_1=729865854329815051
|
||||||
|
DISCORD_ADMIN_ROLE_ID_1=1024693717434650736
|
||||||
|
DISCORD_MOD_ROLE_ID_1=1024693170958766141
|
||||||
|
|
||||||
|
# Server 2: Piccadilly
|
||||||
|
DISCORD_GUILD_ID_2=730907665802330224
|
||||||
|
DISCORD_ADMIN_ROLE_ID_2=1458595551514988584
|
||||||
|
DISCORD_MOD_ROLE_ID_2=1458591909210488914
|
||||||
```
|
```
|
||||||
|
|
||||||
**Hinweis**: `DISCORD_GUILD_ID` wird nur für den Discord OAuth Login verwendet, nicht für den Bot selbst.
|
**Hinweis**: Die Guild-IDs werden nur für den Discord OAuth Login verwendet, nicht für den Bot selbst.
|
||||||
|
|
||||||
|
### OAuth Login-Logik
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User loggt sich via Discord OAuth ein
|
||||||
|
2. Für jeden konfigurierten Server:
|
||||||
|
- Ist User Mitglied? → Rollen prüfen
|
||||||
|
- Admin-Rolle → superadmin
|
||||||
|
- Mod-Rolle → moderator
|
||||||
|
- Nur Mitglied → user
|
||||||
|
3. Höchste Berechtigung aus allen Servern wird verwendet
|
||||||
|
4. Nicht in mindestens einem Server → Login verweigert
|
||||||
|
```
|
||||||
|
|
||||||
|
| User ist in... | Bacanaks Rolle | Piccadilly Rolle | GSM Rolle |
|
||||||
|
|----------------|----------------|------------------|-----------|
|
||||||
|
| Nur Bacanaks | Admin | - | superadmin |
|
||||||
|
| Nur Piccadilly | - | Mod | moderator |
|
||||||
|
| Beide | Mitglied | Admin | superadmin |
|
||||||
|
| Keinem | - | - | ❌ Kein Login |
|
||||||
|
|
||||||
### Developer Portal Einstellungen
|
### Developer Portal Einstellungen
|
||||||
|
|
||||||
@@ -158,12 +193,84 @@ DISCORD_MOD_ROLE_ID=1024693170958766141
|
|||||||
|
|
||||||
| Datei | Beschreibung |
|
| Datei | Beschreibung |
|
||||||
|-------|--------------|
|
|-------|--------------|
|
||||||
|
| `backend/services/discord.js` | OAuth-Logik, Multi-Guild Membership-Prüfung |
|
||||||
| `backend/services/discordBot.js` | Bot-Logik und Event-Handler |
|
| `backend/services/discordBot.js` | Bot-Logik und Event-Handler |
|
||||||
|
| `backend/routes/auth.js` | Auth-Endpoints (Login, Callback, Refresh) |
|
||||||
| `backend/db/init.js` | Guild-Settings DB-Funktionen |
|
| `backend/db/init.js` | Guild-Settings DB-Funktionen |
|
||||||
| `frontend/src/pages/Dashboard.jsx` | Invite-Button im Dashboard |
|
| `frontend/src/pages/Dashboard.jsx` | Invite-Button im Dashboard |
|
||||||
|
|
||||||
|
## Implementierungsdetails
|
||||||
|
|
||||||
|
### Multi-Guild OAuth
|
||||||
|
|
||||||
|
Die OAuth-Implementierung in `discord.js` verwendet **lazy initialization** für die Guild-Konfigurationen:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let _guildConfigs = null;
|
||||||
|
|
||||||
|
function getGuildConfigs() {
|
||||||
|
if (_guildConfigs === null) {
|
||||||
|
_guildConfigs = [
|
||||||
|
{ name: 'Bacanaks', guildId: process.env.DISCORD_GUILD_ID_1, ... },
|
||||||
|
{ name: 'Piccadilly', guildId: process.env.DISCORD_GUILD_ID_2, ... }
|
||||||
|
].filter(config => config.guildId);
|
||||||
|
}
|
||||||
|
return _guildConfigs;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig**: Die Konfiguration darf NICHT beim Modul-Import initialisiert werden, da zu diesem Zeitpunkt dotenv die `.env` noch nicht geladen hat. Die lazy initialization stellt sicher, dass die Umgebungsvariablen verfügbar sind.
|
||||||
|
|
||||||
|
### Funktionen
|
||||||
|
|
||||||
|
| Funktion | Beschreibung |
|
||||||
|
|----------|--------------|
|
||||||
|
| `getGuildMemberships(userId)` | Prüft alle konfigurierten Server, gibt Array von Memberships zurück |
|
||||||
|
| `getUserRoleFromMemberships(memberships)` | Bestimmt höchste Rolle aus allen Memberships |
|
||||||
|
| `getGuildMember(userId)` | Legacy-Funktion, gibt ersten Match zurück |
|
||||||
|
| `getUserRole(memberRoles)` | Legacy-Funktion für einzelne Rollen-Liste |
|
||||||
|
|
||||||
|
### Rollen-Priorität
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ROLE_PRIORITY = { superadmin: 3, moderator: 2, user: 1 };
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei mehreren Memberships wird immer die höchste Rolle verwendet.
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
- Der Bot muss auf **allen** konfigurierten Discord-Servern sein
|
||||||
|
- Der Bot braucht Zugriff auf die Guild Members API
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Login schlägt fehl mit "nicht Mitglied"
|
||||||
|
|
||||||
|
1. **Bot auf allen Servern?** Der Bot muss auf Bacanaks UND Piccadilly eingeladen sein
|
||||||
|
2. **Env-Variablen prüfen**:
|
||||||
|
```bash
|
||||||
|
grep GUILD /opt/gameserver-monitor/backend/.env
|
||||||
|
```
|
||||||
|
3. **Mit --update-env neustarten**:
|
||||||
|
```bash
|
||||||
|
pm2 restart gameserver-backend --update-env
|
||||||
|
```
|
||||||
|
4. **Logs prüfen**:
|
||||||
|
```bash
|
||||||
|
pm2 logs gameserver-backend --lines 30 | grep -i discord
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rolle wird nicht erkannt
|
||||||
|
|
||||||
|
1. **Rollen-IDs prüfen** - Discord Developer Mode aktivieren, Rechtsklick auf Rolle → ID kopieren
|
||||||
|
2. **User-Rollen abfragen**:
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: Bot BOT_TOKEN" \
|
||||||
|
"https://discord.com/api/v10/guilds/GUILD_ID/members/USER_ID" | jq '.roles'
|
||||||
|
```
|
||||||
|
3. **Konfigurierte IDs vergleichen** mit den tatsächlichen Rollen des Users
|
||||||
|
|
||||||
### Bot erstellt keine Channels
|
### Bot erstellt keine Channels
|
||||||
|
|
||||||
- Prüfen ob Bot "Manage Channels" Permission hat
|
- Prüfen ob Bot "Manage Channels" Permission hat
|
||||||
@@ -180,18 +287,52 @@ Suche nach `[DiscordBot]` Log-Einträgen.
|
|||||||
### Bot aus Datenbank entfernen
|
### Bot aus Datenbank entfernen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sqlite3 /opt/gameserver-monitor/backend/users.sqlite
|
cd /opt/gameserver-monitor/backend
|
||||||
|
node -e "
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
const db = new Database('./db/users.sqlite');
|
||||||
|
db.prepare('DELETE FROM guild_settings WHERE guild_id = ?').run('GUILD_ID_HIER');
|
||||||
|
console.log('Deleted');
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ mit sqlite3 (falls installiert):
|
||||||
|
```bash
|
||||||
|
sqlite3 /opt/gameserver-monitor/backend/db/users.sqlite
|
||||||
DELETE FROM guild_settings WHERE guild_id = 'xxx';
|
DELETE FROM guild_settings WHERE guild_id = 'xxx';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Missing Access Fehler
|
||||||
|
|
||||||
|
Wenn der Bot "Missing Access" meldet obwohl er eingeladen wurde:
|
||||||
|
|
||||||
|
1. **ViewChannel Permission prüfen**: Der Bot braucht explizit `ViewChannel` für jeden Channel
|
||||||
|
2. Im Discord: Rechtsklick auf Channel → Bearbeiten → Berechtigungen → Bot auswählen → "Kanal ansehen" aktivieren
|
||||||
|
3. Logs prüfen: `pm2 logs gameserver-backend --lines 50 | grep -i "missing\|access"`
|
||||||
|
|
||||||
|
### Status-Nachricht wurde gelöscht
|
||||||
|
|
||||||
|
Wenn die Status-Nachricht manuell gelöscht wurde, erscheint "Unknown Message" in den Logs. Fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/gameserver-monitor/backend
|
||||||
|
node -e "
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
const db = new Database('./db/users.sqlite');
|
||||||
|
// Status Message ID auf NULL setzen, Bot erstellt neue beim nächsten Update
|
||||||
|
db.prepare('UPDATE guild_settings SET status_message_id = NULL WHERE guild_id = ?').run('GUILD_ID_HIER');
|
||||||
|
console.log('Reset status_message_id');
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
## Login vs. Bot
|
## Login vs. Bot
|
||||||
|
|
||||||
| Feature | Login (OAuth) | Bot |
|
| Feature | Login (OAuth) | Bot |
|
||||||
|---------|--------------|-----|
|
|---------|--------------|-----|
|
||||||
| Erfordert Mitgliedschaft | Haupt-Discord | Nein |
|
| Erfordert Mitgliedschaft | Bacanaks oder Piccadilly | Nein |
|
||||||
| Server-Steuerung | Ja (je nach Rolle) | Nein |
|
| Server-Steuerung | Ja (je nach Rolle) | Nein |
|
||||||
| Status sehen | Ja | Ja |
|
| Status sehen | Ja | Ja |
|
||||||
| Alerts erhalten | Nein | Ja |
|
| Alerts erhalten | Nein | Ja |
|
||||||
| Verfügbar für | Haupt-Discord Mitglieder | Alle mit Bot |
|
| Verfügbar für | Mitglieder beider Discord-Server | Alle mit Bot |
|
||||||
|
|
||||||
Der Login zur Webapp erfordert Mitgliedschaft im Haupt-Discord-Server (DISCORD_GUILD_ID). Der Bot ist davon unabhängig und zeigt nur passive Status-Updates.
|
Der Login zur Webapp erfordert Mitgliedschaft in mindestens einem der konfigurierten Discord-Server (Bacanaks oder Piccadilly). Die höchste Rolle aus beiden Servern bestimmt die GSM-Berechtigung. Der Bot ist davon unabhängig und zeigt nur passive Status-Updates.
|
||||||
|
|||||||
Reference in New Issue
Block a user