@stamhoofd/backend 2.71.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 +10 -10
- package/src/audit-logs/OrganizationLogger.ts +1 -1
- package/src/audit-logs/PlatformLogger.ts +1 -0
- package/src/endpoints/auth/CreateTokenEndpoint.ts +11 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +26 -2
- package/src/endpoints/auth/PatchUserEndpoint.ts +24 -2
- package/src/endpoints/auth/SignupEndpoint.ts +1 -1
- package/src/endpoints/global/members/GetMembersEndpoint.ts +5 -3
- package/src/endpoints/global/sso/GetSSOEndpoint.ts +8 -1
- package/src/endpoints/global/sso/SetSSOEndpoint.ts +4 -0
- package/src/helpers/AdminPermissionChecker.ts +4 -3
- package/src/services/SSOService.ts +68 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
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": "
|
|
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
|
-
|
|
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,8 +1,8 @@
|
|
|
1
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';
|
|
@@ -135,6 +135,28 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
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
|
+
|
|
138
160
|
// password changes
|
|
139
161
|
await editUser.changePassword(request.body.password);
|
|
140
162
|
await PasswordToken.clearFor(editUser.id);
|
|
@@ -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.
|
|
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
|
|
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 = {
|
|
@@ -44,6 +44,13 @@ export class GetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
44
44
|
throw Context.auth.error();
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
const configuration = service.configuration.clone();
|
|
48
|
+
|
|
49
|
+
// Remove secret by placeholder asterisks
|
|
50
|
+
if (configuration.clientSecret.length > 0) {
|
|
51
|
+
configuration.clientSecret = OpenIDClientConfiguration.placeholderClientSecret;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return new Response(configuration);
|
|
48
55
|
}
|
|
49
56
|
}
|
|
@@ -41,6 +41,10 @@ export class SetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
41
41
|
throw Context.auth.error();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
if (request.body.clientSecret === OpenIDClientConfiguration.placeholderClientSecret) {
|
|
45
|
+
delete request.body.clientSecret;
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
const newConfig: OpenIDClientConfiguration = service.configuration.patch(request.body);
|
|
45
49
|
await service.setConfiguration(newConfig);
|
|
46
50
|
|
|
@@ -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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DecodedRequest, Response } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
|
|
3
3
|
import { Organization, Platform, Token, User, Webshop } from '@stamhoofd/models';
|
|
4
|
-
import { LoginProviderType, OpenIDClientConfiguration, StartOpenIDFlowStruct, Token as TokenStruct } from '@stamhoofd/structures';
|
|
4
|
+
import { LoginMethod, LoginProviderType, OpenIDClientConfiguration, StartOpenIDFlowStruct, Token as TokenStruct } from '@stamhoofd/structures';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
6
|
import { generators, Issuer } from 'openid-client';
|
|
7
7
|
import { Context } from '../helpers/Context';
|
|
@@ -81,8 +81,8 @@ export class SSOService {
|
|
|
81
81
|
const platform = await Platform.getShared();
|
|
82
82
|
|
|
83
83
|
const service = new SSOService({ provider, platform, organization, user: Context.user });
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
service.validate();
|
|
85
|
+
|
|
86
86
|
return service;
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -117,6 +117,51 @@ export class SSOService {
|
|
|
117
117
|
return configuration;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
get loginConfiguration() {
|
|
121
|
+
if (this.organization) {
|
|
122
|
+
throw new SimpleError({
|
|
123
|
+
code: 'invalid_client',
|
|
124
|
+
message: 'Login configuration not yet supported for organization users',
|
|
125
|
+
statusCode: 400,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const loginConfiguration = this.platform.config.loginMethods.get(this.provider as unknown as LoginMethod);
|
|
130
|
+
if (!loginConfiguration) {
|
|
131
|
+
throw new SimpleError({
|
|
132
|
+
code: 'invalid_client',
|
|
133
|
+
message: 'SSO not configured (correctly)',
|
|
134
|
+
statusCode: 400,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return loginConfiguration;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
validate() {
|
|
142
|
+
// Validate configuration exists
|
|
143
|
+
const _ = this.configuration;
|
|
144
|
+
const __ = this.loginConfiguration;
|
|
145
|
+
|
|
146
|
+
if (this.user) {
|
|
147
|
+
this.validateEmail(this.user.email);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
validateEmail(email: string) {
|
|
152
|
+
// Validate configuration
|
|
153
|
+
const loginConfiguration = this.loginConfiguration;
|
|
154
|
+
|
|
155
|
+
if (!loginConfiguration.isEnabledForEmail(email)) {
|
|
156
|
+
throw new SimpleError({
|
|
157
|
+
code: 'invalid_user',
|
|
158
|
+
message: 'User not allowed to use this login method',
|
|
159
|
+
human: 'Je kan deze inlogmethode niet gebruiken',
|
|
160
|
+
statusCode: 400,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
120
165
|
async setConfiguration(configuration: OpenIDClientConfiguration) {
|
|
121
166
|
if (this.provider === LoginProviderType.SSO) {
|
|
122
167
|
if (this.organization) {
|
|
@@ -289,8 +334,15 @@ export class SSOService {
|
|
|
289
334
|
const client = await this.getClient();
|
|
290
335
|
await SSOService.storeSession(response, session);
|
|
291
336
|
|
|
337
|
+
const scopes = ['openid', 'email', 'profile'];
|
|
338
|
+
|
|
339
|
+
if (this.provider === LoginProviderType.SSO) {
|
|
340
|
+
// Google doesn't support this scope
|
|
341
|
+
scopes.push('offline_access');
|
|
342
|
+
}
|
|
343
|
+
|
|
292
344
|
const redirect = client.authorizationUrl({
|
|
293
|
-
scope: '
|
|
345
|
+
scope: scopes.join(' '),
|
|
294
346
|
code_challenge,
|
|
295
347
|
code_challenge_method: 'S256',
|
|
296
348
|
response_mode: 'form_post',
|
|
@@ -300,6 +352,9 @@ export class SSOService {
|
|
|
300
352
|
prompt: prompt ?? undefined,
|
|
301
353
|
login_hint: this.user?.email ?? undefined,
|
|
302
354
|
redirect_uri: this.externalRedirectUri,
|
|
355
|
+
|
|
356
|
+
// Google has this instead of the offline_access scope
|
|
357
|
+
access_type: this.provider === LoginProviderType.Google ? 'offline' : undefined,
|
|
303
358
|
});
|
|
304
359
|
|
|
305
360
|
response.headers['location'] = redirect;
|
|
@@ -387,6 +442,13 @@ export class SSOServiceWithSession {
|
|
|
387
442
|
}
|
|
388
443
|
}
|
|
389
444
|
|
|
445
|
+
if (tokenSet.refresh_token) {
|
|
446
|
+
console.log('OK. Refresh token received!');
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
console.log('No refresh token');
|
|
450
|
+
}
|
|
451
|
+
|
|
390
452
|
if (!claims.email) {
|
|
391
453
|
throw new SimpleError({
|
|
392
454
|
code: 'invalid_user',
|
|
@@ -403,6 +465,8 @@ export class SSOServiceWithSession {
|
|
|
403
465
|
});
|
|
404
466
|
}
|
|
405
467
|
|
|
468
|
+
this.service.validateEmail(claims.email);
|
|
469
|
+
|
|
406
470
|
// Get user from database
|
|
407
471
|
let user = await User.getForRegister(this.service.organization?.id ?? null, claims.email);
|
|
408
472
|
if (!user) {
|