@stamhoofd/backend 2.136.3 → 2.137.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.136.3",
3
+ "version": "2.137.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -46,34 +46,34 @@
46
46
  "test:reset": "yarn -s build:full && yarn -s test"
47
47
  },
48
48
  "dependencies": {
49
- "@aws-sdk/client-s3": "3.1068.0",
50
- "@aws-sdk/client-ses": "3.1068.0",
51
- "@aws-sdk/client-sesv2": "3.1068.0",
52
- "@aws-sdk/client-sqs": "3.1068.0",
53
- "@aws-sdk/s3-request-presigner": "3.1068.0",
49
+ "@aws-sdk/client-s3": "3.1090.0",
50
+ "@aws-sdk/client-ses": "3.1090.0",
51
+ "@aws-sdk/client-sesv2": "3.1090.0",
52
+ "@aws-sdk/client-sqs": "3.1090.0",
53
+ "@aws-sdk/s3-request-presigner": "3.1090.0",
54
54
  "@bwip-js/node": "4.11.1",
55
- "@mollie/api-client": "4.5.0",
55
+ "@mollie/api-client": "4.6.0",
56
56
  "@simonbackx/simple-database": "1.37.1",
57
57
  "@simonbackx/simple-encoding": "2.26.11",
58
58
  "@simonbackx/simple-endpoints": "1.21.1",
59
59
  "@simonbackx/simple-errors": "1.5.0",
60
60
  "@simonbackx/simple-logging": "1.0.1",
61
- "@stamhoofd/backend-env": "2.136.3",
62
- "@stamhoofd/backend-i18n": "2.136.3",
63
- "@stamhoofd/backend-middleware": "2.136.3",
64
- "@stamhoofd/crons": "2.136.3",
65
- "@stamhoofd/email": "2.136.3",
66
- "@stamhoofd/excel-writer": "2.136.3",
67
- "@stamhoofd/logging": "2.136.3",
68
- "@stamhoofd/models": "2.136.3",
69
- "@stamhoofd/object-differ": "2.136.3",
70
- "@stamhoofd/queues": "2.136.3",
71
- "@stamhoofd/sql": "2.136.3",
72
- "@stamhoofd/structures": "2.136.3",
73
- "@stamhoofd/types": "2.136.3",
74
- "@stamhoofd/utility": "2.136.3",
61
+ "@stamhoofd/backend-env": "2.137.0",
62
+ "@stamhoofd/backend-i18n": "2.137.0",
63
+ "@stamhoofd/backend-middleware": "2.137.0",
64
+ "@stamhoofd/crons": "2.137.0",
65
+ "@stamhoofd/email": "2.137.0",
66
+ "@stamhoofd/excel-writer": "2.137.0",
67
+ "@stamhoofd/logging": "2.137.0",
68
+ "@stamhoofd/models": "2.137.0",
69
+ "@stamhoofd/object-differ": "2.137.0",
70
+ "@stamhoofd/queues": "2.137.0",
71
+ "@stamhoofd/sql": "2.137.0",
72
+ "@stamhoofd/structures": "2.137.0",
73
+ "@stamhoofd/types": "2.137.0",
74
+ "@stamhoofd/utility": "2.137.0",
75
75
  "archiver": "7.0.1",
76
- "axios": "1.16.0",
76
+ "axios": "1.18.0",
77
77
  "base-x": "3.0.11",
78
78
  "chalk": "5.6.2",
79
79
  "cookie": "0.7.2",
@@ -86,18 +86,18 @@
86
86
  "luxon": "3.7.2",
87
87
  "mailparser": "3.9.9",
88
88
  "mockdate": "3.0.5",
89
- "mysql2": "3.22.5",
89
+ "mysql2": "3.23.1",
90
90
  "node-rsa": "2.0.0",
91
91
  "openid-client": "5.7.1",
92
- "stripe": "16.12.0"
92
+ "stripe": "16.12.0",
93
+ "uuid": "14.0.0"
93
94
  },
94
95
  "devDependencies": {
95
- "@stamhoofd/test-utils": "2.136.3",
96
+ "@stamhoofd/test-utils": "2.137.0",
96
97
  "@types/cookie": "0.6.0",
97
98
  "@types/luxon": "3.7.1",
98
99
  "@types/mailparser": "3.4.6",
99
- "@types/mysql": "2.15.27",
100
- "@types/node": "22.15.34",
100
+ "@types/node": "22.20.1",
101
101
  "@types/uuid": "8.3.4",
102
102
  "nock": "14.0.15",
103
103
  "qs": "6.15.2",
@@ -108,5 +108,5 @@
108
108
  "publishConfig": {
109
109
  "access": "public"
110
110
  },
111
- "gitHead": "ef990c776a392a3857dc81309ca6a0bb4d524f63"
111
+ "gitHead": "f327bce84bd0c5ca7160e3e71ad03700d520b086"
112
112
  }
