From e0acbf5457b0b43260a8e2a669fe0db6bd711fbc Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Thu, 25 Jun 2026 11:48:26 +0800 Subject: [PATCH] feat(bot): lock settings reads from guests + persist guestMode Co-Authored-By: Claude Opus 4.8 (1M context) --- src/web/api/bot.test.ts | 55 +++++++++++++++++++++++++++++++++++++++++ src/web/api/bot.ts | 26 +++++++++++++++++-- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/web/api/bot.test.ts b/src/web/api/bot.test.ts index 5172567..9dd17f3 100644 --- a/src/web/api/bot.test.ts +++ b/src/web/api/bot.test.ts @@ -159,3 +159,58 @@ describe("bot router /settings", () => { } }); }); + +describe("bot router /settings guest-mode gating + persistence", () => { + let tmpDir: string; + let configPath: string; + let config: BotConfig; + let botDb: BotDatabase; + + beforeEach(() => { + botDb = createDatabase(":memory:"); + tmpDir = mkdtempSync(join(tmpdir(), "botsettings-gm-")); + configPath = join(tmpDir, "config.json"); + config = getDefaultConfig(); + }); + + afterEach(() => { + botDb.close(); + rmSync(tmpDir, { recursive: true, force: true }); + }); + + /** Mounts createBotRouter with an injected req.user (no session/cookie). */ + function mountBot(injectUser: () => unknown): express.Express { + const fakeManager = { getAllBots: () => [] } as unknown as BotManager; + const avatarStore = createAvatarStore(tmpDir); + const app = express(); + app.use(express.json()); + app.use((req, _res, next) => { (req as { user?: unknown }).user = injectUser(); next(); }); + app.use( + "/api/bot", + createBotRouter(fakeManager, config, configPath, pino({ level: "silent" }), botDb, avatarStore), + ); + return app; + } + + it("GET /settings is 403 for guests and includes guestMode for admins", async () => { + const guestApp = mountBot(() => ({ role: "guest", guest: {} })); + expect((await request(guestApp).get("/api/bot/settings")).status).toBe(403); + const adminApp = mountBot(() => ({ role: "admin" })); + const res = await request(adminApp).get("/api/bot/settings"); + expect(res.status).toBe(200); + expect(res.body.guestMode).toBeDefined(); + expect(res.body.guestMode.enabled).toBe(false); + }); + + it("POST /settings persists a guestMode block", async () => { + const adminApp = mountBot(() => ({ role: "admin" })); + const res = await request(adminApp).post("/api/bot/settings").send({ + guestMode: { enabled: true, bots: ["bot1"], permissions: { playNext: true } }, + }); + expect(res.status).toBe(200); + expect(res.body.guestMode.enabled).toBe(true); + expect(res.body.guestMode.bots).toEqual(["bot1"]); + expect(res.body.guestMode.permissions.playNext).toBe(true); + expect(res.body.guestMode.permissions.addToQueue).toBe(true); // untouched default + }); +}); diff --git a/src/web/api/bot.ts b/src/web/api/bot.ts index 35e9dea..a42376c 100755 --- a/src/web/api/bot.ts +++ b/src/web/api/bot.ts @@ -6,6 +6,8 @@ import type { Logger } from "../../logger.js"; import type { BotDatabase } from "../../data/database.js"; import type { AvatarStore } from "../../data/avatars.js"; import { requirePermission, requireBotAccess } from "../middleware/requirePermission.js"; +import { requireNotGuest } from "../middleware/requireNotGuest.js"; +import { GUEST_PERMISSION_FLAGS } from "../../data/permissions.js"; export function createBotRouter( botManager: BotManager, @@ -29,17 +31,18 @@ export function createBotRouter( // GET /api/bot/settings — 读取全局 bot 行为设置 // NOTE: must be registered before "/:id" so it isn't shadowed by the param route. - router.get("/settings", (_req, res) => { + router.get("/settings", requireNotGuest, (_req, res) => { res.json({ idleTimeoutMinutes: config.idleTimeoutMinutes ?? 0, autoPauseOnEmpty: config.autoPauseOnEmpty, + guestMode: config.guestMode, }); }); // POST /api/bot/settings — 保存全局 bot 行为设置 (gated: changing global bot // behavior is a bot.manage operation, consistent with PR #80's permission model) router.post("/settings", requirePermission("bot.manage"), (req, res) => { - const { idleTimeoutMinutes, autoPauseOnEmpty } = req.body; + const { idleTimeoutMinutes, autoPauseOnEmpty, guestMode } = req.body; const hasIdle = idleTimeoutMinutes !== undefined; if (hasIdle && (typeof idleTimeoutMinutes !== "number" || idleTimeoutMinutes < 0)) { @@ -51,6 +54,24 @@ export function createBotRouter( if (hasIdle) config.idleTimeoutMinutes = idleTimeoutMinutes; if (hasAutoPause) config.autoPauseOnEmpty = autoPauseOnEmpty; + + if (guestMode !== undefined && guestMode !== null && typeof guestMode === "object") { + const gm = config.guestMode; + if (typeof guestMode.enabled === "boolean") gm.enabled = guestMode.enabled; + if (guestMode.bots === "all") { + gm.bots = "all"; + } else if (Array.isArray(guestMode.bots)) { + gm.bots = guestMode.bots.filter((id: unknown): id is string => typeof id === "string"); + } + if (guestMode.permissions && typeof guestMode.permissions === "object") { + for (const f of GUEST_PERMISSION_FLAGS) { + if (typeof guestMode.permissions[f] === "boolean") { + gm.permissions[f] = guestMode.permissions[f]; + } + } + } + } + saveConfig(configPath, config); // 通知所有 bot 实例更新 @@ -62,6 +83,7 @@ export function createBotRouter( res.json({ idleTimeoutMinutes: config.idleTimeoutMinutes ?? 0, autoPauseOnEmpty: config.autoPauseOnEmpty, + guestMode: config.guestMode, }); });