@stamhoofd/backend 2.66.0 → 2.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.66.0",
3
+ "version": "2.68.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -37,14 +37,14 @@
37
37
  "@simonbackx/simple-encoding": "2.19.0",
38
38
  "@simonbackx/simple-endpoints": "1.15.0",
39
39
  "@simonbackx/simple-logging": "^1.0.1",
40
- "@stamhoofd/backend-i18n": "2.66.0",
41
- "@stamhoofd/backend-middleware": "2.66.0",
42
- "@stamhoofd/email": "2.66.0",
43
- "@stamhoofd/models": "2.66.0",
44
- "@stamhoofd/queues": "2.66.0",
45
- "@stamhoofd/sql": "2.66.0",
46
- "@stamhoofd/structures": "2.66.0",
47
- "@stamhoofd/utility": "2.66.0",
40
+ "@stamhoofd/backend-i18n": "2.68.0",
41
+ "@stamhoofd/backend-middleware": "2.68.0",
42
+ "@stamhoofd/email": "2.68.0",
43
+ "@stamhoofd/models": "2.68.0",
44
+ "@stamhoofd/queues": "2.68.0",
45
+ "@stamhoofd/sql": "2.68.0",
46
+ "@stamhoofd/structures": "2.68.0",
47
+ "@stamhoofd/utility": "2.68.0",
48
48
  "archiver": "^7.0.1",
49
49
  "aws-sdk": "^2.885.0",
50
50
  "axios": "1.6.8",
@@ -64,5 +64,5 @@
64
64
  "publishConfig": {
65
65
  "access": "public"
66
66
  },
67
- "gitHead": "e4c406ad52c3f2018f5fbff17869a2003ec773c2"
67
+ "gitHead": "6356e14541b66873ca0d2ccd4b660825ac39a7fd"
68
68
  }
@@ -2,8 +2,10 @@ import { AnyDecoder, Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
 
5
- import { Context } from '../../../../helpers/Context';
6
- import { OpenIDConnectHelper } from '../../../../helpers/OpenIDConnectHelper';
5
+ import { Context } from '../../helpers/Context';
6
+ import { OpenIDConnectHelper } from '../../helpers/OpenIDConnectHelper';
7
+ import { OpenIDClientConfiguration } from '@stamhoofd/structures';
8
+ import { Platform } from '@stamhoofd/models';
7
9
 
8
10
  type Params = Record<string, never>;
9
11
  type Query = undefined;
@@ -27,13 +29,21 @@ export class OpenIDConnectCallbackEndpoint extends Endpoint<Params, Query, Body,
27
29
  }
28
30
 
