Refactor: Admin-Login aus allen Plugins entfernt

Duplizierte Auth-Logik aus Notifications, Game Library und Streaming
Plugins komplett entfernt (-251 Zeilen). Alle Plugins nutzen jetzt
die zentrale Auth aus core/auth.ts via isAdmin Prop.

Admin-Buttons (Settings-Zahnrad) erscheinen nur noch wenn global
eingeloggt. Kein separater Login pro Tab mehr noetig.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-09 11:22:07 +01:00
parent b3080fb763
commit f27093b87a
5 changed files with 28 additions and 251 deletions

View file

@ -1,6 +1,7 @@
import type express from 'express';
import type { Plugin, PluginContext } from '../../core/plugin.js';
import { sseBroadcast } from '../../core/sse.js';
import { requireAdmin as requireAdminFactory } from '../../core/auth.js';
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
@ -58,34 +59,6 @@ const STEAM_API_KEY = process.env.STEAM_API_KEY || '2481D43449821AA4FF66502A9166
// ── Admin auth helpers (same system as soundboard) ──
function readCookie(req: express.Request, name: string): string | undefined {
const raw = req.headers.cookie || '';
const match = raw.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : undefined;
}
function b64url(str: string): string {
return Buffer.from(str).toString('base64url');
}
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 { iat: number; exp: number };
return typeof payload.exp === 'number' && Date.now() < payload.exp;
} catch { return false; }
}
function signAdminToken(adminPwd: string): string {
const payload = { iat: Date.now(), exp: Date.now() + 7 * 24 * 3600_000 };
const body = b64url(JSON.stringify(payload));
const sig = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
return `${body}.${sig}`;
}
// ── Data Persistence ──
@ -893,37 +866,7 @@ const gameLibraryPlugin: Plugin = {
// Admin endpoints (same auth as soundboard)
// ═══════════════════════════════════════════════════════════════════
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();
};
// ── GET /api/game-library/admin/status ──
app.get('/api/game-library/admin/status', (req, res) => {
if (!ctx.adminPwd) { res.json({ admin: false, configured: false }); return; }
const valid = verifyAdminToken(ctx.adminPwd, readCookie(req, 'admin'));
res.json({ admin: valid, configured: true });
});
// ── POST /api/game-library/admin/login ──
app.post('/api/game-library/admin/login', (req, res) => {
const password = String(req.body?.password || '');
if (!ctx.adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
if (password !== ctx.adminPwd) { res.status(401).json({ error: 'Falsches Passwort' }); return; }
const token = signAdminToken(ctx.adminPwd);
res.setHeader('Set-Cookie', `admin=${token}; HttpOnly; Path=/; Max-Age=${7 * 24 * 3600}; SameSite=Lax`);
res.json({ ok: true });
});
// ── POST /api/game-library/admin/logout ──
app.post('/api/game-library/admin/logout', (_req, res) => {
res.setHeader('Set-Cookie', 'admin=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax');
res.json({ ok: true });
});
const requireAdmin = requireAdminFactory(ctx.adminPwd);
// ── GET /api/game-library/admin/profiles ── Alle Profile mit Details
app.get('/api/game-library/admin/profiles', requireAdmin, (_req, res) => {

View file

@ -1,8 +1,8 @@
import type express from 'express';
import crypto from 'node:crypto';
import { Client, EmbedBuilder, TextChannel, ChannelType } from 'discord.js';
import type { Plugin, PluginContext } from '../../core/plugin.js';
import { getState, setState } from '../../core/persistence.js';
import { requireAdmin as requireAdminFactory } from '../../core/auth.js';
const NB = '[Notifications]';
@ -26,40 +26,6 @@ let _client: Client | null = null;
let _ctx: PluginContext | null = null;
let _publicUrl = '';
// ── Admin Auth (JWT-like with HMAC) ──
type AdminPayload = { iat: number; exp: number };
function readCookie(req: express.Request, name: string): string | undefined {
const header = req.headers.cookie;
if (!header) return undefined;
const match = header.split(';').map(s => s.trim()).find(s => s.startsWith(`${name}=`));
return match?.split('=').slice(1).join('=');
}
function b64url(str: string): string {
return Buffer.from(str).toString('base64url');
}
function verifyAdminToken(adminPwd: string, token: string | undefined): boolean {
if (!adminPwd || !token) return false;
const parts = token.split('.');
if (parts.length !== 2) return false;
const [body, sig] = parts;
const expected = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
if (expected !== sig) return false;
try {
const payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as AdminPayload;
return typeof payload.exp === 'number' && Date.now() < payload.exp;
} catch { return false; }
}
function signAdminToken(adminPwd: string): string {
const payload: AdminPayload = { iat: Date.now(), exp: Date.now() + 7 * 24 * 3600_000 };
const body = b64url(JSON.stringify(payload));
const sig = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
return `${body}.${sig}`;
}
// ── Exported notification functions (called by other plugins) ──
@ -159,33 +125,7 @@ const notificationsPlugin: Plugin = {
},
registerRoutes(app, ctx) {
const requireAdmin = (req: express.Request, res: express.Response, next: () => void): 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 status
app.get('/api/notifications/admin/status', (req, res) => {
if (!ctx.adminPwd) { res.json({ admin: false }); return; }
res.json({ admin: verifyAdminToken(ctx.adminPwd, readCookie(req, 'admin')) });
});
// Admin login
app.post('/api/notifications/admin/login', (req, res) => {
if (!ctx.adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
const { password } = req.body ?? {};
if (password !== ctx.adminPwd) { res.status(401).json({ error: 'Falsches Passwort' }); return; }
const token = signAdminToken(ctx.adminPwd);
res.setHeader('Set-Cookie', `admin=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${7 * 86400}`);
res.json({ ok: true });
});
// Admin logout
app.post('/api/notifications/admin/logout', (_req, res) => {
res.setHeader('Set-Cookie', 'admin=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
res.json({ ok: true });
});
const requireAdmin = requireAdminFactory(ctx.adminPwd);
// List available text channels (requires admin)
app.get('/api/notifications/channels', requireAdmin, async (_req, res) => {