From 696b224f8d92a76b4870c2e7ba127b7ca1eabe06 Mon Sep 17 00:00:00 2001 From: saopig1 <4x7sw862st@gmail.com> Date: Sat, 30 May 2026 13:21:24 +0800 Subject: [PATCH] feat(perm): load capabilities + bot access onto req.user; expose via /me Co-Authored-By: Claude Opus 4.8 (1M context) --- src/web/api/audit.test.ts | 4 +++- src/web/api/session.test.ts | 4 +++- src/web/api/session.ts | 14 ++++++++++++-- src/web/api/users.test.ts | 6 ++++-- src/web/middleware/requireAdmin.test.ts | 4 +++- src/web/middleware/requireAuth.test.ts | 4 +++- src/web/middleware/requireAuth.ts | 12 ++++++++++-- src/web/server.ts | 6 ++++-- 8 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/web/api/audit.test.ts b/src/web/api/audit.test.ts index 5e65d48..b65a658 100644 --- a/src/web/api/audit.test.ts +++ b/src/web/api/audit.test.ts @@ -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 { createAuditStore } from "../../data/audit.js"; +import { createPermissionStore } from "../../data/permissions.js"; import { createRequireAuth } from "../middleware/requireAuth.js"; import { createAuditRouter } from "./audit.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; @@ -20,6 +21,7 @@ describe("audit router", () => { const users = createUserStore(botDb.db); const sessions = createSessionStore(botDb.db); const audit = createAuditStore(botDb.db); + const permissions = createPermissionStore(botDb.db); const alice = await users.createUser("alice", "pw-alice", "admin"); cookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(alice.id).token}`; for (let i = 0; i < 3; i++) { @@ -32,7 +34,7 @@ describe("audit router", () => { app = express(); app.use(express.json()); app.use(cookieParser()); - app.use("/api", createRequireAuth(sessions)); + app.use("/api", createRequireAuth(sessions, permissions)); app.use("/api/audit", createAuditRouter(audit)); }); diff --git a/src/web/api/session.test.ts b/src/web/api/session.test.ts index a46f081..cd7970f 100644 --- a/src/web/api/session.test.ts +++ b/src/web/api/session.test.ts @@ -7,6 +7,7 @@ import { createDatabase, type BotDatabase } from "../../data/database.js"; import { createUserStore, type UserStore } from "../../data/users.js"; import { createSessionStore, type SessionStore } from "../../data/sessions.js"; import { createAuditStore } from "../../data/audit.js"; +import { createPermissionStore } from "../../data/permissions.js"; import { createSessionRouter } from "./session.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; @@ -15,7 +16,8 @@ function makeApp(botDb: BotDatabase, users: UserStore, sessions: SessionStore) { app.use(express.json()); app.use(cookieParser()); const audit = createAuditStore(botDb.db); - app.use("/api/session", createSessionRouter(users, sessions, audit, pino({ level: "silent" }))); + const permissions = createPermissionStore(botDb.db); + app.use("/api/session", createSessionRouter(users, sessions, audit, pino({ level: "silent" }), permissions)); return app; } diff --git a/src/web/api/session.ts b/src/web/api/session.ts index 21e8721..85b26b6 100644 --- a/src/web/api/session.ts +++ b/src/web/api/session.ts @@ -4,6 +4,7 @@ import type { Logger } from "../../logger.js"; import type { UserStore } from "../../data/users.js"; import type { SessionStore } from "../../data/sessions.js"; import type { AuditStore } from "../../data/audit.js"; +import { resolvePermissionContext, type PermissionStore } from "../../data/permissions.js"; import { SESSION_TTL_MS } from "../../data/sessions.js"; import { SESSION_COOKIE_NAME, validateSessionFromHeaders, extractSessionToken } from "../auth/validateSession.js"; @@ -49,7 +50,8 @@ export function createSessionRouter( users: UserStore, sessions: SessionStore, audit: AuditStore, - logger: Logger + logger: Logger, + permissions: PermissionStore ): Router { const router = Router(); @@ -133,7 +135,15 @@ export function createSessionRouter( }); router.get("/me", requireAuthInline, (req, res) => { - res.json(req.user); + const user = req.user!; + const ctx = resolvePermissionContext(user.role, user.id, permissions); + res.json({ + id: user.id, + username: user.username, + role: user.role, + capabilities: [...ctx.capabilities], + bots: ctx.bots === "all" ? "all" : [...ctx.bots], + }); }); router.post("/change-password", requireAuthInline, async (req, res) => { diff --git a/src/web/api/users.test.ts b/src/web/api/users.test.ts index 67efd4c..669395e 100644 --- a/src/web/api/users.test.ts +++ b/src/web/api/users.test.ts @@ -7,6 +7,7 @@ import { createDatabase, type BotDatabase } from "../../data/database.js"; import { createUserStore, type UserStore } from "../../data/users.js"; import { createSessionStore, type SessionStore } from "../../data/sessions.js"; import { createAuditStore } from "../../data/audit.js"; +import { createPermissionStore } from "../../data/permissions.js"; import { createRequireAuth } from "../middleware/requireAuth.js"; import { createUsersRouter } from "./users.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; @@ -15,7 +16,8 @@ function makeApp(botDb: BotDatabase, users: UserStore, sessions: SessionStore) { const app = express(); app.use(express.json()); app.use(cookieParser()); - const requireAuth = createRequireAuth(sessions); + const permissions = createPermissionStore(botDb.db); + const requireAuth = createRequireAuth(sessions, permissions); const audit = createAuditStore(botDb.db); app.use("/api", requireAuth); app.use("/api/users", createUsersRouter(users, sessions, audit, pino({ level: "silent" }))); @@ -148,7 +150,7 @@ describe("users router", () => { const localApp = express(); localApp.use(express.json()); localApp.use(cookieParser()); - localApp.use("/api", createRequireAuth(sessions)); + localApp.use("/api", createRequireAuth(sessions, createPermissionStore(botDb.db))); localApp.use( "/api/users", createUsersRouter(users, sessions, brokenAudit, pino({ level: "silent" })) diff --git a/src/web/middleware/requireAdmin.test.ts b/src/web/middleware/requireAdmin.test.ts index 40ccd02..df020d3 100644 --- a/src/web/middleware/requireAdmin.test.ts +++ b/src/web/middleware/requireAdmin.test.ts @@ -5,6 +5,7 @@ import request from "supertest"; 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 { createRequireAuth } from "./requireAuth.js"; import { requireAdmin } from "./requireAdmin.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; @@ -19,13 +20,14 @@ describe("requireAdmin middleware", () => { botDb = createDatabase(":memory:"); const users = createUserStore(botDb.db); const sessions = createSessionStore(botDb.db); + const permissions = createPermissionStore(botDb.db); const admin = await users.createUser("admin", "pw-admin-pw", "admin"); const member = await users.createUser("member", "pw-member-pw", "member"); adminCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(admin.id).token}`; memberCookie = `${SESSION_COOKIE_NAME}=${sessions.createSession(member.id).token}`; app = express(); app.use(cookieParser()); - app.use(createRequireAuth(sessions)); + app.use(createRequireAuth(sessions, permissions)); app.use(requireAdmin); app.get("/admin-only", (_req, res) => res.json({ ok: true })); }); diff --git a/src/web/middleware/requireAuth.test.ts b/src/web/middleware/requireAuth.test.ts index 0025bd1..fe9780f 100644 --- a/src/web/middleware/requireAuth.test.ts +++ b/src/web/middleware/requireAuth.test.ts @@ -5,6 +5,7 @@ import request from "supertest"; 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 { createRequireAuth } from "./requireAuth.js"; import { SESSION_COOKIE_NAME } from "../auth/validateSession.js"; @@ -17,12 +18,13 @@ describe("requireAuth middleware", () => { botDb = createDatabase(":memory:"); const users = createUserStore(botDb.db); const sessions = createSessionStore(botDb.db); + const permissions = createPermissionStore(botDb.db); const u = await users.createUser("alice", "pw-alice", "admin"); validToken = sessions.createSession(u.id).token; app = express(); app.use(cookieParser()); - app.use(createRequireAuth(sessions)); + app.use(createRequireAuth(sessions, permissions)); app.get("/protected", (req, res) => { res.json({ ok: true, user: (req as any).user }); }); diff --git a/src/web/middleware/requireAuth.ts b/src/web/middleware/requireAuth.ts index 9a74f55..6865f21 100644 --- a/src/web/middleware/requireAuth.ts +++ b/src/web/middleware/requireAuth.ts @@ -1,6 +1,7 @@ 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 { validateSessionFromHeaders, extractSessionToken, @@ -19,7 +20,7 @@ declare module "express-serve-static-core" { } } -export function createRequireAuth(sessions: SessionStore): RequestHandler { +export function createRequireAuth(sessions: SessionStore, permissions: PermissionStore): RequestHandler { return function requireAuth(req: Request, res: Response, next: NextFunction) { const result = validateSessionFromHeaders(req.headers.cookie, sessions); if (!result) { @@ -27,7 +28,14 @@ export function createRequireAuth(sessions: SessionStore): RequestHandler { res.status(401).json({ error: "unauthenticated" }); return; } - req.user = { id: result.userId, username: result.username, role: result.role }; + const ctx = resolvePermissionContext(result.role, result.userId, permissions); + req.user = { + id: result.userId, + username: result.username, + role: result.role, + capabilities: ctx.capabilities, + bots: ctx.bots, + }; const token = extractSessionToken(req.headers.cookie); if (token) { res.cookie(SESSION_COOKIE_NAME, token, { diff --git a/src/web/server.ts b/src/web/server.ts index 753fbe1..78e09e2 100755 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -21,6 +21,7 @@ import { createAuditRouter } from "./api/audit.js"; import { setupWebSocket } from "./websocket.js"; import { createUserStore } from "../data/users.js"; import { createSessionStore } from "../data/sessions.js"; +import { createPermissionStore } from "../data/permissions.js"; import { createRequireAuth } from "./middleware/requireAuth.js"; import { requireAdmin } from "./middleware/requireAdmin.js"; import { csrfOriginCheck } from "./middleware/csrf.js"; @@ -73,6 +74,7 @@ export function createWebServer(options: WebServerOptions): WebServer { const users = createUserStore(options.database.db); const sessions = createSessionStore(options.database.db); const audit = createAuditStore(options.database.db); + const permissions = createPermissionStore(options.database.db); // ─── Public routes (no auth, no CSRF) ─────────────────────────────────── app.get("/api/health", (_req, res) => { @@ -92,10 +94,10 @@ export function createWebServer(options: WebServerOptions): WebServer { app.use("/api/session/login", loginLimit); app.use("/api/session/setup", setupLimit); - app.use("/api/session", createSessionRouter(users, sessions, audit, logger)); + app.use("/api/session", createSessionRouter(users, sessions, audit, logger, permissions)); // ─── Gates for everything else under /api ─────────────────────────────── - const requireAuth = createRequireAuth(sessions); + const requireAuth = createRequireAuth(sessions, permissions); app.use("/api", csrfOriginCheck); app.use("/api", requireAuth);