@oneuptime/common 10.0.66 → 10.0.68

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.
Files changed (132) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +370 -0
  2. package/Models/DatabaseModels/Alert.ts +2 -0
  3. package/Models/DatabaseModels/ApiKey.ts +2 -0
  4. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +3 -0
  5. package/Models/DatabaseModels/GlobalConfig.ts +56 -0
  6. package/Models/DatabaseModels/Incident.ts +2 -0
  7. package/Models/DatabaseModels/Index.ts +4 -0
  8. package/Models/DatabaseModels/Label.ts +2 -0
  9. package/Models/DatabaseModels/Monitor.ts +2 -0
  10. package/Models/DatabaseModels/OnCallDutyPolicy.ts +2 -0
  11. package/Models/DatabaseModels/Project.ts +110 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +2 -0
  13. package/Models/DatabaseModels/StatusPage.ts +2 -0
  14. package/Models/DatabaseModels/Team.ts +2 -0
  15. package/Models/DatabaseModels/TelegramLog.ts +1025 -0
  16. package/Models/DatabaseModels/UserNotificationRule.ts +49 -0
  17. package/Models/DatabaseModels/UserNotificationSetting.ts +17 -0
  18. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +48 -0
  19. package/Models/DatabaseModels/UserTelegram.ts +312 -0
  20. package/Server/API/UserTelegramAPI.ts +167 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1776761171349-MigrationName.ts +325 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1776801030808-MigrationName.ts +35 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  24. package/Server/Services/AuditLogService.ts +574 -0
  25. package/Server/Services/DatabaseService.ts +71 -0
  26. package/Server/Services/Index.ts +8 -0
  27. package/Server/Services/TelegramLogService.ts +15 -0
  28. package/Server/Services/TelegramService.ts +139 -0
  29. package/Server/Services/UserNotificationRuleService.ts +350 -1
  30. package/Server/Services/UserNotificationSettingService.ts +114 -0
  31. package/Server/Services/UserTelegramService.ts +140 -0
  32. package/Server/Utils/Monitor/MonitorResource.ts +29 -15
  33. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +29 -16
  34. package/Tests/Types/Date.test.ts +158 -0
  35. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +1 -0
  36. package/Types/AuditLog/AuditLogAction.ts +7 -0
  37. package/Types/BaseDatabase/EnableAuditLogOn.ts +5 -0
  38. package/Types/Database/EnableAuditLog.ts +18 -0
  39. package/Types/Date.ts +12 -3
  40. package/Types/Icon/IconProp.ts +1 -0
  41. package/Types/Permission.ts +24 -0
  42. package/Types/Telegram/TelegramMessage.ts +9 -0
  43. package/Types/TelegramStatus.ts +14 -0
  44. package/UI/Components/Icon/Icon.tsx +15 -0
  45. package/build/dist/Models/AnalyticsModels/AuditLog.js +337 -0
  46. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -0
  47. package/build/dist/Models/DatabaseModels/Alert.js +2 -0
  48. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  49. package/build/dist/Models/DatabaseModels/ApiKey.js +2 -0
  50. package/build/dist/Models/DatabaseModels/ApiKey.js.map +1 -1
  51. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  52. package/build/dist/Models/DatabaseModels/GlobalConfig.js +59 -0
  53. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  55. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  56. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  57. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  58. package/build/dist/Models/DatabaseModels/Label.js +2 -0
  59. package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
  60. package/build/dist/Models/DatabaseModels/Monitor.js +2 -0
  61. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  62. package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js +2 -0
  63. package/build/dist/Models/DatabaseModels/OnCallDutyPolicy.js.map +1 -1
  64. package/build/dist/Models/DatabaseModels/Project.js +114 -0
  65. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  66. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +2 -0
  67. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/StatusPage.js +2 -0
  69. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  70. package/build/dist/Models/DatabaseModels/Team.js +2 -0
  71. package/build/dist/Models/DatabaseModels/Team.js.map +1 -1
  72. package/build/dist/Models/DatabaseModels/TelegramLog.js +1056 -0
  73. package/build/dist/Models/DatabaseModels/TelegramLog.js.map +1 -0
  74. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +49 -0
  75. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  76. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +19 -0
  77. package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
  78. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +48 -0
  79. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  80. package/build/dist/Models/DatabaseModels/UserTelegram.js +331 -0
  81. package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -0
  82. package/build/dist/Server/API/UserTelegramAPI.js +99 -0
  83. package/build/dist/Server/API/UserTelegramAPI.js.map +1 -0
  84. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776761171349-MigrationName.js +116 -0
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776761171349-MigrationName.js.map +1 -0
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776801030808-MigrationName.js +18 -0
  87. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776801030808-MigrationName.js.map +1 -0
  88. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  89. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  90. package/build/dist/Server/Services/AuditLogService.js +402 -0
  91. package/build/dist/Server/Services/AuditLogService.js.map +1 -0
  92. package/build/dist/Server/Services/DatabaseService.js +68 -8
  93. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  94. package/build/dist/Server/Services/Index.js +8 -0
  95. package/build/dist/Server/Services/Index.js.map +1 -1
  96. package/build/dist/Server/Services/TelegramLogService.js +13 -0
  97. package/build/dist/Server/Services/TelegramLogService.js.map +1 -0
  98. package/build/dist/Server/Services/TelegramService.js +100 -0
  99. package/build/dist/Server/Services/TelegramService.js.map +1 -0
  100. package/build/dist/Server/Services/UserNotificationRuleService.js +272 -21
  101. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  102. package/build/dist/Server/Services/UserNotificationSettingService.js +94 -0
  103. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  104. package/build/dist/Server/Services/UserTelegramService.js +133 -0
  105. package/build/dist/Server/Services/UserTelegramService.js.map +1 -0
  106. package/build/dist/Server/Utils/Monitor/MonitorResource.js +25 -12
  107. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  108. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +24 -12
  109. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  110. package/build/dist/Tests/Types/Date.test.js +96 -0
  111. package/build/dist/Tests/Types/Date.test.js.map +1 -1
  112. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +1 -0
  113. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  114. package/build/dist/Types/AuditLog/AuditLogAction.js +8 -0
  115. package/build/dist/Types/AuditLog/AuditLogAction.js.map +1 -0
  116. package/build/dist/Types/BaseDatabase/EnableAuditLogOn.js +2 -0
  117. package/build/dist/Types/BaseDatabase/EnableAuditLogOn.js.map +1 -0
  118. package/build/dist/Types/Database/EnableAuditLog.js +15 -0
  119. package/build/dist/Types/Database/EnableAuditLog.js.map +1 -0
  120. package/build/dist/Types/Date.js +9 -3
  121. package/build/dist/Types/Date.js.map +1 -1
  122. package/build/dist/Types/Icon/IconProp.js +1 -0
  123. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  124. package/build/dist/Types/Permission.js +21 -0
  125. package/build/dist/Types/Permission.js.map +1 -1
  126. package/build/dist/Types/Telegram/TelegramMessage.js +2 -0
  127. package/build/dist/Types/Telegram/TelegramMessage.js.map +1 -0
  128. package/build/dist/Types/TelegramStatus.js +15 -0
  129. package/build/dist/Types/TelegramStatus.js.map +1 -0
  130. package/build/dist/UI/Components/Icon/Icon.js +5 -0
  131. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  132. package/package.json +2 -2
