feat: Discord OAuth Login + User Settings GUI
- Neues unified Login-Modal (Discord, Steam, Admin) ersetzt alten Admin-Login
- Discord OAuth2 Backend (server/src/core/discord-auth.ts)
- User Settings Panel: Entrance/Exit Sounds per Web-GUI konfigurierbar
- API-Endpoints: /api/soundboard/user/{sounds,entrance,exit}
- Session-Management via HMAC-signierte Cookies (hub_session)
- Steam-Button als Platzhalter (bald verfuegbar)
- Backward-kompatibel mit bestehendem Admin-Cookie
Benoetigte neue Env-Vars: DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET
Discord Redirect URI: PUBLIC_URL/api/auth/discord/callback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a7e8407996
commit
99d69f30ba
7 changed files with 1435 additions and 60 deletions
|
|
@ -17,6 +17,7 @@ import nacl from 'tweetnacl';
|
|||
import { ChannelType, Events, type VoiceBasedChannel, type VoiceState, type Message } from 'discord.js';
|
||||
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
||||
import { sseBroadcast } from '../../core/sse.js';
|
||||
import { getSession } from '../../core/discord-auth.js';
|
||||
|
||||
// ── Config (env) ──
|
||||
const SOUNDS_DIR = process.env.SOUNDS_DIR ?? '/data/sounds';
|
||||
|
|
@ -1244,6 +1245,101 @@ const soundboardPlugin: Plugin = {
|
|||
});
|
||||
});
|
||||
|
||||
// ── User Sound Preferences (Discord-authenticated) ──
|
||||
// Get current user's entrance/exit sounds
|
||||
app.get('/api/soundboard/user/sounds', (req, res) => {
|
||||
const session = getSession(req);
|
||||
if (!session?.discordId) {
|
||||
res.status(401).json({ error: 'Nicht eingeloggt' });
|
||||
return;
|
||||
}
|
||||
const userId = session.discordId;
|
||||
const entrance = persistedState.entranceSounds?.[userId] ?? null;
|
||||
const exit = persistedState.exitSounds?.[userId] ?? null;
|
||||
res.json({ entrance, exit });
|
||||
});
|
||||
|
||||
// Set entrance sound
|
||||
app.post('/api/soundboard/user/entrance', (req, res) => {
|
||||
const session = getSession(req);
|
||||
if (!session?.discordId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
||||
const { fileName } = req.body ?? {};
|
||||
if (!fileName || typeof fileName !== 'string') { res.status(400).json({ error: 'fileName erforderlich' }); return; }
|
||||
if (!/\.(mp3|wav)$/i.test(fileName)) { res.status(400).json({ error: 'Nur .mp3 oder .wav' }); return; }
|
||||
// Resolve file path (same logic as DM handler)
|
||||
const resolve = (() => {
|
||||
try {
|
||||
if (fs.existsSync(path.join(SOUNDS_DIR, fileName))) return fileName;
|
||||
for (const d of fs.readdirSync(SOUNDS_DIR, { withFileTypes: true })) {
|
||||
if (!d.isDirectory()) continue;
|
||||
if (fs.existsSync(path.join(SOUNDS_DIR, d.name, fileName))) return `${d.name}/${fileName}`;
|
||||
}
|
||||
return '';
|
||||
} catch { return ''; }
|
||||
})();
|
||||
if (!resolve) { res.status(404).json({ error: 'Datei nicht gefunden' }); return; }
|
||||
persistedState.entranceSounds = persistedState.entranceSounds ?? {};
|
||||
persistedState.entranceSounds[session.discordId] = resolve;
|
||||
writeState();
|
||||
console.log(`[Soundboard] User ${session.username} (${session.discordId}) set entrance: ${resolve}`);
|
||||
res.json({ ok: true, entrance: resolve });
|
||||
});
|
||||
|
||||
// Set exit sound
|
||||
app.post('/api/soundboard/user/exit', (req, res) => {
|
||||
const session = getSession(req);
|
||||
if (!session?.discordId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
||||
const { fileName } = req.body ?? {};
|
||||
if (!fileName || typeof fileName !== 'string') { res.status(400).json({ error: 'fileName erforderlich' }); return; }
|
||||
if (!/\.(mp3|wav)$/i.test(fileName)) { res.status(400).json({ error: 'Nur .mp3 oder .wav' }); return; }
|
||||
const resolve = (() => {
|
||||
try {
|
||||
if (fs.existsSync(path.join(SOUNDS_DIR, fileName))) return fileName;
|
||||
for (const d of fs.readdirSync(SOUNDS_DIR, { withFileTypes: true })) {
|
||||
if (!d.isDirectory()) continue;
|
||||
if (fs.existsSync(path.join(SOUNDS_DIR, d.name, fileName))) return `${d.name}/${fileName}`;
|
||||
}
|
||||
return '';
|
||||
} catch { return ''; }
|
||||
})();
|
||||
if (!resolve) { res.status(404).json({ error: 'Datei nicht gefunden' }); return; }
|
||||
persistedState.exitSounds = persistedState.exitSounds ?? {};
|
||||
persistedState.exitSounds[session.discordId] = resolve;
|
||||
writeState();
|
||||
console.log(`[Soundboard] User ${session.username} (${session.discordId}) set exit: ${resolve}`);
|
||||
res.json({ ok: true, exit: resolve });
|
||||
});
|
||||
|
||||
// Remove entrance sound
|
||||
app.delete('/api/soundboard/user/entrance', (req, res) => {
|
||||
const session = getSession(req);
|
||||
if (!session?.discordId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
||||
if (persistedState.entranceSounds) {
|
||||
delete persistedState.entranceSounds[session.discordId];
|
||||
writeState();
|
||||
}
|
||||
console.log(`[Soundboard] User ${session.username} (${session.discordId}) removed entrance sound`);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// Remove exit sound
|
||||
app.delete('/api/soundboard/user/exit', (req, res) => {
|
||||
const session = getSession(req);
|
||||
if (!session?.discordId) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
||||
if (persistedState.exitSounds) {
|
||||
delete persistedState.exitSounds[session.discordId];
|
||||
writeState();
|
||||
}
|
||||
console.log(`[Soundboard] User ${session.username} (${session.discordId}) removed exit sound`);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// List available sounds (for user settings dropdown) - no auth required
|
||||
app.get('/api/soundboard/user/available-sounds', (_req, res) => {
|
||||
const allSounds = listAllSounds();
|
||||
res.json(allSounds.map(s => ({ name: s.name, fileName: s.fileName, folder: s.folder, relativePath: s.relativePath })));
|
||||
});
|
||||
|
||||
// ── Health ──
|
||||
app.get('/api/soundboard/health', (_req, res) => {
|
||||
res.json({ ok: true, totalPlays: persistedState.totalPlays ?? 0, categories: (persistedState.categories ?? []).length, sounds: listAllSounds().length });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue