mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat(auth): guest-aware requireAuth + disable invalidates guest sessions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0514162824
commit
c9a0719128
7 files changed
+79
-12
No files matched your search
@@ -7,6 +7,7 @@ import { createUserStore } from "../../data/users.js";
|
||||
import { createSessionStore } from "../../data/sessions.js";
|
||||
import { createAuditStore } from "../../data/audit.js";
|
||||
import { createPermissionStore } from "../../data/permissions.js";
|
||||
import { getDefaultConfig } from "../../data/config.js";
|
||||
import { createRequireAuth } from "../middleware/requireAuth.js";
|
||||
import { createAuditRouter } from "./audit.js";
|
||||
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
||||
@@ -34,7 +35,7 @@ describe("audit router", () => {
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use("/api", createRequireAuth(sessions, permissions));
|
||||
app.use("/api", createRequireAuth(sessions, permissions, () => getDefaultConfig().guestMode));
|
||||
app.use("/api/audit", createAuditRouter(audit));
|
||||
});
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("bot router /settings", () => {
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db)));
|
||||
app.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db), () => getDefaultConfig().guestMode));
|
||||
app.use(
|
||||
"/api/bot",
|
||||
createBotRouter(fakeManager, config, configPath, pino({ level: "silent" }), botDb, avatarStore),
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createUserStore, type UserStore } from "../../data/users.js";
|
||||
import { createSessionStore, type SessionStore } from "../../data/sessions.js";
|
||||
import { createAuditStore, type AuditStore } from "../../data/audit.js";
|
||||
import { createPermissionStore, type PermissionStore } from "../../data/permissions.js";
|
||||
import { getDefaultConfig } from "../../data/config.js";
|
||||
import { createRequireAuth } from "../middleware/requireAuth.js";
|
||||
import { createUsersRouter } from "./users.js";
|
||||
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
||||
@@ -17,7 +18,7 @@ function makeApp(botDb: BotDatabase, users: UserStore, sessions: SessionStore) {
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
const permissions = createPermissionStore(botDb.db);
|
||||
const requireAuth = createRequireAuth(sessions, permissions);
|
||||
const requireAuth = createRequireAuth(sessions, permissions, () => getDefaultConfig().guestMode);
|
||||
const audit = createAuditStore(botDb.db);
|
||||
app.use("/api", requireAuth);
|
||||
app.use("/api/users", createUsersRouter(users, sessions, audit, pino({ level: "silent" }), permissions));
|
||||
@@ -152,7 +153,7 @@ describe("users router", () => {
|
||||
const localApp = express();
|
||||
localApp.use(express.json());
|
||||
localApp.use(cookieParser());
|
||||
localApp.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db)));
|
||||
localApp.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db), () => getDefaultConfig().guestMode));
|
||||
localApp.use(
|
||||
"/api/users",
|
||||
createUsersRouter(users, sessions, brokenAudit, pino({ level: "silent" }), createPermissionStore(botDb.db))
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createDatabase, type BotDatabase } from "../../data/database.js";
|
||||
import { createUserStore } from "../../data/users.js";
|
||||
import { createSessionStore } from "../../data/sessions.js";
|
||||
import { createPermissionStore } from "../../data/permissions.js";
|
||||
import { getDefaultConfig } from "../../data/config.js";
|
||||
import { createRequireAuth } from "./requireAuth.js";
|
||||
import { requireAdmin } from "./requireAdmin.js";
|
||||
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
||||
@@ -27,7 +28,7 @@ describe("requireAdmin middleware", () => {
|
||||
memberCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(member.id).token}`;
|
||||
app = express();
|
||||
app.use(cookieParser());
|
||||
app.use(createRequireAuth(sessions, permissions));
|
||||
app.use(createRequireAuth(sessions, permissions, () => getDefaultConfig().guestMode));
|
||||
app.use(requireAdmin);
|
||||
app.get("/admin-only", (_req, res) => res.json({ ok: true }));
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import express from "express";
|
||||
import cookieParser from "cookie-parser";
|
||||
import request from "supertest";
|
||||
@@ -24,7 +24,21 @@ describe("requireAuth middleware", () => {
|
||||
|
||||
app = express();
|
||||
app.use(cookieParser());
|
||||
app.use(createRequireAuth(sessions, permissions));
|
||||
app.use(
|
||||
createRequireAuth(sessions, permissions, () => ({
|
||||
enabled: true,
|
||||
bots: "all",
|
||||
permissions: {
|
||||
addToQueue: true,
|
||||
playNext: true,
|
||||
playNow: true,
|
||||
skip: true,
|
||||
transport: true,
|
||||
removeClear: true,
|
||||
playMode: true,
|
||||
},
|
||||
}))
|
||||
);
|
||||
app.get("/protected", (req, res) => {
|
||||
res.json({ ok: true, user: (req as any).user });
|
||||
});
|
||||
@@ -68,4 +82,34 @@ describe("requireAuth middleware", () => {
|
||||
expect(refreshed).toBeDefined();
|
||||
expect(refreshed!).toMatch(/Max-Age=\d+/);
|
||||
});
|
||||
|
||||
// A guest session is rejected (401) when guest mode is disabled.
|
||||
it("rejects a guest session when guest mode is disabled", () => {
|
||||
const sessions: any = { validateAndTouch: () => ({ userId: "__guest__", username: "游客", role: "guest" }) };
|
||||
const permissions: any = { getCapabilities: () => [], getBotAccess: () => [] };
|
||||
const getGuestConfig = () => ({ enabled: false, bots: "all" as const, permissions: {} as any });
|
||||
const mw = createRequireAuth(sessions, permissions, getGuestConfig);
|
||||
const req: any = { headers: { cookie: "tsmb_session=x" } };
|
||||
const res: any = { statusCode: 0, cleared: false, clearCookie() { this.cleared = true; }, status(c: number) { this.statusCode = c; return this; }, json() { return this; }, cookie() {} };
|
||||
const next = vi.fn();
|
||||
mw(req, res, next);
|
||||
expect(res.statusCode).toBe(401);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attaches guest permissions when guest mode is enabled", () => {
|
||||
const sessions: any = { validateAndTouch: () => ({ userId: "__guest__", username: "游客", role: "guest" }) };
|
||||
const permissions: any = { getCapabilities: () => [], getBotAccess: () => [] };
|
||||
const perms = { addToQueue: true, playNext: false, playNow: false, skip: false, transport: false, removeClear: false, playMode: false };
|
||||
const getGuestConfig = () => ({ enabled: true, bots: ["bot1"], permissions: perms });
|
||||
const mw = createRequireAuth(sessions, permissions, getGuestConfig);
|
||||
const req: any = { headers: { cookie: "tsmb_session=x" }, secure: false };
|
||||
const res: any = { status() { return this; }, json() { return this; }, cookie() {}, clearCookie() {} };
|
||||
const next = vi.fn();
|
||||
mw(req, res, next);
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(req.user.role).toBe("guest");
|
||||
expect(req.user.guest.addToQueue).toBe(true);
|
||||
expect(req.user.bots instanceof Set && req.user.bots.has("bot1")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Request, Response, NextFunction, RequestHandler } from "express";
|
||||
import type { SessionStore } from "../../data/sessions.js";
|
||||
import { SESSION_TTL_MS } from "../../data/sessions.js";
|
||||
import { resolvePermissionContext, type PermissionStore } from "../../data/permissions.js";
|
||||
import { resolvePermissionContext, type PermissionStore, type GuestPermissions } from "../../data/permissions.js";
|
||||
import type { GuestModeConfig } from "../../data/config.js";
|
||||
import {
|
||||
validateSessionFromHeaders,
|
||||
extractSessionToken,
|
||||
@@ -13,14 +14,19 @@ declare module "express-serve-static-core" {
|
||||
user?: {
|
||||
id: string;
|
||||
username: string;
|
||||
role: "admin" | "member";
|
||||
role: "admin" | "member" | "guest";
|
||||
capabilities?: Set<string>;
|
||||
bots?: "all" | Set<string>;
|
||||
guest?: GuestPermissions;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createRequireAuth(sessions: SessionStore, permissions: PermissionStore): RequestHandler {
|
||||
export function createRequireAuth(
|
||||
sessions: SessionStore,
|
||||
permissions: PermissionStore,
|
||||
getGuestConfig: () => GuestModeConfig
|
||||
): RequestHandler {
|
||||
return function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const result = validateSessionFromHeaders(req.headers.cookie, sessions);
|
||||
if (!result) {
|
||||
@@ -28,13 +34,27 @@ export function createRequireAuth(sessions: SessionStore, permissions: Permissio
|
||||
res.status(401).json({ error: "unauthenticated" });
|
||||
return;
|
||||
}
|
||||
const ctx = resolvePermissionContext(result.role, result.userId, permissions);
|
||||
// A guest session is only valid while guest mode is enabled. Disabling it
|
||||
// immediately invalidates any in-flight guest sessions.
|
||||
const guestCfg = getGuestConfig();
|
||||
if (result.role === "guest" && !guestCfg.enabled) {
|
||||
res.clearCookie(SESSION_COOKIE_NAME, { path: "/" });
|
||||
res.status(401).json({ error: "unauthenticated" });
|
||||
return;
|
||||
}
|
||||
const ctx = resolvePermissionContext(
|
||||
result.role,
|
||||
result.userId,
|
||||
permissions,
|
||||
result.role === "guest" ? { bots: guestCfg.bots, permissions: guestCfg.permissions } : undefined
|
||||
);
|
||||
req.user = {
|
||||
id: result.userId,
|
||||
username: result.username,
|
||||
role: result.role,
|
||||
capabilities: ctx.capabilities,
|
||||
bots: ctx.bots,
|
||||
guest: ctx.guest,
|
||||
};
|
||||
const token = extractSessionToken(req.headers.cookie);
|
||||
if (token) {
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
||||
app.use("/api/session", createSessionRouter(users, sessions, audit, logger, permissions));
|
||||
|
||||
// ─── Gates for everything else under /api ───────────────────────────────
|
||||
const requireAuth = createRequireAuth(sessions, permissions);
|
||||
const requireAuth = createRequireAuth(sessions, permissions, () => options.config.guestMode);
|
||||
app.use("/api", csrfOriginCheck);
|
||||
app.use("/api", requireAuth);
|
||||
|
||||
|
||||
Reference in new issue
Block a user