/** * 把 ShrimpServer 内联的 Express routes 抽成纯函数。 * * routes 完全不知道: * - 数据库类型(只通过 dataSource 抽象访问) * - JWT(只通过 ITokenAuth 注入) * - 自己的字段名(通过 IUserAccessor 适配) */ import { Router, Request, Response, NextFunction } from "express" import { Shrimp } from "../Shrimp/Shrimp" import { ShrimpCode, IMeResult, IShrimpOperator, ShrimpError } from "../IShrimpType" import type { IShrimpRouterDeps } from "./types" declare global { namespace Express { interface Request { user: { _id: string; name: string; power: Set; roleId: string[]; isSuperAdmin: boolean } } } } export function buildShrimpRouter(deps: IShrimpRouterDeps): Router { const { shrimp, userModule, roleModule, operationLogModule, tokenAuth, config, globalOptions, forcedOptionAccess, userAccessor, } = deps const router = Router() router.get("/config", (_req: Request, res: Response) => { res.json(shrimp.success(config)) }) router.post("/login", async (req: Request, res: Response) => { const ip = req.ip || "" const { username, password } = req.body const args = [{ username, password }] try { const user = await userModule.getUser(username, password) const token = tokenAuth.sign(userAccessor.getJwtPayload(user)) operationLogModule.record(user._id, ip, ["login"], args, true) res.json(shrimp.success({ token })) } catch (error: any) { const msg = error instanceof ShrimpError ? error.tip : { t: "shrimp.loginFailed" } const errMsg = error instanceof ShrimpError ? JSON.stringify(error.tip) : (error?.message || "") operationLogModule.record(null, ip, ["login"], args, false, errMsg) res.json(shrimp.fail(ShrimpCode.FAIL, msg)) } }) router.use(async (req: Request, res: Response, next: NextFunction) => { try { const auth = req.headers.authorization if (!auth || !auth.startsWith("Bearer ")) { return res.json(shrimp.fail(ShrimpCode.UNAUTHORIZED, { t: "shrimp.relogin" }, "No token")) } const token = auth.split(" ")[1] const decoded = tokenAuth.verify(token) const user = await userModule.dataSource.get(decoded._id) if (!user || !userAccessor.isUserActive(user)) { return res.json(shrimp.fail(ShrimpCode.UNAUTHORIZED, { t: "shrimp.userDisabled" }, "User disabled")) } const roleIds = userAccessor.getUserRoleIds(user) const roles = await Promise.all(roleIds.map(id => roleModule.dataSource.get(id))) const isSuperAdmin = roles.some(r => userAccessor.isRoleSuperAdmin(r)) const powers = isSuperAdmin ? new Set((await shrimp.allPower()).map(p => p.key)) : new Set(roles.flatMap(r => userAccessor.getRolePowers(r))) req.user = { _id: user._id, name: userAccessor.getUserDisplayName(user), power: powers, roleId: roleIds, isSuperAdmin } next() } catch (error: any) { const msg = error instanceof ShrimpError ? error.tip : { t: "shrimp.relogin" } return res.json(shrimp.fail(ShrimpCode.UNAUTHORIZED, msg)) } }) router.get("/me", async (req: Request, res: Response) => { try { const moduleInfo = await userModule.getModule(req.user._id, roleModule) const user = await userModule.dataSource.get(req.user._id) if (!user) { return res.json(shrimp.fail(ShrimpCode.NOT_FOUND, { t: "shrimp.userNotFound" })) } const roleIds = userAccessor.getUserRoleIds(user) const roles = await Promise.all(roleIds.map(id => roleModule.dataSource.get(id))) const isSuperAdmin = roles.some(r => userAccessor.isRoleSuperAdmin(r)) const powers = isSuperAdmin ? (await shrimp.allPower()).map(p => p.key) : roles.flatMap(r => userAccessor.getRolePowers(r)) const filteredMenuTree = shrimp.filterMenuTree(shrimp.getMenuTree(), powers) const neededKeys = new Set() for (const mod of moduleInfo) { for (const field of mod.fields || []) { if (typeof field.options === "string") { neededKeys.add(field.options) } } } const filteredOptions: Record = {} for (const key of neededKeys) { if (globalOptions[key] != null) { filteredOptions[key] = globalOptions[key] } } for (const [key, access] of forcedOptionAccess) { if (globalOptions[key] != null) { if (isSuperAdmin || powers.includes(access.powerKey)) { filteredOptions[key] = globalOptions[key] } } } res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate") res.json(shrimp.success({ nickname: userAccessor.getUserDisplayName(user), moduleInfo, menuTree: filteredMenuTree, options: filteredOptions, })) } catch (error: any) { if (error instanceof ShrimpError) { res.json(shrimp.fail(error.code, error.tip)) } else { res.json(shrimp.fail(ShrimpCode.FAIL, { t: "shrimp.serverError" }, error.message)) } } }) router.post("/*path", async (req: Request, res: Response) => { const raw = req.params.path const path = (Array.isArray(raw) ? raw.join("/") : raw).split("/").filter(Boolean) const args = Array.isArray(req.body) ? req.body : [req.body] const ip = req.ip || "" const shouldLog = operationLogModule.shouldLog(path) try { if (path.length === 0) { return res.json(shrimp.fail(ShrimpCode.FAIL, { t: "shrimp.emptyPath" })) } const powers = req.user.power if (!shrimp.checkEvent(powers, path)) { return res.json(shrimp.fail(ShrimpCode.FORBIDDEN, { t: "shrimp.forbidden" })) } const operator: IShrimpOperator = { userId: req.user._id, roleId: req.user.roleId, isSuperAdmin: req.user.isSuperAdmin, } const result = await Shrimp.asyncLocalStorage.run(operator, () => shrimp.call(path, ...args)) if (shouldLog) operationLogModule.record(req.user._id, ip, path, args, true) res.json(shrimp.success(result)) } catch (error: any) { if (shouldLog) { const errMsg = error instanceof ShrimpError ? JSON.stringify(error.tip) : (error?.message || "") operationLogModule.record(req.user?._id ?? null, ip, path, args, false, errMsg) } if (error instanceof ShrimpError) { res.json(shrimp.fail(error.code, error.tip)) } else { res.json(shrimp.fail(ShrimpCode.SERVER_ERROR, { t: "shrimp.serverError" }, error.message)) } } }) return router }