@stamhoofd/backend 2.57.1 → 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.
Files changed (48) hide show
  1. package/index.ts +6 -1
  2. package/package.json +13 -13
  3. package/src/audit-logs/EventLogger.ts +30 -0
  4. package/src/audit-logs/GroupLogger.ts +95 -0
  5. package/src/audit-logs/MemberLogger.ts +24 -0
  6. package/src/audit-logs/MemberPlatformMembershipLogger.ts +57 -0
  7. package/src/audit-logs/MemberResponsibilityRecordLogger.ts +69 -0
  8. package/src/audit-logs/ModelLogger.ts +218 -0
  9. package/src/audit-logs/OrderLogger.ts +57 -0
  10. package/src/audit-logs/OrganizationLogger.ts +26 -0
  11. package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +77 -0
  12. package/src/audit-logs/PaymentLogger.ts +43 -0
  13. package/src/audit-logs/PlatformLogger.ts +13 -0
  14. package/src/audit-logs/RegistrationLogger.ts +53 -0
  15. package/src/audit-logs/RegistrationPeriodLogger.ts +21 -0
  16. package/src/audit-logs/StripeAccountLogger.ts +47 -0
  17. package/src/audit-logs/WebshopLogger.ts +35 -0
  18. package/src/crons.ts +2 -1
  19. package/src/endpoints/global/events/PatchEventsEndpoint.ts +12 -24
  20. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +4 -18
  21. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -3
  22. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +3 -18
  23. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +0 -15
  24. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +5 -2
  25. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -2
  26. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +5 -15
  27. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +18 -28
  28. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +2 -1
  29. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -1
  30. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +6 -3
  31. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -4
  32. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -2
  33. package/src/helpers/AuthenticatedStructures.ts +16 -1
  34. package/src/helpers/Context.ts +8 -2
  35. package/src/helpers/MemberUserSyncer.ts +45 -40
  36. package/src/helpers/PeriodHelper.ts +31 -27
  37. package/src/helpers/TagHelper.ts +23 -20
  38. package/src/seeds/1722344162-update-membership.ts +2 -2
  39. package/src/seeds/1726572303-schedule-stock-updates.ts +2 -1
  40. package/src/services/AuditLogService.ts +89 -216
  41. package/src/services/BalanceItemPaymentService.ts +1 -1
  42. package/src/services/BalanceItemService.ts +14 -5
  43. package/src/services/MemberNumberService.ts +120 -0
  44. package/src/services/PaymentService.ts +199 -193
  45. package/src/services/PlatformMembershipService.ts +284 -0
  46. package/src/services/RegistrationService.ts +76 -27
  47. package/src/services/explainPatch.ts +528 -316
  48. package/src/helpers/MembershipHelper.ts +0 -54
@@ -1,8 +1,8 @@
1
- import { AutoEncoderPatchType, Decoder, isPatchableArray, ObjectData, PatchableArrayAutoEncoder, patchObject } from '@simonbackx/simple-encoding';
1
+ import { AutoEncoderPatchType, cloneObject, Decoder, isPatchableArray, ObjectData, PatchableArrayAutoEncoder, patchObject } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
4
4
  import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, SetupStepUpdater, StripeAccount, Webshop } from '@stamhoofd/models';
5
- import { AuditLogType, BuckarooSettings, Company, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PlatformConfig } from '@stamhoofd/structures';
5
+ import { BuckarooSettings, Company, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PlatformConfig } from '@stamhoofd/structures';
6
6
  import { Formatter } from '@stamhoofd/utility';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
@@ -10,7 +10,6 @@ import { BuckarooHelper } from '../../../../helpers/BuckarooHelper';
10
10
  import { Context } from '../../../../helpers/Context';
11
11
  import { TagHelper } from '../../../../helpers/TagHelper';
12
12
  import { ViesHelper } from '../../../../helpers/ViesHelper';
13
- import { AuditLogService } from '../../../../services/AuditLogService';
14
13
 
15
14
  type Params = Record<string, never>;
16
15
  type Query = undefined;
@@ -136,7 +135,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
136
135
 
137
136
  // Apply payconiq patch
