From 1df780fe60b23084e7e2def31d476bfb2914fa44 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 5 Mar 2026 23:48:23 +0100 Subject: [PATCH] feat: add ADMIN_PWD and ALLOWED_GUILD_IDS support - Add ADMIN_PWD and ALLOWED_GUILD_IDS env vars to config - Extend PluginContext with adminPwd and allowedGuildIds - Add adminAuth and guildFilter middleware for plugins - Add /api/admin/login endpoint Co-Authored-By: Claude Opus 4.6 --- server/src/core/middleware.ts | 41 +++++++++++++++++++++++++++++++++++ server/src/core/plugin.ts | 2 ++ server/src/index.ts | 16 +++++++++++++- 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 server/src/core/middleware.ts diff --git a/server/src/core/middleware.ts b/server/src/core/middleware.ts new file mode 100644 index 0000000..fc29a4b --- /dev/null +++ b/server/src/core/middleware.ts @@ -0,0 +1,41 @@ +import { Request, Response, NextFunction } from 'express'; +import type { PluginContext } from './plugin.js'; + +/** + * Admin authentication middleware. + * Checks `x-admin-password` header against ADMIN_PWD env var. + */ +export function adminAuth(ctx: PluginContext) { + return (req: Request, res: Response, next: NextFunction): void => { + if (!ctx.adminPwd) { + res.status(503).json({ error: 'ADMIN_PWD not configured' }); + return; + } + const pwd = req.headers['x-admin-password'] as string | undefined; + if (pwd !== ctx.adminPwd) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + next(); + }; +} + +/** + * Guild filter middleware. + * If ALLOWED_GUILD_IDS is set, only allows requests for those guilds. + * Reads guildId from req.params.guildId or req.body.guildId or req.query.guildId. + */ +export function guildFilter(ctx: PluginContext) { + return (req: Request, res: Response, next: NextFunction): void => { + if (ctx.allowedGuildIds.length === 0) { next(); return; } + + const guildId = req.params.guildId ?? req.body?.guildId ?? req.query.guildId; + if (!guildId) { next(); return; } + + if (!ctx.allowedGuildIds.includes(String(guildId))) { + res.status(403).json({ error: 'Guild not allowed' }); + return; + } + next(); + }; +} diff --git a/server/src/core/plugin.ts b/server/src/core/plugin.ts index 8d8d2fe..fb41d8c 100644 --- a/server/src/core/plugin.ts +++ b/server/src/core/plugin.ts @@ -25,6 +25,8 @@ export interface Plugin { export interface PluginContext { client: Client; dataDir: string; + adminPwd: string; + allowedGuildIds: string[]; } const loadedPlugins: Plugin[] = []; diff --git a/server/src/index.ts b/server/src/index.ts index da21fcc..7801321 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -10,6 +10,9 @@ import radioPlugin from './plugins/radio/index.js'; const PORT = Number(process.env.PORT ?? 8080); const DATA_DIR = process.env.DATA_DIR ?? '/data'; const DISCORD_TOKEN = process.env.DISCORD_TOKEN ?? ''; +const ADMIN_PWD = process.env.ADMIN_PWD ?? ''; +const ALLOWED_GUILD_IDS = (process.env.ALLOWED_GUILD_IDS ?? '') + .split(',').map(s => s.trim()).filter(Boolean); // ── Persistence ── loadState(); @@ -20,7 +23,7 @@ app.use(express.json()); app.use(express.static(path.join(import.meta.dirname ?? __dirname, '..', '..', 'web', 'dist'))); // ── Plugin Context ── -const ctx: PluginContext = { client, dataDir: DATA_DIR }; +const ctx: PluginContext = { client, dataDir: DATA_DIR, adminPwd: ADMIN_PWD, allowedGuildIds: ALLOWED_GUILD_IDS }; // ── SSE Events ── app.get('/api/events', (_req, res) => { @@ -60,6 +63,17 @@ app.get('/api/health', (_req, res) => { }); }); +// ── Admin Login ── +app.post('/api/admin/login', (req, res) => { + if (!ADMIN_PWD) { res.status(503).json({ error: 'ADMIN_PWD not configured' }); return; } + const { password } = req.body ?? {}; + if (password === ADMIN_PWD) { + res.json({ ok: true }); + } else { + res.status(401).json({ error: 'Invalid password' }); + } +}); + // ── API: List plugins ── app.get('/api/plugins', (_req, res) => { res.json(getPlugins().map(p => ({