mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(mw): add unified authorize() gate and requireNotGuest
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8fbc522d06
commit
821fa0669d
4 files changed
+90
No files matched your search
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { authorize } from "./authorize.js";
|
||||||
|
|
||||||
|
function run(user: any, opts: any) {
|
||||||
|
const req: any = { user };
|
||||||
|
const res: any = { statusCode: 0, body: null, status(c: number) { this.statusCode = c; return this; }, json(b: any) { this.body = b; return this; } };
|
||||||
|
const next = vi.fn();
|
||||||
|
authorize(opts)(req, res, next);
|
||||||
|
return { res, next };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("authorize", () => {
|
||||||
|
it("401 when unauthenticated", () => {
|
||||||
|
const { res, next } = run(undefined, { capability: "player.queue" });
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(next).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it("admin always passes", () => {
|
||||||
|
const { next } = run({ role: "admin" }, { capability: "bot.manage" });
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it("member passes only with the capability", () => {
|
||||||
|
expect(run({ role: "member", capabilities: new Set(["player.queue"]) }, { capability: "player.queue" }).next).toHaveBeenCalled();
|
||||||
|
expect(run({ role: "member", capabilities: new Set() }, { capability: "player.queue" }).res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
it("guest passes only when its flag is enabled", () => {
|
||||||
|
expect(run({ role: "guest", guest: { playNext: true } }, { capability: "player.control", guestFlag: "playNext" }).next).toHaveBeenCalled();
|
||||||
|
expect(run({ role: "guest", guest: { playNext: false } }, { capability: "player.control", guestFlag: "playNext" }).res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
it("guest is denied on routes with no guestFlag (e.g. play-song)", () => {
|
||||||
|
expect(run({ role: "guest", guest: { addToQueue: true } }, { capability: "player.control" }).res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Request, Response, NextFunction, RequestHandler } from "express";
|
||||||
|
import type { GuestFlag } from "../../data/permissions.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified authorization gate.
|
||||||
|
* - admin → always allowed (unchanged from requirePermission)
|
||||||
|
* - member → allowed iff it holds `capability` (unchanged from requirePermission)
|
||||||
|
* - guest → allowed iff `guestFlag` is set AND that flag is enabled in the
|
||||||
|
* guest's resolved permissions; a route with no `guestFlag` is
|
||||||
|
* denied to guests by default.
|
||||||
|
* Generic over the route-param shape `P` for the same reason requirePermission is.
|
||||||
|
*/
|
||||||
|
export function authorize<P = Record<string, string>>(opts: {
|
||||||
|
capability?: string;
|
||||||
|
guestFlag?: GuestFlag;
|
||||||
|
}): RequestHandler<P> {
|
||||||
|
return (req: Request<P>, res: Response, next: NextFunction) => {
|
||||||
|
const user = req.user;
|
||||||
|
if (!user) { res.status(401).json({ error: "unauthenticated" }); return; }
|
||||||
|
if (user.role === "admin") { next(); return; }
|
||||||
|
if (user.role === "guest") {
|
||||||
|
if (opts.guestFlag && user.guest?.[opts.guestFlag]) { next(); return; }
|
||||||
|
res.status(403).json({ error: "forbidden" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (opts.capability && user.capabilities?.has(opts.capability)) { next(); return; }
|
||||||
|
res.status(403).json({ error: "forbidden" });
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { requireNotGuest } from "./requireNotGuest.js";
|
||||||
|
|
||||||
|
function run(user: any) {
|
||||||
|
const req: any = { user };
|
||||||
|
const res: any = { statusCode: 0, status(c: number) { this.statusCode = c; return this; }, json() { return this; } };
|
||||||
|
const next = vi.fn();
|
||||||
|
requireNotGuest(req, res, next);
|
||||||
|
return { res, next };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("requireNotGuest", () => {
|
||||||
|
it("401 when no user", () => { expect(run(undefined).res.statusCode).toBe(401); });
|
||||||
|
it("403 for guests", () => { expect(run({ role: "guest" }).res.statusCode).toBe(403); });
|
||||||
|
it("passes admins and members", () => {
|
||||||
|
expect(run({ role: "admin" }).next).toHaveBeenCalled();
|
||||||
|
expect(run({ role: "member" }).next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
|
/** Allow admins and members; deny login-less guests (used for config reads
|
||||||
|
* that must never leak to guests, e.g. GET /api/bot/settings, GET /api/music/quality). */
|
||||||
|
export function requireNotGuest(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
if (!req.user) { res.status(401).json({ error: "unauthenticated" }); return; }
|
||||||
|
if (req.user.role === "guest") { res.status(403).json({ error: "forbidden" }); return; }
|
||||||
|
next();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user