@h-ai/audit 0.1.0-alpha5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +113 -0
- package/dist/index.d.ts +365 -0
- package/dist/index.js +594 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { core, err, ok } from '@h-ai/core';
|
|
3
|
+
import { validateIdentifiers, BaseReldbCrudRepository, reldb } from '@h-ai/reldb';
|
|
4
|
+
|
|
5
|
+
// src/audit-config.ts
|
|
6
|
+
var AuditInitConfigSchema = z.object({
|
|
7
|
+
/** 用户表名,用于 list 查询时 LEFT JOIN 获取用户名 */
|
|
8
|
+
userTable: z.string().default("hai_iam_users"),
|
|
9
|
+
/** 用户表主键列名,用于 JOIN 条件 */
|
|
10
|
+
userIdColumn: z.string().default("id"),
|
|
11
|
+
/** 用户表用户名列名,用于 SELECT 输出 */
|
|
12
|
+
userNameColumn: z.string().default("username")
|
|
13
|
+
});
|
|
14
|
+
function toVoid(result) {
|
|
15
|
+
if (!result.success) {
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
return ok(void 0);
|
|
19
|
+
}
|
|
20
|
+
function createHelper(logFn) {
|
|
21
|
+
return {
|
|
22
|
+
async login(userId, ip, ua) {
|
|
23
|
+
const result = await logFn({ userId, action: "login", resource: "auth", ipAddress: ip, userAgent: ua });
|
|
24
|
+
return toVoid(result);
|
|
25
|
+
},
|
|
26
|
+
async logout(userId, ip, ua) {
|
|
27
|
+
const result = await logFn({ userId, action: "logout", resource: "auth", ipAddress: ip, userAgent: ua });
|
|
28
|
+
return toVoid(result);
|
|
29
|
+
},
|
|
30
|
+
async register(userId, ip, ua) {
|
|
31
|
+
const result = await logFn({ userId, action: "register", resource: "auth", resourceId: userId, ipAddress: ip, userAgent: ua });
|
|
32
|
+
return toVoid(result);
|
|
33
|
+
},
|
|
34
|
+
async passwordResetRequest(email, ip, ua) {
|
|
35
|
+
const result = await logFn({ action: "password_reset_request", resource: "auth", details: { email }, ipAddress: ip, userAgent: ua });
|
|
36
|
+
return toVoid(result);
|
|
37
|
+
},
|
|
38
|
+
async passwordResetComplete(userId, ip, ua) {
|
|
39
|
+
const result = await logFn({ userId, action: "password_reset", resource: "auth", ipAddress: ip, userAgent: ua });
|
|
40
|
+
return toVoid(result);
|
|
41
|
+
},
|
|
42
|
+
async crud(input) {
|
|
43
|
+
const result = await logFn({
|
|
44
|
+
userId: input.userId,
|
|
45
|
+
action: input.action,
|
|
46
|
+
resource: input.resource,
|
|
47
|
+
resourceId: input.resourceId,
|
|
48
|
+
details: input.details,
|
|
49
|
+
ipAddress: input.ip,
|
|
50
|
+
userAgent: input.ua
|
|
51
|
+
});
|
|
52
|
+
return toVoid(result);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// messages/en-US.json
|
|
58
|
+
var en_US_default = {
|
|
59
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
60
|
+
audit_notInitialized: "Audit module not initialized, call audit.init() first",
|
|
61
|
+
audit_initFailed: "Audit module initialization failed: {error}",
|
|
62
|
+
audit_logFailed: "Failed to record audit log: {error}",
|
|
63
|
+
audit_queryFailed: "Failed to query audit logs: {error}",
|
|
64
|
+
audit_cleanupFailed: "Failed to cleanup audit logs: {error}",
|
|
65
|
+
audit_statsFailed: "Failed to query audit statistics: {error}",
|
|
66
|
+
audit_configError: "Audit config validation failed: {error}",
|
|
67
|
+
audit_initInProgress: "Audit module initialization is already in progress",
|
|
68
|
+
audit_invalidInput: "Invalid audit input for {field}: {reason}"
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// messages/zh-CN.json
|
|
72
|
+
var zh_CN_default = {
|
|
73
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
74
|
+
audit_notInitialized: "\u5BA1\u8BA1\u6A21\u5757\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 audit.init()",
|
|
75
|
+
audit_initFailed: "\u5BA1\u8BA1\u6A21\u5757\u521D\u59CB\u5316\u5931\u8D25\uFF1A{error}",
|
|
76
|
+
audit_logFailed: "\u5BA1\u8BA1\u65E5\u5FD7\u8BB0\u5F55\u5931\u8D25\uFF1A{error}",
|
|
77
|
+
audit_queryFailed: "\u5BA1\u8BA1\u65E5\u5FD7\u67E5\u8BE2\u5931\u8D25\uFF1A{error}",
|
|
78
|
+
audit_cleanupFailed: "\u5BA1\u8BA1\u65E5\u5FD7\u6E05\u7406\u5931\u8D25\uFF1A{error}",
|
|
79
|
+
audit_statsFailed: "\u5BA1\u8BA1\u7EDF\u8BA1\u67E5\u8BE2\u5931\u8D25\uFF1A{error}",
|
|
80
|
+
audit_configError: "\u5BA1\u8BA1\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A{error}",
|
|
81
|
+
audit_initInProgress: "\u5BA1\u8BA1\u6A21\u5757\u6B63\u5728\u521D\u59CB\u5316\u4E2D\uFF0C\u8BF7\u52FF\u5E76\u53D1\u8C03\u7528",
|
|
82
|
+
audit_invalidInput: "\u5BA1\u8BA1\u8F93\u5165\u53C2\u6570\u65E0\u6548\uFF08{field}\uFF09\uFF1A{reason}"
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/audit-i18n.ts
|
|
86
|
+
var auditM = core.i18n.createMessageGetter({
|
|
87
|
+
"zh-CN": zh_CN_default,
|
|
88
|
+
"en-US": en_US_default
|
|
89
|
+
});
|
|
90
|
+
var AuditErrorInfo = {
|
|
91
|
+
LOG_FAILED: "001:500",
|
|
92
|
+
QUERY_FAILED: "002:500",
|
|
93
|
+
CLEANUP_FAILED: "003:500",
|
|
94
|
+
STATS_FAILED: "004:500",
|
|
95
|
+
INIT_IN_PROGRESS: "005:409",
|
|
96
|
+
NOT_INITIALIZED: "010:500",
|
|
97
|
+
CONFIG_ERROR: "012:500"
|
|
98
|
+
};
|
|
99
|
+
var HaiAuditError = core.error.buildHaiErrorsDef("audit", AuditErrorInfo);
|
|
100
|
+
|
|
101
|
+
// src/audit-repository-log.ts
|
|
102
|
+
var logger = core.logger.child({ module: "audit", scope: "repository" });
|
|
103
|
+
var AUDIT_TABLE = "hai_audit_logs";
|
|
104
|
+
var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
105
|
+
repoConfig;
|
|
106
|
+
isSqlite;
|
|
107
|
+
/**
|
|
108
|
+
* @param config - 仓库配置(用户表映射)
|
|
109
|
+
*/
|
|
110
|
+
constructor(config) {
|
|
111
|
+
super(reldb, {
|
|
112
|
+
table: AUDIT_TABLE,
|
|
113
|
+
idColumn: "id",
|
|
114
|
+
generateId: () => core.id.withPrefix("audit_"),
|
|
115
|
+
fields: [
|
|
116
|
+
{ fieldName: "id", columnName: "id", def: { type: "TEXT", primaryKey: true }, select: true, create: true, update: false },
|
|
117
|
+
{ fieldName: "userId", columnName: "user_id", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
118
|
+
{ fieldName: "action", columnName: "action", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
|
|
119
|
+
{ fieldName: "resource", columnName: "resource", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
|
|
120
|
+
{ fieldName: "resourceId", columnName: "resource_id", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
121
|
+
{ fieldName: "details", columnName: "details", def: { type: "JSON" }, select: true, create: true, update: false },
|
|
122
|
+
{ fieldName: "ipAddress", columnName: "ip_address", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
123
|
+
{ fieldName: "userAgent", columnName: "user_agent", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
124
|
+
{ fieldName: "createdAt", columnName: "created_at", def: { type: "TIMESTAMP" }, select: true, create: true, update: false }
|
|
125
|
+
]
|
|
126
|
+
});
|
|
127
|
+
this.repoConfig = config;
|
|
128
|
+
this.isSqlite = this.db.config?.type === "sqlite";
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 将 Date 转为适合当前数据库类型的 SQL 参数
|
|
132
|
+
*
|
|
133
|
+
* SQLite 存储 TIMESTAMP 为毫秒时间戳,其他数据库使用 ISO 字符串。
|
|
134
|
+
*
|
|
135
|
+
* @param date - 要转换的日期
|
|
136
|
+
* @returns SQLite 返回毫秒时间戳(number),其他返回 ISO 字符串
|
|
137
|
+
*/
|
|
138
|
+
toDateParam(date) {
|
|
139
|
+
return this.isSqlite ? date.getTime() : date.toISOString();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 记录一条审计日志
|
|
143
|
+
*
|
|
144
|
+
* @param input - 日志内容
|
|
145
|
+
* @param input.userId - 操作用户 ID(系统操作可省略)
|
|
146
|
+
* @param input.action - 操作类型(如 login / create)
|
|
147
|
+
* @param input.resource - 资源类型(如 auth / users)
|
|
148
|
+
* @param input.resourceId - 资源 ID(可选)
|
|
149
|
+
* @param input.details - 操作详情对象(可选)
|
|
150
|
+
* @param input.ipAddress - 客户端 IP(可选)
|
|
151
|
+
* @param input.userAgent - 客户端 User-Agent(可选)
|
|
152
|
+
* @returns 成功时返回创建的 AuditLog;失败时返回 LOG_FAILED
|
|
153
|
+
*/
|
|
154
|
+
async log(input) {
|
|
155
|
+
logger.debug("Recording audit log", { action: input.action, resource: input.resource });
|
|
156
|
+
const id = core.id.withPrefix("audit_");
|
|
157
|
+
const now = /* @__PURE__ */ new Date();
|
|
158
|
+
const data = {
|
|
159
|
+
id,
|
|
160
|
+
userId: input.userId ?? null,
|
|
161
|
+
action: input.action,
|
|
162
|
+
resource: input.resource,
|
|
163
|
+
resourceId: input.resourceId ?? null,
|
|
164
|
+
details: input.details ?? null,
|
|
165
|
+
ipAddress: input.ipAddress ?? null,
|
|
166
|
+
userAgent: input.userAgent ?? null,
|
|
167
|
+
createdAt: now
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
const createResult = await this.create(data);
|
|
171
|
+
if (!createResult.success) {
|
|
172
|
+
logger.error("Failed to record audit log", { action: input.action, error: createResult.error.message });
|
|
173
|
+
return err(
|
|
174
|
+
HaiAuditError.LOG_FAILED,
|
|
175
|
+
auditM("audit_logFailed", { params: { error: createResult.error.message } }),
|
|
176
|
+
createResult.error
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const auditLog = {
|
|
180
|
+
id,
|
|
181
|
+
userId: input.userId ?? null,
|
|
182
|
+
action: input.action,
|
|
183
|
+
resource: input.resource,
|
|
184
|
+
resourceId: input.resourceId ?? null,
|
|
185
|
+
details: input.details ? JSON.stringify(input.details) : null,
|
|
186
|
+
ipAddress: input.ipAddress ?? null,
|
|
187
|
+
userAgent: input.userAgent ?? null,
|
|
188
|
+
createdAt: now
|
|
189
|
+
};
|
|
190
|
+
logger.info("Audit log recorded", { id, action: input.action, resource: input.resource });
|
|
191
|
+
return ok(auditLog);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.error("Failed to record audit log", { action: input.action, error });
|
|
194
|
+
return err(
|
|
195
|
+
HaiAuditError.LOG_FAILED,
|
|
196
|
+
auditM("audit_logFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
197
|
+
error
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 分页查询日志列表(含用户名 LEFT JOIN)
|
|
203
|
+
*
|
|
204
|
+
* @param options - 过滤条件与分页参数
|
|
205
|
+
* @returns 成功时返回 { items, total };失败时返回 QUERY_FAILED
|
|
206
|
+
*/
|
|
207
|
+
async listWithUser(options = {}) {
|
|
208
|
+
logger.debug("Querying audit logs", { userId: options.userId, action: options.action, resource: options.resource });
|
|
209
|
+
const conditions = [];
|
|
210
|
+
const params = [];
|
|
211
|
+
const { userTable, userIdColumn, userNameColumn } = this.repoConfig;
|
|
212
|
+
if (options.userId) {
|
|
213
|
+
conditions.push("a.user_id = ?");
|
|
214
|
+
params.push(options.userId);
|
|
215
|
+
}
|
|
216
|
+
if (options.action) {
|
|
217
|
+
conditions.push("a.action = ?");
|
|
218
|
+
params.push(options.action);
|
|
219
|
+
}
|
|
220
|
+
if (options.resource) {
|
|
221
|
+
conditions.push("a.resource = ?");
|
|
222
|
+
params.push(options.resource);
|
|
223
|
+
}
|
|
224
|
+
if (options.startDate) {
|
|
225
|
+
conditions.push("a.created_at >= ?");
|
|
226
|
+
params.push(this.toDateParam(options.startDate));
|
|
227
|
+
}
|
|
228
|
+
if (options.endDate) {
|
|
229
|
+
conditions.push("a.created_at <= ?");
|
|
230
|
+
params.push(this.toDateParam(options.endDate));
|
|
231
|
+
}
|
|
232
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
233
|
+
try {
|
|
234
|
+
const result = await this.sql().queryPage({
|
|
235
|
+
sql: `SELECT a.id, a.user_id AS userId, a.action, a.resource, a.resource_id AS resourceId,
|
|
236
|
+
a.details, a.ip_address AS ipAddress, a.user_agent AS userAgent,
|
|
237
|
+
a.created_at AS createdAt, u.${userNameColumn} AS username
|
|
238
|
+
FROM ${AUDIT_TABLE} a
|
|
239
|
+
LEFT JOIN ${userTable} u ON a.user_id = u.${userIdColumn}
|
|
240
|
+
${whereClause}
|
|
241
|
+
ORDER BY a.created_at DESC`,
|
|
242
|
+
params,
|
|
243
|
+
pagination: { page: options.page, pageSize: options.pageSize },
|
|
244
|
+
overrides: { defaultPageSize: 20 }
|
|
245
|
+
});
|
|
246
|
+
if (!result.success) {
|
|
247
|
+
logger.error("Failed to query audit logs", { error: result.error.message });
|
|
248
|
+
return err(
|
|
249
|
+
HaiAuditError.QUERY_FAILED,
|
|
250
|
+
auditM("audit_queryFailed", { params: { error: result.error.message } }),
|
|
251
|
+
result.error
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return ok({ items: result.data.items, total: result.data.total });
|
|
255
|
+
} catch (error) {
|
|
256
|
+
logger.error("Failed to query audit logs", { error });
|
|
257
|
+
return err(
|
|
258
|
+
HaiAuditError.QUERY_FAILED,
|
|
259
|
+
auditM("audit_queryFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
260
|
+
error
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* 获取指定用户的最近活动
|
|
266
|
+
*
|
|
267
|
+
* @param userId - 用户 ID
|
|
268
|
+
* @param limit - 最大返回条数,默认 10
|
|
269
|
+
* @returns 成功时返回 AuditLog 数组(按时间倒序);失败时返回 QUERY_FAILED
|
|
270
|
+
*/
|
|
271
|
+
async getUserRecent(userId, limit = 10) {
|
|
272
|
+
logger.debug("Getting user recent activity", { userId, limit });
|
|
273
|
+
try {
|
|
274
|
+
const result = await this.findAll({
|
|
275
|
+
where: "user_id = ?",
|
|
276
|
+
params: [userId],
|
|
277
|
+
orderBy: "created_at DESC",
|
|
278
|
+
limit
|
|
279
|
+
});
|
|
280
|
+
if (!result.success) {
|
|
281
|
+
return err(
|
|
282
|
+
HaiAuditError.QUERY_FAILED,
|
|
283
|
+
auditM("audit_queryFailed", { params: { error: result.error.message } }),
|
|
284
|
+
result.error
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return ok(result.data);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return err(
|
|
290
|
+
HaiAuditError.QUERY_FAILED,
|
|
291
|
+
auditM("audit_queryFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
292
|
+
error
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 清理指定天数之前的旧日志
|
|
298
|
+
*
|
|
299
|
+
* @param olderThanDays - 保留天数,默认 90;清理此天数之前的日志
|
|
300
|
+
* @returns 成功时返回删除的记录数;失败时返回 CLEANUP_FAILED
|
|
301
|
+
*/
|
|
302
|
+
async cleanupOld(olderThanDays = 90) {
|
|
303
|
+
logger.debug("Cleaning up old audit logs", { olderThanDays });
|
|
304
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
305
|
+
cutoff.setDate(cutoff.getDate() - olderThanDays);
|
|
306
|
+
try {
|
|
307
|
+
const result = await this.sql().execute(
|
|
308
|
+
`DELETE FROM ${AUDIT_TABLE} WHERE created_at < ?`,
|
|
309
|
+
[this.toDateParam(cutoff)]
|
|
310
|
+
);
|
|
311
|
+
if (!result.success) {
|
|
312
|
+
logger.error("Failed to cleanup audit logs", { error: result.error.message });
|
|
313
|
+
return err(
|
|
314
|
+
HaiAuditError.CLEANUP_FAILED,
|
|
315
|
+
auditM("audit_cleanupFailed", { params: { error: result.error.message } }),
|
|
316
|
+
result.error
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const deleted = result.data.changes;
|
|
320
|
+
logger.info("Audit logs cleaned up", { olderThanDays, deleted });
|
|
321
|
+
return ok(deleted);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
logger.error("Failed to cleanup audit logs", { error });
|
|
324
|
+
return err(
|
|
325
|
+
HaiAuditError.CLEANUP_FAILED,
|
|
326
|
+
auditM("audit_cleanupFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
327
|
+
error
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 获取指定天数内的操作统计(按 action 分组计数)
|
|
333
|
+
*
|
|
334
|
+
* @param days - 统计天数,默认 7
|
|
335
|
+
* @returns 成功时返回 AuditStatItem 数组(按 count 倒序);失败时返回 STATS_FAILED
|
|
336
|
+
*/
|
|
337
|
+
async getStats(days = 7) {
|
|
338
|
+
logger.debug("Getting audit statistics", { days });
|
|
339
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
340
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
341
|
+
try {
|
|
342
|
+
const result = await this.sql().query(
|
|
343
|
+
`SELECT action, COUNT(*) as count
|
|
344
|
+
FROM ${AUDIT_TABLE}
|
|
345
|
+
WHERE created_at >= ?
|
|
346
|
+
GROUP BY action
|
|
347
|
+
ORDER BY count DESC`,
|
|
348
|
+
[this.toDateParam(cutoff)]
|
|
349
|
+
);
|
|
350
|
+
if (!result.success) {
|
|
351
|
+
logger.error("Failed to query audit statistics", { error: result.error.message });
|
|
352
|
+
return err(
|
|
353
|
+
HaiAuditError.STATS_FAILED,
|
|
354
|
+
auditM("audit_statsFailed", { params: { error: result.error.message } }),
|
|
355
|
+
result.error
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return ok(result.data.map((item) => ({ action: item.action, count: Number(item.count) })));
|
|
359
|
+
} catch (error) {
|
|
360
|
+
logger.error("Failed to query audit statistics", { error });
|
|
361
|
+
return err(
|
|
362
|
+
HaiAuditError.STATS_FAILED,
|
|
363
|
+
auditM("audit_statsFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
364
|
+
error
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// src/audit-main.ts
|
|
371
|
+
var logger2 = core.logger.child({ module: "audit", scope: "main" });
|
|
372
|
+
var MAX_TEXT_LENGTH = 256;
|
|
373
|
+
function isNonEmptyString(value) {
|
|
374
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
375
|
+
}
|
|
376
|
+
function isValidTextLength(value, maxLength = MAX_TEXT_LENGTH) {
|
|
377
|
+
return value.length <= maxLength;
|
|
378
|
+
}
|
|
379
|
+
var currentRepo = null;
|
|
380
|
+
var currentHelper = null;
|
|
381
|
+
var initInProgress = false;
|
|
382
|
+
var notInitialized = core.module.createNotInitializedKit(
|
|
383
|
+
HaiAuditError.NOT_INITIALIZED,
|
|
384
|
+
() => auditM("audit_notInitialized")
|
|
385
|
+
);
|
|
386
|
+
var notInitializedHelper = notInitialized.proxy();
|
|
387
|
+
var audit = {
|
|
388
|
+
/**
|
|
389
|
+
* 初始化审计模块
|
|
390
|
+
*
|
|
391
|
+
* 会先关闭已有实例(如已初始化),再用新配置重新初始化。
|
|
392
|
+
* 内部创建 AuditLogRepository 实例(BaseReldbCrudRepository 自动建表)。
|
|
393
|
+
* 依赖 @h-ai/reldb 已初始化。
|
|
394
|
+
*
|
|
395
|
+
* @param config - 初始化配置(可选,所有字段均有默认值)
|
|
396
|
+
* @returns 成功时返回 ok(undefined);失败时返回 CONFIG_ERROR
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```ts
|
|
400
|
+
* const result = await audit.init()
|
|
401
|
+
* if (!result.success) {
|
|
402
|
+
* logger.error('Audit init failed', { error: result.error.message })
|
|
403
|
+
* }
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
async init(config) {
|
|
407
|
+
if (initInProgress) {
|
|
408
|
+
logger2.warn("Audit init already in progress, skipping concurrent call");
|
|
409
|
+
return err(
|
|
410
|
+
HaiAuditError.INIT_IN_PROGRESS,
|
|
411
|
+
auditM("audit_initInProgress")
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
initInProgress = true;
|
|
415
|
+
try {
|
|
416
|
+
if (currentRepo) {
|
|
417
|
+
logger2.warn("Audit module is already initialized, reinitializing");
|
|
418
|
+
await audit.close();
|
|
419
|
+
}
|
|
420
|
+
logger2.info("Initializing audit module");
|
|
421
|
+
const parseResult = AuditInitConfigSchema.safeParse(config ?? {});
|
|
422
|
+
if (!parseResult.success) {
|
|
423
|
+
logger2.error("Audit config validation failed", { error: parseResult.error.message });
|
|
424
|
+
return err(
|
|
425
|
+
HaiAuditError.CONFIG_ERROR,
|
|
426
|
+
auditM("audit_configError", { params: { error: parseResult.error.message } }),
|
|
427
|
+
parseResult.error
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const parsed = parseResult.data;
|
|
431
|
+
const identifierResult = validateIdentifiers([parsed.userTable, parsed.userIdColumn, parsed.userNameColumn]);
|
|
432
|
+
if (!identifierResult.success) {
|
|
433
|
+
logger2.error("Audit config contains invalid identifiers", { error: identifierResult.error.message });
|
|
434
|
+
return err(
|
|
435
|
+
HaiAuditError.CONFIG_ERROR,
|
|
436
|
+
auditM("audit_configError", { params: { error: identifierResult.error.message } }),
|
|
437
|
+
identifierResult.error
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
currentRepo = new AuditLogRepository({
|
|
441
|
+
userTable: parsed.userTable,
|
|
442
|
+
userIdColumn: parsed.userIdColumn,
|
|
443
|
+
userNameColumn: parsed.userNameColumn
|
|
444
|
+
});
|
|
445
|
+
currentHelper = createHelper((input) => currentRepo.log(input));
|
|
446
|
+
logger2.info("Audit module initialized");
|
|
447
|
+
return ok(void 0);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
logger2.error("Audit module initialization failed", { error });
|
|
450
|
+
return err(
|
|
451
|
+
HaiAuditError.CONFIG_ERROR,
|
|
452
|
+
auditM("audit_initFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
453
|
+
error
|
|
454
|
+
);
|
|
455
|
+
} finally {
|
|
456
|
+
initInProgress = false;
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
/** 当前是否已初始化 */
|
|
460
|
+
get isInitialized() {
|
|
461
|
+
return currentRepo !== null;
|
|
462
|
+
},
|
|
463
|
+
/**
|
|
464
|
+
* 记录一条审计日志
|
|
465
|
+
*
|
|
466
|
+
* @param input - 日志内容(action 和 resource 为必填)
|
|
467
|
+
* @returns 成功时返回创建的 AuditLog;未初始化时返回 NOT_INITIALIZED
|
|
468
|
+
*/
|
|
469
|
+
log(input) {
|
|
470
|
+
if (!currentRepo) {
|
|
471
|
+
return Promise.resolve(notInitialized.result());
|
|
472
|
+
}
|
|
473
|
+
if (!isNonEmptyString(input.action) || !isNonEmptyString(input.resource)) {
|
|
474
|
+
return Promise.resolve(err(
|
|
475
|
+
HaiAuditError.LOG_FAILED,
|
|
476
|
+
auditM("audit_invalidInput", { params: { field: "action/resource", reason: "must be non-empty string" } })
|
|
477
|
+
));
|
|
478
|
+
}
|
|
479
|
+
if (!isValidTextLength(input.action) || !isValidTextLength(input.resource)) {
|
|
480
|
+
return Promise.resolve(err(
|
|
481
|
+
HaiAuditError.LOG_FAILED,
|
|
482
|
+
auditM("audit_invalidInput", { params: { field: "action/resource", reason: "exceeds max length" } })
|
|
483
|
+
));
|
|
484
|
+
}
|
|
485
|
+
return currentRepo.log(input);
|
|
486
|
+
},
|
|
487
|
+
/**
|
|
488
|
+
* 分页查询审计日志列表(含用户名 LEFT JOIN)
|
|
489
|
+
*
|
|
490
|
+
* @param options - 过滤条件与分页参数
|
|
491
|
+
* @returns 成功时返回 { items, total };未初始化时返回 NOT_INITIALIZED
|
|
492
|
+
*/
|
|
493
|
+
list(options) {
|
|
494
|
+
if (!currentRepo) {
|
|
495
|
+
return Promise.resolve(notInitialized.result());
|
|
496
|
+
}
|
|
497
|
+
if (options?.startDate && options?.endDate && options.startDate > options.endDate) {
|
|
498
|
+
return Promise.resolve(err(
|
|
499
|
+
HaiAuditError.QUERY_FAILED,
|
|
500
|
+
auditM("audit_invalidInput", { params: { field: "dateRange", reason: "startDate must be before endDate" } })
|
|
501
|
+
));
|
|
502
|
+
}
|
|
503
|
+
return currentRepo.listWithUser(options);
|
|
504
|
+
},
|
|
505
|
+
/**
|
|
506
|
+
* 获取指定用户的最近活动记录
|
|
507
|
+
*
|
|
508
|
+
* @param userId - 用户 ID
|
|
509
|
+
* @param limit - 最大返回条数,默认 10
|
|
510
|
+
* @returns 成功时返回 AuditLog 数组;未初始化时返回 NOT_INITIALIZED
|
|
511
|
+
*/
|
|
512
|
+
getUserRecent(userId, limit) {
|
|
513
|
+
if (!currentRepo) {
|
|
514
|
+
return Promise.resolve(notInitialized.result());
|
|
515
|
+
}
|
|
516
|
+
if (!isNonEmptyString(userId)) {
|
|
517
|
+
return Promise.resolve(err(
|
|
518
|
+
HaiAuditError.QUERY_FAILED,
|
|
519
|
+
auditM("audit_invalidInput", { params: { field: "userId", reason: "must be non-empty string" } })
|
|
520
|
+
));
|
|
521
|
+
}
|
|
522
|
+
if (typeof limit === "number" && (!Number.isInteger(limit) || limit <= 0)) {
|
|
523
|
+
return Promise.resolve(err(
|
|
524
|
+
HaiAuditError.QUERY_FAILED,
|
|
525
|
+
auditM("audit_invalidInput", { params: { field: "limit", reason: "must be a positive integer" } })
|
|
526
|
+
));
|
|
527
|
+
}
|
|
528
|
+
return currentRepo.getUserRecent(userId, limit);
|
|
529
|
+
},
|
|
530
|
+
/**
|
|
531
|
+
* 清理指定天数之前的旧日志
|
|
532
|
+
*
|
|
533
|
+
* @param olderThanDays - 保留天数,默认 90
|
|
534
|
+
* @returns 成功时返回删除的记录数;未初始化时返回 NOT_INITIALIZED
|
|
535
|
+
*/
|
|
536
|
+
cleanup(olderThanDays) {
|
|
537
|
+
if (!currentRepo) {
|
|
538
|
+
return Promise.resolve(notInitialized.result());
|
|
539
|
+
}
|
|
540
|
+
if (typeof olderThanDays === "number" && (!Number.isInteger(olderThanDays) || olderThanDays < 0)) {
|
|
541
|
+
return Promise.resolve(err(
|
|
542
|
+
HaiAuditError.CLEANUP_FAILED,
|
|
543
|
+
auditM("audit_invalidInput", { params: { field: "olderThanDays", reason: "must be a non-negative integer" } })
|
|
544
|
+
));
|
|
545
|
+
}
|
|
546
|
+
return currentRepo.cleanupOld(olderThanDays);
|
|
547
|
+
},
|
|
548
|
+
/**
|
|
549
|
+
* 获取指定天数内的操作统计(按 action 分组计数)
|
|
550
|
+
*
|
|
551
|
+
* @param days - 统计天数,默认 7
|
|
552
|
+
* @returns 成功时返回 AuditStatItem 数组;未初始化时返回 NOT_INITIALIZED
|
|
553
|
+
*/
|
|
554
|
+
getStats(days) {
|
|
555
|
+
if (!currentRepo) {
|
|
556
|
+
return Promise.resolve(notInitialized.result());
|
|
557
|
+
}
|
|
558
|
+
if (typeof days === "number" && (!Number.isInteger(days) || days < 0)) {
|
|
559
|
+
return Promise.resolve(err(
|
|
560
|
+
HaiAuditError.STATS_FAILED,
|
|
561
|
+
auditM("audit_invalidInput", { params: { field: "days", reason: "must be a non-negative integer" } })
|
|
562
|
+
));
|
|
563
|
+
}
|
|
564
|
+
return currentRepo.getStats(days);
|
|
565
|
+
},
|
|
566
|
+
/**
|
|
567
|
+
* 便捷记录器
|
|
568
|
+
*
|
|
569
|
+
* 未初始化时调用任意方法均返回 NOT_INITIALIZED 错误。
|
|
570
|
+
*/
|
|
571
|
+
get helper() {
|
|
572
|
+
if (!currentHelper) {
|
|
573
|
+
return notInitializedHelper;
|
|
574
|
+
}
|
|
575
|
+
return currentHelper;
|
|
576
|
+
},
|
|
577
|
+
/**
|
|
578
|
+
* 关闭审计模块,释放内部状态
|
|
579
|
+
*/
|
|
580
|
+
async close() {
|
|
581
|
+
if (!currentRepo) {
|
|
582
|
+
logger2.info("Audit module already closed, skipping");
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
logger2.info("Closing audit module");
|
|
586
|
+
currentRepo = null;
|
|
587
|
+
currentHelper = null;
|
|
588
|
+
logger2.info("Audit module closed");
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
export { AuditInitConfigSchema, HaiAuditError, audit, createHelper };
|
|
593
|
+
//# sourceMappingURL=index.js.map
|
|
594
|
+
//# sourceMappingURL=index.js.map
|