138
137
  if (request.body.privateMeta.payconiqAccounts !== undefined) {
139
- organization.privateMeta.payconiqAccounts = patchObject(organization.privateMeta.payconiqAccounts, request.body.privateMeta.payconiqAccounts);
138
+ organization.privateMeta.payconiqAccounts = patchObject(organization.privateMeta.payconiqAccounts, cloneObject(request.body.privateMeta.payconiqAccounts as any));
140
139
 
141
140
  for (const account of organization.privateMeta.payconiqAccounts) {
142
141
  if (account.merchantId === null) {
@@ -214,8 +213,6 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
214
213
  await this.validateCompanies(organization, request.body.meta.companies);
215
214
  shouldUpdateSetupSteps = true;
216
215
  }
217
- const oldMeta = organization.meta.clone();
218
-
219
216
  const savedPackages = organization.meta.packages;
220
217
  organization.meta.patchOrPut(request.body.meta);
221
218
  organization.meta.packages = savedPackages;
@@ -283,13 +280,6 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
283
280
 
284
281
  updateTags = true;
285
282
  }
286
-
287
- await AuditLogService.log({
288
- type: AuditLogType.OrganizationSettingsChanged,
289
- organization,
290
- oldMeta,
291
- patch: request.body.meta,
292
- });
293
283
  }
294
284
 
295
285
  if (request.body.active !== undefined) {
@@ -393,8 +383,8 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
393
383
  if (updateTags) {
394
384
  await TagHelper.updateOrganizations();
395
385
  }
396
-
397
- return new Response(await AuthenticatedStructures.organization(organization));
386
+ const struct = await AuthenticatedStructures.organization(organization);
387
+ return new Response(struct);
398
388
  }
399
389
 
400
390
  async validateCompanies(organization: Organization, companies: PatchableArrayAutoEncoder<Company> | Company[]) {
@@ -1,12 +1,11 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { AuditLogType, GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
2
+ import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
3
3
 
4
4
  import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
6
- import { Group, Member, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod, SetupStepUpdater } from '@stamhoofd/models';
6
+ import { Group, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod, SetupStepUpdater } from '@stamhoofd/models';
7
7
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
8
8
  import { Context } from '../../../../helpers/Context';
9
- import { AuditLogService } from '../../../../services/AuditLogService';
10
9
 
11
10
  type Params = Record<string, never>;
12
11
  type Query = undefined;
@@ -81,6 +80,20 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
81
80
 
82
81
  let deleteUnreachable = false;
83
82
  const allowedIds: string[] = [];
83
+ let wasInvalid = false;
84
+
85
+ // Check if have an initial invalid state
86
+ if (patch.settings) {
87
+ // Already clean up the categories (not yet delete the groups)
88
+ const groups = await Group.getAll(organization.id, organizationPeriod.periodId);
89
+ await organizationPeriod.cleanCategories(groups);
90
+
91
+ for (const category of organizationPeriod.settings.categories) {
92
+ if (category.groupIds.length && category.categoryIds.length) {
93
+ wasInvalid = true;
94
+ }
95
+ }
96
+ }
84
97
 
85
98
  // #region prevent patch category lock if no full platform access
86
99
  const originalCategories = organizationPeriod.settings.categories;
@@ -90,7 +103,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
90
103
  if (patch.settings.categories) {
91
104
  deleteUnreachable = true;
92
105
  }
93
- organizationPeriod.settings.patchOrPut(patch.settings);
106
+ organizationPeriod.settings = organizationPeriod.settings.patch(patch.settings);
94
107
  }
95
108
  }
96
109
  else {
@@ -123,7 +136,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
123
136
  }
124
137
 
125
138
  // Check if we have a category with groups and categories combined
