@stamhoofd/backend 2.58.0 → 2.59.0
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/index.ts +6 -1
- package/package.json +12 -12
- package/src/audit-logs/EventLogger.ts +30 -0
- package/src/audit-logs/GroupLogger.ts +95 -0
- package/src/audit-logs/MemberLogger.ts +24 -0
- package/src/audit-logs/MemberPlatformMembershipLogger.ts +57 -0
- package/src/audit-logs/MemberResponsibilityRecordLogger.ts +69 -0
- package/src/audit-logs/ModelLogger.ts +218 -0
- package/src/audit-logs/OrderLogger.ts +57 -0
- package/src/audit-logs/OrganizationLogger.ts +26 -0
- package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +77 -0
- package/src/audit-logs/PaymentLogger.ts +43 -0
- package/src/audit-logs/PlatformLogger.ts +13 -0
- package/src/audit-logs/RegistrationLogger.ts +53 -0
- package/src/audit-logs/RegistrationPeriodLogger.ts +21 -0
- package/src/audit-logs/StripeAccountLogger.ts +47 -0
- package/src/audit-logs/WebshopLogger.ts +35 -0
- package/src/crons.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +12 -24
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +4 -18
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +5 -13
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +3 -11
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +0 -15
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +5 -2
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +0 -19
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -12
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +18 -33
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +0 -6
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +0 -6
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +5 -14
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -4
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -2
- package/src/helpers/AuthenticatedStructures.ts +16 -1
- package/src/helpers/Context.ts +8 -2
- package/src/helpers/MemberUserSyncer.ts +45 -40
- package/src/helpers/PeriodHelper.ts +3 -4
- package/src/helpers/TagHelper.ts +23 -20
- package/src/seeds/1722344162-update-membership.ts +2 -2
- package/src/seeds/1726572303-schedule-stock-updates.ts +2 -1
- package/src/services/AuditLogService.ts +83 -295
- package/src/services/BalanceItemPaymentService.ts +1 -1
- package/src/services/BalanceItemService.ts +14 -5
- package/src/services/MemberNumberService.ts +120 -0
- package/src/services/PaymentService.ts +199 -193
- package/src/services/PlatformMembershipService.ts +284 -0
- package/src/services/RegistrationService.ts +76 -27
- package/src/services/explainPatch.ts +110 -41
- package/src/helpers/MembershipHelper.ts +0 -54
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import backendEnv from '@stamhoofd/backend-env';
|
|
2
2
|
backendEnv.load();
|
|
3
3
|
|
|
4
|
-
import { Column, Database, Migration } from '@simonbackx/simple-database';
|
|
4
|
+
import { Column, Database, Migration, Model } from '@simonbackx/simple-database';
|
|
5
5
|
import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
|
|
6
6
|
import { I18n } from '@stamhoofd/backend-i18n';
|
|
7
7
|
import { CORSMiddleware, LogMiddleware, VersionMiddleware } from '@stamhoofd/backend-middleware';
|
|
@@ -14,6 +14,8 @@ import { stopCrons, startCrons, waitForCrons } from '@stamhoofd/crons';
|
|
|
14
14
|
import { resumeEmails } from './src/helpers/EmailResumer';
|
|
15
15
|
import { ContextMiddleware } from './src/middleware/ContextMiddleware';
|
|
16
16
|
import { Platform } from '@stamhoofd/models';
|
|
17
|
+
import { AuditLogService } from './src/services/AuditLogService';
|
|
18
|
+
import { PlatformMembershipService } from './src/services/PlatformMembershipService';
|
|
17
19
|
|
|
18
20
|
process.on('unhandledRejection', (error: Error) => {
|
|
19
21
|
console.error('unhandledRejection');
|
|
@@ -184,6 +186,9 @@ const start = async () => {
|
|
|
184
186
|
await import('./src/crons');
|
|
185
187
|
startCrons();
|
|
186
188
|
seeds().catch(console.error);
|
|
189
|
+
|
|
190
|
+
AuditLogService.listen();
|
|
191
|
+
PlatformMembershipService.listen();
|
|
187
192
|
};
|
|
188
193
|
|
|
189
194
|
start().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.59.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -33,18 +33,18 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@bwip-js/node": "^4.5.1",
|
|
35
35
|
"@mollie/api-client": "3.7.0",
|
|
36
|
-
"@simonbackx/simple-database": "1.
|
|
37
|
-
"@simonbackx/simple-encoding": "2.
|
|
36
|
+
"@simonbackx/simple-database": "1.27.0",
|
|
37
|
+
"@simonbackx/simple-encoding": "2.18.0",
|
|
38
38
|
"@simonbackx/simple-endpoints": "1.15.0",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.59.0",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.59.0",
|
|
42
|
+
"@stamhoofd/email": "2.59.0",
|
|
43
|
+
"@stamhoofd/models": "2.59.0",
|
|
44
|
+
"@stamhoofd/queues": "2.59.0",
|
|
45
|
+
"@stamhoofd/sql": "2.59.0",
|
|
46
|
+
"@stamhoofd/structures": "2.59.0",
|
|
47
|
+
"@stamhoofd/utility": "2.59.0",
|
|
48
48
|
"archiver": "^7.0.1",
|
|
49
49
|
"aws-sdk": "^2.885.0",
|
|
50
50
|
"axios": "1.6.8",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "a7043be8f8fde4fd1c91943495132a21921ebbe1"
|
|
68
68
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Event } from '@stamhoofd/models';
|
|
2
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
3
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
export const EventLogger = new ModelLogger(Event, {
|
|
6
|
+
optionsGenerator: getDefaultGenerator({
|
|
7
|
+
created: AuditLogType.EventAdded,
|
|
8
|
+
updated: AuditLogType.EventEdited,
|
|
9
|
+
deleted: AuditLogType.EventDeleted,
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
createReplacements(model) {
|
|
13
|
+
return new Map([
|
|
14
|
+
['e', AuditLogReplacement.create({
|
|
15
|
+
id: model.id,
|
|
16
|
+
value: model.name,
|
|
17
|
+
type: AuditLogReplacementType.Event,
|
|
18
|
+
})],
|
|
19
|
+
]);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
postProcess(event, options, log) {
|
|
23
|
+
// Replace groupId key by 'registrations'
|
|
24
|
+
for (const item of log.patchList) {
|
|
25
|
+
if (item.key.value === 'groupId') {
|
|
26
|
+
item.key = AuditLogReplacement.key('registrations');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Group } from '@stamhoofd/models';
|
|
2
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, GroupType } from '@stamhoofd/structures';
|
|
3
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
4
|
+
|
|
5
|
+
const defaultGenerator = getDefaultGenerator({
|
|
6
|
+
created: AuditLogType.GroupAdded,
|
|
7
|
+
updated: AuditLogType.GroupEdited,
|
|
8
|
+
deleted: AuditLogType.GroupDeleted,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const GroupLogger = new ModelLogger(Group, {
|
|
12
|
+
skipKeys: ['periodId', 'cycle', 'organizationId', 'stockReservations', 'settings.registeredMembers'],
|
|
13
|
+
async optionsGenerator(event) {
|
|
14
|
+
const result = await defaultGenerator(event);
|
|
15
|
+
|
|
16
|
+
if (!result) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const model = event.model;
|
|
21
|
+
if (model.type === GroupType.WaitingList) {
|
|
22
|
+
// Change event type
|
|
23
|
+
switch (result.type) {
|
|
24
|
+
case AuditLogType.GroupAdded:
|
|
25
|
+
result.type = AuditLogType.WaitingListAdded;
|
|
26
|
+
break;
|
|
27
|
+
case AuditLogType.GroupEdited:
|
|
28
|
+
result.type = AuditLogType.WaitingListEdited;
|
|
29
|
+
break;
|
|
30
|
+
case AuditLogType.GroupDeleted:
|
|
31
|
+
result.type = AuditLogType.WaitingListDeleted;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (model.type === GroupType.EventRegistration) {
|
|
37
|
+
// Change event type
|
|
38
|
+
switch (result.type) {
|
|
39
|
+
case AuditLogType.GroupAdded:
|
|
40
|
+
// do not log
|
|
41
|
+
return;
|
|
42
|
+
case AuditLogType.GroupEdited:
|
|
43
|
+
result.type = AuditLogType.EventEdited;
|
|
44
|
+
|
|
45
|
+
if (model.settings.eventId) {
|
|
46
|
+
result.objectId = model.settings.eventId;
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
case AuditLogType.GroupDeleted:
|
|
50
|
+
// do not log
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
postProcess(event, _options, log) {
|
|
59
|
+
if (log.type === AuditLogType.EventEdited) {
|
|
60
|
+
// Prefix changes
|
|
61
|
+
for (const item of log.patchList) {
|
|
62
|
+
item.key = item.key.prepend(AuditLogReplacement.key('registrations'));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
createReplacements: (model) => {
|
|
68
|
+
if (model.type === GroupType.EventRegistration) {
|
|
69
|
+
if (model.settings.eventId) {
|
|
70
|
+
return new Map([
|
|
71
|
+
['e', AuditLogReplacement.create({
|
|
72
|
+
id: model.settings.eventId,
|
|
73
|
+
value: model.settings.name,
|
|
74
|
+
type: AuditLogReplacementType.Event,
|
|
75
|
+
})],
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
return new Map([
|
|
79
|
+
['e', AuditLogReplacement.create({
|
|
80
|
+
id: model.id,
|
|
81
|
+
value: model.settings.name,
|
|
82
|
+
type: AuditLogReplacementType.Group,
|
|
83
|
+
})],
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return new Map([
|
|
88
|
+
['g', AuditLogReplacement.create({
|
|
89
|
+
id: model.id,
|
|
90
|
+
value: model.settings.name,
|
|
91
|
+
type: AuditLogReplacementType.Group,
|
|
92
|
+
})],
|
|
93
|
+
]);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Member } from '@stamhoofd/models';
|
|
2
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
3
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
export const MemberLogger = new ModelLogger(Member, {
|
|
6
|
+
// Skip repeated auto generated fields
|
|
7
|
+
skipKeys: ['firstName', 'lastName', 'birthDay', 'memberNumber'],
|
|
8
|
+
|
|
9
|
+
optionsGenerator: getDefaultGenerator({
|
|
10
|
+
created: AuditLogType.MemberAdded,
|
|
11
|
+
updated: AuditLogType.MemberEdited,
|
|
12
|
+
deleted: AuditLogType.MemberDeleted,
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
createReplacements(model) {
|
|
16
|
+
return new Map([
|
|
17
|
+
['m', AuditLogReplacement.create({
|
|
18
|
+
id: model.id,
|
|
19
|
+
value: model.details.name,
|
|
20
|
+
type: AuditLogReplacementType.Member,
|
|
21
|
+
})],
|
|
22
|
+
]);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Member, MemberPlatformMembership } from '@stamhoofd/models';
|
|
2
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, uuidToName } from '@stamhoofd/structures';
|
|
3
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
4
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
5
|
+
|
|
6
|
+
const defaultGenerator = getDefaultGenerator({
|
|
7
|
+
created: AuditLogType.MemberPlatformMembershipAdded,
|
|
8
|
+
updated: AuditLogType.MemberPlatformMembershipEdited,
|
|
9
|
+
deleted: AuditLogType.MemberPlatformMembershipDeleted,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const MemberPlatformMembershipLogger = new ModelLogger(MemberPlatformMembership, {
|
|
13
|
+
skipKeys: ['balanceItemId'],
|
|
14
|
+
async optionsGenerator(event) {
|
|
15
|
+
const result = await defaultGenerator(event);
|
|
16
|
+
|
|
17
|
+
if (!result) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const member = await Member.getByID(event.model.memberId);
|
|
22
|
+
|
|
23
|
+
if (!member) {
|
|
24
|
+
console.log('No member found for MemberPlatformMembership', event.model.id);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...result,
|
|
30
|
+
data: {
|
|
31
|
+
member,
|
|
32
|
+
},
|
|
33
|
+
objectId: event.model.memberId,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
generateDescription(event, options) {
|
|
38
|
+
return Formatter.capitalizeFirstLetter(Formatter.dateRange(event.model.startDate, event.model.endDate));
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
createReplacements(model, options) {
|
|
42
|
+
const map = new Map([
|
|
43
|
+
['pm', AuditLogReplacement.create({
|
|
44
|
+
id: model.membershipTypeId,
|
|
45
|
+
value: uuidToName(model.membershipTypeId) || undefined,
|
|
46
|
+
type: AuditLogReplacementType.PlatformMembershipType,
|
|
47
|
+
})],
|
|
48
|
+
['m', AuditLogReplacement.create({
|
|
49
|
+
id: options.data.member.id,
|
|
50
|
+
value: options.data.member.details.name,
|
|
51
|
+
type: AuditLogReplacementType.Member,
|
|
52
|
+
})],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
return map;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Group, Member, MemberResponsibilityRecord, Organization, Platform } from '@stamhoofd/models';
|
|
2
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
|
|
3
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
4
|
+
|
|
5
|
+
const defaultGenerator = getDefaultGenerator({
|
|
6
|
+
created: AuditLogType.MemberResponsibilityRecordAdded,
|
|
7
|
+
updated: AuditLogType.MemberResponsibilityRecordEdited,
|
|
8
|
+
deleted: AuditLogType.MemberResponsibilityRecordDeleted,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const MemberResponsibilityRecordLogger = new ModelLogger(MemberResponsibilityRecord, {
|
|
12
|
+
skipKeys: ['balanceItemId'],
|
|
13
|
+
async optionsGenerator(event) {
|
|
14
|
+
const result = await defaultGenerator(event);
|
|
15
|
+
|
|
16
|
+
if (!result) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const member = await Member.getByID(event.model.memberId);
|
|
21
|
+
const group = event.model.groupId ? (await Group.getByID(event.model.groupId)) : null;
|
|
22
|
+
|
|
23
|
+
if (!member) {
|
|
24
|
+
console.log('No member found for MemberResponsibilityRecord', event.model.id);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (event.type === 'updated' && event.changedFields.endDate && event.originalFields.endDate === null) {
|
|
29
|
+
result.type = AuditLogType.MemberResponsibilityRecordDeleted;
|
|
30
|
+
result.generatePatchList = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
...result,
|
|
35
|
+
data: {
|
|
36
|
+
member,
|
|
37
|
+
group,
|
|
38
|
+
platform: await Platform.getSharedStruct(),
|
|
39
|
+
},
|
|
40
|
+
objectId: event.model.memberId,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
createReplacements(model, options) {
|
|
45
|
+
const name = options.data.platform.config.responsibilities.find(r => r.id === model.responsibilityId)?.name;
|
|
46
|
+
const map = new Map([
|
|
47
|
+
['r', AuditLogReplacement.create({
|
|
48
|
+
id: model.responsibilityId,
|
|
49
|
+
value: name,
|
|
50
|
+
type: AuditLogReplacementType.MemberResponsibility,
|
|
51
|
+
})],
|
|
52
|
+
['m', AuditLogReplacement.create({
|
|
53
|
+
id: options.data.member.id,
|
|
54
|
+
value: options.data.member.details.name,
|
|
55
|
+
type: AuditLogReplacementType.Member,
|
|
56
|
+
})],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
if (options.data.group) {
|
|
60
|
+
map.set('g', AuditLogReplacement.create({
|
|
61
|
+
id: options.data.group.id,
|
|
62
|
+
value: options.data.group.settings.name,
|
|
63
|
+
type: AuditLogReplacementType.Group,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return map;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Model, ModelEvent } from '@simonbackx/simple-database';
|
|
2
|
+
import { AuditLog } from '@stamhoofd/models';
|
|
3
|
+
import { AuditLogReplacement, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
|
|
4
|
+
import { ContextInstance } from '../helpers/Context';
|
|
5
|
+
import { AuditLogService } from '../services/AuditLogService';
|
|
6
|
+
import { createUnknownChangeHandler } from '../services/explainPatch';
|
|
7
|
+
|
|
8
|
+
export type ModelEventLogOptions<D> = {
|
|
9
|
+
type: AuditLogType;
|
|
10
|
+
generatePatchList?: boolean;
|
|
11
|
+
data: D;
|
|
12
|
+
mergeInto?: AuditLog;
|
|
13
|
+
objectId?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type DefaultLogOptionsType = {
|
|
17
|
+
created?: AuditLogType;
|
|
18
|
+
updated?: AuditLogType;
|
|
19
|
+
deleted?: AuditLogType;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type EventOptionsGenerator<M extends Model, D> = (event: ModelEvent<M>) => ModelEventLogOptions<D> | Promise<ModelEventLogOptions<D> | undefined> | undefined;
|
|
23
|
+
|
|
24
|
+
export type ModelLoggerOptions<M extends Model, D = undefined> = {
|
|
25
|
+
optionsGenerator: EventOptionsGenerator<M, D>;
|
|
26
|
+
skipKeys?: string[];
|
|
27
|
+
generateDescription?(event: ModelEvent<M>, options: ModelEventLogOptions<D>): string | null | undefined;
|
|
28
|
+
createReplacements?(model: M, options: ModelEventLogOptions<D>): Map<string, AuditLogReplacement>;
|
|
29
|
+
postProcess?(event: ModelEvent<M>, options: ModelEventLogOptions<D>, log: AuditLog): Promise<void> | void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const ModelEventsMap = new WeakMap<Model, { events: AuditLog[] }>();
|
|
33
|
+
|
|
34
|
+
export function getDefaultGenerator(types: DefaultLogOptionsType): EventOptionsGenerator<Model, undefined> {
|
|
35
|
+
return (event: ModelEvent<Model>) => {
|
|
36
|
+
if (event.type === 'created') {
|
|
37
|
+
if (types.created) {
|
|
38
|
+
return { type: types.created, data: undefined };
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let mergeInto: AuditLog | undefined = undefined;
|
|
44
|
+
|
|
45
|
+
if (event.type === 'updated') {
|
|
46
|
+
// Generate changes list
|
|
47
|
+
if ('deletedAt' in event.changedFields && ((event.model as any).deletedAt)) {
|
|
48
|
+
if (!('deletedAt' in event.originalFields) || !event.originalFields.deletedAt) {
|
|
49
|
+
event = {
|
|
50
|
+
...event,
|
|
51
|
+
type: 'deleted',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (types.deleted) {
|
|
55
|
+
return { type: types.deleted, data: undefined };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (types.updated) {
|
|
61
|
+
if ('createdAt' in event.model && event.model.createdAt instanceof Date && event.model.createdAt.getTime() >= Date.now() - 1000) {
|
|
62
|
+
// Ignore quick model changes after insertion: these are often side effects of code that should not be logged
|
|
63
|
+
// (the created event normally doesn't contain what has been added - so no need to log what has been changed since creation)
|
|
64
|
+
if (types.created && types.created !== types.updated) {
|
|
65
|
+
// Merge into create event
|
|
66
|
+
const events = ModelEventsMap.get(event.model)?.events;
|
|
67
|
+
if (events && events.length > 0) {
|
|
68
|
+
mergeInto = events[0];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { type: types.updated, generatePatchList: true, data: undefined, mergeInto };
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (event.type === 'deleted') {
|
|
79
|
+
if (types.deleted) {
|
|
80
|
+
return { type: types.deleted, data: undefined };
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export class ModelLogger<ModelType extends typeof Model, M extends InstanceType<ModelType>, D> {
|
|
88
|
+
model: ModelType;
|
|
89
|
+
optionsGenerator: EventOptionsGenerator<M, D>;
|
|
90
|
+
skipKeys: string[] = [];
|
|
91
|
+
generateDescription?: (event: ModelEvent<M>, options: ModelEventLogOptions<D>) => string | null | undefined;
|
|
92
|
+
createReplacements?: (model: M, options: ModelEventLogOptions<D>) => Map<string, AuditLogReplacement>;
|
|
93
|
+
postProcess?: (event: ModelEvent<M>, options: ModelEventLogOptions<D>, log: AuditLog) => Promise<void> | void;
|
|
94
|
+
static sharedSkipKeys = ['id', 'createdAt', 'updatedAt', 'deletedAt'];
|
|
95
|
+
|
|
96
|
+
constructor(model: ModelType, options: ModelLoggerOptions<M, D>) {
|
|
97
|
+
this.model = model;
|
|
98
|
+
this.optionsGenerator = options.optionsGenerator;
|
|
99
|
+
this.skipKeys = options.skipKeys ?? [];
|
|
100
|
+
this.generateDescription = options.generateDescription;
|
|
101
|
+
this.createReplacements = options.createReplacements;
|
|
102
|
+
this.postProcess = options.postProcess;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async logEvent(event: ModelEvent<M>) {
|
|
106
|
+
try {
|
|
107
|
+
const context = ContextInstance.optional;
|
|
108
|
+
const log = new AuditLog();
|
|
109
|
+
const settings = AuditLogService.getContext();
|
|
110
|
+
const userId = settings?.userId !== undefined ? settings?.userId : (context?.optionalAuth?.user?.id ?? settings?.fallbackUserId ?? null);
|
|
111
|
+
log.userId = userId;
|
|
112
|
+
|
|
113
|
+
log.organizationId = context?.organization?.id ?? settings?.fallbackOrganizationId ?? null;
|
|
114
|
+
|
|
115
|
+
if ('organizationId' in event.model && typeof event.model['organizationId'] === 'string') {
|
|
116
|
+
// Always override
|
|
117
|
+
log.organizationId = event.model.organizationId;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (settings?.source) {
|
|
121
|
+
log.source = settings.source;
|
|
122
|
+
}
|
|
123
|
+
else if (context) {
|
|
124
|
+
log.source = userId ? AuditLogSource.User : AuditLogSource.Anonymous;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
log.source = AuditLogSource.System;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const options = await this.optionsGenerator(event);
|
|
131
|
+
|
|
132
|
+
if (!options) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
log.type = options.type;
|
|
137
|
+
log.objectId = options.objectId ?? event.model.getPrimaryKey()?.toString() ?? null;
|
|
138
|
+
log.replacements = this.createReplacements ? this.createReplacements(event.model as M, options) : new Map();
|
|
139
|
+
log.description = (this.generateDescription ? this.generateDescription(event, options) : '') ?? '';
|
|
140
|
+
|
|
141
|
+
if (options.generatePatchList && event.type === 'updated') {
|
|
142
|
+
const oldModel = event.getOldModel();
|
|
143
|
+
|
|
144
|
+
// Generate changes list
|
|
145
|
+
const changedProperties = Object.keys(event.changedFields).filter(k => !this.skipKeys?.includes(k) && !ModelLogger.sharedSkipKeys.includes(k));
|
|
146
|
+
|
|
147
|
+
if (changedProperties.length > 0) {
|
|
148
|
+
for (const key of changedProperties) {
|
|
149
|
+
if (changedProperties[key] instanceof Model) {
|
|
150
|
+
// Ignore relations
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
log.patchList.push(...createUnknownChangeHandler(key)(
|
|
154
|
+
key in oldModel ? oldModel[key] : undefined,
|
|
155
|
+
key in event.model ? event.model[key] : undefined,
|
|
156
|
+
));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Remove skipped keys
|
|
160
|
+
log.patchList = log.patchList.filter(p => !this.skipKeys?.includes(p.key.toKey()));
|
|
161
|
+
|
|
162
|
+
if (log.patchList.length === 0 && !log.description) {
|
|
163
|
+
// No changes or all skipped
|
|
164
|
+
console.log('No changes after secundary filtering');
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.log('No changes');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (this.postProcess) {
|
|
175
|
+
await this.postProcess(event, options, log);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options.mergeInto) {
|
|
179
|
+
// Merge into
|
|
180
|
+
console.log('Merging new audit log into existing log', options.mergeInto.id);
|
|
181
|
+
|
|
182
|
+
if (!options.mergeInto.description) {
|
|
183
|
+
options.mergeInto.description = log.description;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (log.replacements.size) {
|
|
187
|
+
options.mergeInto.replacements = log.replacements;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!options.mergeInto.patchList.length) {
|
|
191
|
+
options.mergeInto.patchList = log.patchList;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (options.mergeInto.userId === null) {
|
|
195
|
+
options.mergeInto.userId = log.userId;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (options.mergeInto.organizationId === null) {
|
|
199
|
+
options.mergeInto.organizationId = log.organizationId;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return await options.mergeInto.save();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const saved = await log.save();
|
|
206
|
+
|
|
207
|
+
// Assign to map
|
|
208
|
+
ModelEventsMap.set(event.model, {
|
|
209
|
+
events: [...(ModelEventsMap.get(event.model)?.events ?? []), log],
|
|
210
|
+
});
|
|
211
|
+
return saved;
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
console.error('Failed to save log', e);
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Order, Webshop } from '@stamhoofd/models';
|
|
2
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
3
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, OrderStatus } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
const defaultGenerator = getDefaultGenerator({
|
|
6
|
+
created: AuditLogType.OrderAdded,
|
|
7
|
+
updated: AuditLogType.OrderEdited,
|
|
8
|
+
deleted: AuditLogType.OrderDeleted,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const OrderLogger = new ModelLogger(Order, {
|
|
12
|
+
skipKeys: ['amount', 'timeSlotTime', 'validAt', 'paymentId'],
|
|
13
|
+
async optionsGenerator(event) {
|
|
14
|
+
const result = await defaultGenerator(event);
|
|
15
|
+
|
|
16
|
+
if (!result) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// todo: when placing an order / marking an order as paid
|
|
21
|
+
// we should not log any stock changes
|
|
22
|
+
|
|
23
|
+
const webshop = Order.webshop.isLoaded(event.model) ? (event.model as (Order & Record<'webshop', Webshop>)).webshop : (await Webshop.getByID(event.model.webshopId));
|
|
24
|
+
if (!webshop) {
|
|
25
|
+
console.log('No webshop found for order', event.model.id);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (event.type === 'updated' && event.changedFields.status === OrderStatus.Deleted) {
|
|
30
|
+
result.type = AuditLogType.OrderDeleted;
|
|
31
|
+
result.generatePatchList = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...result,
|
|
36
|
+
data: {
|
|
37
|
+
webshop,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
createReplacements: (model, options) => {
|
|
43
|
+
return new Map([
|
|
44
|
+
['w', AuditLogReplacement.create({
|
|
45
|
+
id: options.data.webshop.id,
|
|
46
|
+
value: options.data.webshop.meta.name,
|
|
47
|
+
type: AuditLogReplacementType.Webshop,
|
|
48
|
+
})],
|
|
49
|
+
['org', AuditLogReplacement.create({
|
|
50
|
+
id: model.id,
|
|
51
|
+
value: model.number ? `bestelling #${model.number}` : `bestelling van ${model.data.customer.name}`,
|
|
52
|
+
type: AuditLogReplacementType.Order,
|
|
53
|
+
description: model.number ? model.data.customer.name : 'Deze bestelling heeft nog geen nummer',
|
|
54
|
+
})],
|
|
55
|
+
]);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Organization } from '@stamhoofd/models';
|
|
2
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
3
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
export const OrganizationLogger = new ModelLogger(Organization, {
|
|
6
|
+
skipKeys: ['searchIndex'],
|
|
7
|
+
optionsGenerator: getDefaultGenerator({
|
|
8
|
+
created: AuditLogType.OrganizationAdded,
|
|
9
|
+
updated: AuditLogType.OrganizationEdited,
|
|
10
|
+
deleted: AuditLogType.OrganizationDeleted,
|
|
11
|
+
}),
|
|
12
|
+
|
|
13
|
+
createReplacements(model) {
|
|
14
|
+
return new Map([
|
|
15
|
+
['o', AuditLogReplacement.create({
|
|
16
|
+
id: model.id,
|
|
17
|
+
value: model.name,
|
|
18
|
+
type: AuditLogReplacementType.Organization,
|
|
19
|
+
})],
|
|
20
|
+
]);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
postProcess(event, options, log) {
|
|
24
|
+
log.organizationId = event.model.id;
|
|
25
|
+
},
|
|
26
|
+
});
|