@stamhoofd/backend 2.87.1 → 2.88.1
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 +10 -10
- package/src/endpoints/auth/CreateTokenEndpoint.ts +6 -6
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
- package/src/endpoints/auth/OpenIDConnectCallbackEndpoint.ts +1 -1
- package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +1 -1
- package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +1 -1
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +1 -1
- package/src/endpoints/auth/SignupEndpoint.ts +1 -1
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +1 -1
- package/src/endpoints/global/files/GetFileCache.ts +1 -1
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +2 -2
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +0 -7
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/retrieveUitpasSocialTariffPrice.ts +69 -0
- package/src/helpers/AuthenticatedStructures.ts +11 -0
- package/src/helpers/Context.ts +37 -8
- package/src/helpers/MemberUserSyncer.ts +2 -0
- package/src/helpers/UitpasNumberValidator.test.ts +23 -0
- package/src/helpers/UitpasNumberValidator.ts +157 -0
- package/src/helpers/UitpasTokenRepository.ts +146 -0
- package/src/sql-filters/groups.ts +1 -18
- package/tests/jest.global.setup.ts +5 -9
- package/tests/jest.setup.ts +7 -10
- package/.env.ci.json +0 -58
- package/.env.template.json +0 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.88.1",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"@simonbackx/simple-encoding": "2.22.0",
|
|
45
45
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
46
46
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
47
|
-
"@stamhoofd/backend-i18n": "2.
|
|
48
|
-
"@stamhoofd/backend-middleware": "2.
|
|
49
|
-
"@stamhoofd/email": "2.
|
|
50
|
-
"@stamhoofd/models": "2.
|
|
51
|
-
"@stamhoofd/queues": "2.
|
|
52
|
-
"@stamhoofd/sql": "2.
|
|
53
|
-
"@stamhoofd/structures": "2.
|
|
54
|
-
"@stamhoofd/utility": "2.
|
|
47
|
+
"@stamhoofd/backend-i18n": "2.88.1",
|
|
48
|
+
"@stamhoofd/backend-middleware": "2.88.1",
|
|
49
|
+
"@stamhoofd/email": "2.88.1",
|
|
50
|
+
"@stamhoofd/models": "2.88.1",
|
|
51
|
+
"@stamhoofd/queues": "2.88.1",
|
|
52
|
+
"@stamhoofd/sql": "2.88.1",
|
|
53
|
+
"@stamhoofd/structures": "2.88.1",
|
|
54
|
+
"@stamhoofd/utility": "2.88.1",
|
|
55
55
|
"archiver": "^7.0.1",
|
|
56
56
|
"axios": "^1.8.2",
|
|
57
57
|
"cookie": "^0.7.0",
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"publishConfig": {
|
|
70
70
|
"access": "public"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "33241749f01af23fbe52480a8a89a2bfd695b22d"
|
|
73
73
|
}
|
|
@@ -34,7 +34,7 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
34
34
|
// - check if not multiple attempts for the same username are started in parallel
|
|
35
35
|
// - Limit the amount of failed attemps by IP (will only make it a bit harder)
|
|
36
36
|
// - Detect attacks on random accounts (using email list + most used passwords) and temorary require CAPTCHA on all accounts
|
|
37
|
-
const organization = await Context.setOptionalOrganizationScope();
|
|
37
|
+
const organization = await Context.setOptionalOrganizationScope({ willAuthenticate: false });
|
|
38
38
|
|
|
39
39
|
switch (request.body.grantType) {
|
|
40
40
|
case 'refresh_token': {
|
|
@@ -60,8 +60,11 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
60
60
|
const token = await Token.createToken(oldToken.user);
|
|
61
61
|
|
|
62
62
|
// In the rare event our response doesn't reach the client anymore, we don't want the client to sign out...
|
|
63
|
-
// So we
|
|
64
|
-
|
|
63
|
+
// So we allow a small rotation overlap period
|
|
64
|
+
const leeway = 60 * 1000;
|
|
65
|
+
oldToken.refreshTokenValidUntil = new Date(Math.min(oldToken.refreshTokenValidUntil.getTime(), Date.now() + leeway));
|
|
66
|
+
|
|
67
|
+
// Invalidate the corresponding access token
|
|
65
68
|
oldToken.accessTokenValidUntil = new Date(Date.now() - 60 * 60 * 1000);
|
|
66
69
|
|
|
67
70
|
// Do not delete the old one, only expire it fast so it will get deleted in the future
|
|
@@ -81,9 +84,6 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
case 'password': {
|
|
84
|
-
// Increase timout for legacy
|
|
85
|
-
request.request.request?.setTimeout(30 * 1000);
|
|
86
|
-
|
|
87
87
|
if (STAMHOOFD.userMode === 'platform') {
|
|
88
88
|
const platform = await Platform.getSharedPrivateStruct();
|
|
89
89
|
const config = platform.config.loginMethods.get(LoginMethod.Password);
|
|
@@ -28,7 +28,7 @@ export class ForgotPasswordEndpoint extends Endpoint<Params, Query, Body, Respon
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
|
-
const organization = await Context.setOptionalOrganizationScope();
|
|
31
|
+
const organization = await Context.setOptionalOrganizationScope({ willAuthenticate: false });
|
|
32
32
|
|
|
33
33
|
if (STAMHOOFD.userMode === 'platform') {
|
|
34
34
|
const platform = await Platform.getSharedPrivateStruct();
|
|
@@ -26,7 +26,7 @@ export class OpenIDConnectCallbackEndpoint extends Endpoint<Params, Query, Body,
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
29
|
-
await Context.setUserOrganizationScope();
|
|
29
|
+
await Context.setUserOrganizationScope({ willAuthenticate: false });
|
|
30
30
|
const ssoService = await SSOServiceWithSession.fromSession(request);
|
|
31
31
|
return await ssoService.callback();
|
|
32
32
|
}
|
|
@@ -28,7 +28,7 @@ export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
28
28
|
|
|
29
29
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
30
|
// Check webshop and/or organization
|
|
31
|
-
await Context.setUserOrganizationScope();
|
|
31
|
+
await Context.setUserOrganizationScope({ willAuthenticate: false });
|
|
32
32
|
const service = await SSOService.fromContext(request.query.provider);
|
|
33
33
|
return await service.validateAndStartAuthCodeFlow(request.query);
|
|
34
34
|
}
|
|
@@ -27,7 +27,7 @@ export class PollEmailVerificationEndpoint extends Endpoint<Params, Query, Body,
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
-
const organization = await Context.setOptionalOrganizationScope();
|
|
30
|
+
const organization = await Context.setOptionalOrganizationScope({ willAuthenticate: false });
|
|
31
31
|
const valid = await EmailVerificationCode.poll(organization?.id ?? null, request.body.token);
|
|
32
32
|
|
|
33
33
|
return new Response(PollEmailVerificationResponse.create({
|
|
@@ -27,7 +27,7 @@ export class PollEmailVerificationEndpoint extends Endpoint<Params, Query, Body,
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
-
const organization = await Context.setOptionalOrganizationScope();
|
|
30
|
+
const organization = await Context.setOptionalOrganizationScope({ willAuthenticate: false });
|
|
31
31
|
const valid = await EmailVerificationCode.poll(organization?.id ?? null, request.body.token);
|
|
32
32
|
|
|
33
33
|
if (valid) {
|
|
@@ -28,7 +28,7 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
|
-
const organization = await Context.setUserOrganizationScope();
|
|
31
|
+
const organization = await Context.setUserOrganizationScope({ willAuthenticate: false });
|
|
32
32
|
|
|
33
33
|
if (STAMHOOFD.userMode === 'platform') {
|
|
34
34
|
const platform = await Platform.getShared();
|
|
@@ -28,7 +28,7 @@ export class VerifyEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
|
-
const organization = await Context.setOptionalOrganizationScope();
|
|
31
|
+
const organization = await Context.setOptionalOrganizationScope({ willAuthenticate: false });
|
|
32
32
|
|
|
33
33
|
const code = await EmailVerificationCode.verify(organization?.id ?? null, request.body.token, request.body.code);
|
|
34
34
|
|
|
@@ -45,7 +45,7 @@ export class GetFileCache extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
48
|
-
await Context.setOptionalOrganizationScope();
|
|
48
|
+
await Context.setOptionalOrganizationScope({ willAuthenticate: false });
|
|
49
49
|
|
|
50
50
|
limiter.track(request.request.getIP(), 1);
|
|
51
51
|
|
|
@@ -101,7 +101,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
101
101
|
|
|
102
102
|
// We risk creating a new member without being able to access it manually afterwards
|
|
103
103
|
// Cache access to this member temporarily in memory
|
|
104
|
-
await Context.auth.temporarilyGrantMemberAccess(member, PermissionLevel.
|
|
104
|
+
await Context.auth.temporarilyGrantMemberAccess(member, PermissionLevel.Full);
|
|
105
105
|
|
|
106
106
|
if (STAMHOOFD.userMode !== 'platform' && !member.organizationId) {
|
|
107
107
|
throw new SimpleError({
|
|
@@ -925,7 +925,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
925
925
|
console.log('checkSecurityCode: security code is correct - for ' + member.id);
|
|
926
926
|
|
|
927
927
|
// Grant temporary access to this member without needing to enter the security code again
|
|
928
|
-
await Context.auth.temporarilyGrantMemberAccess(member, PermissionLevel.
|
|
928
|
+
await Context.auth.temporarilyGrantMemberAccess(member, PermissionLevel.Full);
|
|
929
929
|
|
|
930
930
|
const log = new AuditLog();
|
|
931
931
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
2
|
+
import { OrganizationFactory } from '@stamhoofd/models';
|
|
3
3
|
import { Organization } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { testServer } from '../../../../tests/helpers/TestServer';
|
|
@@ -14,7 +14,7 @@ describe('Endpoint.GetOrganizationFromDomain', () => {
|
|
|
14
14
|
|
|
15
15
|
const r = Request.buildJson('GET', '/v2/organization-from-domain');
|
|
16
16
|
r.query = {
|
|
17
|
-
domain: organization.uri + '.
|
|
17
|
+
domain: organization.uri + '.' + STAMHOOFD.domains.registration!['']!,
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const response = await testServer.test(endpoint, r);
|
|
@@ -45,13 +45,6 @@ export class GetOrganizationFromUriEndpoint extends Endpoint<Params, Query, Body
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
if (!organization.active) {
|
|
49
|
-
throw new SimpleError({
|
|
50
|
-
code: 'archived_organization',
|
|
51
|
-
message: 'This organization has been archived',
|
|
52
|
-
statusCode: 404,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
48
|
return new Response(await AuthenticatedStructures.organization(organization));
|
|
56
49
|
}
|
|
57
50
|
}
|
|
@@ -56,6 +56,7 @@ export class SearchOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
56
56
|
const whereMatch: SQLWhere = new SQLMatch(SQL.column(Organization.table, 'searchIndex'), scalarToSQLExpression(matchValue));
|
|
57
57
|
|
|
58
58
|
let organizations = await Organization.select()
|
|
59
|
+
.where('active', true)
|
|
59
60
|
.where(whereMatch)
|
|
60
61
|
.orderBy(whereMatch, 'DESC')
|
|
61
62
|
.limit(limit).fetch();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
-
import { Document, DocumentTemplate,
|
|
5
|
-
import { Document as DocumentStruct,
|
|
4
|
+
import { Document, DocumentTemplate, Member, Registration } from '@stamhoofd/models';
|
|
5
|
+
import { DocumentStatus, Document as DocumentStruct, PermissionLevel } from '@stamhoofd/structures';
|
|
6
6
|
|
|
7
7
|
import { Context } from '../../../../helpers/Context';
|
|
8
8
|
|
|
@@ -39,7 +39,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
42
|
-
const organization = await Context.setOrganizationScope(
|
|
42
|
+
const organization = await Context.setOrganizationScope();
|
|
43
43
|
await Context.authenticate();
|
|
44
44
|
|
|
45
45
|
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts
CHANGED
|
@@ -147,7 +147,7 @@ export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body,
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
static async buildDetailedData(requestQuery: LimitedFilteredRequest) {
|
|
150
|
-
const organization = Context.organization ?? await Context.setOrganizationScope();
|
|
150
|
+
const organization = Context.organization ?? await Context.setOrganizationScope({ willAuthenticate: false });
|
|
151
151
|
const { data, next } = await GetReceivableBalancesEndpoint.buildDataHelper(requestQuery);
|
|
152
152
|
|
|
153
153
|
return new PaginatedResponse<DetailedReceivableBalance[], LimitedFilteredRequest>({
|
|
@@ -43,7 +43,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
46
|
-
const organization = await Context.setOptionalOrganizationScope();
|
|
46
|
+
const organization = await Context.setOptionalOrganizationScope({ willAuthenticate: false });
|
|
47
47
|
if (!request.query.exchange) {
|
|
48
48
|
await Context.optionalAuthenticate();
|
|
49
49
|
}
|
|
@@ -28,7 +28,7 @@ export class CheckWebshopDiscountCodesEndpoint extends Endpoint<Params, Query, B
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
|
-
const organization = await Context.setOrganizationScope();
|
|
31
|
+
const organization = await Context.setOrganizationScope({ willAuthenticate: false });
|
|
32
32
|
const webshop = await Webshop.getByID(request.params.id);
|
|
33
33
|
if (!webshop || webshop.organizationId !== organization.id) {
|
|
34
34
|
throw new SimpleError({
|
|
@@ -26,7 +26,7 @@ export class GetOrderByPaymentEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
29
|
-
const organization = await Context.setOrganizationScope();
|
|
29
|
+
const organization = await Context.setOrganizationScope({ willAuthenticate: false });
|
|
30
30
|
const payment = await Payment.getByID(request.params.paymentId);
|
|
31
31
|
|
|
32
32
|
if (!payment || payment.organizationId !== organization.id) {
|
|
@@ -24,7 +24,7 @@ export class GetOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBody
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
27
|
-
const organization = await Context.setOrganizationScope();
|
|
27
|
+
const organization = await Context.setOrganizationScope({ willAuthenticate: false });
|
|
28
28
|
const order = await Order.getByID(request.params.orderId);
|
|
29
29
|
|
|
30
30
|
if (!order || order.webshopId !== request.params.id || order.organizationId !== organization.id) {
|
|
@@ -42,7 +42,7 @@ export class GetTicketsEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
45
|
-
const organization = await Context.setOrganizationScope();
|
|
45
|
+
const organization = await Context.setOrganizationScope({ willAuthenticate: false });
|
|
46
46
|
|
|
47
47
|
if (request.query.secret) {
|
|
48
48
|
const [ticket] = await Ticket.where({
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { UitpasPriceCheckRequest, UitpasPriceCheckResponse } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
import { UitpasNumberValidator } from '../../../helpers/UitpasNumberValidator';
|
|
6
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
7
|
+
type Params = Record<string, never>;
|
|
8
|
+
type Query = undefined;
|
|
9
|
+
type Body = UitpasPriceCheckRequest;
|
|
10
|
+
type ResponseBody = UitpasPriceCheckResponse;
|
|
11
|
+
|
|
12
|
+
export class retrieveUitpasSocialTariffPrice extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
13
|
+
bodyDecoder = UitpasPriceCheckRequest as Decoder<UitpasPriceCheckRequest>;
|
|
14
|
+
|
|
15
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
16
|
+
if (request.method !== 'POST') {
|
|
17
|
+
return [false];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const params = Endpoint.parseParameters(request.url, '/uitpas', {});
|
|
21
|
+
|
|
22
|
+
if (params) {
|
|
23
|
+
return [true, params as Params];
|
|
24
|
+
}
|
|
25
|
+
return [false];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
29
|
+
if (request.body.uitpasEventId) {
|
|
30
|
+
// OFFICIAL FLOW
|
|
31
|
+
if (!request.body.uitpasNumber) {
|
|
32
|
+
// STATIC CHECK
|
|
33
|
+
// request shouldn't include a reduced price
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// OFFICIAL FLOW with an UiTPAS number
|
|
37
|
+
// request should include a reduced price (estimate by the frontend)
|
|
38
|
+
}
|
|
39
|
+
throw new SimpleError({
|
|
40
|
+
code: 'not_implemented',
|
|
41
|
+
message: 'Official flow not yet implemented',
|
|
42
|
+
human: 'De officiële flow voor het valideren van een UiTPAS-nummer wordt nog niet ondersteund.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// NON-OFFICIAL FLOW
|
|
47
|
+
// request should include UiTPAS-number, reduced price AND base price
|
|
48
|
+
if (!request.body.reducedPrice) {
|
|
49
|
+
throw new SimpleError({
|
|
50
|
+
code: 'missing_reduced_price',
|
|
51
|
+
message: 'Reduced price must be provided for non-official flow.',
|
|
52
|
+
human: $t('Je moet een verlaagd tarief opgeven voor de UiTPAS.'),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!request.body.uitpasNumber) {
|
|
56
|
+
throw new SimpleError({
|
|
57
|
+
code: 'missing_uitpas_number',
|
|
58
|
+
message: 'Uitpas number must be provided for non-official flow.',
|
|
59
|
+
human: $t('Je moet een UiTPAS-nummer opgeven.'),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
await UitpasNumberValidator.checkUitpasNumber(request.body.uitpasNumber);
|
|
63
|
+
const uitpasPriceCheckResponse = UitpasPriceCheckResponse.create({
|
|
64
|
+
price: request.body.reducedPrice,
|
|
65
|
+
});
|
|
66
|
+
return new Response(uitpasPriceCheckResponse);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -443,6 +443,17 @@ export class AuthenticatedStructures {
|
|
|
443
443
|
}
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
const membershipOrganizationId = Platform.shared.membershipOrganizationId;
|
|
447
|
+
if (Context.auth.hasSomePlatformAccess() && membershipOrganizationId) {
|
|
448
|
+
if (await Context.auth.hasSomeAccess(membershipOrganizationId)) {
|
|
449
|
+
const found = organizations.get(membershipOrganizationId);
|
|
450
|
+
if (!found) {
|
|
451
|
+
const organization = await Context.auth.getOrganization(membershipOrganizationId);
|
|
452
|
+
organizations.set(organization.id, organization);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
446
457
|
const memberBlobs: MemberWithRegistrationsBlob[] = [];
|
|
447
458
|
for (const member of members) {
|
|
448
459
|
for (const registration of member.registrations) {
|
package/src/helpers/Context.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { isSimpleError, SimpleError } from '@simonbackx/simple-errors';
|
|
3
3
|
import { I18n } from '@stamhoofd/backend-i18n';
|
|
4
4
|
import { Organization, Platform, RateLimiter, Token, User } from '@stamhoofd/models';
|
|
5
5
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
@@ -149,12 +149,15 @@ export class ContextInstance {
|
|
|
149
149
|
return this.#auth;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
async setOptionalOrganizationScope() {
|
|
152
|
+
async setOptionalOrganizationScope(options?: { willAuthenticate?: boolean }) {
|
|
153
153
|
try {
|
|
154
|
-
return await this.setOrganizationScope();
|
|
154
|
+
return await this.setOrganizationScope(options);
|
|
155
155
|
}
|
|
156
156
|
catch (e) {
|
|
157
|
-
|
|
157
|
+
if (isSimpleError(e) && e.hasCode('invalid_host')) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
throw e;
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
163
|
|
|
@@ -170,15 +173,21 @@ export class ContextInstance {
|
|
|
170
173
|
/**
|
|
171
174
|
* Require organization scope if userMode is not platform
|
|
172
175
|
*/
|
|
173
|
-
async setUserOrganizationScope() {
|
|
176
|
+
async setUserOrganizationScope(options?: { willAuthenticate?: boolean }) {
|
|
174
177
|
if (STAMHOOFD.userMode === 'platform') {
|
|
175
178
|
return null;
|
|
176
179
|
}
|
|
177
|
-
return await this.setOrganizationScope();
|
|
180
|
+
return await this.setOrganizationScope(options);
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
async setOrganizationScope(options?: {
|
|
181
|
-
|
|
183
|
+
async setOrganizationScope(options?: { willAuthenticate?: boolean }) {
|
|
184
|
+
if (!options) {
|
|
185
|
+
options = {};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const organization = await Organization.fromApiHost(this.request.host, {
|
|
189
|
+
allowInactive: options.willAuthenticate ?? true,
|
|
190
|
+
});
|
|
182
191
|
|
|
183
192
|
this.organization = organization;
|
|
184
193
|
this.i18n.switchToLocale({ country: organization.address.country });
|
|
@@ -192,6 +201,14 @@ export class ContextInstance {
|
|
|
192
201
|
}
|
|
193
202
|
catch (e) {
|
|
194
203
|
if (e.code === 'not_authenticated') {
|
|
204
|
+
// Do not allow to optional authenticate to inactive organizations
|
|
205
|
+
if (this.organization && !this.organization.active) {
|
|
206
|
+
throw new SimpleError({
|
|
207
|
+
code: 'not_authenticated',
|
|
208
|
+
message: 'You need to authenticate to view inactive organizations',
|
|
209
|
+
statusCode: 401,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
195
212
|
return {};
|
|
196
213
|
}
|
|
197
214
|
throw e;
|
|
@@ -290,6 +307,18 @@ export class ContextInstance {
|
|
|
290
307
|
|
|
291
308
|
this.#auth = new AdminPermissionChecker(user, await Platform.getSharedPrivateStruct(), this.organization);
|
|
292
309
|
|
|
310
|
+
if (this.organization && !this.organization.active) {
|
|
311
|
+
// For inactive organizations, you always need permissions to view them
|
|
312
|
+
if (!await Context.auth.hasFullAccess(this.organization.id)) {
|
|
313
|
+
throw new SimpleError({
|
|
314
|
+
code: 'archived',
|
|
315
|
+
message: 'Full access is required to view inactive organizations',
|
|
316
|
+
human: $t('Je moet een hoofdbeheerder zijn om inactieve verenigingen te bekijken'),
|
|
317
|
+
statusCode: 401,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
293
322
|
return { user, token };
|
|
294
323
|
}
|
|
295
324
|
}
|
|
@@ -156,6 +156,8 @@ export class MemberUserSyncerStatic {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
async updateInheritedPermissions(user: User) {
|
|
159
|
+
// Fetch all members for this user
|
|
160
|
+
|
|
159
161
|
const responsibilities = user.memberId ? (await this.getResponsibilitiesForMembers([user.memberId])) : [];
|
|
160
162
|
|
|
161
163
|
// Check if the member has active registrations
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { STExpect } from '@stamhoofd/test-utils';
|
|
2
|
+
import { UitpasNumberValidator } from './UitpasNumberValidator';
|
|
3
|
+
|
|
4
|
+
describe.skip('UitpasNumberValidator', () => {
|
|
5
|
+
it('should validate a correct Uitpas number with kansentarief', async () => {
|
|
6
|
+
const validNumber = '0900000067513';
|
|
7
|
+
await expect(UitpasNumberValidator.checkUitpasNumber(validNumber)).resolves.toBeUndefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should throw an error for an invalid Uitpas number', async () => {
|
|
11
|
+
const invalidNumber = '1234567890123';
|
|
12
|
+
await expect(UitpasNumberValidator.checkUitpasNumber(invalidNumber)).rejects.toThrow(
|
|
13
|
+
STExpect.simpleError({ code: 'unsuccessful_but_expected_response_retrieving_pass_by_uitpas_number' }),
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should throw an error for a Uitpas number with kansentarief expired', async () => {
|
|
18
|
+
const expiredNumber = '0900000058918';
|
|
19
|
+
await expect(UitpasNumberValidator.checkUitpasNumber(expiredNumber)).rejects.toThrow(
|
|
20
|
+
STExpect.simpleError({ code: 'uitpas_number_issue' }),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { UitpasTokenRepository } from './UitpasTokenRepository';
|
|
3
|
+
import { DataValidator } from '@stamhoofd/utility';
|
|
4
|
+
|
|
5
|
+
type UitpasNumberSuccessfulResponse = {
|
|
6
|
+
socialTariff: {
|
|
7
|
+
status: 'ACTIVE' | 'EXPIRED' | 'NONE';
|
|
8
|
+
};
|
|
9
|
+
messages?: Array<{
|
|
10
|
+
text: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type UitpasNumberErrorResponse = {
|
|
15
|
+
title: string; // e.g., "Invalid uitpas number"
|
|
16
|
+
endUserMessage?: {
|
|
17
|
+
nl: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function assertIsUitpasNumberSuccessfulResponse(
|
|
22
|
+
json: unknown,
|
|
23
|
+
): asserts json is UitpasNumberSuccessfulResponse {
|
|
24
|
+
if (
|
|
25
|
+
typeof json !== 'object'
|
|
26
|
+
|| json === null
|
|
27
|
+
|| !('socialTariff' in json)
|
|
28
|
+
|| typeof json.socialTariff !== 'object'
|
|
29
|
+
|| json.socialTariff === null
|
|
30
|
+
|| !('status' in json.socialTariff)
|
|
31
|
+
|| typeof json.socialTariff.status !== 'string'
|
|
32
|
+
|| (json.socialTariff.status !== 'ACTIVE' && json.socialTariff.status !== 'EXPIRED' && json.socialTariff.status !== 'NONE')
|
|
33
|
+
|| ('messages' in json && (!Array.isArray(json.messages) || !json.messages.every(
|
|
34
|
+
(message: unknown) => typeof message === 'object' && message !== null && 'text' in message && typeof message.text === 'string')))
|
|
35
|
+
) {
|
|
36
|
+
console.error('Invalid response when retrieving pass by UiTPAS number:', json);
|
|
37
|
+
throw new SimpleError({
|
|
38
|
+
code: 'invalid_response_retrieving_pass_by_uitpas_number',
|
|
39
|
+
message: `Invalid response when retrieving pass by UiTPAS number`,
|
|
40
|
+
human: $t(`Er is een fout opgetreden bij het ophalen van je UiTPAS. Kijk je het nummer even na?`),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isUitpasNumberErrorResponse(
|
|
46
|
+
json: unknown,
|
|
47
|
+
): json is UitpasNumberErrorResponse {
|
|
48
|
+
return typeof json === 'object'
|
|
49
|
+
&& json !== null
|
|
50
|
+
&& 'title' in json
|
|
51
|
+
&& typeof json.title === 'string'
|
|
52
|
+
&& (!('endUserMessage' in json)
|
|
53
|
+
|| (typeof json.endUserMessage === 'object' && json.endUserMessage !== null && 'nl' in json.endUserMessage && typeof json.endUserMessage.nl === 'string')
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class UitpasNumberValidatorStatic {
|
|
58
|
+
async checkUitpasNumber(uitpasNumber: string) {
|
|
59
|
+
// static check (using regex)
|
|
60
|
+
if (!DataValidator.isUitpasNumberValid(uitpasNumber)) {
|
|
61
|
+
throw new SimpleError({
|
|
62
|
+
code: 'invalid_uitpas_number',
|
|
63
|
+
message: `Invalid UiTPAS number: ${uitpasNumber}`,
|
|
64
|
+
human: $t(
|
|
65
|
+
`Het opgegeven UiTPAS-nummer is ongeldig. Controleer het nummer en probeer het opnieuw.`,
|
|
66
|
+
),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(); // for nothing, means for platform
|
|
70
|
+
const baseUrl = 'https://api-test.uitpas.be'; // TO DO: Use the URL from environment variables
|
|
71
|
+
|
|
72
|
+
const url = `${baseUrl}/passes/${uitpasNumber}`;
|
|
73
|
+
const myHeaders = new Headers();
|
|
74
|
+
myHeaders.append('Authorization', 'Bearer ' + access_token);
|
|
75
|
+
const requestOptions = {
|
|
76
|
+
method: 'GET',
|
|
77
|
+
headers: myHeaders,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const response = await fetch(url, requestOptions).catch(() => {
|
|
81
|
+
// Handle network errors
|
|
82
|
+
throw new SimpleError({
|
|
83
|
+
code: 'uitpas_unreachable_retrieving_pass_by_uitpas_number',
|
|
84
|
+
message: `Network issue when retrieving pass by UiTPAS number`,
|
|
85
|
+
human: $t(
|
|
86
|
+
`We konden UiTPAS niet bereiken om jouw UiTPAS-nummer te valideren. Probeer het later opnieuw.`,
|
|
87
|
+
),
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const json: unknown = await response.json().catch(() => { /* ignore */ });
|
|
92
|
+
let endUserMessage = '';
|
|
93
|
+
|
|
94
|
+
if (json) {
|
|
95
|
+
console.error(`UiTPAS API returned an error for UiTPAS number ${uitpasNumber}:`, json);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.error(`UiTPAS API returned an error for UiTPAS number ${uitpasNumber}:`, response.statusText);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isUitpasNumberErrorResponse(json)) {
|
|
102
|
+
endUserMessage = json.endUserMessage ? json.endUserMessage.nl : '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (endUserMessage) {
|
|
106
|
+
throw new SimpleError({
|
|
107
|
+
code: 'unsuccessful_but_expected_response_retrieving_pass_by_uitpas_number',
|
|
108
|
+
message: `Unsuccesful response with message when retrieving pass by UiTPAS number, message: ${endUserMessage}`,
|
|
109
|
+
human: endUserMessage,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
throw new SimpleError({
|
|
114
|
+
code: 'unsuccessful_and_unexpected_response_retrieving_pass_by_uitpas_number',
|
|
115
|
+
message: `Unsuccesful response without message when retrieving pass by UiTPAS number`,
|
|
116
|
+
human: $t(`Er is een fout opgetreden bij het ophalen van je UiTPAS. Kijk je het nummer even na?`),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const json = await response.json().catch(() => {
|
|
121
|
+
// Handle JSON parsing errors
|
|
122
|
+
throw new SimpleError({
|
|
123
|
+
code: 'invalid_json_retrieving_pass_by_uitpas_number',
|
|
124
|
+
message: `Invalid json when retrieving pass by UiTPAS number`,
|
|
125
|
+
human: $t(
|
|
126
|
+
`Er is een fout opgetreden bij het communiceren met UiTPAS. Probeer het later opnieuw.`,
|
|
127
|
+
),
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
assertIsUitpasNumberSuccessfulResponse(json);
|
|
131
|
+
if (json.messages) {
|
|
132
|
+
const humanMessage = json.messages[0].text; // only display the first message
|
|
133
|
+
|
|
134
|
+
// alternatively, join all messages
|
|
135
|
+
// const text = json.messages.map((message: any) => message.text).join(', ');
|
|
136
|
+
|
|
137
|
+
throw new SimpleError({
|
|
138
|
+
code: 'uitpas_number_issue',
|
|
139
|
+
message: `UiTPAS API returned an error: ${humanMessage}`,
|
|
140
|
+
human: humanMessage,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (json.socialTariff.status !== 'ACTIVE') {
|
|
144
|
+
// THIS SHOULD NOT HAPPEN, as in that case json.messages should be present
|
|
145
|
+
throw new SimpleError({
|
|
146
|
+
code: 'non_active_social_tariff',
|
|
147
|
+
message: `UiTPAS social tariff is not ACTIVE but ${json.socialTariff.status}`,
|
|
148
|
+
human: $t(
|
|
149
|
+
`Het opgegeven UiTPAS-nummer heeft geen actief kansentarief. Neem contact op met de UiTPAS-organisatie voor meer informatie.`,
|
|
150
|
+
),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// no errors -> the uitpas number is valid and social tariff is applicable
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const UitpasNumberValidator = new UitpasNumberValidatorStatic();
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
+
import { UitpasClientCredential } from '@stamhoofd/models';
|
|
3
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
4
|
+
|
|
5
|
+
type UitpasTokenResponse = {
|
|
6
|
+
access_token: string;
|
|
7
|
+
expires_in: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function assertIsUitpasTokenResponse(json: unknown): asserts json is UitpasTokenResponse {
|
|
11
|
+
if (
|
|
12
|
+
typeof json !== 'object'
|
|
13
|
+
|| json === null
|
|
14
|
+
|| !('access_token' in json)
|
|
15
|
+
|| !('expires_in' in json)
|
|
16
|
+
|| typeof json.access_token !== 'string'
|
|
17
|
+
|| typeof json.expires_in !== 'number'
|
|
18
|
+
|| json.expires_in <= 0
|
|
19
|
+
) {
|
|
20
|
+
console.error('Invalid response when fetching UiTPAS token:', json);
|
|
21
|
+
throw new SimpleError({
|
|
22
|
+
code: 'invalid_response_fetching_uitpas_token',
|
|
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.`),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class UitpasTokenRepository {
|
|
30
|
+
accessToken?: string;
|
|
31
|
+
expiresOn: Date = new Date(0); // Set to minimum time initially
|
|
32
|
+
uitpasClientCredential: UitpasClientCredential;
|
|
33
|
+
|
|
34
|
+
constructor(uitpasClientCredential: UitpasClientCredential) {
|
|
35
|
+
this.uitpasClientCredential = uitpasClientCredential;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* organizationId (null means platform) -> UitpasTokenRepository
|
|
40
|
+
*/
|
|
41
|
+
static knownTokens: Map<string | null, UitpasTokenRepository> = new Map();
|
|
42
|
+
|
|
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 {
|
|
62
|
+
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.`),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const newRepo = new UitpasTokenRepository(uitpasClientCredential);
|
|
70
|
+
this.knownTokens.set(organizationId, newRepo);
|
|
71
|
+
return newRepo;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async getNewAccessToken(): Promise<string> {
|
|
75
|
+
const url = 'https://account-test.uitid.be/realms/uitid/protocol/openid-connect/token';
|
|
76
|
+
const myHeaders = new Headers();
|
|
77
|
+
myHeaders.append('Content-Type', 'application/x-www-form-urlencoded');
|
|
78
|
+
const params = new URLSearchParams({
|
|
79
|
+
grant_type: 'client_credentials',
|
|
80
|
+
client_id: this.uitpasClientCredential.clientId,
|
|
81
|
+
client_secret: this.uitpasClientCredential.clientSecret,
|
|
82
|
+
});
|
|
83
|
+
const requestOptions: RequestInit = {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: myHeaders,
|
|
86
|
+
body: params.toString(),
|
|
87
|
+
};
|
|
88
|
+
const response = await fetch(url, requestOptions).catch(() => {
|
|
89
|
+
// Handle network errors
|
|
90
|
+
throw new SimpleError({
|
|
91
|
+
code: 'uitpas_unreachable_fetching_uitpas_token',
|
|
92
|
+
message: `Network issue when fetching UiTPAS token`,
|
|
93
|
+
human: $t(`We konden UiTPAS niet bereiken. Probeer het later opnieuw.`),
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
console.error(`Unsuccessful response when fetching UiTPAS token for organization with id ${this.uitpasClientCredential.organizationId}:`, response.statusText);
|
|
98
|
+
throw new SimpleError({
|
|
99
|
+
code: 'unsuccessful_response_fetching_uitpas_token',
|
|
100
|
+
message: `Unsuccesful response when fetching UiTPAS token`,
|
|
101
|
+
human: $t(`Er is een fout opgetreden bij het verbinden met UiTPAS. Probeer het later opnieuw.`),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const json: unknown = await response.json().catch(() => {
|
|
105
|
+
// Handle JSON parsing errors
|
|
106
|
+
throw new SimpleError({
|
|
107
|
+
code: 'invalid_json_fetching_uitpas_token',
|
|
108
|
+
message: `Invalid json when fetching UiTPAS token`,
|
|
109
|
+
human: $t(`Er is een fout opgetreden bij het communiceren met UiTPAS. Probeer het later opnieuw.`),
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
assertIsUitpasTokenResponse(json);
|
|
113
|
+
this.accessToken = json.access_token;
|
|
114
|
+
this.expiresOn = new Date((Date.now() + json.expires_in * 1000) - 10000); // Set expiration 10 seconds earlier to be safe
|
|
115
|
+
return this.accessToken;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the access token for the organization or platform.
|
|
120
|
+
* @param organizationId the organization ID for which to get the access token. If null, it means the platform.
|
|
121
|
+
* @param forceRefresh if true, the access token will be refreshed even if a previously stored token it is still valid
|
|
122
|
+
* @returns Promise<string> the access token for the organization or platform
|
|
123
|
+
* @throws SimpleError if the token cannot be obtained or the API is not configured
|
|
124
|
+
*/
|
|
125
|
+
static async getAccessTokenFor(organizationId: string | null = null, forceRefresh: boolean = false): Promise<string> {
|
|
126
|
+
let repo = UitpasTokenRepository.knownTokens.get(organizationId);
|
|
127
|
+
if (repo && repo.accessToken && !forceRefresh && repo.expiresOn > new Date()) {
|
|
128
|
+
return repo.accessToken;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Prevent multiple concurrent requests for the same organization, asking for an access token to the UiTPAS API.
|
|
132
|
+
// The queue can only run one at a time for the same organizationId
|
|
133
|
+
return await QueueHandler.schedule('uitpas/token-' + (organizationId ?? 'platform'), async () => {
|
|
134
|
+
// 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);
|
|
136
|
+
if (repo && repo.accessToken && !forceRefresh && repo.expiresOn > new Date()) {
|
|
137
|
+
return repo.accessToken;
|
|
138
|
+
}
|
|
139
|
+
if (!repo) {
|
|
140
|
+
repo = await UitpasTokenRepository.createRepoFromDb(organizationId);
|
|
141
|
+
}
|
|
142
|
+
// ask for a new access token
|
|
143
|
+
return repo.getNewAccessToken(); ;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { baseModernSQLFilterCompilers, createColumnFilter, createWildcardColumnFilter, SQL, SQLJsonExtract, SQLModernFilterDefinitions, SQLModernValueType } from '@stamhoofd/sql';
|
|
2
2
|
|
|
3
3
|
export const groupFilterCompilers: SQLModernFilterDefinitions = {
|
|
4
4
|
...baseModernSQLFilterCompilers,
|
|
@@ -47,21 +47,4 @@ export const groupFilterCompilers: SQLModernFilterDefinitions = {
|
|
|
47
47
|
}),
|
|
48
48
|
}),
|
|
49
49
|
),
|
|
50
|
-
|
|
51
|
-
/* name: createSQLExpressionFilterCompiler(
|
|
52
|
-
SQL.jsonValue(SQL.column('groups', 'settings'), '$.value.name'),
|
|
53
|
-
{ isJSONValue: true, type: SQLValueType.JSONString },
|
|
54
|
-
),
|
|
55
|
-
status: createSQLExpressionFilterCompiler(
|
|
56
|
-
SQL.column('groups', 'status'),
|
|
57
|
-
{ isJSONValue: true, type: SQLValueType.JSONString },
|
|
58
|
-
),
|
|
59
|
-
defaultAgeGroupId: createSQLColumnFilterCompiler(SQL.column('groups', 'defaultAgeGroupId'), { nullable: true }),
|
|
60
|
-
|
|
61
|
-
bundleDiscountIds: createSQLExpressionFilterCompiler(
|
|
62
|
-
SQL.jsonKeys(
|
|
63
|
-
SQL.jsonValue(SQL.column('groups', 'settings'), '$.value.prices[0].bundleDiscounts'),
|
|
64
|
-
),
|
|
65
|
-
{ isJSONValue: true, isJSONObject: true },
|
|
66
|
-
), */
|
|
67
50
|
};
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
backendEnv.load({ path: __dirname + '/../../.env.test.json' });
|
|
3
|
-
|
|
4
|
-
import { Database, Migration } from '@simonbackx/simple-database';
|
|
5
|
-
import * as jose from 'jose';
|
|
1
|
+
import { TestUtils } from '@stamhoofd/test-utils';
|
|
6
2
|
import nock from 'nock';
|
|
7
3
|
import path from 'path';
|
|
8
|
-
import { GlobalHelper } from '../src/helpers/GlobalHelper';
|
|
9
4
|
const emailPath = require.resolve('@stamhoofd/email');
|
|
10
5
|
const modelsPath = require.resolve('@stamhoofd/models');
|
|
11
|
-
import { TestUtils } from '@stamhoofd/test-utils';
|
|
12
6
|
|
|
13
7
|
// Disable network requests
|
|
14
8
|
nock.disableNetConnect();
|
|
@@ -22,6 +16,10 @@ if (new Date().getTimezoneOffset() !== 0) {
|
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
export default async () => {
|
|
19
|
+
await TestUtils.globalSetup();
|
|
20
|
+
|
|
21
|
+
const { Database, Migration } = await import('@simonbackx/simple-database');
|
|
22
|
+
|
|
25
23
|
// External migrations
|
|
26
24
|
await Migration.runAll(path.dirname(modelsPath) + '/migrations');
|
|
27
25
|
await Migration.runAll(path.dirname(emailPath) + '/migrations');
|
|
@@ -30,6 +28,4 @@ export default async () => {
|
|
|
30
28
|
await Migration.runAll(__dirname + '/src/migrations');
|
|
31
29
|
|
|
32
30
|
await Database.end();
|
|
33
|
-
|
|
34
|
-
await TestUtils.globalSetup();
|
|
35
31
|
};
|
package/tests/jest.setup.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import backendEnv from '@stamhoofd/backend-env';
|
|
2
|
-
backendEnv.load({ path: __dirname + '/../../.env.test.json' });
|
|
3
|
-
|
|
4
1
|
import { Column, Database } from '@simonbackx/simple-database';
|
|
5
2
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
4
|
import { Email, EmailMocker } from '@stamhoofd/email';
|
|
5
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
7
6
|
import { Version } from '@stamhoofd/structures';
|
|
7
|
+
import { TestUtils } from '@stamhoofd/test-utils';
|
|
8
8
|
import { sleep } from '@stamhoofd/utility';
|
|
9
|
+
import * as jose from 'jose';
|
|
9
10
|
import nock from 'nock';
|
|
10
11
|
import { GlobalHelper } from '../src/helpers/GlobalHelper';
|
|
11
|
-
import * as jose from 'jose';
|
|
12
|
-
import { TestUtils } from '@stamhoofd/test-utils';
|
|
13
|
-
import './toMatchMap';
|
|
14
|
-
import { PayconiqMocker } from './helpers/PayconiqMocker';
|
|
15
12
|
import { BalanceItemService } from '../src/services/BalanceItemService';
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
13
|
+
import { PayconiqMocker } from './helpers/PayconiqMocker';
|
|
14
|
+
import './toMatchMap';
|
|
18
15
|
|
|
19
16
|
// Set version of saved structures
|
|
20
17
|
Column.setJSONVersion(Version);
|
|
@@ -75,7 +72,7 @@ beforeAll(async () => {
|
|
|
75
72
|
await GlobalHelper.load();
|
|
76
73
|
|
|
77
74
|
// Override default $t handlers
|
|
78
|
-
TestUtils.loadEnvironment();
|
|
75
|
+
await TestUtils.loadEnvironment();
|
|
79
76
|
});
|
|
80
77
|
|
|
81
78
|
afterAll(async () => {
|
package/.env.ci.json
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"environment": "test",
|
|
3
|
-
"domains": {
|
|
4
|
-
"dashboard": "dashboard.stamhoofd",
|
|
5
|
-
"marketing": {
|
|
6
|
-
"": "www.be.stamhoofd",
|
|
7
|
-
"BE": "www.be.stamhoofd",
|
|
8
|
-
"NL": "www.nl.stamhoofd"
|
|
9
|
-
},
|
|
10
|
-
"webshop": {
|
|
11
|
-
"": "shop.be.stamhoofd",
|
|
12
|
-
"BE": "shop.be.stamhoofd",
|
|
13
|
-
"NL": "shop.nl.stamhoofd"
|
|
14
|
-
},
|
|
15
|
-
"api": "api.stamhoofd",
|
|
16
|
-
"rendererApi": "renderer.stamhoofd",
|
|
17
|
-
|
|
18
|
-
"defaultTransactionalEmail": {
|
|
19
|
-
"": "stamhoofd.be"
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
"defaultBroadcastEmail": {
|
|
23
|
-
"": "stamhoofd.email"
|
|
24
|
-
},
|
|
25
|
-
"webshopCname": "shop.stamhoofd"
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
"PORT": 9091,
|
|
29
|
-
"DB_HOST": "127.0.0.1",
|
|
30
|
-
"DB_USER": "root",
|
|
31
|
-
"DB_PASS": "root",
|
|
32
|
-
"DB_DATABASE": "stamhoofd-tests",
|
|
33
|
-
|
|
34
|
-
"SMTP_HOST": "0.0.0.0",
|
|
35
|
-
"SMTP_USERNAME": "test",
|
|
36
|
-
"SMTP_PASSWORD": "test",
|
|
37
|
-
"SMTP_PORT": 587,
|
|
38
|
-
|
|
39
|
-
"AWS_ACCESS_KEY_ID": "",
|
|
40
|
-
"AWS_SECRET_ACCESS_KEY": "",
|
|
41
|
-
"AWS_REGION": "",
|
|
42
|
-
|
|
43
|
-
"SPACES_ENDPOINT": "anydomain.example",
|
|
44
|
-
"SPACES_BUCKET": "example",
|
|
45
|
-
"SPACES_KEY": "test",
|
|
46
|
-
"SPACES_SECRET": "test",
|
|
47
|
-
|
|
48
|
-
"INTERNAL_SECRET_KEY": "test",
|
|
49
|
-
|
|
50
|
-
"STRIPE_SECRET_KEY": "sk_test_test",
|
|
51
|
-
"STRIPE_ENDPOINT_SECRET": "sk_test",
|
|
52
|
-
"translationNamespace": "digit",
|
|
53
|
-
"platformName": "ravot",
|
|
54
|
-
"userMode": "organization",
|
|
55
|
-
"WHITELISTED_EMAIL_DESTINATIONS": [],
|
|
56
|
-
"MEMBER_NUMBER_ALGORITHM": "Incremental",
|
|
57
|
-
"MEMBER_NUMBER_ALGORITHM_LENGTH": 10
|
|
58
|
-
}
|
package/.env.template.json
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"environment": "development",
|
|
3
|
-
"domains": {
|
|
4
|
-
"dashboard": "dashboard.stamhoofd",
|
|
5
|
-
"registration": {
|
|
6
|
-
"": "be.stamhoofd",
|
|
7
|
-
"BE": "be.stamhoofd",
|
|
8
|
-
"NL": "nl.stamhoofd"
|
|
9
|
-
},
|
|
10
|
-
"marketing": {
|
|
11
|
-
"": "www.be.stamhoofd",
|
|
12
|
-
"BE": "www.be.stamhoofd",
|
|
13
|
-
"NL": "www.nl.stamhoofd"
|
|
14
|
-
},
|
|
15
|
-
"documentation": {
|
|
16
|
-
"": "www.be.stamhoofd/docs",
|
|
17
|
-
"BE": "www.be.stamhoofd/docs",
|
|
18
|
-
"NL": "www.nl.stamhoofd/docs"
|
|
19
|
-
},
|
|
20
|
-
"webshop": {
|
|
21
|
-
"": "shop.be.stamhoofd",
|
|
22
|
-
"BE": "shop.be.stamhoofd",
|
|
23
|
-
"NL": "shop.nl.stamhoofd"
|
|
24
|
-
},
|
|
25
|
-
"legacyWebshop": "shop.stamhoofd",
|
|
26
|
-
"api": "api.stamhoofd",
|
|
27
|
-
"rendererApi": "renderer.stamhoofd",
|
|
28
|
-
"webshopCname": "shop.stamhoofd"
|
|
29
|
-
},
|
|
30
|
-
"translationNamespace": "stamhoofd",
|
|
31
|
-
"platformName": "stamhoofd",
|
|
32
|
-
"userMode": "organization",
|
|
33
|
-
|
|
34
|
-
"PORT": 9091,
|
|
35
|
-
"DB_HOST": "127.0.0.1",
|
|
36
|
-
"DB_USER": "",
|
|
37
|
-
"DB_PASS": "",
|
|
38
|
-
"DB_DATABASE": "",
|
|
39
|
-
|
|
40
|
-
"SMTP_HOST": "0.0.0.0",
|
|
41
|
-
"SMTP_USERNAME": "username",
|
|
42
|
-
"SMTP_PASSWORD": "password",
|
|
43
|
-
"SMTP_PORT": 1025,
|
|
44
|
-
|
|
45
|
-
"TRANSACTIONAL_SMTP_HOST": "0.0.0.0",
|
|
46
|
-
"TRANSACTIONAL_SMTP_USERNAME": "username",
|
|
47
|
-
"TRANSACTIONAL_SMTP_PASSWORD": "password",
|
|
48
|
-
"TRANSACTIONAL_SMTP_PORT": 1025,
|
|
49
|
-
|
|
50
|
-
"AWS_ACCESS_KEY_ID": "",
|
|
51
|
-
"AWS_SECRET_ACCESS_KEY": "",
|
|
52
|
-
"AWS_REGION": "",
|
|
53
|
-
|
|
54
|
-
"SPACES_ENDPOINT": "",
|
|
55
|
-
"SPACES_BUCKET": "",
|
|
56
|
-
"SPACES_KEY": "",
|
|
57
|
-
"SPACES_SECRET": "",
|
|
58
|
-
|
|
59
|
-
"MOLLIE_CLIENT_ID": "",
|
|
60
|
-
"MOLLIE_SECRET": "",
|
|
61
|
-
"MOLLIE_API_KEY": "",
|
|
62
|
-
"MOLLIE_ORGANIZATION_TOKEN": "",
|
|
63
|
-
|
|
64
|
-
"LATEST_IOS_VERSION": 0,
|
|
65
|
-
"LATEST_ANDROID_VERSION": 0,
|
|
66
|
-
|
|
67
|
-
"NOLT_SSO_SECRET_KEY": "optional",
|
|
68
|
-
"INTERNAL_SECRET_KEY": "",
|
|
69
|
-
"CRONS_DISABLED": false,
|
|
70
|
-
"WHITELISTED_EMAIL_DESTINATIONS": ["*"],
|
|
71
|
-
"CACHE_PATH": "<fill in a safe path exlusive for Stamhoofd to store cached files>"
|
|
72
|
-
}
|