@@ -0,0 +1,574 @@
1
+ import ClickhouseDatabase from "../Infrastructure/ClickhouseDatabase";
2
+ import AnalyticsDatabaseService from "./AnalyticsDatabaseService";
3
+ import ProjectService from "./ProjectService";
4
+ import { IsBillingEnabled, IsEnterpriseEdition } from "../EnvironmentConfig";
5
+ import logger from "../Utils/Logger";
6
+ import AuditLog from "../../Models/AnalyticsModels/AuditLog";
7
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
8
+ import Project from "../../Models/DatabaseModels/Project";
9
+ import User from "../../Models/DatabaseModels/User";
10
+ import AuditLogAction from "../../Types/AuditLog/AuditLogAction";
11
+ import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
12
+ import { PlanType } from "../../Types/Billing/SubscriptionPlan";
13
+ import { getColumnAccessControlForAllColumns } from "../../Types/Database/AccessControl/ColumnAccessControl";
14
+ import { JSONArray, JSONObject } from "../../Types/JSON";
15
+ import ObjectID from "../../Types/ObjectID";
16
+ import UserType from "../../Types/UserType";
17
+ import UserService from "./UserService";
18
+ import OneUptimeDate from "../../Types/Date";
19
+
20
+ const PROJECT_SETTINGS_CACHE_TTL_MS: number = 60 * 1000;
21
+ const USER_CACHE_TTL_MS: number = 5 * 60 * 1000;
22
+
23
+ const SKIPPED_FIELDS: ReadonlySet<string> = new Set<string>([
24
+ "_id",
25
+ "id",
26
+ "createdAt",
27
+ "updatedAt",
28
+ "deletedAt",
29
+ "version",
30
+ "slug",
31
+ ]);
32
+
33
+ const NAME_CANDIDATE_FIELDS: ReadonlyArray<string> = [
34
+ "name",
35
+ "title",
36
+ "displayName",
37
+ ];
38
+
39
+ interface CachedProjectSettings {
40
+ enableAuditLogs: boolean;
41
+ retentionInDays: number;
42
+ planName: PlanType | undefined;
43
+ expiresAt: number;
44
+ }
45
+
46
+ interface CachedUser {
47
+ name: string | null;
48
+ email: string | null;
49
+ expiresAt: number;
50
+ }
51
+
52
+ export class AuditLogService extends AnalyticsDatabaseService<AuditLog> {
53
+ private projectSettingsCache: Map<string, CachedProjectSettings> = new Map();
54
+ private userCache: Map<string, CachedUser> = new Map();
55
+
56
+ public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
57
+ super({ modelType: AuditLog, database: clickhouseDatabase });
58
+ }
59
+
60
+ public invalidateProjectSettings(projectId: ObjectID): void {
61
+ this.projectSettingsCache.delete(projectId.toString());
62
+ }
63
+
64
+ public async recordCreate<TModel extends BaseModel>(data: {
65
+ model: TModel;
66
+ createdItem: TModel;
67
+ props: DatabaseCommonInteractionProps;
68
+ }): Promise<void> {
69
+ try {
70
+ const projectId: ObjectID | undefined = this.resolveProjectId(
71
+ data.model,
72
+ data.createdItem,
73
+ data.props,
74
+ );
75
+
76
+ if (!projectId) {
77
+ return;
78
+ }
79
+
80
+ const settings: CachedProjectSettings | null =
81
+ await this.getProjectSettings(projectId);
82
+
83
+ if (!this.isEligible(settings, data.props)) {
84
+ return;
85
+ }
86
+
87
+ const redactedFields: Set<string> = this.getRedactedFields(data.model);
88
+ const changes: JSONArray = this.buildSnapshotChanges({
89
+ model: data.createdItem,
90
+ redactedFields,
91
+ valueKey: "newValue",
92
+ });
93
+
94
+ await this.insert({
95
+ projectId,
96
+ resourceType: this.getResourceType(data.model),
97
+ resourceId: data.createdItem.id ?? null,
98
+ resourceName: this.getResourceName(data.createdItem),
99
+ action: AuditLogAction.Create,
100
+ changes,
101
+ props: data.props,
102
+ retentionInDays: settings!.retentionInDays,
103
+ });
104
+ } catch (err) {
105
+ logger.warn("AuditLog: failed to record create event");
106
+ logger.warn(err);
107
+ }
108
+ }
109
+
110
+ public async recordUpdate<TModel extends BaseModel>(data: {
111
+ model: TModel;
112
+ before: TModel;
113
+ updatedFields: JSONObject;
114
+ itemId: ObjectID;
115
+ props: DatabaseCommonInteractionProps;
116
+ }): Promise<void> {
117
+ try {
118
+ const projectId: ObjectID | undefined = this.resolveProjectId(
119
+ data.model,
120
+ data.before,
121
+ data.props,
122
+ );
123
+
124
+ if (!projectId) {
125
+ return;
126
+ }
127
+
128
+ const settings: CachedProjectSettings | null =
129
+ await this.getProjectSettings(projectId);
130
+
131
+ if (!this.isEligible(settings, data.props)) {
132
+ return;
133
+ }
134
+
135
+ const redactedFields: Set<string> = this.getRedactedFields(data.model);
136
+ const changes: JSONArray = this.buildUpdateDiff({
137
+ before: data.before,
138
+ updatedFields: data.updatedFields,
139
+ redactedFields,
140
+ });
141
+
142
+ if (changes.length === 0) {
143
+ return;
144
+ }
145
+
146
+ await this.insert({
147
+ projectId,
148
+ resourceType: this.getResourceType(data.model),
149
+ resourceId: data.itemId,
150
+ resourceName: this.getResourceName(data.before),
151
+ action: AuditLogAction.Update,
152
+ changes,
153
+ props: data.props,
154
+ retentionInDays: settings!.retentionInDays,
155
+ });
156
+ } catch (err) {
157
+ logger.warn("AuditLog: failed to record update event");
158
+ logger.warn(err);
159
+ }
160
+ }
161
+
162
+ public async recordDelete<TModel extends BaseModel>(data: {
163
+ model: TModel;
164
+ deletedItem: TModel;
165
+ itemId: ObjectID;
166
+ props: DatabaseCommonInteractionProps;
167
+ }): Promise<void> {
168
+ try {
169
+ const projectId: ObjectID | undefined = this.resolveProjectId(
170
+ data.model,
171
+ data.deletedItem,
172
+ data.props,
173
+ );
174
+
175
+ if (!projectId) {
176
+ return;
177
+ }
178
+
179
+ const settings: CachedProjectSettings | null =
180
+ await this.getProjectSettings(projectId);
181
+
182
+ if (!this.isEligible(settings, data.props)) {
183
+ return;
184
+ }
185
+
186
+ const redactedFields: Set<string> = this.getRedactedFields(data.model);
187
+ const changes: JSONArray = this.buildSnapshotChanges({
188
+ model: data.deletedItem,
189
+ redactedFields,
190
+ valueKey: "oldValue",
191
+ });
192
+
193
+ await this.insert({
194
+ projectId,
195
+ resourceType: this.getResourceType(data.model),
196
+ resourceId: data.itemId,
197
+ resourceName: this.getResourceName(data.deletedItem),
198
+ action: AuditLogAction.Delete,
199
+ changes,
200
+ props: data.props,
201
+ retentionInDays: settings!.retentionInDays,
202
+ });
203
+ } catch (err) {
204
+ logger.warn("AuditLog: failed to record delete event");
205
+ logger.warn(err);
206
+ }
207
+ }
208
+
209
+ private async insert(params: {
210
+ projectId: ObjectID;
211
+ resourceType: string;
212
+ resourceId: ObjectID | null;
213
+ resourceName: string | null;
214
+ action: AuditLogAction;
215
+ changes: JSONArray;
216
+ props: DatabaseCommonInteractionProps;
217
+ retentionInDays: number;
218
+ }): Promise<void> {
219
+ const auditLog: AuditLog = new AuditLog();
220
+ auditLog.projectId = params.projectId;
221
+ auditLog.resourceType = params.resourceType;
222
+ if (params.resourceId) {
223
+ auditLog.resourceId = params.resourceId;
224
+ }
225
+ if (params.resourceName) {
226
+ auditLog.resourceName = params.resourceName;
227
+ }
228
+ auditLog.action = params.action;
229
+
230
+ const actor: {
231
+ userId: ObjectID | null;
232
+ userName: string | null;
233
+ userEmail: string | null;
234
+ userType: string | null;
235
+ } = await this.resolveActor(params.props);
236
+
237
+ if (actor.userId) {
238
+ auditLog.userId = actor.userId;
239
+ }
240
+ if (actor.userName) {
241
+ auditLog.userName = actor.userName;
242
+ }
243
+ if (actor.userEmail) {
244
+ auditLog.userEmail = actor.userEmail;
245
+ }
246
+ if (actor.userType) {
247
+ auditLog.userType = actor.userType;
248
+ }
249
+
250
+ auditLog.changes = params.changes;
251
+ auditLog.retentionDate = this.computeRetentionDate(params.retentionInDays);
252
+
253
+ await this.create({
254
+ data: auditLog,
255
+ props: { isRoot: true },
256
+ });
257
+ }
258
+
259
+ private computeRetentionDate(retentionInDays: number): Date {
260
+ const days: number = Math.max(1, Math.min(retentionInDays || 7, 180));
261
+ return OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), days);
262
+ }
263
+
264
+ private isEligible(
265
+ settings: CachedProjectSettings | null,
266
+ _props: DatabaseCommonInteractionProps,
267
+ ): boolean {
268
+ if (!settings) {
269
+ return false;
270
+ }
271
+
272
+ if (!settings.enableAuditLogs) {
273
+ return false;
274
+ }
275
+
276
+ if (IsEnterpriseEdition) {
277
+ return true;
278
+ }
279
+
280
+ if (IsBillingEnabled) {
281
+ return settings.planName === PlanType.Enterprise;
282
+ }
283
+
284
+ /*
285
+ * Neither enterprise edition nor billing is enabled — audit logs are not
286
+ * available on the free self-hosted build.
287
+ */
288
+ return false;
289
+ }
290
+
291
+ private async getProjectSettings(
292
+ projectId: ObjectID,
293
+ ): Promise<CachedProjectSettings | null> {
294
+ const key: string = projectId.toString();
295
+ const now: number = Date.now();
296
+ const cached: CachedProjectSettings | undefined =
297
+ this.projectSettingsCache.get(key);
298
+
299
+ if (cached && cached.expiresAt > now) {
300
+ return cached;
301
+ }
302
+
303
+ const project: Project | null = await ProjectService.findOneById({
304
+ id: projectId,
305
+ select: {
306
+ _id: true,
307
+ enableAuditLogs: true,
308
+ auditLogsRetentionInDays: true,
309
+ planName: true,
310
+ },
311
+ props: { isRoot: true },
312
+ });
313
+
314
+ if (!project) {
315
+ return null;
316
+ }
317
+
318
+ const settings: CachedProjectSettings = {
319
+ enableAuditLogs: Boolean(project.enableAuditLogs),
320
+ retentionInDays: project.auditLogsRetentionInDays ?? 7,
321
+ planName: project.planName,
322
+ expiresAt: now + PROJECT_SETTINGS_CACHE_TTL_MS,
323
+ };
324
+
325
+ this.projectSettingsCache.set(key, settings);
326
+ return settings;
327
+ }
328
+
329
+ private async resolveActor(props: DatabaseCommonInteractionProps): Promise<{
330
+ userId: ObjectID | null;
331
+ userName: string | null;
332
+ userEmail: string | null;
333
+ userType: string | null;
334
+ }> {
335
+ const userType: string | null = props.userType
336
+ ? String(props.userType)
337
+ : props.isRoot
338
+ ? "System"
339
+ : null;
340
+
341
+ if (!props.userId) {
342
+ return {
343
+ userId: null,
344
+ userName: null,
345
+ userEmail: null,
346
+ userType,
347
+ };
348
+ }
349
+
350
+ // API-key actions don't have a user record to look up.
351
+ if (props.userType === UserType.API) {
352
+ return {
353
+ userId: props.userId,
354
+ userName: null,
355
+ userEmail: null,
356
+ userType,
357
+ };
358
+ }
359
+
360
+ const cached: { name: string | null; email: string | null } | null =
361
+ await this.getUserInfo(props.userId);
362
+
363
+ return {
364
+ userId: props.userId,
365
+ userName: cached?.name ?? null,
366
+ userEmail: cached?.email ?? null,
367
+ userType,
368
+ };
369
+ }
370
+
371
+ private async getUserInfo(
372
+ userId: ObjectID,
373
+ ): Promise<{ name: string | null; email: string | null } | null> {
374
+ const key: string = userId.toString();
375
+ const now: number = Date.now();
376
+ const cached: CachedUser | undefined = this.userCache.get(key);
377
+
378
+ if (cached && cached.expiresAt > now) {
379
+ return { name: cached.name, email: cached.email };
380
+ }
381
+
382
+ const user: User | null = await UserService.findOneById({
383
+ id: userId,
384
+ select: { _id: true, name: true, email: true },
385
+ props: { isRoot: true },
386
+ });
387
+
388
+ const name: string | null = user?.name?.toString() ?? null;
389
+ const email: string | null = user?.email?.toString() ?? null;
390
+
391
+ this.userCache.set(key, {
392
+ name,
393
+ email,
394
+ expiresAt: now + USER_CACHE_TTL_MS,
395
+ });
396
+
397
+ return { name, email };
398
+ }
399
+
400
+ private resolveProjectId<TModel extends BaseModel>(
401
+ model: TModel,
402
+ item: TModel,
403
+ props: DatabaseCommonInteractionProps,
404
+ ): ObjectID | undefined {
405
+ if (props.tenantId) {
406
+ return props.tenantId;
407
+ }
408
+
409
+ const tenantColumn: string | null = model.getTenantColumn();
410
+ if (!tenantColumn) {
411
+ return undefined;
412
+ }
413
+
414
+ const value: ObjectID | undefined = item.getValue<ObjectID>(tenantColumn);
415
+ return value ?? undefined;
416
+ }
417
+
418
+ private getRedactedFields<TModel extends BaseModel>(
419
+ model: TModel,
420
+ ): Set<string> {
421
+ const redacted: Set<string> = new Set<string>();
422
+ const allAccessControl: { [key: string]: { read?: unknown[] } } =
423
+ getColumnAccessControlForAllColumns(model) as {
424
+ [key: string]: { read?: unknown[] };
425
+ };
426
+
427
+ for (const key of Object.keys(allAccessControl)) {
428
+ const ac: { read?: unknown[] } | undefined = allAccessControl[key];
429
+ if (ac && Array.isArray(ac.read) && ac.read.length === 0) {
430
+ redacted.add(key);
431
+ }
432
+ }
433
+
434
+ return redacted;
435
+ }
436
+
437
+ private buildSnapshotChanges<TModel extends BaseModel>(data: {
438
+ model: TModel;
439
+ redactedFields: Set<string>;
440
+ valueKey: "oldValue" | "newValue";
441
+ }): JSONArray {
442
+ const changes: JSONArray = [];
443
+ const columns: Array<string> = data.model.getTableColumns().columns;
444
+
445
+ for (const column of columns) {
446
+ if (SKIPPED_FIELDS.has(column)) {
447
+ continue;
448
+ }
449
+
450
+ if (data.redactedFields.has(column)) {
451
+ continue;
452
+ }
453
+
454
+ const value: unknown = (data.model as unknown as Record<string, unknown>)[
455
+ column
456
+ ];
457
+
458
+ if (value === undefined) {
459
+ continue;
460
+ }
461
+
462
+ changes.push({
463
+ field: column,
464
+ [data.valueKey]: this.serializeValue(value),
465
+ } as JSONObject);
466
+ }
467
+
468
+ return changes;
469
+ }
470
+
471
+ private buildUpdateDiff<TModel extends BaseModel>(data: {
472
+ before: TModel;
473
+ updatedFields: JSONObject;
474
+ redactedFields: Set<string>;
475
+ }): JSONArray {
476
+ const changes: JSONArray = [];
477
+
478
+ for (const field of Object.keys(data.updatedFields)) {
479
+ if (SKIPPED_FIELDS.has(field)) {
480
+ continue;
481
+ }
482
+
483
+ if (data.redactedFields.has(field)) {
484
+ continue;
485
+ }
486
+
487
+ const newValue: unknown = data.updatedFields[field];
488
+ const oldValue: unknown = (
489
+ data.before as unknown as Record<string, unknown>
490
+ )[field];
491
+
492
+ if (this.areValuesEqual(oldValue, newValue)) {
493
+ continue;
494
+ }
495
+
496
+ changes.push({
497
+ field,
498
+ oldValue: this.serializeValue(oldValue),
499
+ newValue: this.serializeValue(newValue),
500
+ } as JSONObject);
501
+ }
502
+
503
+ return changes;
504
+ }
505
+
506
+ private areValuesEqual(a: unknown, b: unknown): boolean {
507
+ if (a === b) {
508
+ return true;
509
+ }
510
+ if (a === null || a === undefined) {
511
+ return b === null || b === undefined;
512
+ }
513
+ if (b === null || b === undefined) {
514
+ return false;
515
+ }
516
+ try {
517
+ return JSON.stringify(a) === JSON.stringify(b);
518
+ } catch {
519
+ return false;
520
+ }
521
+ }
522
+
523
+ private serializeValue(value: unknown): unknown {
524
+ if (value === null || value === undefined) {
525
+ return null;
526
+ }
527
+ if (value instanceof ObjectID) {
528
+ return value.toString();
529
+ }
530
+ if (value instanceof Date) {
531
+ return value.toISOString();
532
+ }
533
+ if (
534
+ typeof value === "string" ||
535
+ typeof value === "number" ||
536
+ typeof value === "boolean"
537
+ ) {
538
+ return value;
539
+ }
540
+ try {
541
+ return JSON.parse(JSON.stringify(value));
542
+ } catch {
543
+ return String(value);
544
+ }
545
+ }
546
+
547
+ private getResourceType<TModel extends BaseModel>(model: TModel): string {
548
+ if (model.singularName) {
549
+ return model.singularName;
550
+ }
551
+ return (model as unknown as { constructor: { name: string } }).constructor
552
+ .name;
553
+ }
554
+
555
+ private getResourceName<TModel extends BaseModel>(
556
+ item: TModel,
557
+ ): string | null {
558
+ for (const field of NAME_CANDIDATE_FIELDS) {
559
+ const columns: Array<string> = item.getTableColumns().columns;
560
+ if (!columns.includes(field)) {
561
+ continue;
562
+ }
563
+ const value: unknown = (item as unknown as Record<string, unknown>)[
564
+ field
565
+ ];
566
+ if (typeof value === "string" && value.length > 0) {
567
+ return value;
568
+ }
569
+ }
570
+ return null;
571
+ }
572
+ }
573
+
574
+ export default new AuditLogService();
@@ -31,6 +31,7 @@ import UpdateByIDAndFetch from "../Types/Database/UpdateByIDAndFetch";
31
31
  import UpdateOneBy from "../Types/Database/UpdateOneBy";
