- Shrimp.ts (875 行) 按类拆分到 Shrimp/(7 文件: Shrimp, ShrimpModule, ShrimpTableModule, ShrimpPower, ShrimpCrud, UserRoleModule, OperationLogModule) - forMongo/: MongoShrimpCrud + MongoShrimp(原 ShrimpServer 的 mongo 装配部分,无 express) - forExpress/: buildShrimpRouter + jwtAuth + types - main/: index(0 依赖核心) + expmon(express+mongo 整合,导出 ShrimpServer) + types(纯协议) - 入口子路径 server → expmon(express+mongo 整合方案命名更准确) - 删除冗余的 index.ts / core.ts(合并为 main/index.ts)
166 lines
7.3 KiB
TypeScript
166 lines
7.3 KiB
TypeScript
/**
|
|
* 把 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<string>; 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<string>()
|
|
for (const mod of moduleInfo) {
|
|
for (const field of mod.fields || []) {
|
|
if (typeof field.options === "string") {
|
|
neededKeys.add(field.options)
|
|
}
|
|
}
|
|
}
|
|
const filteredOptions: Record<string, any> = {}
|
|
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<IMeResult>({
|
|
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
|
|
}
|