@stamhoofd/backend 2.89.1 → 2.90.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 (66) hide show
  1. package/package.json +12 -11
  2. package/src/boot.ts +2 -0
  3. package/src/crons/balance-emails.ts +1 -6
  4. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +1 -1
  5. package/src/endpoints/admin/organizations/SearchUitpasOrganizersEndpoint.ts +42 -0
  6. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +4 -4
  7. package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +3 -3
  8. package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +23 -2
  10. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +6 -6
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +8 -6
  12. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
  13. package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -0
  14. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +10 -8
  15. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -0
  16. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +2 -2
  17. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +3 -6
  18. package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +38 -0
  19. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +31 -1
  20. package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +108 -0
  21. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +1 -1
  22. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -2
  23. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +1 -1
  24. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +9 -1
  25. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +3 -2
  26. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +1 -1
  27. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +2 -9
  28. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +1 -7
  29. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +68 -1
  30. package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +27 -20
  31. package/src/helpers/AdminPermissionChecker.ts +129 -22
  32. package/src/helpers/AuthenticatedStructures.ts +13 -10
  33. package/src/helpers/Context.ts +1 -1
  34. package/src/helpers/UitpasTokenRepository.ts +125 -35
  35. package/src/helpers/ViesHelper.ts +2 -1
  36. package/src/seeds/0000000002-clear-stamhoofd-email-templates.ts +13 -0
  37. package/src/seeds/0000000003-default-email-templates.ts +20 -0
  38. package/src/seeds/data/default-email-templates.sql +55 -0
  39. package/src/services/RegistrationService.ts +6 -4
  40. package/src/services/uitpas/UitpasService.test.ts +23 -0
  41. package/src/services/uitpas/UitpasService.ts +222 -0
  42. package/src/services/uitpas/checkPermissionsFor.ts +111 -0
  43. package/src/services/uitpas/checkUitpasNumbers.ts +180 -0
  44. package/src/services/uitpas/getSocialTariffForEvent.ts +90 -0
  45. package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +181 -0
  46. package/src/services/uitpas/searchUitpasOrganizers.ts +93 -0
  47. package/src/sql-filters/audit-logs.ts +26 -6
  48. package/src/sql-filters/balance-item-payments.ts +23 -8
  49. package/src/sql-filters/base-registration-filter-compilers.ts +74 -23
  50. package/src/sql-filters/documents.ts +46 -13
  51. package/src/sql-filters/event-notifications.ts +48 -12
  52. package/src/sql-filters/events.ts +62 -26
  53. package/src/sql-filters/groups.ts +12 -12
  54. package/src/sql-filters/members.ts +325 -137
  55. package/src/sql-filters/orders.ts +96 -48
  56. package/src/sql-filters/organization-registration-periods.ts +16 -4
  57. package/src/sql-filters/organizations.ts +105 -99
  58. package/src/sql-filters/payments.ts +97 -47
  59. package/src/sql-filters/receivable-balances.ts +56 -19
  60. package/src/sql-filters/registration-periods.ts +16 -4
  61. package/src/sql-filters/registrations.ts +2 -2
  62. package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +14 -8
  63. package/src/sql-filters/tickets.ts +26 -6
  64. package/tests/e2e/charge-members.test.ts +1 -0
  65. package/src/helpers/UitpasNumberValidator.test.ts +0 -23
  66. package/src/helpers/UitpasNumberValidator.ts +0 -185