32
32
  import Encryption from "../Utils/Encryption";
33
33
  import logger, { LogAttributes } from "../Utils/Logger";
34
+ import AuditLogService from "./AuditLogService";
34
35
  import BaseService from "./BaseService";
35
36
  import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
36
37
  import { WorkflowRoute } from "../../ServiceRoute";
@@ -772,6 +773,17 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
772
773
  );
773
774
  }
774
775
 
776
+ if (
777
+ !createBy.props.ignoreHooks &&
778
+ this.getModel().enableAuditLogOn?.create
779
+ ) {
780
+ await AuditLogService.recordCreate({
781
+ model: this.getModel(),
782
+ createdItem: createBy.data,
783
+ props: createBy.props,
784
+ });
785
+ }
786
+
775
787
  return createBy.data;
776
788
  } catch (error) {
777
789
  await this.onCreateError(error as Exception);
@@ -1117,6 +1129,18 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1117
1129
  (select as any)[this.getModel().getTenantColumn() as string] = true;
1118
1130
  }
1119
1131
 
1132
+ /*
1133
+ * If audit logging on delete is enabled, fetch all scalar columns so we
1134
+ * can record a full snapshot of the record before it is deleted.
1135
+ */
1136
+ if (this.getModel().enableAuditLogOn?.delete) {
1137
+ const allColumns: Array<string> =
1138
+ this.getModel().getTableColumns().columns;
1139
+ for (const columnName of allColumns) {
1140
+ (select as any)[columnName] = true;
1141
+ }
1142
+ }
1143
+
1120
1144
  const items: Array<TBaseModel> = await this._findBy({
1121
1145
  query: beforeDeleteBy.query,
1122
1146
  skip: beforeDeleteBy.skip.toNumber(),
@@ -1199,6 +1223,19 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1199
1223
  );
1200
1224
  }