126
- if (patch.settings) {
139
+ if (patch.settings && !wasInvalid) {
127
140
  for (const category of organizationPeriod.settings.categories) {
128
141
  if (category.groupIds.length && category.categoryIds.length) {
129
142
  throw new SimpleError({
@@ -281,12 +294,6 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
281
294
 
282
295
  model.deletedAt = new Date();
283
296
  await model.save();
284
- Member.updateMembershipsForGroupId(id);
285
-
286
- await AuditLogService.log({
287
- type: AuditLogType.GroupDeleted,
288
- group: model,
289
- });
290
297
  }
291
298
 
292
299
  static async patchGroup(struct: AutoEncoderPatchType<GroupStruct>, period?: RegistrationPeriod | null) {
@@ -295,7 +302,6 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
295
302
  if (!model || !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
296
303
  throw Context.auth.error('Je hebt geen toegangsrechten om deze groep te wijzigen');
297
304
  }
298
- const originalStruct = (await AuthenticatedStructures.group(model)).clone(); // Clone is required for deep changes
299
305
 
300
306
  const previousProperties = {
301
307
  deletedAt: model.deletedAt,
@@ -420,17 +426,6 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
420
426
  previousProperties,
421
427
  });
422
428
  await model.save();
423
-
424
- if (struct.deletedAt !== undefined || struct.defaultAgeGroupId !== undefined) {
425
- Member.updateMembershipsForGroupId(model.id);
426
- }
427
-
428
- await AuditLogService.log({
429
- type: AuditLogType.GroupEdited,
430
- group: model,
431
- oldData: originalStruct,
432
- patch,
433
- });
434
429
  }
435
430
 
436
431
  static async createGroup(struct: GroupStruct, organizationId: string, period: RegistrationPeriod, options?: { allowedIds?: string[] }): Promise<Group> {
@@ -512,11 +507,6 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
512
507
  await model.save();
513
508
  await model.updateOccupancy({ isNew: true }); // Force update steps
514
509
 
515
- await AuditLogService.log({
516
- type: AuditLogType.GroupAdded,
517
- group: model,
518
- });
519
-
520
510
  return model;
521
511
  }
522
512
  }
@@ -1,11 +1,12 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
2
  import { SimpleError } from '@simonbackx/simple-errors';
3
3
  import { StripeAccount } from '@stamhoofd/models';
4
- import { PermissionLevel, StripeAccount as StripeAccountStruct } from '@stamhoofd/structures';
4
+ import { AuditLogType, PermissionLevel, StripeAccount as StripeAccountStruct } from '@stamhoofd/structures';
5
5
  import Stripe from 'stripe';
6
6
 
7
7
  import { Context } from '../../../../helpers/Context';
8
8
  import { StripeHelper } from '../../../../helpers/StripeHelper';
9
+ import { AuditLogService } from '../../../../services/AuditLogService';
9
10
  type Params = Record<string, never>;
10
11
  type Body = undefined;
11
12
  type Query = undefined;
@@ -1,10 +1,11 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
2
  import { StripeAccount } from '@stamhoofd/models';
3
- import { PermissionLevel } from '@stamhoofd/structures';
3
+ import { AuditLogType, PermissionLevel } from '@stamhoofd/structures';
4
4
 
5
5
  import { Context } from '../../../../helpers/Context';
6
6
  import { StripeHelper } from '../../../../helpers/StripeHelper';
7
7
  import { SimpleError } from '@simonbackx/simple-errors';
8
+ import { AuditLogService } from '../../../../services/AuditLogService';
8
9
 
9
10
  type Params = { id: string };
10
11
  type Body = undefined;
@@ -1,9 +1,10 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
2
  import { StripeAccount } from '@stamhoofd/models';
3
- import { PermissionLevel, StripeAccount as StripeAccountStruct } from '@stamhoofd/structures';
3
+ import { AuditLogSource, PermissionLevel, StripeAccount as StripeAccountStruct } from '@stamhoofd/structures';
4
4
 
5
5
  import { Context } from '../../../../helpers/Context';
6
6
  import { StripeHelper } from '../../../../helpers/StripeHelper';
7
+ import { AuditLogService } from '../../../../services/AuditLogService';
7
8
 
8
9
  type Params = { id: string };
9
10
  type Body = undefined;
@@ -43,9 +44,11 @@ export class UpdateStripeAccountEndpoint extends Endpoint<Params, Query, Body, R
43
44
  // Get account
44
45
  const stripe = StripeHelper.getInstance();
45
46
  const account = await stripe.accounts.retrieve(model.accountId);
46
- model.setMetaFromStripeAccount(account);
47
- await model.save();
48
47
 
48
+ await AuditLogService.setContext({ userId: null, source: AuditLogSource.System }, async () => {
49
+ model.setMetaFromStripeAccount(account);
50
+ await model.save();
51
+ });
49
52
  return new Response(StripeAccountStruct.create(model));
50
53
  }
51
54
  }
@@ -3,9 +3,10 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { BalanceItem, BalanceItemPayment, Order, Payment, Token, Webshop } from '@stamhoofd/models';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
- import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment, Webshop as WebshopStruct } from '@stamhoofd/structures';
6
+ import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, PrivatePayment, Webshop as WebshopStruct } from '@stamhoofd/structures';
7
7
 
