mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52: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 { createSessionStore } from "../../data/sessions.js";
|
||||||
import { createAuditStore } from "../../data/audit.js";
|
import { createAuditStore } from "../../data/audit.js";
|
||||||
import { createPermissionStore } from "../../data/permissions.js";
|
import { createPermissionStore } from "../../data/permissions.js";
|
||||||
|
import { getDefaultConfig } from "../../data/config.js";
|
||||||
import { createRequireAuth } from "../middleware/requireAuth.js";
|
import { createRequireAuth } from "../middleware/requireAuth.js";
|
||||||
import { createAuditRouter } from "./audit.js";
|
import { createAuditRouter } from "./audit.js";
|
||||||
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
import { SESSION_COOKIE_NAME } from "../auth/validateSession.js";
|
||||||
@@ -34,7 +35,7 @@ describe("audit router", () => {
|
|||||||
app = express();
|
app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use("/api", createRequireAuth(sessions, permissions));
|
app.use("/api", createRequireAuth(sessions, permissions, () => getDefaultConfig().guestMode));
|
||||||
app.use("/api/audit", createAuditRouter(audit));
|
app.use("/api/audit", createAuditRouter(audit));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ describe("bot router /settings", () => {
|
|||||||
app = express();
|
app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db)));
|
app.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db), () => getDefaultConfig().guestMode));
|
||||||
app.use(
|
app.use(
|
||||||
"/api/bot",
|
"/api/bot",
|
||||||
createBotRouter(fakeManager, config, configPath, pino({ level: "silent" }), botDb, avatarStore),
|
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 { createSessionStore, type SessionStore } from "../../data/sessions.js";
|
||||||
import { createAuditStore, type AuditStore } from "../../data/audit.js";
|
import { createAuditStore, type AuditStore } from "../../data/audit.js";
|
||||||
import { createPermissionStore, type PermissionStore } from "../../data/permissions.js";
|
import { createPermissionStore, type PermissionStore } from "../../data/permissions.js";
|
||||||
|
import { getDefaultConfig } from "../../data/config.js";
|
||||||
import { createRequireAuth } from "../middleware/requireAuth.js";
|
import { createRequireAuth } from "../middleware/requireAuth.js";
|
||||||
import { createUsersRouter } from "./users.js";
|
import { createUsersRouter } from "./users.js";
|
||||||
import { SESSION_COOKIE_NAME } from "../auth/validateSession.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(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
const permissions = createPermissionStore(botDb.db);
|
const permissions = createPermissionStore(botDb.db);
|
||||||
const requireAuth = createRequireAuth(sessions, permissions);
|
const requireAuth = createRequireAuth(sessions, permissions, () => getDefaultConfig().guestMode);
|
||||||
const audit = createAuditStore(botDb.db);
|
const audit = createAuditStore(botDb.db);
|
||||||
app.use("/api", requireAuth);
|
app.use("/api", requireAuth);
|
||||||
app.use("/api/users", createUsersRouter(users, sessions, audit, pino({ level: "silent" }), permissions));
|
app.use("/api/users", createUsersRouter(users, sessions, audit, pino({ level: "silent" }), permissions));
|
||||||
@@ -152,7 +153,7 @@ describe("users router", () => {
|
|||||||
const localApp = express();
|
const localApp = express();
|
||||||
localApp.use(express.json());
|
localApp.use(express.json());
|
||||||
localApp.use(cookieParser());
|
localApp.use(cookieParser());
|
||||||
localApp.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db)));
|
localApp.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db), () => getDefaultConfig().guestMode));
|
||||||
localApp.use(
|
localApp.use(
|
||||||
"/api/users",
|
"/api/users",
|
||||||
createUsersRouter(users, sessions, brokenAudit, pino({ level: "silent" }), createPermissionStore(botDb.db))
|
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 { createUserStore } from "../../data/users.js";
|
||||||
import { createSessionStore } from "../../data/sessions.js";
|
import { createSessionStore } from "../../data/sessions.js";
|
||||||
import { createPermissionStore } from "../../data/permissions.js";
|
import { createPermissionStore } from "../../data/permissions.js";
|
||||||
|
import { getDefaultConfig } from "../../data/config.js";
|
||||||
import { createRequireAuth } from "./requireAuth.js";
|
import { createRequireAuth } from "./requireAuth.js";
|
||||||
import { requireAdmin } from "./requireAdmin.js";
|
import { requireAdmin } from "./requireAdmin.js";
|
||||||
import { SESSION_COOKIE_NAME } from "../auth/validateSession.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}`;
|
memberCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(member.id).token}`;
|
||||||
app = express();
|
app = express();
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(createRequireAuth(sessions, permissions));
|
app.use(createRequireAuth(sessions, permissions, () => getDefaultConfig().guestMode));
|
||||||
app.use(requireAdmin);
|
app.use(requireAdmin);
|
||||||
app.get("/admin-only", (_req, res) => res.json({ ok: true }));
|
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 express from "express";
|
||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
@@ -24,7 +24,21 @@ describe("requireAuth middleware", () => {
|
|||||||
|
|
||||||
app = express();
|
app = express();
|
||||||
app.use(cookieParser());
|
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) => {
|
app.get("/protected", (req, res) => {
|
||||||
res.json({ ok: true, user: (req as any).user });
|
res.json({ ok: true, user: (req as any).user });
|
||||||
});
|
});
|
||||||
@@ -68,4 +82,34 @@ describe("requireAuth middleware", () => {
|
|||||||
expect(refreshed).toBeDefined();
|
expect(refreshed).toBeDefined();
|
||||||
expect(refreshed!).toMatch(/Max-Age=\d+/);
|
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 { Request, Response, NextFunction, RequestHandler } from "express";
|
||||||
import type { SessionStore } from "../../data/sessions.js";
|
import type { SessionStore } from "../../data/sessions.js";
|
||||||
import { SESSION_TTL_MS } 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 {
|
import {
|
||||||
validateSessionFromHeaders,
|
validateSessionFromHeaders,
|
||||||
extractSessionToken,
|
extractSessionToken,
|
||||||
@@ -13,14 +14,19 @@ declare module "express-serve-static-core" {
|
|||||||
user?: {
|
user?: {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
role: "admin" | "member";
|
role: "admin" | "member" | "guest";
|
||||||
capabilities?: Set<string>;
|
capabilities?: Set<string>;
|
||||||
bots?: "all" | 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) {
|
return function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||||
const result = validateSessionFromHeaders(req.headers.cookie, sessions);
|
const result = validateSessionFromHeaders(req.headers.cookie, sessions);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -28,13 +34,27 @@ export function createRequireAuth(sessions: SessionStore, permissions: Permissio
|
|||||||
res.status(401).json({ error: "unauthenticated" });
|
res.status(401).json({ error: "unauthenticated" });
|
||||||
return;
|
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 = {
|
req.user = {
|
||||||
id: result.userId,
|
id: result.userId,
|
||||||
username: result.username,
|
username: result.username,
|
||||||
role: result.role,
|
role: result.role,
|
||||||
capabilities: ctx.capabilities,
|
capabilities: ctx.capabilities,
|
||||||
bots: ctx.bots,
|
bots: ctx.bots,
|
||||||
|
guest: ctx.guest,
|
||||||
};
|
};
|
||||||
const token = extractSessionToken(req.headers.cookie);
|
const token = extractSessionToken(req.headers.cookie);
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
|
|||||||
app.use("/api/session", createSessionRouter(users, sessions, audit, logger, permissions));
|
app.use("/api/session", createSessionRouter(users, sessions, audit, logger, permissions));
|
||||||
|
|
||||||
// ─── Gates for everything else under /api ───────────────────────────────
|
// ─── 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", csrfOriginCheck);
|
||||||
app.use("/api", requireAuth);
|
app.use("/api", requireAuth);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user