1201
1225
 
1226
+ if (this.getModel().enableAuditLogOn?.delete && items.length > 0) {
1227
+ for (const item of items) {
1228
+ if (item.id) {
1229
+ await AuditLogService.recordDelete({
1230
+ model: this.getModel(),
1231
+ deletedItem: item,
1232
+ itemId: item.id,
1233
+ props: deleteBy.props,
1234
+ });
1235
+ }
1236
+ }
1237
+ }
1238
+
1202
1239
  return numberOfDocsAffected;
1203
1240
  } catch (error) {
1204
1241
  await this.onDeleteError(error as Exception);
@@ -1533,6 +1570,26 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1533
1570
  true;
1534
1571
  }
1535
1572
 
1573
+ /*
1574
+ * When audit logging on update is enabled, ensure the resource's display
1575
+ * name is loaded on the `before` snapshot so the audit entry records the
1576
+ * human-readable resource name even when the update doesn't touch it.
1577
+ */
1578
+ if (this.getModel().enableAuditLogOn?.update) {
1579
+ const nameCandidates: ReadonlyArray<string> = [
1580
+ "name",
1581
+ "title",
1582
+ "displayName",
1583
+ ];
1584
+ const modelColumns: Array<string> =
1585
+ this.getModel().getTableColumns().columns;
1586
+ for (const candidate of nameCandidates) {
1587
+ if (modelColumns.includes(candidate)) {
1588
+ (selectColumns as any)[candidate] = true;
1589
+ }
1590
+ }
1591
+ }
1592
+
1536
1593
  const items: Array<TBaseModel> = await this._findBy({
1537
1594
  query: beforeUpdateBy.query,
1538
1595
  skip: updateBy.skip.toNumber(),
@@ -1582,6 +1639,20 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
1582
1639
  );
1583
1640
  }
1584
1641
  }
1642
+
1643
+ if (
1644
+ this.getModel().enableAuditLogOn?.update &&
1645
+ !this.hasSameValues({ item, updatedItem }) &&
1646
+ item.id
1647
+ ) {
1648
+ await AuditLogService.recordUpdate({
1649
+ model: this.getModel(),
1650
+ before: item,
1651
+ updatedFields: data as JSONObject,
1652
+ itemId: item.id,
1653
+ props: updateBy.props,
1654
+ });
1655
+ }
1585
1656
  }
1586
1657
 
1587
1658
  /*