- 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)
85 lines
3.5 KiB
TypeScript
85 lines
3.5 KiB
TypeScript
/**
|
|
* OperationLogModule
|
|
* 操作日志子表 - 挂在 userModule 下,记录"某用户做了什么"
|
|
* - 默认只记 mutation(add/edit/delete/clear/import) + login
|
|
* - args 自动脱敏 + JSON.stringify + 截断到 2000 字符
|
|
* - 日志失败不影响主调用
|
|
* - 只暴露 list/get,不开 add/edit/delete 防止误改
|
|
*/
|
|
|
|
import { IFieldConfig, IOperationLog, WithId, ILabel } from "../IShrimpType"
|
|
import { Shrimp } from "./Shrimp"
|
|
import { ShrimpModule } from "./ShrimpModule"
|
|
import { ShrimpTableModule } from "./ShrimpTableModule"
|
|
import type { ShrimpCrud } from "./ShrimpCrud"
|
|
|
|
const SENSITIVE_KEY_RE = /(password|pwd|token|secret)/i
|
|
const ARGS_MAX_LEN = 2000
|
|
|
|
const operationLogFields: IFieldConfig[] = [
|
|
{ key: "ip", label: { t: "operationLog.ip" }, type: "string", searchable: true, editable: "readonly" },
|
|
{ key: "method", label: { t: "operationLog.method" }, type: "string", searchable: true, filterable: true, editable: "readonly" },
|
|
{ key: "args", label: { t: "operationLog.args" }, type: "textarea", editable: "readonly" },
|
|
{ key: "success", label: { t: "operationLog.success" }, type: "boolean", filterable: true, editable: "readonly" },
|
|
{ key: "errorMsg", label: { t: "operationLog.errorMsg" }, type: "string", editable: "readonly" },
|
|
{ key: "createdAt", label: { t: "operationLog.createdAt" }, type: "date", editable: "readonly", sortable: true, filterable: true },
|
|
]
|
|
|
|
export class OperationLogModule extends ShrimpTableModule<WithId<IOperationLog>> {
|
|
logEvents: Set<string> = new Set(['add', 'edit', 'delete', 'clear', 'import', 'login'])
|
|
|
|
constructor(parent: Shrimp | ShrimpModule, dataSource: ShrimpCrud<WithId<IOperationLog>>, fields?: IFieldConfig[], label?: ILabel) {
|
|
super(parent, "operationLog", dataSource, fields || operationLogFields, label || { t: "user.operationLog" }, undefined, {
|
|
icon: "Tickets",
|
|
ownerKey: "operatorId",
|
|
})
|
|
this.addEvent("list", this.power)
|
|
this.addEvent("get", this.power)
|
|
}
|
|
|
|
shouldLog(path: string[]): boolean {
|
|
if (path.length === 0) return false
|
|
if (path.includes('operationLog')) return false
|
|
const last = path[path.length - 1]
|
|
return this.logEvents.has(last)
|
|
}
|
|
|
|
sanitizeArgs(args: any[]): string {
|
|
const walk = (val: any): any => {
|
|
if (val == null) return val
|
|
if (Array.isArray(val)) return val.map(walk)
|
|
if (typeof val === 'object') {
|
|
const out: Record<string, any> = {}
|
|
for (const [k, v] of Object.entries(val)) {
|
|
out[k] = SENSITIVE_KEY_RE.test(k) ? '***' : walk(v)
|
|
}
|
|
return out
|
|
}
|
|
return val
|
|
}
|
|
let str: string
|
|
try {
|
|
str = JSON.stringify(walk(args))
|
|
} catch {
|
|
str = '[unserializable]'
|
|
}
|
|
return str.length > ARGS_MAX_LEN ? str.substring(0, ARGS_MAX_LEN) + '...' : str
|
|
}
|
|
|
|
async record(operatorId: any, ip: string, path: string[], args: any[], success: boolean, errorMsg: string = ''): Promise<void> {
|
|
try {
|
|
await this.dataSource.add({
|
|
operatorId: operatorId == null ? null : String(operatorId),
|
|
ip,
|
|
method: path.join('.'),
|
|
args: this.sanitizeArgs(args),
|
|
success,
|
|
errorMsg,
|
|
createdAt: Date.now(),
|
|
} as Omit<WithId<IOperationLog>, '_id'>)
|
|
} catch (e: any) {
|
|
console.warn('[OperationLog] record failed', e?.message || e)
|
|
}
|
|
}
|
|
}
|