Refactor: Zentralisiertes Admin-Login für alle Tabs
Admin-Auth aus Soundboard-Plugin in core/auth.ts extrahiert. Ein Login-Button im Header gilt jetzt für die gesamte Webseite. Cookie-basiert (HMAC-SHA256, 7 Tage) — überlebt Page-Reload. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8abe0775a5
commit
b3080fb763
6 changed files with 101 additions and 133 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 { requireAdmin as requireAdminFactory } from '../../core/auth.js';
|
||||
|
||||
// ── Config (env) ──
|
||||
const SOUNDS_DIR = process.env.SOUNDS_DIR ?? '/data/sounds';
|
||||
|
|
@ -583,33 +584,6 @@ async function playFilePath(guildId: string, channelId: string, filePath: string
|
|||
if (relativeKey) incrementPlaysFor(relativeKey);
|
||||
}
|
||||
|
||||
// ── Admin Auth (JWT-like with HMAC) ──
|
||||
type AdminPayload = { iat: number; exp: number };
|
||||
function b64url(input: Buffer | string): string {
|
||||
return Buffer.from(input).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||
}
|
||||
function signAdminToken(adminPwd: string, payload: AdminPayload): string {
|
||||
const body = b64url(JSON.stringify(payload));
|
||||
const sig = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
|
||||
return `${body}.${sig}`;
|
||||
}
|
||||
function verifyAdminToken(adminPwd: string, token: string | undefined): boolean {
|
||||
if (!token || !adminPwd) return false;
|
||||
const [body, sig] = token.split('.');
|
||||
if (!body || !sig) return false;
|
||||
const expected = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
|
||||
if (expected !== sig) return false;
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(body, 'base64').toString('utf8')) as AdminPayload;
|
||||
return typeof payload.exp === 'number' && Date.now() < payload.exp;
|
||||
} catch { return false; }
|
||||
}
|
||||
function readCookie(req: express.Request, key: string): string | undefined {
|
||||
const c = req.headers.cookie;
|
||||
if (!c) return undefined;
|
||||
for (const part of c.split(';')) { const [k, v] = part.trim().split('='); if (k === key) return decodeURIComponent(v || ''); }
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ── Party Mode ──
|
||||
function schedulePartyPlayback(guildId: string, channelId: string) {
|
||||
|
|
@ -775,28 +749,7 @@ const soundboardPlugin: Plugin = {
|
|||
},
|
||||
|
||||
registerRoutes(app: express.Application, ctx: PluginContext) {
|
||||
const requireAdmin = (req: express.Request, res: express.Response, next: () => void) => {
|
||||
if (!ctx.adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
|
||||
if (!verifyAdminToken(ctx.adminPwd, readCookie(req, 'admin'))) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
||||
next();
|
||||
};
|
||||
|
||||
// ── Admin Auth ──
|
||||
app.post('/api/soundboard/admin/login', (req, res) => {
|
||||
if (!ctx.adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
|
||||
const { password } = req.body ?? {};
|
||||
if (!password || password !== ctx.adminPwd) { res.status(401).json({ error: 'Falsches Passwort' }); return; }
|
||||
const token = signAdminToken(ctx.adminPwd, { iat: Date.now(), exp: Date.now() + 7 * 24 * 3600 * 1000 });
|
||||
res.setHeader('Set-Cookie', `admin=${encodeURIComponent(token)}; HttpOnly; Path=/; Max-Age=${7 * 24 * 3600}; SameSite=Lax`);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
app.post('/api/soundboard/admin/logout', (_req, res) => {
|
||||
res.setHeader('Set-Cookie', 'admin=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax');
|
||||
res.json({ ok: true });
|
||||
});
|
||||
app.get('/api/soundboard/admin/status', (req, res) => {
|
||||
res.json({ authenticated: verifyAdminToken(ctx.adminPwd, readCookie(req, 'admin')) });
|
||||
});
|
||||
const requireAdmin = requireAdminFactory(ctx.adminPwd);
|
||||
|
||||
// ── Sounds ──
|
||||
app.get('/api/soundboard/sounds', (req, res) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue