@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.
- package/package.json +12 -11
- package/src/boot.ts +2 -0
- package/src/crons/balance-emails.ts +1 -6
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/SearchUitpasOrganizersEndpoint.ts +42 -0
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +4 -4
- package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +3 -3
- package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +23 -2
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +6 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +8 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
- package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +10 -8
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -0
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +3 -6
- package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +38 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +31 -1
- package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +108 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -2
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +9 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +2 -9
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +1 -7
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +68 -1
- package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +27 -20
- package/src/helpers/AdminPermissionChecker.ts +129 -22
- package/src/helpers/AuthenticatedStructures.ts +13 -10
- package/src/helpers/Context.ts +1 -1
- package/src/helpers/UitpasTokenRepository.ts +125 -35
- package/src/helpers/ViesHelper.ts +2 -1
- package/src/seeds/0000000002-clear-stamhoofd-email-templates.ts +13 -0
- package/src/seeds/0000000003-default-email-templates.ts +20 -0
- package/src/seeds/data/default-email-templates.sql +55 -0
- package/src/services/RegistrationService.ts +6 -4
- package/src/services/uitpas/UitpasService.test.ts +23 -0
- package/src/services/uitpas/UitpasService.ts +222 -0
- package/src/services/uitpas/checkPermissionsFor.ts +111 -0
- package/src/services/uitpas/checkUitpasNumbers.ts +180 -0
- package/src/services/uitpas/getSocialTariffForEvent.ts +90 -0
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +181 -0
- package/src/services/uitpas/searchUitpasOrganizers.ts +93 -0
- package/src/sql-filters/audit-logs.ts +26 -6
- package/src/sql-filters/balance-item-payments.ts +23 -8
- package/src/sql-filters/base-registration-filter-compilers.ts +74 -23
- package/src/sql-filters/documents.ts +46 -13
- package/src/sql-filters/event-notifications.ts +48 -12
- package/src/sql-filters/events.ts +62 -26
- package/src/sql-filters/groups.ts +12 -12
- package/src/sql-filters/members.ts +325 -137
- package/src/sql-filters/orders.ts +96 -48
- package/src/sql-filters/organization-registration-periods.ts +16 -4
- package/src/sql-filters/organizations.ts +105 -99
- package/src/sql-filters/payments.ts +97 -47
- package/src/sql-filters/receivable-balances.ts +56 -19
- package/src/sql-filters/registration-periods.ts +16 -4
- package/src/sql-filters/registrations.ts +2 -2
- package/src/sql-filters/shared/EmailRelationFilterCompilers.ts +14 -8
- package/src/sql-filters/tickets.ts +26 -6
- package/tests/e2e/charge-members.test.ts +1 -0
- package/src/helpers/UitpasNumberValidator.test.ts +0 -23
- 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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1120
|
-
|
|
1121
|
-
const organizationPermissions = await this.getOrganizationPermissions(
|
|
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
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
+
return base;
|
|
497
|
+
}),
|
|
498
|
+
),
|
|
496
499
|
details: member.details,
|
|
497
500
|
users: member.users.map(u => u.getStructure()),
|
|
498
501
|
});
|
package/src/helpers/Context.ts
CHANGED
|
@@ -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('
|
|
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(`
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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: '
|
|
64
|
-
message:
|
|
65
|
-
human: $t(
|
|
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
|
-
|
|
70
|
-
this.knownTokens.set(organizationId, newRepo);
|
|
71
|
-
return newRepo;
|
|
95
|
+
return null; // not found in database
|
|
72
96
|
}
|
|
73
97
|
|
|
74
|
-
private async getNewAccessToken()
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|