@@ -45,6 +45,10 @@ export const GroupLogger = new ModelLogger(Group, {
45
45
  if (model.settings.eventId) {
46
46
  result.objectId = model.settings.eventId;
47
47
  }
48
+
49
+ if (model.eventId) {
50
+ result.objectId = model.eventId;
51
+ }
48
52
  break;
49
53
  case AuditLogType.GroupDeleted:
50
54
  // do not log
@@ -75,6 +79,15 @@ export const GroupLogger = new ModelLogger(Group, {
75
79
  })],
76
80
  ]);
77
81
  }
82
+ if (model.eventId) {
83
+ return new Map([
84
+ ['e', AuditLogReplacement.create({
85
+ id: model.eventId,
86
+ value: model.settings.name.toString(),
87
+ type: AuditLogReplacementType.Event,
88
+ })],
89
+ ]);
90
+ }
78
91
  return new Map([
79
92
  ['e', AuditLogReplacement.create({
80
93
  id: model.id,
@@ -527,7 +527,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
527
527
  });
528
528
  }
529
529
 
530
- if (existingGroup.settings.eventId !== null && existingGroup.settings.eventId !== event.id) {
530
+ if (existingGroup.eventId !== null && existingGroup.eventId !== event.id) {
531
531
  throw new SimpleError({
532
532
  code: 'invalid_group',
533
533
  message: 'Group is already linked to another event',
@@ -544,7 +544,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
544
544
  existingGroup.type = GroupType.EventRegistration;
545
545
  }
546
546
 
547
- existingGroup.settings.eventId = event.id;
547
+ existingGroup.eventId = event.id;
548
548
 
549
549
  // update group categories
550
550
  const period = await RegistrationPeriod.getByID(existingGroup.periodId);
@@ -572,7 +572,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
572
572
  } catch (e) {
573
573
  // reset the group
574
574
  existingGroup.type = originalGroupType;
575
- existingGroup.settings.eventId = null;
575
+ existingGroup.eventId = null;
576
576
  await existingGroup.save();
577
577
 
578
578
  // delete the event again if failed to link the group
@@ -54,7 +54,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
54
54
 
55
55
  // Validate domains
56
56
 
57
- if (request.body.registerDomain !== null && !request.body.registerDomain.match(/^(?:[a-z0-9-]+\.)?[a-z0-9-]+\.[a-z]+$/i)) {
57
+ if (request.body.registerDomain !== null && !request.body.registerDomain.match(/^(?:[a-z0-9-]+\.)?(?:[a-z0-9-]+\.)?[a-z0-9-]+\.[a-z]+$/i)) {
58
58
  throw new SimpleError({
59
59
  code: 'invalid_domain',
60
60
  message: 'registerDomain is invalid',
@@ -389,6 +389,12 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
389
389
  await event.save();
390
390
  }
391
391
 
392
+ const groups = await Group.select().where('waitingListId', id).fetch();
393
+ for (const group of groups) {
394
+ group.waitingListId = null;
395
+ await group.save();
396
+ }
397
+
392
398
  // delete invitations
393
399
  await RegistrationInvitation.delete().where('groupId', id);
394
400
  }
@@ -574,6 +580,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
574
580
  };
575
581
 
576
582
  const patch = struct;
583
+ let syncGroupWithEvent = false;
577
584
  if (patch.waitingList !== undefined) {
578
585
  if (patch.waitingList === null) {
579
586
  // delete
@@ -586,7 +593,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
586
593
  field: 'waitingList',
587
594
  message: 'Cannot patch waitingList',
588
595
  });
589
- } else {
596
+ } else if (model.waitingListId !== patch.waitingList.id) {
590
597
  if (model.waitingListId) {
591
598
  model.waitingListId = null;
592
599
  }
@@ -612,6 +619,8 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
612
619
  }
613
620
 
614
621
  model.waitingListId = existing.id;
622
+ existing.eventId = model.eventId;
623
+ syncGroupWithEvent = true;
615
624
  } else {
616
625
  throw new SimpleError({
617
626
  code: 'invalid_waiting_list',
@@ -648,6 +657,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
648
657
  await model.updateOccupancy();
649
658
  await model.save();
650
659
 
660
+ if (model.eventId && syncGroupWithEvent) {
661
+ const event = await Event.getByID(model.eventId);
662
+ if (event) {
663
+ await event.syncGroupRequirements(model);
664
+ }
665
+ }
666
+
651
667
  return model;
652
668
  }
653
669
 
@@ -75,7 +75,8 @@ export class RetrieveUitpasSocialTariffPricesEndpoint extends Endpoint<Params, Q
75
75
  human: $t('%189'),
76
76
  });
77
77
  }
78
- await UitpasService.checkUitpasNumbers(request.body.uitpasNumbers); // Throws if invalid
78
+ const organization = await Context.setOrganizationScope({ willAuthenticate: false });
79
+ await UitpasService.checkUitpasNumbers(organization.id, request.body.uitpasNumbers); // Throws if invalid
79
80
  const uitpasPriceCheckResponse = UitpasPriceCheckResponse.create({
80
81
  prices: request.body.uitpasNumbers.map(_ => reducedPrice), // All reduced prices are the same in this non-official flow
81
82
  });
@@ -120,11 +120,15 @@ export class AuthenticatedStructures {
120
120
  const structs: GroupStruct[] = [];
121
121
  for (const group of groups) {
122
122
  const waitingList = waitingLists.find(g => g.id === group.waitingListId) ?? null;
123
- const waitingListStruct = waitingList ? GroupStruct.create(waitingList) : null;
123
+ let waitingListStruct = waitingList ? GroupStruct.create(waitingList) : null;
124
124
  if (waitingList && waitingListStruct && !await Context.optionalAuth?.canAccessGroup(waitingList)) {
125
125
  waitingListStruct.privateSettings = null;
126
126
  }
127
127
 
128
+ if (waitingListStruct?.deletedAt) {
129
+ waitingListStruct = null;
130
+ }
131
+
128
132
  const struct = GroupStruct.create({
129
133
  ...group,
130
134
  waitingList: waitingListStruct,
@@ -163,6 +167,7 @@ export class AuthenticatedStructures {
163
167
  const whereWaitingList = SQL.where('organizationId', organizationIds)
164
168
  .and('periodId', periodIds)
165
169
  .and('type', GroupType.WaitingList)
170
+ .and('eventId', null)
166
171
  .and('deletedAt', null);
167
172
 
168
173
  if (groupIds.length) {
@@ -64,12 +64,15 @@ export class UitpasTokenRepository {
64
64
  }
65
65
 
66
66
  /**
67
- * organizationId (null means platform) -> UitpasTokenRepository
67
+ * organizationId (null means platform) -> UitpasTokenRepository or null if no creds are configured for this org/platform
68
68
  */
69
- static knownTokens: Map<string | null, UitpasTokenRepository> = new Map();
69
+ private static knownRepos: Map<string | null, UitpasTokenRepository | null> = new Map();
70
70
 
71
- private static getRepoFromMemory(organizationId: string | null): UitpasTokenRepository | null {
72
- return UitpasTokenRepository.knownTokens.get(organizationId) ?? null;
71
+ /**
72
+ * @returns null if no creds configured, undefined if not found in memory, UitpasTokenRepository if found in memory
73
+ */
74
+ private static getRepoFromMemory(organizationId: string | null): UitpasTokenRepository | undefined | null {
75
+ return UitpasTokenRepository.knownRepos.get(organizationId);
73
76
  }
74
77
 
75
78
  private static async getModelFromDb(organizationId: string | null) {
@@ -154,9 +157,8 @@ export class UitpasTokenRepository {
154
157
  return this.accessToken;
155
158
  }
156
159
 
157
- private static setRepoInMemory(organizationId: string | null, repo: UitpasTokenRepository) {
158
- UitpasTokenRepository.knownTokens.set(organizationId, repo);
159
- return repo;
160
+ private static setRepoInMemory(organizationId: string | null, repo: UitpasTokenRepository | null) {
161
+ UitpasTokenRepository.knownRepos.set(organizationId, repo);
160
162
  }
161
163
 
162
164
  private static async setModelInDb(organizationId: string | null, model: UitpasClientCredential) {
@@ -173,6 +175,17 @@ export class UitpasTokenRepository {
173
175
  return model; // return new model
174
176
  }
175
177
 
178
+ static async getAccessTokenForWithPlatformFallback(orgId: string, forceRefresh: boolean = false): Promise<string> {
179
+ try {
180
+ return await UitpasTokenRepository.getAccessTokenFor(orgId, forceRefresh);
181
+ } catch (error) {
182
+ if (error instanceof SimpleError && error.code === 'uitpas_api_not_configured_for_this_organization') {
183
+ return await UitpasTokenRepository.getAccessTokenFor(null, forceRefresh); // fallback to platform
184
+ }
185
+ throw error;
186
+ }
187
+ }
188
+
176
189
  /**
177
190
  * Get the access token for the organization or platform.
178
191
  * @param organizationId the organization ID for which to get the access token. If null, it means the platform.
@@ -180,60 +193,101 @@ export class UitpasTokenRepository {
180
193
  * @returns Promise<string> the access token for the organization or platform
181
194
  * @throws SimpleError if the token cannot be obtained or the API is not configured
182
195
  */
183
- static async getAccessTokenFor(organizationId: string | null = null, forceRefresh: boolean = false): Promise<string> {
184
- let repo = UitpasTokenRepository.getRepoFromMemory(organizationId);
185
- if (repo && repo.accessToken && !forceRefresh && repo.expiresOn > new Date()) {
186
- return repo.accessToken;
196
+ static async getAccessTokenFor(organizationId: string | null, forceRefresh: boolean = false): Promise<string> {
197
+ const notConfiguredError = new SimpleError({
198
+ code: 'uitpas_api_not_configured_for_this_organization',
199
+ message: `UiTPAS api not configured for organization with id ${organizationId}`,
200
+ human: $t('%1BJ'),
201
+ });
202
+
203
+ const tryFromMemory = (): string | UitpasTokenRepository | undefined => {
204
+ const cached = UitpasTokenRepository.getRepoFromMemory(organizationId);
205
+ if (cached === null) {
206
+ throw notConfiguredError;
207
+ }
208
+ if (cached && cached.accessToken && !forceRefresh && cached.expiresOn > new Date()) {
209
+ return cached.accessToken;
210
+ }
211
+ return cached; // not found in memory or expired or no access token or forceRefresh
212
+ };
213
+
214
+ const memoryResult = tryFromMemory();
215
+ if (typeof memoryResult === 'string') {
216
+ return memoryResult; // access token found in memory
187
217
  }
188
218
 
189
219
  // Prevent multiple concurrent requests for the same organization, asking for an access token to the UiTPAS API.
190
220
  // The queue can only run one at a time for the same organizationId
191
221
  return await UitpasTokenRepository.handleInQueue(organizationId, async () => {
192
222
  // we re-search for the repo, as another call to this funcion might have added while we we're waiting in the queue
193
- repo = UitpasTokenRepository.getRepoFromMemory(organizationId);
194
- if (repo && repo.accessToken && !forceRefresh && repo.expiresOn > new Date()) {
195
- return repo.accessToken;
223
+ let repo: UitpasTokenRepository;
224
+ const memoryResult = tryFromMemory();
225
+ if (typeof memoryResult === 'string') {
226
+ return memoryResult; // access token found in memory
196
227
  }
197
- if (!repo) {
228
+ if (!memoryResult) {
198
229
  const model = await UitpasTokenRepository.getModelFromDb(organizationId);
199
230
  if (!model) {
200
- throw new SimpleError({
201
- code: 'uitpas_api_not_configured_for_this_organization',
202
- message: `UiTPAS api not configured for organization with id ${organizationId}`,
203
- human: $t('%1BJ'),
204
- });
231
+ UitpasTokenRepository.setRepoInMemory(organizationId, null);
232
+ throw notConfiguredError;
205
233
  }
206
- repo = UitpasTokenRepository.setRepoInMemory(organizationId, new UitpasTokenRepository(model)); // store in memory
234
+ repo = new UitpasTokenRepository(model);
235
+ UitpasTokenRepository.setRepoInMemory(organizationId, repo);
236
+ } else {
237
+ repo = memoryResult;
207
238
  }
208
239
  // ask for a new access token
209
- return repo.getNewAccessToken();
240
+ return await repo.getNewAccessToken();
210
241
  });
211
242
  }
212
243
 
213
- static async getClientIdFor(organizationId: string | null): Promise<string> {
214
- const repo = UitpasTokenRepository.getRepoFromMemory(organizationId);
215
- if (!repo) {
216
- const model = await UitpasClientCredential.select().where('organizationId', organizationId).first(false);
217
- if (!model) {
218
- return ''; // no client ID and secret configured
244
+ static async getClientIdFor(organizationId: string): Promise<string> {
245
+ const tryFromMemory = (): string | undefined => {
246
+ const cached = UitpasTokenRepository.getRepoFromMemory(organizationId);
247
+ if (cached === null) {
248
+ return '';
249
+ }
250
+ if (cached) {
251
+ return cached.uitpasClientCredential.clientId;
219
252
  }
220
- return model.clientId; // client ID configured, but not in memory
253
+ };
254
+
255
+ const memoryResult = tryFromMemory();
256
+ if (typeof memoryResult === 'string') {
257
+ return memoryResult; // client ID found in memory
221
258
  }
222
- return repo.uitpasClientCredential.clientId; // client ID configured and in memory
259
+
260
+ return await UitpasTokenRepository.handleInQueue(organizationId, async () => {
261
+ // re-search memory, as another call to this function might have populated it while we were waiting in the queue
262
+ const memoryResult = tryFromMemory();
263
+ if (typeof memoryResult === 'string') {
264
+ return memoryResult; // client ID found in memory
265
+ }
266
+ const model = await UitpasTokenRepository.getModelFromDb(organizationId);
267
+ if (!model) {
268
+ UitpasTokenRepository.setRepoInMemory(organizationId, null); // remember there are no creds configured
269
+ return '';
270
+ }
271
+ UitpasTokenRepository.setRepoInMemory(organizationId, new UitpasTokenRepository(model)); // store in memory
272
+ return model.clientId; // client ID configured, now also in memory
273
+ });
223
274
  }
224
275
 
225
276
  static async clearClientCredentialsFor(organizationId: string | null): Promise<boolean> {
226
277
  return await UitpasTokenRepository.handleInQueue(organizationId, async () => {
227
- const repo = UitpasTokenRepository.getRepoFromMemory(organizationId);
228
- if (repo) {
278
+ const cached = UitpasTokenRepository.getRepoFromMemory(organizationId);
279
+ if (cached) {
229
280
  // in memory, thus also in db
230
- await repo.uitpasClientCredential.delete(); // remove from db
231
- UitpasTokenRepository.knownTokens.delete(organizationId); // remove from memory
281
+ await cached.uitpasClientCredential.delete(); // remove from db
282
+ UitpasTokenRepository.setRepoInMemory(organizationId, null); // remove from memory
232
283
  return true;
233
284
  }
234
285
  // not in memory, maybe in db
235
286
  const model = await UitpasTokenRepository.getModelFromDb(organizationId);
236
287
  if (model) {
288
+ if (cached === null) {
289
+ console.warn(`Clearing UiTPAS client credentials for organization with id ${organizationId} although it was not in memory, but found in database. This is a bug, memory and database are out of sync.`);
290
+ }
237
291
  await model.delete(); // remove from database
238
292
  return true;
239
293
  }
@@ -0,0 +1,35 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { Event, Group } from '@stamhoofd/models';
3
+
4
+ export default new Migration(async () => {
5
+ if (STAMHOOFD.environment === 'test') {
6
+ console.log('skipped in tests');
7
+ return;
8
+ }
9
+
10
+ await setEventIdOnGroups();
11
+ });
12
+
13
+ export async function setEventIdOnGroups() {
14
+ let count = 0;
15
+
16
+ // Loop all events and link the associated group (and its waiting list) back to the event.
17
+ for await (const event of Event.select().all()) {
18
+ if (!event.groupId) {
19
+ continue;
20
+ }
21
+
22
+ const group = await Group.getByID(event.groupId);
23
+ if (!group) {
24
+ continue;
25
+ }
26
+
27
+ if (group.eventId !== event.id) {
28
+ group.eventId = event.id;
29
+ await event.syncGroupRequirements(group);
30
+ count++;
31
+ }
32
+ }
33
+
34
+ console.log(`Set eventId on ${count} groups`);
35
+ }
@@ -0,0 +1,33 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { BalanceItem, CachedBalance, MergedMember } from '@stamhoofd/models';
3
+ import { SQL } from '@stamhoofd/sql';
4
+ import { SeedTools } from '../helpers/SeedTools.js';
5
+
6
+ export default new Migration(async () => {
7
+ if (STAMHOOFD.environment === 'test') {
8
+ console.log('skipped in tests');
9
+ return;
10
+ }
11
+
12
+ if (STAMHOOFD.platformName === 'stamhoofd') {
13
+ return;
14
+ }
15
+
16
+ // Loop all members
17
+ await SeedTools.loop({
18
+ query: MergedMember.select(),
19
+ batchSize: 100,
20
+ action: async (mergedMember) => {
21
+ const items = await SQL.select('organizationId')
22
+ .from(BalanceItem.table)
23
+ .where('memberId', mergedMember.id)
24
+ .groupBy(SQL.column('organizationId'))
25
+ .fetch();
26
+ for (const item of items) {
27
+ await CachedBalance.updateForMembers(item[BalanceItem.table].organizationId as string, [mergedMember.id]);
28
+ }
29
+ },
30
+ });
31
+
32
+ return Promise.resolve();
33
+ });
@@ -5,10 +5,9 @@ import type { Invoice } from '@stamhoofd/models';
5
5
  import { Image, InvoicedBalanceItem, Organization, Payment, Platform } from '@stamhoofd/models';
6
6
  import { render } from '@stamhoofd/models/helpers/Handlebars.js';
7
7
  import type { Address } from '@stamhoofd/structures';
8
- import { CountryHelper, File, getVATExcemptInvoiceNote, getVATExcemptReasonName, PaymentMethod, PaymentMethodHelper, PaymentStatus } from '@stamhoofd/structures';
8
+ import { CountryHelper, File, getVATExcemptInvoiceNote, getVATExcemptReasonName, PaymentMethod, PaymentMethodHelper, PaymentStatus, Version } from '@stamhoofd/structures';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
10
  import fs from 'fs/promises';
11
- import { VERSION } from 'luxon';
12
11
  import { v4 as uuidv4 } from 'uuid';
13
12
 
14
13
  export class InvoicePdfService {
@@ -260,7 +259,7 @@ export class InvoicePdfService {
260
259
 
261
260
  try {
262
261
  // Issue with system trusted CA in development
263
- const result = await fetch((STAMHOOFD.environment === 'development' ? 'http://' : 'https://') + STAMHOOFD.domains.rendererApi + '/v' + VERSION + '/html-to-pdf', {
262
+ const result = await fetch((STAMHOOFD.environment === 'development' ? 'http://' : 'https://') + STAMHOOFD.domains.rendererApi + '/v' + Version + '/html-to-pdf', {
264
263
  method: 'POST',
265
264
  body: form,
266
265
  signal: controller.signal,
@@ -287,6 +287,7 @@ export class SSOService {
287
287
  httpOnly: true,
288
288
  secure: STAMHOOFD.environment !== 'development',
289
289
  expires: data.expires,
290
+ sameSite: 'lax',
290
291
  });
291
292
  }
292
293
 
@@ -4,19 +4,19 @@ import { UitpasService } from './UitpasService.js';
4
4
  describe.skip('UitpasService', () => {
5
5
  it('should validate a correct Uitpas number with kansentarief', async () => {
6
6
  const validNumbers = ['0900000067513'];
7
- await expect(UitpasService.checkUitpasNumbers(validNumbers)).resolves.toBeUndefined();
7
+ await expect(UitpasService.checkUitpasNumbers('unexisting-org-id', validNumbers)).resolves.toBeUndefined();
8
8
  });
9
9
 
10
10
  it('should throw an error for an invalid Uitpas number', async () => {
11
11
  const invalidNumbers = ['1234567890123'];
12
- await expect(UitpasService.checkUitpasNumbers(invalidNumbers)).rejects.toThrow(
12
+ await expect(UitpasService.checkUitpasNumbers('unexisting-org-id', invalidNumbers)).rejects.toThrow(
13
13
  STExpect.simpleError({ code: 'unsuccessful_but_expected_response_retrieving_pass_by_uitpas_number' }),
14
14
  );
15
15
  });
16
16
 
17
17
  it('should throw an error for a Uitpas number with kansentarief expired', async () => {
18
18
  const expiredNumbers = ['0900000058918'];
19
- await expect(UitpasService.checkUitpasNumbers(expiredNumbers)).rejects.toThrow(
19
+ await expect(UitpasService.checkUitpasNumbers('unexisting-org-id', expiredNumbers)).rejects.toThrow(
20
20
  STExpect.simpleError({ code: 'uitpas_number_issue' }),
21
21
  );
22
22
  });
@@ -309,7 +309,7 @@ export class UitpasService {
309
309
 
310
310
  static async searchUitpasOrganizers(name: string): Promise<UitpasOrganizersResponse> {
311
311
  // https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/list-organizers
312
- const access_token = await UitpasTokenRepository.getAccessTokenFor(); // uses platform credentials
312
+ const access_token = await UitpasTokenRepository.getAccessTokenFor(null); // always use platform credentials
313
313
  return await searchUitpasOrganizers(access_token, name);
314
314
  }
315
315
 
@@ -327,7 +327,7 @@ export class UitpasService {
327
327
  * @param organizationId
328
328
  * @returns clientId or empty string if not configured
329
329
  */
330
- static async getClientIdFor(organizationId: string | null): Promise<string> {
330
+ static async getClientIdFor(organizationId: string): Promise<string> {
331
331
  // Get the uitpas client credentials for the organization
332
332
  return await UitpasTokenRepository.getClientIdFor(organizationId);
333
333
  }
@@ -338,9 +338,9 @@ export class UitpasService {
338
338
  * The field of the error will be the index of the uitpas number in the array.
339
339
  * @param uitpasNumbers The uitpas numbers to check
340
340
  */
341
- static async checkUitpasNumbers(uitpasNumbers: string[]) {
341
+ static async checkUitpasNumbers(organizationId: string, uitpasNumbers: string[]) {
342
342
  // https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/get-a-pass
343
- const access_token = await UitpasTokenRepository.getAccessTokenFor(); // use platform credentials
343
+ const access_token = await UitpasTokenRepository.getAccessTokenForWithPlatformFallback(organizationId);
344
344
  return await checkUitpasNumbers(access_token, uitpasNumbers);
345
345
  }
346
346
 
@@ -350,7 +350,7 @@ export class UitpasService {
350
350
  static async getPassByUitpasNumber(uitpasNumber: string): Promise<GetPassResponse> {
351
351
  let access_token!: string;
352
352
  try {
353
- access_token = await UitpasTokenRepository.getAccessTokenFor(); // use platform credentials
353
+ access_token = await UitpasTokenRepository.getAccessTokenFor(null); // always use platform credentials (for now)
354
354
  } catch (e) {
355
355
  if (isSimpleError(e) && e.hasCode('uitpas_api_not_configured_for_platform')) {
356
356
  console.error('UiTPAS API has not been configured for this platform, so defaulting to an unknown status for number ', uitpasNumber);
@@ -387,8 +387,8 @@ export class UitpasService {
387
387
  }
388
388
 
389
389
  static async validateCart(organizationId: string, webshopId: string, cart: Cart, exisitingOrderId?: string): Promise<Cart> {
390
- let access_token_org: string | null = null;
391
- let access_token_platform: string | null = null;
390
+ let access_token: string | undefined;
391
+ const unofficialsToBeChecked: string[][] = [];
392
392
  for (const item of cart.items) {
393
393
  if (item.uitpasNumbers.length === 0) {
394
394
  continue;
@@ -416,8 +416,8 @@ export class UitpasService {
416
416
  });
417
417
  }
418
418
 
419
- access_token_org = access_token_org ?? await UitpasTokenRepository.getAccessTokenFor(organizationId);
420
- const verified = await getSocialTariffForUitpasNumbers(access_token_org, item.uitpasNumbers.map(p => p.uitpasNumber), basePrice, item.product.uitpasEvent.url);
419
+ access_token = access_token ?? await UitpasTokenRepository.getAccessTokenFor(organizationId);
420
+ const verified = await getSocialTariffForUitpasNumbers(access_token, item.uitpasNumbers.map(p => p.uitpasNumber), basePrice, item.product.uitpasEvent.url);
421
421
  if (verified.length < item.uitpasNumbers.length) {
422
422
  throw new SimpleError({
423
423
  code: 'uitpas_social_tariff_price_mismatch',
@@ -441,9 +441,14 @@ export class UitpasService {
441
441
  }
442
442
  }
443
443
  } else {
444
- // non-official flow
445
- access_token_platform = access_token_platform ?? await UitpasTokenRepository.getAccessTokenFor();
446
- await checkUitpasNumbers(access_token_platform, item.uitpasNumbers.map(p => p.uitpasNumber));
444
+ // non-official flow: do all checks after official flows (for maximum token re-usage), but retaining index numbering for error reporting
445
+ unofficialsToBeChecked.push(item.uitpasNumbers.map(p => p.uitpasNumber));
446
+ }
447
+ }
448
+ if (unofficialsToBeChecked.length > 0) {
449
+ const token = access_token ?? await UitpasTokenRepository.getAccessTokenForWithPlatformFallback(organizationId);
450
+ for (const uitpasNumbers of unofficialsToBeChecked) {
451
+ await checkUitpasNumbers(token, uitpasNumbers);
447
452
  }
448
453
  }
449
454
  return cart;
@@ -74,21 +74,22 @@ export async function checkPermissionsFor(access_token: string, organizationId:
74
74
  });
75
75
  });
76
76
  assertIsPermissionsResponse(json);
77
- const neededPermissions = !organizationId
78
- ? [{
79
- permission: 'PASSES_READ',
80
- human: 'Basis UiTPAS informatie ophalen met UiTPAS nummer',
81
- }]
82
- : [{
83
- permission: 'TARIFFS_READ',
84
- human: 'Tarieven opvragen',
85
- }, {
86
- permission: 'TICKETSALES_REGISTER',
87
- human: 'Ticketsales registreren',
88
- }, {
89
- permission: 'TICKETSALES_SEARCH',
90
- human: 'Ticketsales zoeken',
91
- }];
77
+ const neededPermissions = [{
78
+ permission: 'PASSES_READ',
79
+ human: 'Basis UiTPAS informatie ophalen met UiTPAS nummer',
80
+ }];
81
+ if (organizationId) {
82
+ neededPermissions.push(...[{
83
+ permission: 'TARIFFS_READ',
84
+ human: 'Tarieven opvragen',
85
+ }, {
86
+ permission: 'TICKETSALES_REGISTER',
87
+ human: 'Ticketsales registreren',
88
+ }, {
89
+ permission: 'TICKETSALES_SEARCH',
90
+ human: 'Ticketsales zoeken',
91
+ }]);
92
+ }
92
93
  const item = json.find(item => item.organizer.id === uitpasOrganizerId);
93
94
  if (!item) {
94
95
  const organizers = Formatter.joinLast(json.map(i => i.organizer.name), ', ', ' ' + $t('%1BM') + ' ');
@@ -14,6 +14,11 @@ export const groupFilterCompilers: SQLFilterDefinitions = {
14
14
  type: SQLValueType.String,
15
15
  nullable: false,
16
16
  }),
17
+ eventId: createColumnFilter({
18
+ expression: SQL.column('eventId'),
19
+ type: SQLValueType.String,
20
+ nullable: true,
21
+ }),
17
22
  periodId: createColumnFilter({
18
23
  expression: SQL.column('periodId'),
19
24
  type: SQLValueType.String,
@@ -1,67 +0,0 @@
1
- import { Migration } from '@simonbackx/simple-database';
2
- import { CachedBalance, Member, Organization, Platform, Registration, User } from '@stamhoofd/models';
3
- import { SeedTools } from '../helpers/SeedTools.js';
4
-
5
- export default new Migration(async () => {
6
- if (STAMHOOFD.environment === 'test') {
7
- console.log('skipped in tests');
8
- return;
9
- }
10
-
11
- if (STAMHOOFD.platformName !== 'stamhoofd') {
12
- return;
13
- }
14
-
15
- if (STAMHOOFD.userMode !== 'organization') {
16
- return;
17
- }
18
-
19
- // Loop all members
20
- await SeedTools.loopBatched({
21
- query: Organization.select(),
22
- // useTransactionPerBatch: true,
23
- batchSize: 100,
24
- batchAction: async (organizations) => {
25
- for (const organization of organizations) {
26
- for await (const batch of Member.select('id').where('organizationId', organization.id).limit(1000).allBatched()) {
27
- await CachedBalance.updateForMembers(organization.id, batch.map(m => m.id));
28
- }
29
-
30
- for await (const batch of User.select('id').where('organizationId', organization.id).limit(1000).allBatched()) {
31
- await CachedBalance.updateForUsers(organization.id, batch.map(m => m.id));
32
- }
33
-
34
- for await (const batch of Registration.select('id').where('organizationId', organization.id).limit(1000).allBatched()) {
35
- await CachedBalance.updateForRegistrations(organization.id, batch.map(m => m.id));
36
- }
37
- }
38
- },
39
- });
40
-
41
- const membershipOrganizationId = (await Platform.getShared()).membershipOrganizationId;
42
- if (membershipOrganizationId) {
43
- const membershipOrganization = await Organization.getByID(membershipOrganizationId, true);
44
- await SeedTools.loopBatched({
45
- query: Organization.select('id').where('id', '!=', membershipOrganization.id),
46
- // useTransactionPerBatch: true,
47
- batchSize: 1000,
48
- batchAction: async (organizations) => {
49
- await CachedBalance.updateForOrganizations(membershipOrganization.id, organizations.map(m => m.id));
50
- },
51
- });
52
- }
53
-
54
- // await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
55
- // for await (const items of BalanceItem.select().limit(10_000).allBatched()) {
56
- // await BalanceItemService.updatePaidAndPending(items);
57
- // await BalanceItemService.flushAll();
58
- //
59
- // progressLogger.update(items.length);
60
- // }
61
- // });
62
-
63
- // console.log('Updated outstanding balance for ' + progressLogger.total + ' items');
64
-
65
- // Do something here
66
- return Promise.resolve();
67
- });