8
8
  import { Context } from '../../../../helpers/Context';
9
+ import { AuditLogService } from '../../../../services/AuditLogService';
9
10
 
10
11
  type Params = { id: string };
11
12
  type Query = undefined;
@@ -131,7 +132,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
131
132
  order.data.validate(webshopGetter.struct, organization.meta, request.i18n, true);
132
133
 
133
134
  try {
134
- await order.updateStock();
135
+ await order.updateStock(null, true);
135
136
  const totalPrice = order.data.totalPrice;
136
137
 
137
138
  if (totalPrice == 0) {
@@ -286,9 +287,11 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
286
287
  }
287
288
  }
288
289
 
290
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
291
+ await model.setRelation(Order.webshop, webshop).updateStock(previousData, true);
292
+ await model.setRelation(Order.webshop, webshop).updateTickets();
293
+ });
289
294
  await model.save();
290
- await model.setRelation(Order.webshop, webshop).updateStock(previousData);
291
- await model.setRelation(Order.webshop, webshop).updateTickets();
292
295
  }
293
296
 
294
297
  const mapped = orders.map(order => order.setRelation(Order.webshop, webshop));
@@ -6,12 +6,13 @@ import { I18n } from '@stamhoofd/backend-i18n';
6
6
  import { Email } from '@stamhoofd/email';
7
7
  import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
8
8
  import { QueueHandler } from '@stamhoofd/queues';
9
- import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct, WebshopAuthType, BalanceItemType, BalanceItemRelationType, BalanceItemRelation } from '@stamhoofd/structures';
9
+ import { BalanceItemStatus, Order as OrderStruct, OrderData, OrderResponse, Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Version, Webshop as WebshopStruct, WebshopAuthType, BalanceItemType, BalanceItemRelationType, BalanceItemRelation, AuditLogSource } from '@stamhoofd/structures';
10
10
  import { Formatter } from '@stamhoofd/utility';
11
11
 
12
12
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
13
13
  import { Context } from '../../../helpers/Context';
14
14
  import { StripeHelper } from '../../../helpers/StripeHelper';
15
+ import { AuditLogService } from '../../../services/AuditLogService';
15
16
 
16
17
  type Params = { id: string };
17
18
  type Query = undefined;
@@ -140,7 +141,12 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
140
141
  order.userId = Context.user?.id ?? null;
141
142
 
142
143
  // Always reserve the stock
143
- await order.updateStock();
144
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
145
+ await order.updateStock(null, true);
146
+ });
147
+
148
+ await order.save();
149
+
144
150
  return { webshop, order, organization };
145
151
  });
146
152
 
