@stamhoofd/backend 2.75.0 → 2.75.2
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/OpenIDConnectAuthTokenEndpoint.ts +43 -0
- package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +6 -12
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +329 -1
- package/src/helpers/MemberUserSyncer.ts +90 -86
- package/src/services/FileSignService.ts +8 -18
- package/src/services/SSOService.ts +116 -16
- package/tests/e2e/register.test.ts +459 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.75.
|
|
3
|
+
"version": "2.75.2",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"@simonbackx/simple-encoding": "2.20.0",
|
|
38
38
|
"@simonbackx/simple-endpoints": "1.19.0",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.75.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.75.
|
|
42
|
-
"@stamhoofd/email": "2.75.
|
|
43
|
-
"@stamhoofd/models": "2.75.
|
|
44
|
-
"@stamhoofd/queues": "2.75.
|
|
45
|
-
"@stamhoofd/sql": "2.75.
|
|
46
|
-
"@stamhoofd/structures": "2.75.
|
|
47
|
-
"@stamhoofd/utility": "2.75.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.75.2",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.75.2",
|
|
42
|
+
"@stamhoofd/email": "2.75.2",
|
|
43
|
+
"@stamhoofd/models": "2.75.2",
|
|
44
|
+
"@stamhoofd/queues": "2.75.2",
|
|
45
|
+
"@stamhoofd/sql": "2.75.2",
|
|
46
|
+
"@stamhoofd/structures": "2.75.2",
|
|
47
|
+
"@stamhoofd/utility": "2.75.2",
|
|
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": "aafa49d4e89f748e62f364b52a40efe339206779"
|
|
68
68
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AutoEncoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
|
|
4
|
+
import { Context } from '../../helpers/Context';
|
|
5
|
+
import { SSOService } from '../../services/SSOService';
|
|
6
|
+
import { OpenIDAuthTokenResponse } from '@stamhoofd/structures';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = undefined;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = OpenIDAuthTokenResponse;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This endpoint does nothing but build a URL to start the OpenID Connect flow.
|
|
15
|
+
* It is used to provide authenticateion data to the url that is temporarily valid (allows to connect an SSO provider to an existing account)
|
|
16
|
+
*/
|
|
17
|
+
export class OpenIDConnectAuthTokenEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
18
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
19
|
+
if (request.method !== 'POST') {
|
|
20
|
+
return [false];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const params = Endpoint.parseParameters(request.url, '/openid/auth-token', {});
|
|
24
|
+
|
|
25
|
+
if (params) {
|
|
26
|
+
return [true, params as Params];
|
|
27
|
+
}
|
|
28
|
+
return [false];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
32
|
+
// Check webshop and/or organization
|
|
33
|
+
await Context.setUserOrganizationScope();
|
|
34
|
+
await Context.authenticate({ allowWithoutAccount: false });
|
|
35
|
+
|
|
36
|
+
// Create a SSO auth token that can only be used once
|
|
37
|
+
const token = await SSOService.createToken();
|
|
38
|
+
|
|
39
|
+
return new Response(OpenIDAuthTokenResponse.create({
|
|
40
|
+
ssoAuthToken: token,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -6,15 +6,15 @@ import { Context } from '../../helpers/Context';
|
|
|
6
6
|
import { SSOService } from '../../services/SSOService';
|
|
7
7
|
|
|
8
8
|
type Params = Record<string, never>;
|
|
9
|
-
type Query =
|
|
10
|
-
type Body =
|
|
9
|
+
type Query = StartOpenIDFlowStruct;
|
|
10
|
+
type Body = undefined;
|
|
11
11
|
type ResponseBody = undefined;
|
|
12
12
|
|
|
13
13
|
export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
-
|
|
14
|
+
queryDecoder = StartOpenIDFlowStruct as Decoder<StartOpenIDFlowStruct>;
|
|
15
15
|
|
|
16
16
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
-
if (request.method !== '
|
|
17
|
+
if (request.method !== 'GET') {
|
|
18
18
|
return [false];
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -29,13 +29,7 @@ export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
29
29
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
30
|
// Check webshop and/or organization
|
|
31
31
|
await Context.setUserOrganizationScope();
|
|
32
|
-
await
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (Context.user) {
|
|
36
|
-
console.log('User:', Context.user);
|
|
37
|
-
}
|
|
38
|
-
const service = await SSOService.fromContext(request.body.provider);
|
|
39
|
-
return await service.validateAndStartAuthCodeFlow(request.body);
|
|
32
|
+
const service = await SSOService.fromContext(request.query.provider);
|
|
33
|
+
return await service.validateAndStartAuthCodeFlow(request.query);
|
|
40
34
|
}
|
|
41
35
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { Email } from '@stamhoofd/email';
|
|
3
3
|
import { BalanceItemFactory, Group, GroupFactory, MemberFactory, MemberWithRegistrations, Organization, OrganizationFactory, Registration, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
4
|
-
import { BalanceItemCartItem, BalanceItemType, Company, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, OrganizationPackages, PayconiqAccount, PaymentCustomer, PaymentMethod, PermissionLevel, Permissions, STPackageStatus, STPackageType, UserPermissions, Version } from '@stamhoofd/structures';
|
|
4
|
+
import { BalanceItemCartItem, BalanceItemType, Company, GroupOption, GroupOptionMenu, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, OrganizationPackages, PayconiqAccount, PaymentCustomer, PaymentMethod, PermissionLevel, Permissions, ReduceablePrice, RegisterItemOption, STPackageStatus, STPackageType, UserPermissions, Version } from '@stamhoofd/structures';
|
|
5
5
|
import nock from 'nock';
|
|
6
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
7
|
import { testServer } from '../../../../tests/helpers/TestServer';
|
|
@@ -1011,6 +1011,334 @@ describe('Endpoint.RegisterMembers', () => {
|
|
|
1011
1011
|
jest.useFakeTimers().resetAllMocks();
|
|
1012
1012
|
}
|
|
1013
1013
|
});
|
|
1014
|
+
|
|
1015
|
+
test('Should update group stock reservations', async () => {
|
|
1016
|
+
// #region arrange
|
|
1017
|
+
const { organization, group, groupPrice, token, member } = await initData();
|
|
1018
|
+
groupPrice.stock = 5;
|
|
1019
|
+
await group.save();
|
|
1020
|
+
|
|
1021
|
+
const body = IDRegisterCheckout.create({
|
|
1022
|
+
cart: IDRegisterCart.create({
|
|
1023
|
+
items: [
|
|
1024
|
+
IDRegisterItem.create({
|
|
1025
|
+
id: uuidv4(),
|
|
1026
|
+
replaceRegistrationIds: [],
|
|
1027
|
+
options: [],
|
|
1028
|
+
groupPrice,
|
|
1029
|
+
organizationId: organization.id,
|
|
1030
|
+
groupId: group.id,
|
|
1031
|
+
memberId: member.id,
|
|
1032
|
+
}),
|
|
1033
|
+
],
|
|
1034
|
+
balanceItems: [],
|
|
1035
|
+
deleteRegistrationIds: [],
|
|
1036
|
+
}),
|
|
1037
|
+
administrationFee: 0,
|
|
1038
|
+
freeContribution: 0,
|
|
1039
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1040
|
+
totalPrice: 25,
|
|
1041
|
+
asOrganizationId: organization.id,
|
|
1042
|
+
customer: null,
|
|
1043
|
+
});
|
|
1044
|
+
// #endregion
|
|
1045
|
+
|
|
1046
|
+
// #region act and assert
|
|
1047
|
+
expect(group?.stockReservations.length).toBe(0);
|
|
1048
|
+
|
|
1049
|
+
await post(body, organization, token);
|
|
1050
|
+
|
|
1051
|
+
const updatedGroup = await Group.getByID(group.id);
|
|
1052
|
+
expect(updatedGroup?.stockReservations.length).toBe(1);
|
|
1053
|
+
// #endregion
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
test('Should fail if group price stock sold out', async () => {
|
|
1057
|
+
// #region arrange
|
|
1058
|
+
const { organization, group, groupPrice, token, member, otherMembers } = await initData({
|
|
1059
|
+
permissionLevel: PermissionLevel.Read, otherMemberAmount: 3 });
|
|
1060
|
+
groupPrice.stock = 2;
|
|
1061
|
+
await group.save();
|
|
1062
|
+
|
|
1063
|
+
const body = IDRegisterCheckout.create({
|
|
1064
|
+
cart: IDRegisterCart.create({
|
|
1065
|
+
items: [
|
|
1066
|
+
IDRegisterItem.create({
|
|
1067
|
+
id: uuidv4(),
|
|
1068
|
+
replaceRegistrationIds: [],
|
|
1069
|
+
options: [],
|
|
1070
|
+
groupPrice,
|
|
1071
|
+
organizationId: organization.id,
|
|
1072
|
+
groupId: group.id,
|
|
1073
|
+
memberId: member.id,
|
|
1074
|
+
}),
|
|
1075
|
+
...otherMembers.map(m => IDRegisterItem.create({
|
|
1076
|
+
id: uuidv4(),
|
|
1077
|
+
replaceRegistrationIds: [],
|
|
1078
|
+
options: [],
|
|
1079
|
+
groupPrice,
|
|
1080
|
+
organizationId: organization.id,
|
|
1081
|
+
groupId: group.id,
|
|
1082
|
+
memberId: m.id,
|
|
1083
|
+
})),
|
|
1084
|
+
],
|
|
1085
|
+
balanceItems: [],
|
|
1086
|
+
deleteRegistrationIds: [],
|
|
1087
|
+
}),
|
|
1088
|
+
administrationFee: 0,
|
|
1089
|
+
freeContribution: 0,
|
|
1090
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1091
|
+
totalPrice: 75,
|
|
1092
|
+
customer: null,
|
|
1093
|
+
});
|
|
1094
|
+
// #endregion
|
|
1095
|
+
|
|
1096
|
+
// #region act and assert
|
|
1097
|
+
expect(group?.stockReservations.length).toBe(0);
|
|
1098
|
+
|
|
1099
|
+
await expect(async () => await post(body, organization, token))
|
|
1100
|
+
.rejects
|
|
1101
|
+
.toThrow(new RegExp('Maximum reached'));
|
|
1102
|
+
// #endregion
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
test('Should fail if option stock sold out', async () => {
|
|
1106
|
+
// #region arrange
|
|
1107
|
+
const { organization, group, groupPrice, token, member } = await initData();
|
|
1108
|
+
|
|
1109
|
+
const option1 = GroupOption.create({
|
|
1110
|
+
name: 'option 1',
|
|
1111
|
+
stock: 4,
|
|
1112
|
+
price: ReduceablePrice.create({
|
|
1113
|
+
price: 5,
|
|
1114
|
+
reducedPrice: 3,
|
|
1115
|
+
}),
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
const option2 = GroupOption.create({
|
|
1119
|
+
name: 'option 2',
|
|
1120
|
+
stock: 4,
|
|
1121
|
+
price: ReduceablePrice.create({
|
|
1122
|
+
price: 3,
|
|
1123
|
+
reducedPrice: 1,
|
|
1124
|
+
}),
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
const optionMenu = GroupOptionMenu.create({
|
|
1128
|
+
name: 'option menu 1',
|
|
1129
|
+
multipleChoice: true,
|
|
1130
|
+
options: [option1, option2],
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
group.settings.optionMenus = [
|
|
1134
|
+
optionMenu,
|
|
1135
|
+
];
|
|
1136
|
+
|
|
1137
|
+
await group.save();
|
|
1138
|
+
|
|
1139
|
+
const body = IDRegisterCheckout.create({
|
|
1140
|
+
cart: IDRegisterCart.create({
|
|
1141
|
+
items: [
|
|
1142
|
+
IDRegisterItem.create({
|
|
1143
|
+
id: uuidv4(),
|
|
1144
|
+
replaceRegistrationIds: [],
|
|
1145
|
+
options: [
|
|
1146
|
+
RegisterItemOption.create({
|
|
1147
|
+
option: option1,
|
|
1148
|
+
amount: 2,
|
|
1149
|
+
optionMenu,
|
|
1150
|
+
}),
|
|
1151
|
+
RegisterItemOption.create({
|
|
1152
|
+
option: option2,
|
|
1153
|
+
amount: 5,
|
|
1154
|
+
optionMenu,
|
|
1155
|
+
}),
|
|
1156
|
+
],
|
|
1157
|
+
groupPrice,
|
|
1158
|
+
organizationId: organization.id,
|
|
1159
|
+
groupId: group.id,
|
|
1160
|
+
memberId: member.id,
|
|
1161
|
+
}),
|
|
1162
|
+
],
|
|
1163
|
+
balanceItems: [
|
|
1164
|
+
],
|
|
1165
|
+
deleteRegistrationIds: [],
|
|
1166
|
+
}),
|
|
1167
|
+
administrationFee: 0,
|
|
1168
|
+
freeContribution: 0,
|
|
1169
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1170
|
+
totalPrice: 50,
|
|
1171
|
+
customer: null,
|
|
1172
|
+
});
|
|
1173
|
+
// #endregion
|
|
1174
|
+
|
|
1175
|
+
// #region act and assert
|
|
1176
|
+
await expect(async () => await post(body, organization, token))
|
|
1177
|
+
.rejects
|
|
1178
|
+
.toThrow(new RegExp('Stock empty'));
|
|
1179
|
+
// #endregion
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
test('Should fail if max option exceeded', async () => {
|
|
1183
|
+
// #region arrange
|
|
1184
|
+
const { organization, group, groupPrice, token, member } = await initData();
|
|
1185
|
+
|
|
1186
|
+
const option1 = GroupOption.create({
|
|
1187
|
+
name: 'option 1',
|
|
1188
|
+
stock: 4,
|
|
1189
|
+
maximum: 5,
|
|
1190
|
+
allowAmount: true,
|
|
1191
|
+
price: ReduceablePrice.create({
|
|
1192
|
+
price: 5,
|
|
1193
|
+
reducedPrice: 3,
|
|
1194
|
+
}),
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
const option2 = GroupOption.create({
|
|
1198
|
+
name: 'option 2',
|
|
1199
|
+
stock: 5,
|
|
1200
|
+
maximum: 2,
|
|
1201
|
+
allowAmount: true,
|
|
1202
|
+
price: ReduceablePrice.create({
|
|
1203
|
+
price: 3,
|
|
1204
|
+
reducedPrice: 1,
|
|
1205
|
+
}),
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
const optionMenu = GroupOptionMenu.create({
|
|
1209
|
+
name: 'option menu 1',
|
|
1210
|
+
multipleChoice: true,
|
|
1211
|
+
options: [option1, option2],
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
group.settings.optionMenus = [
|
|
1215
|
+
optionMenu,
|
|
1216
|
+
];
|
|
1217
|
+
|
|
1218
|
+
await group.save();
|
|
1219
|
+
|
|
1220
|
+
const body = IDRegisterCheckout.create({
|
|
1221
|
+
cart: IDRegisterCart.create({
|
|
1222
|
+
items: [
|
|
1223
|
+
IDRegisterItem.create({
|
|
1224
|
+
id: uuidv4(),
|
|
1225
|
+
replaceRegistrationIds: [],
|
|
1226
|
+
options: [
|
|
1227
|
+
RegisterItemOption.create({
|
|
1228
|
+
option: option1,
|
|
1229
|
+
amount: 2,
|
|
1230
|
+
optionMenu,
|
|
1231
|
+
}),
|
|
1232
|
+
RegisterItemOption.create({
|
|
1233
|
+
option: option2,
|
|
1234
|
+
amount: 5,
|
|
1235
|
+
optionMenu,
|
|
1236
|
+
}),
|
|
1237
|
+
],
|
|
1238
|
+
groupPrice,
|
|
1239
|
+
organizationId: organization.id,
|
|
1240
|
+
groupId: group.id,
|
|
1241
|
+
memberId: member.id,
|
|
1242
|
+
}),
|
|
1243
|
+
],
|
|
1244
|
+
balanceItems: [
|
|
1245
|
+
],
|
|
1246
|
+
deleteRegistrationIds: [],
|
|
1247
|
+
}),
|
|
1248
|
+
administrationFee: 0,
|
|
1249
|
+
freeContribution: 0,
|
|
1250
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1251
|
+
totalPrice: 50,
|
|
1252
|
+
customer: null,
|
|
1253
|
+
});
|
|
1254
|
+
// #endregion
|
|
1255
|
+
|
|
1256
|
+
// #region act and assert
|
|
1257
|
+
await expect(async () => await post(body, organization, token))
|
|
1258
|
+
.rejects
|
|
1259
|
+
.toThrow(new RegExp('Option maximum exceeded'));
|
|
1260
|
+
// #endregion
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
test('Should not fail if max option not exceeded', async () => {
|
|
1264
|
+
// #region arrange
|
|
1265
|
+
const { organization, group, groupPrice, token, member } = await initData();
|
|
1266
|
+
|
|
1267
|
+
const option1 = GroupOption.create({
|
|
1268
|
+
name: 'option 1',
|
|
1269
|
+
stock: 4,
|
|
1270
|
+
maximum: 5,
|
|
1271
|
+
allowAmount: true,
|
|
1272
|
+
price: ReduceablePrice.create({
|
|
1273
|
+
price: 5,
|
|
1274
|
+
reducedPrice: 3,
|
|
1275
|
+
}),
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
const option2 = GroupOption.create({
|
|
1279
|
+
name: 'option 2',
|
|
1280
|
+
stock: 5,
|
|
1281
|
+
maximum: 5,
|
|
1282
|
+
allowAmount: true,
|
|
1283
|
+
price: ReduceablePrice.create({
|
|
1284
|
+
price: 3,
|
|
1285
|
+
reducedPrice: 1,
|
|
1286
|
+
}),
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
const optionMenu = GroupOptionMenu.create({
|
|
1290
|
+
name: 'option menu 1',
|
|
1291
|
+
multipleChoice: true,
|
|
1292
|
+
options: [option1, option2],
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
group.settings.optionMenus = [
|
|
1296
|
+
optionMenu,
|
|
1297
|
+
];
|
|
1298
|
+
|
|
1299
|
+
await group.save();
|
|
1300
|
+
|
|
1301
|
+
const body = IDRegisterCheckout.create({
|
|
1302
|
+
cart: IDRegisterCart.create({
|
|
1303
|
+
items: [
|
|
1304
|
+
IDRegisterItem.create({
|
|
1305
|
+
id: uuidv4(),
|
|
1306
|
+
replaceRegistrationIds: [],
|
|
1307
|
+
options: [
|
|
1308
|
+
RegisterItemOption.create({
|
|
1309
|
+
option: option1,
|
|
1310
|
+
amount: 2,
|
|
1311
|
+
optionMenu,
|
|
1312
|
+
}),
|
|
1313
|
+
RegisterItemOption.create({
|
|
1314
|
+
option: option2,
|
|
1315
|
+
amount: 5,
|
|
1316
|
+
optionMenu,
|
|
1317
|
+
}),
|
|
1318
|
+
],
|
|
1319
|
+
groupPrice,
|
|
1320
|
+
organizationId: organization.id,
|
|
1321
|
+
groupId: group.id,
|
|
1322
|
+
memberId: member.id,
|
|
1323
|
+
}),
|
|
1324
|
+
],
|
|
1325
|
+
balanceItems: [
|
|
1326
|
+
],
|
|
1327
|
+
deleteRegistrationIds: [],
|
|
1328
|
+
}),
|
|
1329
|
+
administrationFee: 0,
|
|
1330
|
+
freeContribution: 0,
|
|
1331
|
+
paymentMethod: PaymentMethod.PointOfSale,
|
|
1332
|
+
totalPrice: 50,
|
|
1333
|
+
customer: null,
|
|
1334
|
+
});
|
|
1335
|
+
// #endregion
|
|
1336
|
+
|
|
1337
|
+
// #region act and assert
|
|
1338
|
+
const result = await post(body, organization, token);
|
|
1339
|
+
expect(result).toBeDefined();
|
|
1340
|
+
// #endregion
|
|
1341
|
+
});
|
|
1014
1342
|
});
|
|
1015
1343
|
|
|
1016
1344
|
describe('Register by other organization', () => {
|
|
@@ -5,6 +5,7 @@ import crypto from 'crypto';
|
|
|
5
5
|
import basex from 'base-x';
|
|
6
6
|
import { AuditLogService } from '../services/AuditLogService';
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
9
|
|
|
9
10
|
const ALPHABET = '123456789ABCDEFGHJKMNPQRSTUVWXYZ'; // Note: we removed 0, O, I and l to make it easier for humans
|
|
10
11
|
const customBase = basex(ALPHABET);
|
|
@@ -203,116 +204,119 @@ export class MemberUserSyncerStatic {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
async linkUser(email: string, member: MemberWithRegistrations, asParent: boolean, updateNameIfEqual = true) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
207
|
+
// This needs to happen in the queue to prevent creating duplicate users in the database
|
|
208
|
+
await QueueHandler.schedule('link-user/' + email, async () => {
|
|
209
|
+
console.log('Linking user', email, 'to member', member.id, 'as parent', asParent, 'update name if equal', updateNameIfEqual);
|
|
210
|
+
|
|
211
|
+
let user = member.users.find(u => u.email.toLocaleLowerCase() === email.toLocaleLowerCase()) ?? await User.getForAuthentication(member.organizationId, email, { allowWithoutAccount: true });
|
|
212
|
+
|
|
213
|
+
if (user) {
|
|
214
|
+
// console.log("Giving an existing user access to a member: " + user.id + ' - ' + member.id)
|
|
215
|
+
if (!asParent) {
|
|
216
|
+
if (user.memberId && user.memberId !== member.id) {
|
|
217
|
+
console.error('Found conflicting user with multiple members', user.id, 'members', user.memberId, 'to', member.id);
|
|
218
|
+
|
|
219
|
+
const otherMember = await Member.getWithRegistrations(user.memberId);
|
|
220
|
+
|
|
221
|
+
if (otherMember) {
|
|
222
|
+
if (otherMember.registrations.length > 0 && member.registrations.length === 0) {
|
|
223
|
+
// Choose the other member
|
|
224
|
+
// don't make changes
|
|
225
|
+
console.error('Resolved to current member - no changes made');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const responsibilities = await this.getResponsibilitiesForMembers([otherMember.id, member.id]);
|
|
230
|
+
const responsibilitiesOther = responsibilities.filter(r => r.memberId === otherMember.id);
|
|
231
|
+
const responsibilitiesCurrent = responsibilities.filter(r => r.memberId === member.id);
|
|
232
|
+
|
|
233
|
+
if (responsibilitiesOther.length >= responsibilitiesCurrent.length) {
|
|
234
|
+
console.error('Resolved to current member because of more responsibilities - no changes made');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
224
237
|
}
|
|
238
|
+
}
|
|
225
239
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (responsibilitiesOther.length >= responsibilitiesCurrent.length) {
|
|
231
|
-
console.error('Resolved to current member because of more responsibilities - no changes made');
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
240
|
+
if (updateNameIfEqual) {
|
|
241
|
+
user.firstName = member.details.firstName;
|
|
242
|
+
user.lastName = member.details.lastName;
|
|
234
243
|
}
|
|
244
|
+
user.memberId = member.id;
|
|
245
|
+
await this.updateInheritedPermissions(user);
|
|
235
246
|
}
|
|
247
|
+
else {
|
|
248
|
+
let shouldSave = false;
|
|
249
|
+
|
|
250
|
+
if (!user.firstName && !user.lastName) {
|
|
251
|
+
const parents = member.details.parents.filter(p => p.email === email);
|
|
252
|
+
if (parents.length === 1) {
|
|
253
|
+
if (updateNameIfEqual) {
|
|
254
|
+
user.firstName = parents[0].firstName;
|
|
255
|
+
user.lastName = parents[0].lastName;
|
|
256
|
+
}
|
|
257
|
+
shouldSave = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
236
260
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
261
|
+
if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
|
|
262
|
+
user.firstName = null;
|
|
263
|
+
user.lastName = null;
|
|
264
|
+
shouldSave = true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (user.memberId === member.id) {
|
|
268
|
+
// Unlink: parents are never 'equal' to the member
|
|
269
|
+
user.memberId = null;
|
|
270
|
+
await this.updateInheritedPermissions(user);
|
|
271
|
+
}
|
|
272
|
+
if (shouldSave) {
|
|
273
|
+
await user.save();
|
|
274
|
+
}
|
|
240
275
|
}
|
|
241
|
-
user.memberId = member.id;
|
|
242
|
-
await this.updateInheritedPermissions(user);
|
|
243
276
|
}
|
|
244
277
|
else {
|
|
245
|
-
|
|
278
|
+
// Create a new placeholder user
|
|
279
|
+
user = new User();
|
|
280
|
+
user.organizationId = member.organizationId;
|
|
281
|
+
user.email = email;
|
|
246
282
|
|
|
247
|
-
if (!
|
|
283
|
+
if (!asParent) {
|
|
284
|
+
if (updateNameIfEqual) {
|
|
285
|
+
user.firstName = member.details.firstName;
|
|
286
|
+
user.lastName = member.details.lastName;
|
|
287
|
+
}
|
|
288
|
+
user.memberId = member.id;
|
|
289
|
+
await this.updateInheritedPermissions(user);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
248
292
|
const parents = member.details.parents.filter(p => p.email === email);
|
|
249
293
|
if (parents.length === 1) {
|
|
250
294
|
if (updateNameIfEqual) {
|
|
251
295
|
user.firstName = parents[0].firstName;
|
|
252
296
|
user.lastName = parents[0].lastName;
|
|
253
297
|
}
|
|
254
|
-
shouldSave = true;
|
|
255
298
|
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
|
|
259
|
-
user.firstName = null;
|
|
260
|
-
user.lastName = null;
|
|
261
|
-
shouldSave = true;
|
|
262
|
-
}
|
|
263
299
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
await this.updateInheritedPermissions(user);
|
|
268
|
-
}
|
|
269
|
-
if (shouldSave) {
|
|
270
|
-
await user.save();
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
// Create a new placeholder user
|
|
276
|
-
user = new User();
|
|
277
|
-
user.organizationId = member.organizationId;
|
|
278
|
-
user.email = email;
|
|
279
|
-
|
|
280
|
-
if (!asParent) {
|
|
281
|
-
if (updateNameIfEqual) {
|
|
282
|
-
user.firstName = member.details.firstName;
|
|
283
|
-
user.lastName = member.details.lastName;
|
|
284
|
-
}
|
|
285
|
-
user.memberId = member.id;
|
|
286
|
-
await this.updateInheritedPermissions(user);
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
const parents = member.details.parents.filter(p => p.email === email);
|
|
290
|
-
if (parents.length === 1) {
|
|
291
|
-
if (updateNameIfEqual) {
|
|
292
|
-
user.firstName = parents[0].firstName;
|
|
293
|
-
user.lastName = parents[0].lastName;
|
|
300
|
+
if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
|
|
301
|
+
user.firstName = null;
|
|
302
|
+
user.lastName = null;
|
|
294
303
|
}
|
|
295
|
-
}
|
|
296
304
|
|
|
297
|
-
|
|
298
|
-
user.firstName = null;
|
|
299
|
-
user.lastName = null;
|
|
305
|
+
await user.save();
|
|
300
306
|
}
|
|
301
307
|
|
|
302
|
-
|
|
308
|
+
console.log('Created new (placeholder) user that has access to a member: ' + user.id);
|
|
303
309
|
}
|
|
304
310
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (!member.users.find(u => u.id === user.id)) {
|
|
310
|
-
await Member.users.reverse('members').link(user, [member]);
|
|
311
|
-
member.users.push(user);
|
|
311
|
+
// Update model relation to correct response
|
|
312
|
+
if (!member.users.find(u => u.id === user.id)) {
|
|
313
|
+
await Member.users.reverse('members').link(user, [member]);
|
|
314
|
+
member.users.push(user);
|
|
312
315
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
// Update balance of this user, as it could have changed
|
|
317
|
+
await this.updateUserBalance(user.id, member.id);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
316
320
|
}
|
|
317
321
|
|
|
318
322
|
/**
|