/** * ShrimpServer * Shrimp 的服务端实现,接入 express + mongoose + JWT * 提供通用的后台管理 API(登录、鉴权、菜单、事件调度) */ import { Router, Request, Response } from "express" import jsonwebtoken from "jsonwebtoken" import mongoose, { Schema, Model } from "mongoose" import { Shrimp, ShrimpCrud, UserModule, RoleModule, RootShrimpPower } from "./Shrimp" import { MongoShrimpCrud } from "./MongoShrimpCrud" import { OperationLogModule } from "./OperationLogModule" import { ShrimpCode, IMeResult, IUser, IRole, IOperationLog, WithId, IMenuItem, IShrimpOperator, ShrimpError } from "./IShrimpType" declare global { namespace Express { interface Request { user: { _id: string; name: string; power: Set; roleId: string[]; isSuperAdmin: boolean } } } } // ============== 默认 Mongoose Schema(可导出,也可被子类覆盖) ============== export const UserSchema = new Schema({ username: { type: String, required: true }, password: { type: String, required: true }, nickname: { type: String, default: '' }, roleId: { type: [Schema.Types.ObjectId], ref: 'Role', default: [] }, active: { type: Boolean, default: true } }, { timestamps: true }) export const RoleSchema = new Schema({ name: { type: String, required: true }, description: { type: String, default: '' }, isSuperAdmin: { type: Boolean, default: false }, powers: { type: [String], default: [] }, }, { timestamps: true }) export const OperationLogSchema = new Schema({ operatorId: { type: String, default: null }, ip: { type: String, default: '' }, method: { type: String, required: true }, args: { type: String, default: '' }, success: { type: Boolean, default: true }, errorMsg: { type: String, default: '' }, createdAt: { type: Number, default: () => Date.now() }, }, { timestamps: false }) // ============== ShrimpServer ============== export class ShrimpServer extends Shrimp { userModule: UserModule roleModule: RoleModule operationLogModule: OperationLogModule router: Router = Router() jwtSecret: string config: Record globalOptions: Record = {} forcedOptionAccess = new Map() registerOptions(key: string, options: any, access?: RootShrimpPower) { this.globalOptions[key] = options if (access) { this.forcedOptionAccess.set(key, access) } } /** 缓存的 Mongoose Model,避免重复注册 */ protected _userModel?: Model protected _roleModel?: Model protected _operationLogModel?: Model constructor(jwtSecret: string, config: Record = {}, mongodbUri: string = 'mongodb://localhost:27017/shrimp') { super() this.jwtSecret = jwtSecret this.config = config // 初始化数据源 const userCrud = this.createUserCrud() const roleCrud = this.createRoleCrud() const operationLogCrud = this.createOperationLogCrud() // 初始化模块 this.userModule = this.createUserModule(userCrud) this.roleModule = this.createRoleModule(roleCrud) this.operationLogModule = this.createOperationLogModule(operationLogCrud) // 连接数据库 mongoose.connect(mongodbUri).then(() => { console.log(mongodbUri + ' MongoDB connected') this.onDbConnected() }) // ---- 路由 ---- // 后台配置接口(不需要鉴权) this.router.get('/config', (_req: Request, res: Response) => { res.json(this.success(this.config)) }) // 登录接口(不需要鉴权) this.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 this.userModule.getUser(username, password) const token = jsonwebtoken.sign(this.getJwtPayload(user), this.jwtSecret, { expiresIn: '7d' }) this.operationLogModule.record(user._id, ip, ['login'], args, true) res.json(this.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 || '') this.operationLogModule.record(null, ip, ['login'], args, false, errMsg) res.json(this.fail(ShrimpCode.FAIL, msg)) } }) // 鉴权中间件 this.router.use(async (req: Request, res: Response, next) => { try { const auth = req.headers.authorization if (!auth || !auth.startsWith('Bearer ')) { return res.json(this.fail(ShrimpCode.UNAUTHORIZED, { t: 'shrimp.relogin' }, 'No token')) } const token = auth.split(' ')[1] const decoded = jsonwebtoken.verify(token, this.jwtSecret) as any const user = await this.userModule.dataSource.get(decoded._id) if (!user || !this.isUserActive(user)) { return res.json(this.fail(ShrimpCode.UNAUTHORIZED, { t: 'shrimp.userDisabled' }, 'User disabled')) } const roleIds = this.getUserRoleIds(user) const roles = await Promise.all(roleIds.map(id => this.roleModule.dataSource.get(id))) const isSuperAdmin = roles.some(r => this.isRoleSuperAdmin(r)) const powers = isSuperAdmin ? new Set((await this.allPower()).map(p => p.key)) : new Set(roles.flatMap(r => this.getRolePowers(r))) req.user = { _id: user._id, name: this.getUserDisplayName(user), power: powers, roleId: roleIds, isSuperAdmin } next() } catch (error: any) { const msg = error instanceof ShrimpError ? error.tip : { t: 'shrimp.relogin' } return res.json(this.fail(ShrimpCode.UNAUTHORIZED, msg)) } }) // 获取当前用户信息 this.router.get('/me', async (req: Request, res: Response) => { try { const moduleInfo = await this.userModule.getModule(req.user._id, this.roleModule) const user = await this.userModule.dataSource.get(req.user._id) if (!user) { return res.json(this.fail(ShrimpCode.NOT_FOUND, { t: 'shrimp.userNotFound' })) } const roleIds = this.getUserRoleIds(user) const roles = await Promise.all(roleIds.map(id => this.roleModule.dataSource.get(id))) const isSuperAdmin = roles.some(r => this.isRoleSuperAdmin(r)) const powers = isSuperAdmin ? (await this.allPower()).map(p => p.key) : roles.flatMap(r => this.getRolePowers(r)) const filteredMenuTree = this.filterMenuTree(this.getMenuTree(), powers) // 按需收集该用户有权限的模块中引用的全局 options key(自动去重) 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 (this.globalOptions[key] != null) { filteredOptions[key] = this.globalOptions[key] } } for (const [key, access] of this.forcedOptionAccess) { if (this.globalOptions[key] != null) { if (isSuperAdmin || powers.includes(access.powerKey)) { filteredOptions[key] = this.globalOptions[key] } } } res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); res.json(this.success({ nickname: this.getUserDisplayName(user), moduleInfo, menuTree: filteredMenuTree, options: filteredOptions })) } catch (error: any) { if (error instanceof ShrimpError) { res.json(this.fail(error.code, error.tip)) } else { res.json(this.fail(ShrimpCode.FAIL, { t: 'shrimp.serverError' }, error.message)) } } }) // 通用事件调用接口 this.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 = this.operationLogModule.shouldLog(path) try { if (path.length === 0) { return res.json(this.fail(ShrimpCode.FAIL, { t: 'shrimp.emptyPath' })) } const powers = req.user.power if (!this.checkEvent(powers, path)) { return res.json(this.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, () => this.call(path, ...args)) if (shouldLog) this.operationLogModule.record(req.user._id, ip, path, args, true) res.json(this.success(result)) } catch (error: any) { if (shouldLog) { const errMsg = error instanceof ShrimpError ? JSON.stringify(error.tip) : (error?.message || '') this.operationLogModule.record(req.user?._id ?? null, ip, path, args, false, errMsg) } if (error instanceof ShrimpError) { res.json(this.fail(error.code, error.tip)) } else { res.json(this.fail(ShrimpCode.SERVER_ERROR, { t: 'shrimp.serverError' }, error.message)) } } }) } // ============== 可覆盖的 Schema 工厂方法 ============== protected createUserSchema(): mongoose.Schema { return UserSchema } protected createRoleSchema(): mongoose.Schema { return RoleSchema } protected createOperationLogSchema(): mongoose.Schema { return OperationLogSchema } protected getUserModel(): Model { if (!this._userModel) { this._userModel = mongoose.model('shrimp_users', this.createUserSchema()) } return this._userModel } protected getRoleModel(): Model { if (!this._roleModel) { this._roleModel = mongoose.model('shrimp_roles', this.createRoleSchema()) } return this._roleModel } protected getOperationLogModel(): Model { if (!this._operationLogModel) { this._operationLogModel = mongoose.model('shrimp_operation_logs', this.createOperationLogSchema()) } return this._operationLogModel } // ============== 可覆盖的 Crud 工厂方法 ============== protected createUserCrud(): MongoShrimpCrud> { return new MongoShrimpCrud>(this.getUserModel()) } protected createRoleCrud(): MongoShrimpCrud> { return new MongoShrimpCrud>(this.getRoleModel()) } protected createOperationLogCrud(): MongoShrimpCrud> { return new MongoShrimpCrud>(this.getOperationLogModel()) } // ============== 可覆盖的 Module 工厂方法 ============== protected createUserModule(dataSource: ShrimpCrud>): UserModule { return new UserModule(this, dataSource) } protected createRoleModule(dataSource: ShrimpCrud>): RoleModule { return new RoleModule(this, dataSource) } protected createOperationLogModule(dataSource: ShrimpCrud>): OperationLogModule { return new OperationLogModule(this.userModule, dataSource) } // ============== 可覆盖的用户/角色字段访问钩子 ============== /** 从用户对象生成 JWT payload,子类可覆盖 */ protected getJwtPayload(user: WithId): Record { return { _id: user._id, roleId: user.roleId } } /** 判断用户是否可用,子类可覆盖 */ protected isUserActive(user: WithId): boolean { return user.active === true } /** 从用户对象提取角色 ID 列表,子类可覆盖 */ protected getUserRoleIds(user: WithId): string[] { return Array.isArray(user.roleId) ? user.roleId.map(String) : user.roleId ? [String(user.roleId)] : [] } /** 判断角色是否为超级管理员,子类可覆盖 */ protected isRoleSuperAdmin(role: WithId | null): boolean { return !!role?.isSuperAdmin } /** 从角色对象提取权限列表,子类可覆盖 */ protected getRolePowers(role: WithId | null): string[] { return role?.powers || [] } /** 从用户对象提取显示名称,子类可覆盖 */ protected getUserDisplayName(user: WithId): string { return user.nickname || '' } /** 数据库连接后的回调,子类可覆盖以执行连接后的逻辑 */ protected async onDbConnected(): Promise { } /** 初始化默认数据(如默认角色、用户),子类可覆盖 */ protected async initDefaultData(name: string = "Administrator", description: "System administrator with all permissions"): Promise { const result = await this.userModule.dataSource.list({ limit: 1 }) if (result.total > 0) return console.log("初始化默认数据") const adminRole = await this.roleModule.dataSource.add({ name: name, description: description, isSuperAdmin: true, powers: [], createdAt: Date.now(), updatedAt: Date.now() }) await this.userModule.add({ username: "admin", password: "admin", nickname: "Administrator", roleId: [adminRole._id], active: true, createdAt: Date.now(), updatedAt: Date.now() }) } }