@@ -1,6 +1,6 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import { AuditLog, BalanceItem, CachedBalance, Document, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
3
- import { AuditLog as AuditLogStruct, Document as DocumentStruct, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, Document as DocumentStruct, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
 
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { Context } from './Context';
@@ -645,6 +645,8 @@ export class AuthenticatedStructures {
645
645
 
646
646
  const userIds = Formatter.uniqueArray(logs.map(l => l.userId).filter(id => id !== null));
647
647
  const users = await User.getByIDs(...userIds);
648
+ const organizationsIds = Formatter.uniqueArray(logs.map(l => l.organizationId).filter(id => id !== null));
649
+ const organizations = await Organization.getByIDs(...organizationsIds);
648
650
 
649
651
  for (const log of logs) {
650
652
  const user = log.userId ? (users.find(u => u.id === log.userId) ?? null) : null;
@@ -673,9 +675,22 @@ export class AuthenticatedStructures {
673
675
  }
674
676
  }
675
677
 
678
+ let replacements = log.replacements;
679
+
680
+ if (log.organizationId && log.organizationId !== Context.organization?.id) {
681
+ replacements = new Map(log.replacements);
682
+ const org = organizations.find(o => o.id === log.organizationId);
683
+ replacements.set('org', AuditLogReplacement.create({
684
+ id: log.organizationId,
685
+ value: org?.name ?? 'verwijderde vereniging',
686
+ type: AuditLogReplacementType.Organization,
687
+ }));
688
+ }
689
+
676
690
  structs.push(
677
691
  AuditLogStruct.create({
678
692
  ...log,
693
+ replacements,
679
694
  user: userStruct,
680
695
  }),
681
696
  );
@@ -46,9 +46,15 @@ export class ContextInstance {
46
46
 
47
47
  static asyncLocalStorage = new AsyncLocalStorage<ContextInstance>();
48
48
 
49
- static get current(): ContextInstance {
49
+ static get optional(): ContextInstance | null {
50
50
  const c = this.asyncLocalStorage.getStore();
51
51
 
52
+ return c ?? null;
53
+ }
54
+
55
+ static get current(): ContextInstance {
56
+ const c = this.optional;
57
+
52
58
  if (!c) {
53
59
  throw new SimpleError({
54
60
  code: 'no_context',
@@ -220,7 +226,7 @@ export class ContextInstance {
220
226
  export const Context = new Proxy(ContextInstance, {
221
227
  get(target, prop, receiver) {
222
228
  const c = target.current[prop];
223
- if (c && typeof c == 'function') {
229
+ if (c && typeof c === 'function') {
224
230
  return c.bind(target.current);
225
231
  }
226
232
  return c;
@@ -1,8 +1,9 @@
1
1
  import { Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from '@stamhoofd/models';
2
2
  import { SQL } from '@stamhoofd/sql';
3
- import { MemberDetails, Permissions, UserPermissions } from '@stamhoofd/structures';
3
+ import { AuditLogSource, MemberDetails, Permissions, UserPermissions } from '@stamhoofd/structures';
4
4
  import crypto from 'crypto';
5
5
  import basex from 'base-x';
6
+ import { AuditLogService } from '../services/AuditLogService';
6
7
 
7
8
  const ALPHABET = '123456789ABCDEFGHJKMNPQRSTUVWXYZ'; // Note: we removed 0, O, I and l to make it easier for humans
8
9
  const customBase = basex(ALPHABET);
@@ -26,61 +27,63 @@ export class MemberUserSyncerStatic {
26
27
  * - email addresses have changed
27
28
  */
28
29
  async onChangeMember(member: MemberWithRegistrations, unlinkUsers: boolean = false) {
29
- const { userEmails, parentAndUnverifiedEmails } = this.getMemberAccessEmails(member.details);
30
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
31
+ const { userEmails, parentAndUnverifiedEmails } = this.getMemberAccessEmails(member.details);
30
32
 
31
- // Make sure all these users have access to the member
32
- for (const email of userEmails) {
33
+ // Make sure all these users have access to the member
34
+ for (const email of userEmails) {
33
35
  // Link users that are found with these email addresses.
34
- await this.linkUser(email, member, false, true);
35
- }
36
-
37
- for (const email of parentAndUnverifiedEmails) {
38
- if (userEmails.includes(email)) {
39
- continue;
36
+ await this.linkUser(email, member, false, true);
40
37
  }
41
38
 
42
- // Link parents and unverified emails
43
- // Now we add the responsibility permissions to the parent if there are no userEmails
44
- const asParent = userEmails.length > 0 || !member.details.unverifiedEmails.includes(email) || member.details.defaultAge < 16;
45
- await this.linkUser(email, member, asParent, true);
46
- }
39
+ for (const email of parentAndUnverifiedEmails) {
40
+ if (userEmails.includes(email)) {
41
+ continue;
42
+ }
43
+
44
+ // Link parents and unverified emails
45
+ // Now we add the responsibility permissions to the parent if there are no userEmails
46
+ const asParent = userEmails.length > 0 || !member.details.unverifiedEmails.includes(email) || member.details.defaultAge < 16;
47
+ await this.linkUser(email, member, asParent, true);
48
+ }
47
49
 
48
- if (unlinkUsers && !member.details.parentsHaveAccess) {
50
+ if (unlinkUsers && !member.details.parentsHaveAccess) {
49
51
  // Remove access of users that are not in this list
50
52
  // NOTE: we should only do this once a year (preferably on the birthday of the member)
51
53
  // only once because otherwise users loose the access to a member during the creation of the member, or when they have changed their email address
52
54
  // users can regain access to a member after they have lost control by using the normal verification flow when detecting duplicate members
53
55
 
54
- for (const user of member.users) {
55
- if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
56
- await this.unlinkUser(user, member);
56
+ for (const user of member.users) {
57
+ if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
58
+ await this.unlinkUser(user, member);
59
+ }
57
60
  }
58
61
  }
59
- }
60
- else {
62
+ else {
61
63
  // Only auto unlink users that do not have an account
62
- for (const user of member.users) {
63
- if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
64
- if (!user.hasAccount()) {
65
- await this.unlinkUser(user, member);
66
- }
67
- else {
64
+ for (const user of member.users) {
65
+ if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
66
+ if (!user.hasAccount()) {
67
+ await this.unlinkUser(user, member);
68
+ }
69
+ else {
68
70
  // Make sure only linked as a parent, not as user self
69
71
  // This makes sure we don't inherit permissions and aren't counted as 'being' the member
70
- await this.linkUser(user.email, member, true);
72
+ await this.linkUser(user.email, member, true);
73
+ }
71
74
  }
72
75
  }
73
76
  }
74
- }
75
77
 
76
- if (member.details.securityCode === null) {
77
- console.log('Generating security code for member ' + member.id);
78
+ if (member.details.securityCode === null) {
79
+ console.log('Generating security code for member ' + member.id);
78
80
 
79
- const length = 16;
80
- const code = customBase.encode(await randomBytes(100)).toUpperCase().substring(0, length);
81
- member.details.securityCode = code;
82
- await member.save();
83
- }
81
+ const length = 16;
82
+ const code = customBase.encode(await randomBytes(100)).toUpperCase().substring(0, length);
83
+ member.details.securityCode = code;
84
+ await member.save();
85
+ }
86
+ });
84
87
  }
85
88
 
86
89
  getMemberAccessEmails(details: MemberDetails) {
@@ -106,10 +109,12 @@ export class MemberUserSyncerStatic {
106
109
  }
107
110
 
108
111
  async onDeleteMember(member: MemberWithRegistrations) {
109
- for (const u of member.users) {
110
- console.log('Unlinking user ' + u.email + ' from deleted member ' + member.id);
111
- await this.unlinkUser(u, member);
112
- }
112
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
113
+ for (const u of member.users) {
114
+ console.log('Unlinking user ' + u.email + ' from deleted member ' + member.id);
115
+ await this.unlinkUser(u, member);
116
+ }
117
+ });
113
118
  }
114
119
 
115
120
  async getResponsibilitiesForMembers(memberIds: string[]) {
@@ -1,18 +1,20 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import { Group, Member, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod, SetupStepUpdater } from '@stamhoofd/models';
3
3
  import { QueueHandler } from '@stamhoofd/queues';
4
- import { Group as GroupStruct, PermissionLevel } from '@stamhoofd/structures';
4
+ import { AuditLogSource, Group as GroupStruct, PermissionLevel } from '@stamhoofd/structures';
5
5
  import { PatchOrganizationRegistrationPeriodsEndpoint } from '../endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint';
6
6
  import { AuthenticatedStructures } from './AuthenticatedStructures';
7
7
  import { MemberUserSyncer } from './MemberUserSyncer';
8
+ import { AuditLogService } from '../services/AuditLogService';
8
9
 
9
10
  export class PeriodHelper {
10
11
  static async moveOrganizationToPeriod(organization: Organization, period: RegistrationPeriod) {
11
- console.log('moveOrganizationToPeriod', organization.id, period.id);
12
-
13
- await this.createOrganizationPeriodForPeriod(organization, period);
14
- organization.periodId = period.id;
15
- await organization.save();
12
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
13
+ console.log('moveOrganizationToPeriod', organization.id, period.id);
14
+ await this.createOrganizationPeriodForPeriod(organization, period);
15
+ organization.periodId = period.id;
16
+ await organization.save();
17
+ });
16
18
  }
17
19
 
18
20
  static async stopAllResponsibilities() {
@@ -169,29 +171,31 @@ export class PeriodHelper {
169
171
 
170
172
  const batchSize = 100;
171
173
  await QueueHandler.schedule(tag, async () => {
172
- let lastId = '';
173
-
174
- while (true) {
175
- const groups = await Group.where(
176
- {
177
- id: { sign: '>', value: lastId },
178
- periodId: period.id,
179
- },
180
- {
181
- limit: batchSize,
182
- sort: ['id'],
183
- },
184
- );
185
-
186
- for (const group of groups) {
187
- await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(GroupStruct.patch({ id: group.id }), period);
188
- lastId = group.id;
189
- }
174
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
175
+ let lastId = '';
176
+
177
+ while (true) {
178
+ const groups = await Group.where(
179
+ {
180
+ id: { sign: '>', value: lastId },
181
+ periodId: period.id,
182
+ },
183
+ {
184
+ limit: batchSize,
185
+ sort: ['id'],
186
+ },
187
+ );
188
+
189
+ for (const group of groups) {
190
+ await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(GroupStruct.patch({ id: group.id }), period);
191
+ lastId = group.id;
192
+ }
190
193
 
191
- if (groups.length < batchSize) {
192
- break;
194
+ if (groups.length < batchSize) {
195
+ break;
196
+ }
193
197
  }
194
- }
198
+ });
195
199
  });
196
200
  }
197
201
  }
@@ -1,38 +1,41 @@
1
1
  import { Organization, Platform } from '@stamhoofd/models';
2
2
  import { QueueHandler } from '@stamhoofd/queues';
3
- import { OrganizationTag, TagHelper as SharedTagHelper } from '@stamhoofd/structures';
3
+ import { AuditLogSource, OrganizationTag, TagHelper as SharedTagHelper } from '@stamhoofd/structures';
4
4
  import { ModelHelper } from './ModelHelper';
5
+ import { AuditLogService } from '../services/AuditLogService';
5
6
 
6
7
  export class TagHelper extends SharedTagHelper {
7
8
  static async updateOrganizations() {
8
9
  const queueId = 'update-tags-on-organizations';
9
10
  QueueHandler.cancel(queueId);
10
11
 
11
- await QueueHandler.schedule(queueId, async () => {
12
- let platform = await Platform.getShared();
12
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
13
+ await QueueHandler.schedule(queueId, async () => {
14
+ let platform = await Platform.getShared();
13
15
 
14
- const tagCounts = new Map<string, number>();
15
- await this.loopOrganizations(async (organizations) => {
16
- for (const organization of organizations) {
17
- organization.meta.tags = this.getAllTagsFromHierarchy(organization.meta.tags, platform.config.tags);
16
+ const tagCounts = new Map<string, number>();
17
+ await this.loopOrganizations(async (organizations) => {
18
+ for (const organization of organizations) {
19
+ organization.meta.tags = this.getAllTagsFromHierarchy(organization.meta.tags, platform.config.tags);
18
20
 
19
- for (const tag of organization.meta.tags) {
20
- tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
21
+ for (const tag of organization.meta.tags) {
22
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
23
+ }
21
24
  }
22
- }
23
25
 
24
- await Promise.all(organizations.map(organization => organization.save()));
25
- });
26
+ await Promise.all(organizations.map(organization => organization.save()));
27
+ });
26
28
 
27
- // Reload platform to avoid race conditions
28
- platform = await Platform.getShared();
29
- for (const [tag, count] of tagCounts.entries()) {
30
- const tagObject = platform.config.tags.find(t => t.id === tag);
31
- if (tagObject) {
32
- tagObject.organizationCount = count;
29
+ // Reload platform to avoid race conditions
30
+ platform = await Platform.getShared();
31
+ for (const [tag, count] of tagCounts.entries()) {
32
+ const tagObject = platform.config.tags.find(t => t.id === tag);
33
+ if (tagObject) {
34
+ tagObject.organizationCount = count;
35
+ }
33
36
  }
34
- }
35
- await platform.save();
37
+ await platform.save();
38
+ });
36
39
  });
37
40
  }
38
41
 
@@ -1,5 +1,5 @@
1
1
  import { Migration } from '@simonbackx/simple-database';
2
- import { MembershipHelper } from '../helpers/MembershipHelper';
2
+ import { PlatformMembershipService } from '../services/PlatformMembershipService';
3
3
 
4
4
  export default new Migration(async () => {
5
5
  if (STAMHOOFD.environment == 'test') {
@@ -13,5 +13,5 @@ export default new Migration(async () => {
13
13
  }
14
14
 
15
15
  process.stdout.write('\n');
16
- await MembershipHelper.updateAll();
16
+ await PlatformMembershipService.updateAll();
17
17
  });