29
31
  async handle(request: DecodedRequest<Params, Query, Body>) {
30
- const organization = await Context.setOrganizationScope();
31
- const configuration = organization.serverMeta.ssoConfiguration;
32
+ const organization = await Context.setOptionalOrganizationScope();
33
+ let configuration: OpenIDClientConfiguration | null;
34
+ const platform = await Platform.getShared();
35
+
36
+ if (organization) {
37
+ configuration = organization.serverMeta.ssoConfiguration;
38
+ }
39
+ else {
40
+ configuration = platform.serverConfig.ssoConfiguration;
41
+ }
32
42
 
33
43
  if (!configuration) {
34
44
  throw new SimpleError({
35
- code: 'invalid_configuration',
36
- message: 'Invalid configuration',
45
+ code: 'invalid_client',
46
+ message: 'SSO not configured',
37
47
  statusCode: 400,
38
48
  });
39
49
  }
@@ -1,11 +1,11 @@
1
1
  import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
- import { Webshop } from '@stamhoofd/models';
5
- import { StartOpenIDFlowStruct } from '@stamhoofd/structures';
4
+ import { Platform, Webshop } from '@stamhoofd/models';
5
+ import { OpenIDClientConfiguration, StartOpenIDFlowStruct } from '@stamhoofd/structures';
6
6
 
7
- import { Context } from '../../../../helpers/Context';
8
- import { OpenIDConnectHelper } from '../../../../helpers/OpenIDConnectHelper';
7
+ import { Context } from '../../helpers/Context';
8
+ import { OpenIDConnectHelper } from '../../helpers/OpenIDConnectHelper';
9
9
 
10
10
  type Params = Record<string, never>;
11
11
  type Query = undefined;
@@ -30,11 +30,28 @@ export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, Re
30
30
 
31
31
  async handle(request: DecodedRequest<Params, Query, Body>) {
32
32
  // Check webshop and/or organization
33
- const organization = await Context.setOrganizationScope();
33
+ const organization = await Context.setOptionalOrganizationScope();
34
34
  const webshopId = request.body.webshopId;
35
- let redirectUri = 'https://' + organization.getHost();
35
+ const platform = await Platform.getShared();
36
+
37
+ // Host should match correctly
38
+ let redirectUri = 'https://' + STAMHOOFD.domains.dashboard;
39
+
40
+ if (organization) {
41
+ redirectUri = 'https://' + organization.getHost();
42
+ }
43
+
44
+ // todo: also support the app as redirect uri using app schemes (could be required for mobile apps)
36
45
 
37
46
  if (webshopId) {
47
+ if (!organization) {
48
+ throw new SimpleError({
49
+ code: 'invalid_organization',
50
+ message: 'Organization required when specifying webshopId',
51
+ statusCode: 400,
52
+ });
53
+ }
54
+
38
55
  const webshop = await Webshop.getByID(webshopId);
39
56
  if (!webshop || webshop.organizationId !== organization.id) {
40
57
  throw new SimpleError({
@@ -68,7 +85,15 @@ export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, Re
68
85
  });
69
86
  }
70
87
 
71
- const configuration = organization.serverMeta.ssoConfiguration;
88
+ let configuration: OpenIDClientConfiguration | null;
89
+
90
+ if (organization) {
91
+ configuration = organization.serverMeta.ssoConfiguration;
92
+ }
93
+ else {
94
+ configuration = platform.serverConfig.ssoConfiguration;
95
+ }
96
+
72
97
  if (!configuration) {
73
98
  throw new SimpleError({
74
99
  code: 'invalid_client',
@@ -76,7 +101,6 @@ export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, Re
76
101
  statusCode: 400,
77
102
  });
78
103
  }
79
-
80
104
  const helper = new OpenIDConnectHelper(organization, configuration);
81
105
  return await helper.startAuthCodeFlow(redirectUri, request.body.provider, request.body.spaState, request.body.prompt);
82
106
  }
@@ -979,6 +979,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
979
979
  });
980
980
  }
981
981
  const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
982
+ const locale = Context.i18n.locale.replace('-', '_');
982
983
  const molliePayment = await mollieClient.payments.create({
983
984
  amount: {
984
985
  currency: 'EUR',
@@ -993,6 +994,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
993
994
  metadata: {
994
995
  paymentId: payment.id,
995
996
  },
997
+ locale: ['en_US', 'en_GB', 'nl_NL', 'nl_BE', 'fr_FR', 'fr_BE', 'de_DE', 'de_AT', 'de_CH', 'es_ES', 'ca_ES', 'pt_PT', 'it_IT', 'nb_NO', 'sv_SE', 'fi_FI', 'da_DK', 'is_IS', 'hu_HU', 'pl_PL', 'lv_LV', 'lt_LT'].includes(locale) ? (locale as any) : null,
996
998
  });
997
999
  paymentUrl = molliePayment.getCheckoutUrl();
998
1000
 
@@ -1,7 +1,8 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
2
  import { OpenIDClientConfiguration } from '@stamhoofd/structures';
3
3
 
4
- import { Context } from '../../../../helpers/Context';
4
+ import { Context } from '../../../helpers/Context';
5
+ import { Platform } from '@stamhoofd/models';
5
6
 
6
7
  type Params = Record<string, never>;
7
8
  type Query = undefined;
@@ -18,7 +19,7 @@ export class GetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, Re
18
19
  return [false];
19
20
  }
20
21
 
21
- const params = Endpoint.parseParameters(request.url, '/organization/sso', {});
22
+ const params = Endpoint.parseParameters(request.url, '/sso', {});
22
23
 
23
24
  if (params) {
24
25
  return [true, params as Params];
@@ -27,17 +28,18 @@ export class GetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, Re
27
28
  }
28
29
 
29
30
  async handle(_: DecodedRequest<Params, Query, Body>) {
30
- const organization = await Context.setOrganizationScope();
31
+ const organization = await Context.setOptionalOrganizationScope();
31
32
  await Context.authenticate();
32
33
 
33
- if (!await Context.auth.canManageSSOSettings(organization.id)) {
34
+ if (!await Context.auth.canManageSSOSettings(organization?.id ?? null)) {
34
35
  throw Context.auth.error();
35
36
  }
36
37
 
37
- return new Response(organization.serverMeta.ssoConfiguration ?? OpenIDClientConfiguration.create({
38
- clientId: '',
39
- clientSecret: '',
40
- issuer: '',
41
- }));
38
+ if (organization) {
39
+ return new Response(organization.serverMeta.ssoConfiguration ?? OpenIDClientConfiguration.create({}));
40
+ }
41
+
42
+ const platform = await Platform.getShared();
43
+ return new Response(platform.serverConfig.ssoConfiguration ?? OpenIDClientConfiguration.create({}));
42
44
  }
43
45
  }
@@ -0,0 +1,68 @@
1
+ import { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { OpenIDClientConfiguration } from '@stamhoofd/structures';
4
+
5
+ import { Context } from '../../../helpers/Context';
6
+ import { OpenIDConnectHelper } from '../../../helpers/OpenIDConnectHelper';
7
+ import { Platform } from '@stamhoofd/models';
8
+
9
+ type Params = Record<string, never>;
10
+ type Query = undefined;
11
+ type Body = AutoEncoderPatchType<OpenIDClientConfiguration>;
12
+ type ResponseBody = OpenIDClientConfiguration;
13
+
14
+ /**
15
+ * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
16
+ */
17
+
18
+ export class SetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+ bodyDecoder = OpenIDClientConfiguration.patchType() as Decoder<AutoEncoderPatchType<OpenIDClientConfiguration>>;
20
+
21
+ protected doesMatch(request: Request): [true, Params] | [false] {
22
+ if (request.method !== 'POST') {
23
+ return [false];
24
+ }
25
+
26
+ const params = Endpoint.parseParameters(request.url, '/sso', {});
27
+
28
+ if (params) {
29
+ return [true, params as Params];
30
+ }
31
+ return [false];
32
+ }
33
+
34
+ async handle(request: DecodedRequest<Params, Query, Body>) {
35
+ const organization = await Context.setOptionalOrganizationScope();
36
+ await Context.authenticate();
37
+
38
+ if (!await Context.auth.canManageSSOSettings(organization?.id ?? null)) {
39
+ throw Context.auth.error();
40
+ }
41
+
42
+ let newConfig: OpenIDClientConfiguration;
43
+
44
+ if (organization) {
45
+ newConfig = (organization.serverMeta.ssoConfiguration ?? OpenIDClientConfiguration.create({})).patch(request.body);
46
+
47
+ // Validate configuration
48
+ const helper = new OpenIDConnectHelper(organization, newConfig);
49
+ await helper.getClient();
50
+
51
+ organization.serverMeta.ssoConfiguration = newConfig;
52
+ await organization.save();
53
+ }
54
+ else {
55
+ const platform = await Platform.getShared();
56
+ newConfig = (platform.serverConfig.ssoConfiguration ?? OpenIDClientConfiguration.create({})).patch(request.body);
57
+
58
+ // Validate configuration
59
+ const helper = new OpenIDConnectHelper(null, newConfig);
60
+ await helper.getClient();
61
+
62
+ platform.serverConfig.ssoConfiguration = newConfig;
63
+ await platform.save();
64
+ }
65
+
66
+ return new Response(newConfig);
67
+ }
68
+ }
@@ -243,8 +243,22 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
243
243
  }
244
244
  const platform = await Platform.getSharedStruct();
245
245
 
246
- if (platform.config.defaultAgeGroups.find(g => g.id === id)) {
247
- return id;
246
+ const defaultAgeGroup = platform.config.defaultAgeGroups.find(g => g.id === id);
247
+
248
+ if (defaultAgeGroup) {
249
+ const organization = Context.organization;
250
+ const tags = organization?.meta.tags ?? [];
251
+
252
+ if (defaultAgeGroup.isEnabledForTags(tags)) {
253
+ return id;
254
+ }
255
+
256
+ throw new SimpleError({
257
+ code: 'invalid_default_age_group',
258
+ message: 'Invalid default age group',
259
+ human: 'De standaard leeftijdsgroep is niet beschikbaar voor deze organisatie',
260
+ statusCode: 400,
261
+ });
248
262
  }
249
263
 
250
264
  throw new SimpleError({
@@ -292,6 +292,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
292
292
  });
293
293
  }
294
294
  const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
295
+ const locale = Context.i18n.locale.replace('-', '_');
295
296
  const molliePayment = await mollieClient.payments.create({
296
297
  amount: {
297
298
  currency: 'EUR',
@@ -309,6 +310,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
309
310
  webshop: webshop.id,
310
311
  payment: payment.id,
311
312
  },
313
+ locale: ['en_US', 'en_GB', 'nl_NL', 'nl_BE', 'fr_FR', 'fr_BE', 'de_DE', 'de_AT', 'de_CH', 'es_ES', 'ca_ES', 'pt_PT', 'it_IT', 'nb_NO', 'sv_SE', 'fi_FI', 'da_DK', 'is_IS', 'hu_HU', 'pl_PL', 'lv_LV', 'lt_LT'].includes(locale) ? (locale as any) : null,
312
314
  });
313
315
  console.log(molliePayment);
314
316
  paymentUrl = molliePayment.getCheckoutUrl();
@@ -758,7 +758,10 @@ export class AdminPermissionChecker {
758
758
  return this.hasFullAccess(organizationId);
759
759
  }
760
760
 
761
- canManageSSOSettings(organizationId: string) {
761
+ canManageSSOSettings(organizationId: string | null) {
762
+ if (!organizationId) {
763
+ return this.hasPlatformFullAccess();
764
+ }
762
765
  return this.hasFullAccess(organizationId);
763
766
  }
764
767
 
@@ -30,17 +30,22 @@ type SessionContext = {
30
30
  };
31
31
 
32
32
  export class OpenIDConnectHelper {
33
- organization: Organization;
33
+ organization: Organization | null;
34
34
  configuration: OpenIDClientConfiguration;
35
35
 
36
36
  static sessionStorage = new Map<string, SessionContext>();
37
37
 
38
- constructor(organization, configuration: OpenIDClientConfiguration) {
38
+ constructor(organization: Organization | null, configuration: OpenIDClientConfiguration) {
39
39
  this.organization = organization;
40
40
  this.configuration = configuration;
41
41
  }
42
42
 
43
43
  get redirectUri() {
44
+ // todo: we might need a special url for the app here
45
+
46
+ if (!this.organization) {
47
+ return 'https://' + STAMHOOFD.domains.api + '/openid/callback';
48
+ }
44
49
  return 'https://' + this.organization.id + '.' + STAMHOOFD.domains.api + '/openid/callback';
45
50
  }
46
51
 
@@ -209,7 +214,7 @@ export class OpenIDConnectHelper {
209
214
  }
210
215
 
211
216
  // Get user from database
212
- let user = await User.getOrganizationLevelUser(this.organization.id, claims.email);
217
+ let user = await User.getForRegister(this.organization?.id ?? null, claims.email);
213
218
  if (!user) {
214
219
  // Create a new user
215
220
  user = await User.registerSSO(this.organization, {
@@ -1,50 +0,0 @@
1
- import { Decoder } from '@simonbackx/simple-encoding';
2
- import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { OpenIDClientConfiguration } from '@stamhoofd/structures';
4
-
5
- import { Context } from '../../../../helpers/Context';
6
- import { OpenIDConnectHelper } from '../../../../helpers/OpenIDConnectHelper';
7
-
8
- type Params = Record<string, never>;
9
- type Query = undefined;
10
- type Body = OpenIDClientConfiguration;
11
- type ResponseBody = OpenIDClientConfiguration;
12
-
13
- /**
14
- * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
15
- */
16
-
17
- export class SetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
18
- bodyDecoder = OpenIDClientConfiguration as Decoder<OpenIDClientConfiguration>;
19
-
20
- protected doesMatch(request: Request): [true, Params] | [false] {
21
- if (request.method !== 'POST') {
22
- return [false];
23
- }
24
-
25
- const params = Endpoint.parseParameters(request.url, '/organization/sso', {});
26
-
27
- if (params) {
28
- return [true, params as Params];
29
- }
30
- return [false];
31
- }
32
-
33
- async handle(request: DecodedRequest<Params, Query, Body>) {
34
- const organization = await Context.setOrganizationScope();
35
- await Context.authenticate();
36
-
37
- if (!await Context.auth.canManageSSOSettings(organization.id)) {
38
- throw Context.auth.error();
39
- }
40
-
41
- // Validate configuration
42
- const helper = new OpenIDConnectHelper(organization, request.body);
43
- await helper.getClient();
44
-
45
- organization.serverMeta.ssoConfiguration = request.body;
46
- await organization.save();
47
-
48
- return new Response(request.body);
49
- }
50
- }