@stamhoofd/backend 2.70.0 → 2.72.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.70.0",
3
+ "version": "2.72.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.70.0",
41
- "@stamhoofd/backend-middleware": "2.70.0",
42
- "@stamhoofd/email": "2.70.0",
43
- "@stamhoofd/models": "2.70.0",
44
- "@stamhoofd/queues": "2.70.0",
45
- "@stamhoofd/sql": "2.70.0",
46
- "@stamhoofd/structures": "2.70.0",
47
- "@stamhoofd/utility": "2.70.0",
40
+ "@stamhoofd/backend-i18n": "2.72.0",
41
+ "@stamhoofd/backend-middleware": "2.72.0",
42
+ "@stamhoofd/email": "2.72.0",
43
+ "@stamhoofd/models": "2.72.0",
44
+ "@stamhoofd/queues": "2.72.0",
45
+ "@stamhoofd/sql": "2.72.0",
46
+ "@stamhoofd/structures": "2.72.0",
47
+ "@stamhoofd/utility": "2.72.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": "5c6f4bbab24bf118b45da350bdaf3c62756909ad"
67
+ "gitHead": "9ca2f0fff00eafff2a398dbfdbfb032367473d1c"
68
68
  }
@@ -3,7 +3,7 @@ import { AuditLogType } from '@stamhoofd/structures';
3
3
  import { getDefaultGenerator, ModelLogger } from './ModelLogger';
4
4
 
5
5
  export const OrganizationLogger = new ModelLogger(Organization, {
6
- skipKeys: ['searchIndex'],
6
+ skipKeys: ['searchIndex', 'serverMeta'],
7
7
  optionsGenerator: getDefaultGenerator({
8
8
  created: AuditLogType.OrganizationAdded,
9
9
  updated: AuditLogType.OrganizationEdited,
@@ -3,6 +3,7 @@ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
3
3
  import { AuditLogType } from '@stamhoofd/structures';
4
4
 
5
5
  export const PlatformLogger = new ModelLogger(Platform, {
6
+ skipKeys: ['serverConfig'],
6
7
  optionsGenerator: getDefaultGenerator({
7
8
  updated: AuditLogType.PlatformSettingsChanged,
8
9
  }),
@@ -86,7 +86,8 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
86
86
 
87
87
  if (STAMHOOFD.userMode === 'platform') {
88
88
  const platform = await Platform.getShared();
89
- if (!platform.config.loginMethods.includes(LoginMethod.Password)) {
89
+ const config = platform.config.loginMethods.get(LoginMethod.Password);
90
+ if (!config) {
90
91
  throw new SimpleError({
91
92
  code: 'not_supported',
92
93
  message: 'This platform does not support password login',
@@ -94,6 +95,15 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
94
95
  statusCode: 400,
95
96
  });
96
97
  }
98
+
99
+ if (!config.isEnabledForEmail(request.body.username)) {
100
+ throw new SimpleError({
101
+ code: 'not_supported',
102
+ message: 'Login method not supported',
103
+ human: 'Je kan op dit account niet inloggen met een wachtwoord. Gebruik een andere methode om in te loggen.',
104
+ statusCode: 400,
105
+ });
106
+ }
97
107
  }
98
108
 
99
109
  const user = await User.login(organization?.id ?? null, request.body.username, request.body.password);
@@ -1,8 +1,9 @@
1
1
  import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { PasswordToken, sendEmailTemplate, User } from '@stamhoofd/models';
4
- import { EmailTemplateType, ForgotPasswordRequest, Recipient, Replacement } from '@stamhoofd/structures';
3
+ import { PasswordToken, Platform, sendEmailTemplate, User } from '@stamhoofd/models';
4
+ import { EmailTemplateType, ForgotPasswordRequest, LoginMethod, Recipient, Replacement } from '@stamhoofd/structures';
5
5
 
6
+ import { SimpleError } from '@simonbackx/simple-errors';
6
7
  import { Context } from '../../helpers/Context';
7
8
 
8
9
  type Params = Record<string, never>;
@@ -28,6 +29,29 @@ export class ForgotPasswordEndpoint extends Endpoint<Params, Query, Body, Respon
28
29
 
29
30
  async handle(request: DecodedRequest<Params, Query, Body>) {
30
31
  const organization = await Context.setOptionalOrganizationScope();
32
+
33
+ if (STAMHOOFD.userMode === 'platform') {
34
+ const platform = await Platform.getShared();
35
+ const config = platform.config.loginMethods.get(LoginMethod.Password);
36
+ if (!config) {
37
+ throw new SimpleError({
38
+ code: 'not_supported',
39
+ message: 'This platform does not support password login',
40
+ human: 'Dit platform ondersteunt geen wachtwoord login',
41
+ statusCode: 400,
42
+ });
43
+ }
44
+
45
+ if (!config.isEnabledForEmail(request.body.email)) {
46
+ throw new SimpleError({
47
+ code: 'not_supported',
48
+ message: 'Login method not supported',
49
+ human: 'Je kan op dit account geen wachtwoord gebruiken om in te loggen.',
50
+ statusCode: 400,
51
+ });
52
+ }
53
+ }
54
+
31
55
  const user = await User.getForAuthentication(organization?.id ?? null, request.body.email, { allowWithoutAccount: true });
32
56
 
33
57
  if (!user) {
@@ -1,11 +1,8 @@
1
1
  import { AnyDecoder, Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request } from '@simonbackx/simple-endpoints';
3
- import { SimpleError } from '@simonbackx/simple-errors';
4
3
 
5
4
  import { Context } from '../../helpers/Context';
6
- import { OpenIDConnectHelper } from '../../helpers/OpenIDConnectHelper';
7
- import { OpenIDClientConfiguration } from '@stamhoofd/structures';
8
- import { Platform } from '@stamhoofd/models';
5
+ import { SSOServiceWithSession } from '../../services/SSOService';
9
6
 
10
7
  type Params = Record<string, never>;
11
8
  type Query = undefined;
@@ -29,26 +26,8 @@ export class OpenIDConnectCallbackEndpoint extends Endpoint<Params, Query, Body,
29
26
  }
30
27
 
31
28
  async handle(request: DecodedRequest<Params, Query, Body>) {
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
- }
42
-
43
- if (!configuration) {
44
- throw new SimpleError({
45
- code: 'invalid_client',
46
- message: 'SSO not configured',
47
- statusCode: 400,
48
- });
49
- }
50
-
51
- const helper = new OpenIDConnectHelper(organization, configuration);
52
- return await helper.callback(request);
29
+ await Context.setUserOrganizationScope();
30
+ const ssoService = await SSOServiceWithSession.fromSession(request);
31
+ return await ssoService.callback();
53
32
  }
54
33
  }
@@ -1,11 +1,9 @@
1
1
  import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request } from '@simonbackx/simple-endpoints';
3
- import { SimpleError } from '@simonbackx/simple-errors';
4
- import { Platform, Webshop } from '@stamhoofd/models';
5
- import { OpenIDClientConfiguration, StartOpenIDFlowStruct } from '@stamhoofd/structures';
3
+ import { StartOpenIDFlowStruct } from '@stamhoofd/structures';
6
4
 
7
5
  import { Context } from '../../helpers/Context';
8
- import { OpenIDConnectHelper } from '../../helpers/OpenIDConnectHelper';
6
+ import { SSOService } from '../../services/SSOService';
9
7
 
10
8
  type Params = Record<string, never>;
11
9
  type Query = undefined;
@@ -30,78 +28,14 @@ export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, Re
30
28
 
31
29
  async handle(request: DecodedRequest<Params, Query, Body>) {
32
30
  // Check webshop and/or organization
33
- const organization = await Context.setOptionalOrganizationScope();
34
- const webshopId = request.body.webshopId;
35
- const platform = await Platform.getShared();
31
+ await Context.setUserOrganizationScope();
32
+ await Context.optionalAuthenticate({ allowWithoutAccount: false });
33
+ console.log('Full start connect body;', await request.request.body);
36
34
 
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)
45
-
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
-
55
- const webshop = await Webshop.getByID(webshopId);
56
- if (!webshop || webshop.organizationId !== organization.id) {
57
- throw new SimpleError({
58
- code: 'invalid_webshop',
59
- message: 'Invalid webshop',
60
- statusCode: 400,
61
- });
62
- }
63
- redirectUri = 'https://' + webshop.setRelation(Webshop.organization, organization).getHost();
64
- }
65
-
66
- if (request.body.redirectUri) {
67
- try {
68
- const allowedHost = new URL(redirectUri);
69
- const givenUrl = new URL(request.body.redirectUri);
70
-
71
- if (allowedHost.host === givenUrl.host && givenUrl.protocol === 'https:') {
72
- redirectUri = givenUrl.href;
73
- }
74
- }
75
- catch (e) {
76
- console.error('Invalid redirect uri', request.body.redirectUri);
77
- }
78
- }
79
-
80
- if (request.body.spaState.length < 10) {
81
- throw new SimpleError({
82
- code: 'invalid_state',
83
- message: 'Invalid state',
84
- statusCode: 400,
85
- });
86
- }
87
-
88
- let configuration: OpenIDClientConfiguration | null;
89
-
90
- if (organization) {
91
- configuration = organization.serverMeta.ssoConfiguration;
92
- }
93
- else {
94
- configuration = platform.serverConfig.ssoConfiguration;
95
- }
96
-
97
- if (!configuration) {
98
- throw new SimpleError({
99
- code: 'invalid_client',
100
- message: 'SSO not configured',
101
- statusCode: 400,
102
- });
35
+ if (Context.user) {
36
+ console.log('User:', Context.user);
103
37
  }
104
- const helper = new OpenIDConnectHelper(organization, configuration);
105
- return await helper.startAuthCodeFlow(redirectUri, request.body.provider, request.body.spaState, request.body.prompt);
38
+ const service = await SSOService.fromContext(request.body.provider);
39
+ return await service.validateAndStartAuthCodeFlow(request.body);
106
40
  }
107
41
  }
@@ -1,8 +1,8 @@
1
- import { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding';
1
+ import { AutoEncoderPatchType, Decoder, isPatch } 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 { EmailVerificationCode, Member, PasswordToken, Token, User } from '@stamhoofd/models';
5
- import { NewUser, PermissionLevel, SignupResponse, UserPermissions, UserWithMembers } from '@stamhoofd/structures';
4
+ import { EmailVerificationCode, Member, PasswordToken, Platform, Token, User } from '@stamhoofd/models';
5
+ import { LoginMethod, NewUser, PermissionLevel, SignupResponse, UserPermissions, UserWithMembers } from '@stamhoofd/structures';
6
6
 
7
7
  import { Context } from '../../helpers/Context';
8
8
  import { MemberUserSyncer } from '../../helpers/MemberUserSyncer';
@@ -109,13 +109,77 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
109
109
  }
110
110
  }
111
111
 
112
+ if (request.body.meta) {
113
+ if (request.body.meta.loginProviderIds && isPatch(request.body.meta.loginProviderIds)) {
114
+ // Delete deleted login providers
115
+ for (const [key, value] of request.body.meta.loginProviderIds) {
116
+ if (value !== null) {
117
+ // Not allowed
118
+ throw Context.auth.error('You are not allowed to change the login provider ids');
119
+ }
120
+
121
+ if (editUser.meta?.loginProviderIds.has(key)) {
122
+ // Check has remaining method
123
+ if (editUser.meta.loginProviderIds.size <= 1 && !editUser.hasPasswordBasedAccount()) {
124
+ throw new SimpleError({
125
+ code: 'invalid_request',
126
+ message: 'You cannot remove the last login provider',
127
+ human: 'Stel eerst een wachtwoord in voor jouw account voor je deze loginmethode uitschakelt.',
128
+ statusCode: 400,
129
+ });
130
+ }
131
+ editUser.meta.loginProviderIds.delete(key);
132
+ }
133
+ }
134
+ }
135
+ }
136
+
112
137
  if (editUser.id === user.id && request.body.password) {
138
+ if (STAMHOOFD.userMode === 'platform') {
139
+ const platform = await Platform.getShared();
140
+ const config = platform.config.loginMethods.get(LoginMethod.Password);
141
+ if (!config) {
142
+ throw new SimpleError({
143
+ code: 'not_supported',
144
+ message: 'This platform does not support password login',
145
+ human: 'Dit platform ondersteunt geen wachtwoord login',
146
+ statusCode: 400,
147
+ });
148
+ }
149
+
150
+ if (!config.isEnabledForEmail(editUser.email)) {
151
+ throw new SimpleError({
152
+ code: 'not_supported',
153
+ message: 'Login method not supported',
154
+ human: 'Je kan op dit account geen wachtwoord gebruiken om in te loggen.',
155
+ statusCode: 400,
156
+ });
157
+ }
158
+ }
159
+
113
160
  // password changes
114
161
  await editUser.changePassword(request.body.password);
115
162
  await PasswordToken.clearFor(editUser.id);
116
163
  await Token.clearFor(editUser.id, token.accessToken);
117
164
  }
118
165
 
166
+ if (request.body.hasPassword === false) {
167
+ if (editUser.hasPasswordBasedAccount()) {
168
+ // Check other login methods available
169
+ if (!editUser.meta?.loginProviderIds?.size) {
170
+ throw new SimpleError({
171
+ code: 'invalid_request',
172
+ message: 'You cannot remove the last login provider',
173
+ human: 'Je kan jouw wachtwoord niet verwijderen als je geen andere loginmethode hebt.',
174
+ statusCode: 400,
175
+ });
176
+ }
177
+ editUser.password = null;
178
+ await PasswordToken.clearFor(editUser.id);
179
+ await Token.clearFor(editUser.id, token.accessToken);
180
+ }
181
+ }
182
+
119
183
  await editUser.save();
120
184
 
121
185
  if (await Context.auth.canEditUserEmail(editUser)) {
@@ -32,7 +32,7 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
32
32
 
33
33
  if (STAMHOOFD.userMode === 'platform') {
34
34
  const platform = await Platform.getShared();
35
- if (!platform.config.loginMethods.includes(LoginMethod.Password)) {
35
+ if (!platform.config.loginMethods.has(LoginMethod.Password)) {
36
36
  throw new SimpleError({
37
37
  code: 'not_supported',
38
38
  message: 'This platform does not support password login',
@@ -3,15 +3,15 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Member, Platform } from '@stamhoofd/models';
5
5
  import { SQL, compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
6
- import { CountFilteredRequest, Country, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
6
+ import { CountFilteredRequest, Country, CountryCode, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
7
7
  import { DataValidator } from '@stamhoofd/utility';
8
8
 
9
+ import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
9
10
  import parsePhoneNumber from 'libphonenumber-js/max';
10
11
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
11
12
  import { Context } from '../../../helpers/Context';
12
13
  import { memberFilterCompilers } from '../../../sql-filters/members';
13
14
  import { memberSorters } from '../../../sql-sorters/members';
14
- import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
15
15
 
16
16
  type Params = Record<string, never>;
17
17
  type Query = LimitedFilteredRequest;
@@ -141,7 +141,9 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
141
141
  if (!searchFilter && q.search.match(/^\+?[0-9\s-]+$/)) {
142
142
  // Try to format as phone so we have 1:1 space matches
143
143
  try {
144
- const phoneNumber = parsePhoneNumber(q.search, (Context.i18n.country as Country) || Country.Belgium);
144
+ const country = (Context.i18n.country as CountryCode) || Country.Belgium;
145
+
146
+ const phoneNumber = parsePhoneNumber(q.search, country);
145
147
  if (phoneNumber && phoneNumber.isValid()) {
146
148
  const formatted = phoneNumber.formatInternational();
147
149
  searchFilter = {
@@ -1,11 +1,17 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { OpenIDClientConfiguration } from '@stamhoofd/structures';
2
+ import { LoginProviderType, OpenIDClientConfiguration } from '@stamhoofd/structures';
3
3
 
4
4
  import { Context } from '../../../helpers/Context';
5
5
  import { Platform } from '@stamhoofd/models';
6
+ import { AutoEncoder, Decoder, EnumDecoder, field } from '@simonbackx/simple-encoding';
7
+ import { SSOService } from '../../../services/SSOService';
6
8
 
7
9
  type Params = Record<string, never>;
8
- type Query = undefined;
10
+ export class SSOQuery extends AutoEncoder {
11
+ @field({ decoder: new EnumDecoder(LoginProviderType) })
12
+ provider: LoginProviderType;
13
+ }
14
+ type Query = SSOQuery;
9
15
  type Body = undefined;
10
16
  type ResponseBody = OpenIDClientConfiguration;
11
17
 
@@ -14,6 +20,8 @@ type ResponseBody = OpenIDClientConfiguration;
14
20
  */
15
21
 
16
22
  export class GetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
23
+ queryDecoder = SSOQuery as Decoder<Query>;
24
+
17
25
  protected doesMatch(request: Request): [true, Params] | [false] {
18
26
  if (request.method !== 'GET') {
19
27
  return [false];
@@ -27,19 +35,22 @@ export class GetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, Re
27
35
  return [false];
28
36
  }
29
37
 
30
- async handle(_: DecodedRequest<Params, Query, Body>) {
38
+ async handle(request: DecodedRequest<Params, Query, Body>) {
31
39
  const organization = await Context.setOptionalOrganizationScope();
32
40
  await Context.authenticate();
41
+ const service = await SSOService.fromContext(request.query.provider);
33
42
 
34
43
  if (!await Context.auth.canManageSSOSettings(organization?.id ?? null)) {
35
44
  throw Context.auth.error();
36
45
  }
37
46
 
38
- if (organization) {
39
- return new Response(organization.serverMeta.ssoConfiguration ?? OpenIDClientConfiguration.create({}));
47
+ const configuration = service.configuration.clone();
48
+
49
+ // Remove secret by placeholder asterisks
50
+ if (configuration.clientSecret.length > 0) {
51
+ configuration.clientSecret = OpenIDClientConfiguration.placeholderClientSecret;
40
52
  }
41
53
 
42
- const platform = await Platform.getShared();
43
- return new Response(platform.serverConfig.ssoConfiguration ?? OpenIDClientConfiguration.create({}));
54
+ return new Response(configuration);
44
55
  }
45
56
  }
@@ -3,11 +3,11 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { OpenIDClientConfiguration } from '@stamhoofd/structures';
4
4
 
5
5
  import { Context } from '../../../helpers/Context';
6
- import { OpenIDConnectHelper } from '../../../helpers/OpenIDConnectHelper';
7
- import { Platform } from '@stamhoofd/models';
6
+ import { SSOService } from '../../../services/SSOService';
7
+ import { SSOQuery } from './GetSSOEndpoint';
8
8
 
9
9
  type Params = Record<string, never>;
10
- type Query = undefined;
10
+ type Query = SSOQuery;
11
11
  type Body = AutoEncoderPatchType<OpenIDClientConfiguration>;
12
12
  type ResponseBody = OpenIDClientConfiguration;
13
13
 
@@ -17,6 +17,7 @@ type ResponseBody = OpenIDClientConfiguration;
17
17
 
18
18
  export class SetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
19
  bodyDecoder = OpenIDClientConfiguration.patchType() as Decoder<AutoEncoderPatchType<OpenIDClientConfiguration>>;
20
+ queryDecoder = SSOQuery as Decoder<Query>;
20
21
 
21
22
  protected doesMatch(request: Request): [true, Params] | [false] {
22
23
  if (request.method !== 'POST') {
@@ -34,34 +35,18 @@ export class SetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, Re
34
35
  async handle(request: DecodedRequest<Params, Query, Body>) {
35
36
  const organization = await Context.setOptionalOrganizationScope();
36
37
  await Context.authenticate();
38
+ const service = await SSOService.fromContext(request.query.provider);
37
39
 
38
40
  if (!await Context.auth.canManageSSOSettings(organization?.id ?? null)) {
39
41
  throw Context.auth.error();
40
42
  }
41
43
 
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();
44
+ if (request.body.clientSecret === OpenIDClientConfiguration.placeholderClientSecret) {
45
+ delete request.body.clientSecret;
53
46
  }
54
- else {
55
- const platform = await Platform.getShared();
56
- newConfig = (platform.serverConfig.ssoConfiguration ?? OpenIDClientConfiguration.create({})).patch(request.body);
57
47
 
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
- }
48
+ const newConfig: OpenIDClientConfiguration = service.configuration.patch(request.body);
49
+ await service.setConfiguration(newConfig);
65
50
 
66
51
  return new Response(newConfig);
67
52
  }
@@ -538,6 +538,10 @@ export class AdminPermissionChecker {
538
538
  }
539
539
 
540
540
  async canEditUserName(user: User) {
541
+ if (user.hasAccount() && !user.hasPasswordBasedAccount()) {
542
+ return false;
543
+ }
544
+
541
545
  if (user.id === this.user.id) {
542
546
  return true;
543
547
  }
@@ -556,9 +560,6 @@ export class AdminPermissionChecker {
556
560
  }
557
561
 
558
562
  async canEditUserEmail(user: User) {
559
- if (user.meta?.loginProviderIds?.size) {
560
- return false;
561
- }
562
563
  return this.canEditUserName(user);
563
564
  }
564
565
 
@@ -287,6 +287,7 @@ export class AuthenticatedStructures {
287
287
  return UserWithMembers.create({
288
288
  ...user,
289
289
  hasAccount: user.hasAccount(),
290
+ hasPassword: user.hasPasswordBasedAccount(),
290
291
 
291
292
  // Always include the current context organization - because it is possible we switch organization and we don't want to refetch every time
292
293
  members: await this.membersBlob(members, true, user),
@@ -306,6 +307,7 @@ export class AuthenticatedStructures {
306
307
  structs.push(UserWithMembers.create({
307
308
  ...user,
308
309
  hasAccount: user.hasAccount(),
310
+ hasPassword: user.hasPasswordBasedAccount(),
309
311
  members: await this.membersBlob(filteredMembers, false),
310
312
  }));
311
313
  }
@@ -1,10 +1,11 @@
1
- import { Request } from '@simonbackx/simple-endpoints';
1
+ import { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
2
2
  import { 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';
6
6
 
7
7
  import { AdminPermissionChecker } from './AdminPermissionChecker';
8
+ import { AutoEncoder, field, Decoder, StringDecoder } from '@simonbackx/simple-encoding';
8
9
 
9
10
  export const apiUserRateLimiter = new RateLimiter({
10
11
  limits: [
@@ -31,6 +32,11 @@ export const apiUserRateLimiter = new RateLimiter({
31
32
  ],
32
33
  });
33
34
 
35
+ export class AuthorizationPostBody extends AutoEncoder {
36
+ @field({ decoder: StringDecoder })
37
+ header_authorization: string;
38
+ }
39
+
34
40
  export class ContextInstance {
35
41
  request: Request;
36
42
 
@@ -150,15 +156,27 @@ export class ContextInstance {
150
156
  }
151
157
 
152
158
  async optionalAuthenticate({ allowWithoutAccount = false }: { allowWithoutAccount?: boolean } = {}): Promise<{ user?: User }> {
153
- const header = this.request.headers.authorization;
154
- if (!header) {
159
+ try {
160
+ return await this.authenticate({ allowWithoutAccount });
161
+ }
162
+ catch (e) {
155
163
  return {};
156
164
  }
157
- return this.authenticate({ allowWithoutAccount });
158
165
  }
159
166
 
160
167
  async authenticate({ allowWithoutAccount = false }: { allowWithoutAccount?: boolean } = {}): Promise<{ user: User; token: Token }> {
161
- const header = this.request.headers.authorization;
168
+ let header = this.request.headers.authorization;
169
+
170
+ if (!header && this.request.method === 'POST') {
171
+ try {
172
+ const decoded = await DecodedRequest.fromRequest(this.request, undefined, undefined, AuthorizationPostBody as Decoder<AuthorizationPostBody>);
173
+ header = decoded.body.header_authorization;
174
+ }
175
+ catch (e) {
176
+ // Ignore: failed to read from body
177
+ }
178
+ }
179
+
162
180
  if (!header) {
163
181
  throw new SimpleError({
164
182
  code: 'not_authenticated',
@@ -1,10 +1,15 @@
1
- import { DecodedRequest, Response } from '@simonbackx/simple-endpoints';
1
+ import { Response } from '@simonbackx/simple-endpoints';
2
2
  import cookie from 'cookie';
3
+ import http from 'http';
3
4
 
4
- type DecodedRequestWithCookies = DecodedRequest<any, any, any> & { cookies?: Record<string, string> };
5
+ export type ObjectWithHeaders = {
6
+ headers: http.IncomingHttpHeaders;
7
+ };
8
+
9
+ type DecodedRequestWithCookies = ObjectWithHeaders & { cookies?: Record<string, string> };
5
10
 
6
11
  export class CookieHelper {
7
- static getCookies(request: DecodedRequest<any, any, any>): Record<string, string> {
12
+ static getCookies(request: ObjectWithHeaders): Record<string, string> {
8
13
  const r = request as DecodedRequestWithCookies;
9
14
  if (r.cookies) {
10
15
  return r.cookies;
@@ -21,7 +26,7 @@ export class CookieHelper {
21
26
  return r.cookies;
22
27
  }
23
28
 
24
- static getCookie(request: DecodedRequest<any, any, any>, name: string): string | undefined {
29
+ static getCookie(request: ObjectWithHeaders, name: string): string | undefined {
25
30
  const cookies = this.getCookies(request);
26
31
  return cookies[name];
27
32
  }