@@ -152,10 +152,7 @@ export class AdminPermissionChecker {
152
152
  if (organizationId) {
153
153
  // If request is scoped to a different organization
154
154
  if (this.organization && organizationId !== this.organization.id) {
155
- if (STAMHOOFD.userMode === 'organization') {
156
- return false;
157
- }
158
- // Otherwise allow for convenience
155
+ return false;
159
156
  }
160
157
 
161
158
  // If user is limited to scope
@@ -320,6 +317,10 @@ export class AdminPermissionChecker {
320
317
  return true;
321
318
  }
322
319
 
320
+ if (!this.checkScope(member.organizationId)) {
321
+ return false;
322
+ }
323
+
323
324
  if (member.organizationId && await this.hasFullAccess(member.organizationId, permissionLevel)) {
324
325
  return true;
325
326
  }
@@ -353,14 +354,12 @@ export class AdminPermissionChecker {
353
354
  */
354
355
  async canDeleteMember(member: MemberWithRegistrations) {
355
356
  if (member.registrations.length === 0 && this.isUserManager(member)) {
356
- const platformMemberships = await MemberPlatformMembership.where({ memberId: member.id });
357
- if (platformMemberships.length === 0) {
358
- return true;
359
- }
360
-
361
357
  const cachedBalance = await CachedBalance.getForObjects([member.id]);
362
358
  if (cachedBalance.length === 0 || (cachedBalance[0].amountOpen === 0 && cachedBalance[0].amountPending === 0)) {
363
- return true;
359
+ const platformMemberships = await MemberPlatformMembership.where({ memberId: member.id });
360
+ if (platformMemberships.length === 0) {
361
+ return true;
362
+ }
364
363
  }
365
364
  }
366
365
 
@@ -380,6 +379,11 @@ export class AdminPermissionChecker {
380
379
  return false;
381
380
  }
382
381
 
382
+ // Check permissions aren't scoped to a specific organization, and they mismatch
383
+ if (!this.checkScope(registration.organizationId)) {
384
+ return false;
385
+ }
386
+
383
387
  const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
384
388
 
385
389
  if (!organizationPermissions) {
@@ -390,6 +394,15 @@ export class AdminPermissionChecker {
390
394
  // Only full permissions; because non-full doesn't have access to other periods
391
395
  return true;
392
396
  }
397
+ const organization = await this.getOrganization(registration.organizationId);
398
+
399
+ if (registration.periodId !== organization.periodId) {
400
+ if (STAMHOOFD.userMode === 'organization' || registration.periodId !== this.platform.period.id) {
401
+ // We already checked for full permissions - and we don't have full permissions
402
+ // so that also means no permissions for registrations in other periods
403
+ return false;
404
+ }
405
+ }
393
406
 
394
407
  const group = await this.getGroup(registration.groupId);
395
408
  if (!group || group.deletedAt) {
@@ -953,6 +966,11 @@ export class AdminPermissionChecker {
953
966
  return false;
954
967
  }
955
968
 
969
+ // Temporary access
970
+ if (hasTemporaryMemberAccess(this.user.id, member.id, level)) {
971
+ return true;
972
+ }
973
+
956
974
  // First list all organizations this member is part of
957
975
  const organizations: Organization[] = [];
958
976
 
@@ -1033,6 +1051,10 @@ export class AdminPermissionChecker {
1033
1051
  return false;
1034
1052
  }
1035
1053
 
1054
+ if (hasTemporaryMemberAccess(this.user.id, member.id, PermissionLevel.Full)) {
1055
+ return true;
1056
+ }
1057
+
1036
1058
  // First list all organizations this member is part of
1037
1059
  const organizations: Organization[] = [];
1038
1060
 
@@ -1073,6 +1095,65 @@ export class AdminPermissionChecker {
1073
1095
  return false;
1074
1096
  }
1075
1097
 
1098
+ async canFilterMembersOnRecordId(recordId: string): Promise<{ canAccess: false; record: RecordSettings | null } | { canAccess: true; record: RecordSettings }> {
1099
+ const record = await MemberRecordStore.getRecord(recordId);
1100
+ if (!record) {
1101
+ return {
1102
+ canAccess: false,
1103
+ record: null,
1104
+ };
1105
+ }
1106
+
1107
+ if (!this.checkScope(record.organizationId)) {
1108
+ return {
1109
+ canAccess: false,
1110
+ record: record.record,
1111
+ };
1112
+ }
1113
+
1114
+ if (!this.user.permissions) {
1115
+ return {
1116
+ canAccess: false,
1117
+ record: record.record,
1118
+ };
1119
+ }
1120
+
1121
+ if (record.organizationId) {
1122
+ const organizationPermissions = await this.getOrganizationPermissions(record.organizationId);
1123
+ if (organizationPermissions && organizationPermissions.hasResourceAccess(PermissionsResourceType.RecordCategories, record.rootCategoryId, PermissionLevel.Read)) {
1124
+ return {
1125
+ canAccess: true,
1126
+ record: record.record,
1127
+ };
1128
+ }
1129
+ }
1130
+ else {
1131
+ // ONLY check current scoped organization
1132
+ if (this.organization) {
1133
+ const organizationPermissions = await this.getOrganizationPermissions(this.organization.id);
1134
+ if (organizationPermissions && organizationPermissions.hasResourceAccess(PermissionsResourceType.RecordCategories, record.rootCategoryId, PermissionLevel.Read)) {
1135
+ return {
1136
+ canAccess: true,
1137
+ record: record.record,
1138
+ };
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ // 2. Check platform permissions
1144
+ if (this.platformPermissions?.hasResourceAccess(PermissionsResourceType.RecordCategories, record.rootCategoryId, PermissionLevel.Read)) {
1145
+ return {
1146
+ canAccess: true,
1147
+ record: record.record,
1148
+ };
1149
+ }
1150
+
1151
+ return {
1152
+ canAccess: false,
1153
+ record: record.record,
1154
+ };
1155
+ }
1156
+
1076
1157
  async checkRecordAccess(member: MemberWithRegistrations, recordId: string, level: PermissionLevel = PermissionLevel.Read): Promise<{ canAccess: false; record: RecordSettings | null } | { canAccess: true; record: RecordSettings }> {
1077
1158
  const record = await MemberRecordStore.getRecord(recordId);
1078
1159
  if (!record) {
@@ -1116,9 +1197,9 @@ export class AdminPermissionChecker {
1116
1197
  }
1117
1198
  }
1118
1199
  else {
1119
- // 1. Check all organizations (they can give permissions)
1120
- for (const organizationId of this.user.permissions.organizationPermissions.keys()) {
1121
- const organizationPermissions = await this.getOrganizationPermissions(organizationId);
1200
+ // Also check current scoped organization
1201
+ if (this.organization) {
1202
+ const organizationPermissions = await this.getOrganizationPermissions(this.organization.id);
1122
1203
  if (organizationPermissions && organizationPermissions.hasResourceAccess(PermissionsResourceType.RecordCategories, record.rootCategoryId, level)) {
1123
1204
  return {
1124
1205
  canAccess: true,
@@ -1136,16 +1217,42 @@ export class AdminPermissionChecker {
1136
1217
  };
1137
1218
  }
1138
1219
 
1139
- // It is possible that this is a platform admin, and inherits automatic permissions for tags. So'll need to loop all the organizations where this member has an active registration for
1140
- if (!record.organizationId && this.platformPermissions) {
1141
- const organizations = Formatter.uniqueArray(member.registrations.map(r => r.organizationId));
1142
- for (const organizationId of organizations) {
1143
- const organizationPermissions = await this.getOrganizationPermissions(organizationId);
1220
+ if (hasTemporaryMemberAccess(this.user.id, member.id, PermissionLevel.Full)) {
1221
+ return {
1222
+ canAccess: true,
1223
+ record: record.record,
1224
+ };
1225
+ }
1226
+
1227
+ // It is possible that this is a platform admin (or an admin that has access to multiple organizations), and inherits automatic permissions for tags. So'll need to loop all the organizations where this member has an active registration for
1228
+ if (!record.organizationId && !this.organization && this.hasSomePlatformAccess()) {
1229
+ const checkedOrganizations = new Map<string, boolean>();
1230
+ for (const registration of member.registrations) {
1231
+ const permissions = checkedOrganizations.get(registration.organizationId);
1232
+
1233
+ // Checking the organization permissions is faster (and less data lookups required), so we do that first before doing the more expensive registration access check
1234
+ if (permissions !== null) {
1235
+ if (permissions === true && await this.canAccessRegistration(registration, level)) {
1236
+ return {
1237
+ canAccess: true,
1238
+ record: record.record,
1239
+ };
1240
+ }
1241
+ continue;
1242
+ }
1243
+
1244
+ const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
1144
1245
  if (organizationPermissions && organizationPermissions.hasResourceAccess(PermissionsResourceType.RecordCategories, record.rootCategoryId, level)) {
1145
- return {
1146
- canAccess: true,
1147
- record: record.record,
1148
- };
1246
+ checkedOrganizations.set(registration.organizationId, true);
1247
+ if (await this.canAccessRegistration(registration, level)) {
1248
+ return {
1249
+ canAccess: true,
1250
+ record: record.record,
1251
+ };
1252
+ }
1253
+ }
1254
+ else {
1255
+ checkedOrganizations.set(registration.organizationId, false);
1149
1256
  }
1150
1257
  }
1151
1258
  }
@@ -482,17 +482,20 @@ export class AuthenticatedStructures {
482
482
  const blob = MemberWithRegistrationsBlob.create({
483
483
  ...member,
484
484
  balances: memberBalances,
485
- registrations: member.registrations.map((r) => {
486
- const base = r.getStructure();
487
-
488
- base.balances = balancesPermission
489
- ? (balances.filter(b => r.id === b.objectId && b.objectType === ReceivableBalanceType.registration).map((b) => {
490
- return GenericBalance.create(b);
491
- }))
492
- : [];
485
+ registrations: await Promise.all(
486
+ member.registrations.map(async (r) => {
487
+ const base = r.getStructure();
488
+
489
+ base.balances = balancesPermission
490
+ ? (balances.filter(b => r.id === b.objectId && b.objectType === ReceivableBalanceType.registration).map((b) => {
491
+ return GenericBalance.create(b);
492
+ }))
493
+ : [];
494
+ base.group = await this.group(r.group);
493
495
 
494
- return base;
495
- }),
496
+ return base;
497
+ }),
498
+ ),
496
499
  details: member.details,
497
500
  users: member.users.map(u => u.getStructure()),
498
501
  });
@@ -313,7 +313,7 @@ export class ContextInstance {
313
313
  throw new SimpleError({
314
314
  code: 'archived',
315
315
  message: 'Full access is required to view inactive organizations',
316
- human: $t('Je moet een hoofdbeheerder zijn om inactieve verenigingen te bekijken'),
316
+ human: $t('31bc55e4-1cf3-495a-8b35-686a4cc25f69'),
317
317
  statusCode: 401,
318
318
  });
319
319
  }
@@ -21,7 +21,7 @@ function assertIsUitpasTokenResponse(json: unknown): asserts json is UitpasToken
21
21
  throw new SimpleError({
22
22
  code: 'invalid_response_fetching_uitpas_token',
23
23
  message: `Invalid response when fetching UiTPAS token`,
24
- human: $t(`Er is een fout opgetreden bij het communiceren met UiTPAS. Probeer het later opnieuw.`),
24
+ human: $t(`8f217db0-c672-46f0-a8f7-6eba6f080947`),
25
25
  });
26
26
  }
27
27
  }
@@ -35,43 +35,67 @@ export class UitpasTokenRepository {
35
35
  this.uitpasClientCredential = uitpasClientCredential;
36
36
  }
37
37
 
38
+ static async handleInQueue<T>(organizationId: string | null, handler: () => Promise<T>): Promise<T> {
39
+ return await QueueHandler.schedule('uitpas/token-' + (organizationId ?? 'platform'), handler);
40
+ }
41
+
42
+ static async storeIfValid(organizationId: string | null, clientId: string, clientSecret: string): Promise<boolean> {
43
+ if (!clientId || !clientSecret) { // empty strings
44
+ return false; // not valid
45
+ }
46
+ let model = new UitpasClientCredential();
47
+ model.organizationId = organizationId;
48
+ model.clientId = clientId;
49
+ model.clientSecret = clientSecret;
50
+ return await UitpasTokenRepository.handleInQueue(organizationId, async () => {
51
+ let repo = new UitpasTokenRepository(model);
52
+ try {
53
+ await repo.getNewAccessToken();
54
+ }
55
+ catch (e) {
56
+ return false; // not valid
57
+ }
58
+ // valid -> store
59
+ model = await UitpasTokenRepository.setModelInDb(organizationId, model);
60
+ repo.uitpasClientCredential = model; // update the uitpasClientCredential in the repo
61
+ repo = UitpasTokenRepository.setRepoInMemory(organizationId, repo);
62
+ return true;
63
+ });
64
+ }
65
+
38
66
  /**
39
67
  * organizationId (null means platform) -> UitpasTokenRepository
40
68
  */
41
69
  static knownTokens: Map<string | null, UitpasTokenRepository> = new Map();
42
70
 
43
- private static async createRepoFromDb(organizationId: string | null): Promise<UitpasTokenRepository> {
44
- // query db
45
- let uitpasClientCredential = await UitpasClientCredential.select().where('organizationId', organizationId).first(false);
46
- if (!uitpasClientCredential) {
47
- // temporary solution, because platform client id and secret are not yet in the database
48
- if (organizationId === null) {
49
- if (!STAMHOOFD.UITPAS_API_CLIENT_ID || !STAMHOOFD.UITPAS_API_CLIENT_SECRET) {
50
- throw new SimpleError({
51
- code: 'uitpas_api_not_configured_for_platform',
52
- message: 'UiTPAS api is not configured for the platform',
53
- human: $t('UiTPAS is niet volledig geconfigureerd, contacteer de platformbeheerder.'),
54
- });
55
- }
56
- uitpasClientCredential = new UitpasClientCredential();
57
- uitpasClientCredential.clientId = STAMHOOFD.UITPAS_API_CLIENT_ID;
58
- uitpasClientCredential.clientSecret = STAMHOOFD.UITPAS_API_CLIENT_SECRET;
59
- uitpasClientCredential.organizationId = null; // null means platform
60
- }
61
- else {
71
+ private static getRepoFromMemory(organizationId: string | null): UitpasTokenRepository | null {
72
+ return UitpasTokenRepository.knownTokens.get(organizationId) ?? null;
73
+ }
74
+
75
+ private static async getModelFromDb(organizationId: string | null) {
76
+ let model = await UitpasClientCredential.select().where('organizationId', organizationId).first(false);
77
+ if (model) {
78
+ return model; // found in database
79
+ }
80
+ if (organizationId === null) {
81
+ // platform client id and secret are not yet in the database, but should be configured in the environment variables
82
+ if (!STAMHOOFD.UITPAS_API_CLIENT_ID || !STAMHOOFD.UITPAS_API_CLIENT_SECRET) {
62
83
  throw new SimpleError({
63
- code: 'uitpas_api_not_configured_for_this_organization',
64
- message: `UiTPAS api not configured for organization with id ${organizationId}`,
65
- human: $t(`De UiTPAS integratie is niet compleet, contacteer de beheerder.`),
84
+ code: 'uitpas_api_not_configured_for_platform',
85
+ message: 'UiTPAS api is not configured for the platform',
86
+ human: $t('UiTPAS is niet volledig geconfigureerd, contacteer de platformbeheerder.'),
66
87
  });
67
88
  }
89
+ model = new UitpasClientCredential();
90
+ model.clientId = STAMHOOFD.UITPAS_API_CLIENT_ID;
91
+ model.clientSecret = STAMHOOFD.UITPAS_API_CLIENT_SECRET;
92
+ model.organizationId = null; // null means platform
93
+ return model;
68
94
  }
69
- const newRepo = new UitpasTokenRepository(uitpasClientCredential);
70
- this.knownTokens.set(organizationId, newRepo);
71
- return newRepo;
95
+ return null; // not found in database
72
96
  }
73
97
 
74
- private async getNewAccessToken(): Promise<string> {
98
+ private async getNewAccessToken() {
75
99
  const url = 'https://account-test.uitid.be/realms/uitid/protocol/openid-connect/token';
76
100
  const myHeaders = new Headers();
77
101
  myHeaders.append('Content-Type', 'application/x-www-form-urlencoded');
@@ -90,15 +114,23 @@ export class UitpasTokenRepository {
90
114
  throw new SimpleError({
91
115
  code: 'uitpas_unreachable_fetching_uitpas_token',
92
116
  message: `Network issue when fetching UiTPAS token`,
93
- human: $t(`We konden UiTPAS niet bereiken. Probeer het later opnieuw.`),
117
+ human: $t(`6483c4c6-2fe8-456d-9110-f565952b6822`),
94
118
  });
95
119
  });
96
120
  if (!response.ok) {
121
+ if (response.status === 401) {
122
+ // Unauthorized, credentials are invalid
123
+ throw new SimpleError({
124
+ code: 'invalid_uitpas_client_credentials',
125
+ message: `Invalid UiTPAS client credentials`,
126
+ human: $t(`De opgegeven UiTPAS client credentials zijn ongeldig. Controleer ze en probeer opnieuw.`),
127
+ });
128
+ }
97
129
  console.error(`Unsuccessful response when fetching UiTPAS token for organization with id ${this.uitpasClientCredential.organizationId}:`, response.statusText);
98
130
  throw new SimpleError({
99
131
  code: 'unsuccessful_response_fetching_uitpas_token',
100
132
  message: `Unsuccesful response when fetching UiTPAS token`,
101
- human: $t(`Er is een fout opgetreden bij het verbinden met UiTPAS. Probeer het later opnieuw.`),
133
+ human: $t(`dd9b30ca-860f-47aa-8cb1-527fd156d9ca`),
102
134
  });
103
135
  }
104
136
  const json: unknown = await response.json().catch(() => {
@@ -106,7 +138,7 @@ export class UitpasTokenRepository {
106
138
  throw new SimpleError({
107
139
  code: 'invalid_json_fetching_uitpas_token',
108
140
  message: `Invalid json when fetching UiTPAS token`,
109
- human: $t(`Er is een fout opgetreden bij het communiceren met UiTPAS. Probeer het later opnieuw.`),
141
+ human: $t(`8f217db0-c672-46f0-a8f7-6eba6f080947`),
110
142
  });
111
143
  });
112
144
  assertIsUitpasTokenResponse(json);
@@ -115,6 +147,25 @@ export class UitpasTokenRepository {
115
147
  return this.accessToken;
116
148
  }
117
149
 
150
+ private static setRepoInMemory(organizationId: string | null, repo: UitpasTokenRepository) {
151
+ UitpasTokenRepository.knownTokens.set(organizationId, repo);
152
+ return repo;
153
+ }
154
+
155
+ private static async setModelInDb(organizationId: string | null, model: UitpasClientCredential) {
156
+ const oldModel = await UitpasTokenRepository.getModelFromDb(organizationId);
157
+ if (oldModel) {
158
+ // update
159
+ oldModel.clientId = model.clientId;
160
+ oldModel.clientSecret = model.clientSecret;
161
+ await oldModel.save();
162
+ return oldModel; // return updated model
163
+ }
164
+ // create
165
+ await model.save();
166
+ return model; // return new model
167
+ }
168
+
118
169
  /**
119
170
  * Get the access token for the organization or platform.
120
171
  * @param organizationId the organization ID for which to get the access token. If null, it means the platform.
@@ -123,24 +174,63 @@ export class UitpasTokenRepository {
123
174
  * @throws SimpleError if the token cannot be obtained or the API is not configured
124
175
  */
125
176
  static async getAccessTokenFor(organizationId: string | null = null, forceRefresh: boolean = false): Promise<string> {
126
- let repo = UitpasTokenRepository.knownTokens.get(organizationId);
177
+ let repo = UitpasTokenRepository.getRepoFromMemory(organizationId);
127
178
  if (repo && repo.accessToken && !forceRefresh && repo.expiresOn > new Date()) {
128
179
  return repo.accessToken;
129
180
  }
130
181
 
131
182
  // Prevent multiple concurrent requests for the same organization, asking for an access token to the UiTPAS API.
132
183
  // The queue can only run one at a time for the same organizationId
133
- return await QueueHandler.schedule('uitpas/token-' + (organizationId ?? 'platform'), async () => {
184
+ return await UitpasTokenRepository.handleInQueue(organizationId, async () => {
134
185
  // we re-search for the repo, as another call to this funcion might have added while we we're waiting in the queue
135
- repo = UitpasTokenRepository.knownTokens.get(organizationId);
186
+ repo = UitpasTokenRepository.getRepoFromMemory(organizationId);
136
187
  if (repo && repo.accessToken && !forceRefresh && repo.expiresOn > new Date()) {
137
188
  return repo.accessToken;
138
189
  }
139
190
  if (!repo) {
140
- repo = await UitpasTokenRepository.createRepoFromDb(organizationId);
191
+ const model = await UitpasTokenRepository.getModelFromDb(organizationId);
192
+ if (!model) {
193
+ throw new SimpleError({
194
+ code: 'uitpas_api_not_configured_for_this_organization',
195
+ message: `UiTPAS api not configured for organization with id ${organizationId}`,
196
+ human: $t('UiTPAS is nog niet volledig geconfigureerd voor deze organisatie.'),
197
+ });
198
+ }
199
+ repo = UitpasTokenRepository.setRepoInMemory(organizationId, new UitpasTokenRepository(model)); // store in memory
141
200
  }
142
201
  // ask for a new access token
143
- return repo.getNewAccessToken(); ;
202
+ return repo.getNewAccessToken();
203
+ });
204
+ }
205
+
206
+ static async getClientIdFor(organisationId: string | null): Promise<string> {
207
+ const repo = UitpasTokenRepository.getRepoFromMemory(organisationId);
208
+ if (!repo) {
209
+ const model = await UitpasClientCredential.select().where('organizationId', organisationId).first(false);
210
+ if (!model) {
211
+ return ''; // no client ID and secret configured
212
+ }
213
+ return model.clientId; // client ID configured, but not in memory
214
+ }
215
+ return repo.uitpasClientCredential.clientId; // client ID configured and in memory
216
+ }
217
+
218
+ static async clearClientCredentialsFor(organizationId: string | null): Promise<boolean> {
219
+ return await UitpasTokenRepository.handleInQueue(organizationId, async () => {
220
+ const repo = UitpasTokenRepository.getRepoFromMemory(organizationId);
221
+ if (repo) {
222
+ // in memory, thus also in db
223
+ await repo.uitpasClientCredential.delete(); // remove from db
224
+ UitpasTokenRepository.knownTokens.delete(organizationId); // remove from memory
225
+ return true;
226
+ }
227
+ // not in memory, maybe in db
228
+ const model = await UitpasTokenRepository.getModelFromDb(organizationId);
229
+ if (model) {
230
+ await model.delete(); // remove from database
231
+ return true;
232
+ }
233
+ return false; // nothing to clear
144
234
  });
145
235
  }
146
236
  }
@@ -68,7 +68,8 @@ export class ViesHelperStatic {
68
68
  // In Belgium, the company number syntax is the same as VAT number
69
69
  let correctedVATNumber = companyNumber;
70
70
 
71
- if (companyNumber.length > 2 && companyNumber.substr(0, 2) !== country) {
71
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
72
+ if (companyNumber.length > 2 && companyNumber.substr(0, 2).toUpperCase() !== country) {
72
73
  // Add required country in VAT number
73
74
  correctedVATNumber = country + companyNumber;
74
75
  }
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { EmailTemplate } from '@stamhoofd/models';
3
+
4
+ export default new Migration(async () => {
5
+ if (STAMHOOFD.environment !== 'development' && STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
6
+ console.log('Skipped');
7
+ return;
8
+ }
9
+
10
+ console.log('Deleting all default email templates');
11
+ await EmailTemplate.delete()
12
+ .where('organizationId', null);
13
+ });
@@ -0,0 +1,20 @@
1
+ import { Database, Migration } from '@simonbackx/simple-database';
2
+ import { EmailTemplate } from '@stamhoofd/models';
3
+ import { promises as fs } from 'fs';
4
+
5
+ export default new Migration(async () => {
6
+ // Check if total email templates is 0
7
+ const firstEmailTemplate = await EmailTemplate.select().where('organizationId', null).first(false);
8
+ if (firstEmailTemplate) {
9
+ console.log('Skipped, email templates already exist');
10
+ return;
11
+ }
12
+
13
+ // Insert defaults
14
+ console.log('Inserting default email templates');
15
+ const sqlStatement = await fs.readFile(__dirname + '/data/default-email-templates.sql', { encoding: 'utf-8' });
16
+ await Database.statement(sqlStatement);
17
+
18
+ // Do something here
19
+ return Promise.resolve();
20
+ });