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:
parent
b3080fb763
commit
f27093b87a
5 changed files with 28 additions and 251 deletions
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue