@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 +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/OpenIDConnectCallbackEndpoint.ts +4 -25
- package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +9 -75
- package/src/endpoints/auth/PatchUserEndpoint.ts +67 -3
- package/src/endpoints/auth/SignupEndpoint.ts +1 -1
- package/src/endpoints/global/members/GetMembersEndpoint.ts +5 -3
- package/src/endpoints/global/sso/GetSSOEndpoint.ts +18 -7
- package/src/endpoints/global/sso/SetSSOEndpoint.ts +9 -24
- package/src/helpers/AdminPermissionChecker.ts +4 -3
- package/src/helpers/AuthenticatedStructures.ts +2 -0
- package/src/helpers/Context.ts +23 -5
- package/src/helpers/CookieHelper.ts +9 -4
- package/src/services/SSOService.ts +569 -0
- package/src/helpers/OpenIDConnectHelper.ts +0 -295
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,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 {
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
await Context.setUserOrganizationScope();
|
|
32
|
+
await Context.optionalAuthenticate({ allowWithoutAccount: false });
|
|
33
|
+
console.log('Full start connect body;', await request.request.body);
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
105
|
-
return await
|
|
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.
|
|
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 = {
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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 {
|
|
7
|
-
import {
|
|
6
|
+
import { SSOService } from '../../../services/SSOService';
|
|
7
|
+
import { SSOQuery } from './GetSSOEndpoint';
|
|
8
8
|
|
|
9
9
|
type Params = Record<string, never>;
|
|
10
|
-
type Query =
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
}
|
package/src/helpers/Context.ts
CHANGED
|
@@ -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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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 {
|
|
1
|
+
import { Response } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import cookie from 'cookie';
|
|
3
|
+
import http from 'http';
|
|
3
4
|
|
|
4
|
-
type
|
|
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:
|
|
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:
|
|
29
|
+
static getCookie(request: ObjectWithHeaders, name: string): string | undefined {
|
|
25
30
|
const cookies = this.getCookies(request);
|
|
26
31
|
return cookies[name];
|
|
27
32
|
}
|