@stamhoofd/backend 2.78.3 → 2.78.4
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/.env.ci.json +19 -8
- package/index.ts +7 -0
- package/package.json +10 -10
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +726 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +31 -18
- package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +9 -21
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -13
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +2 -2
- package/src/helpers/AdminPermissionChecker.ts +19 -1
- package/src/services/FileSignService.ts +1 -1
package/.env.ci.json
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"environment": "test",
|
|
3
3
|
"domains": {
|
|
4
|
-
"dashboard": "dashboard.stamhoofd
|
|
5
|
-
"
|
|
6
|
-
"": "stamhoofd
|
|
7
|
-
"BE": "
|
|
8
|
-
"NL": "
|
|
4
|
+
"dashboard": "dashboard.stamhoofd",
|
|
5
|
+
"marketing": {
|
|
6
|
+
"": "www.be.stamhoofd",
|
|
7
|
+
"BE": "www.be.stamhoofd",
|
|
8
|
+
"NL": "www.nl.stamhoofd"
|
|
9
9
|
},
|
|
10
|
-
"webshop":
|
|
11
|
-
|
|
10
|
+
"webshop": {
|
|
11
|
+
"": "shop.be.stamhoofd",
|
|
12
|
+
"BE": "shop.be.stamhoofd",
|
|
13
|
+
"NL": "shop.nl.stamhoofd"
|
|
14
|
+
},
|
|
15
|
+
"api": "api.stamhoofd",
|
|
16
|
+
"rendererApi": "renderer.stamhoofd",
|
|
17
|
+
|
|
18
|
+
"defaultTransactionalEmail": {
|
|
19
|
+
"": "stamhoofd.be"
|
|
20
|
+
},
|
|
21
|
+
|
|
12
22
|
"defaultBroadcastEmail": {
|
|
13
23
|
"": "stamhoofd.email"
|
|
14
|
-
}
|
|
24
|
+
},
|
|
25
|
+
"webshopCname": "shop.stamhoofd"
|
|
15
26
|
},
|
|
16
27
|
|
|
17
28
|
"PORT": 9091,
|
package/index.ts
CHANGED
|
@@ -60,10 +60,13 @@ const start = async () => {
|
|
|
60
60
|
await UniqueUserService.check();
|
|
61
61
|
|
|
62
62
|
// Init platform shared struct: otherwise permissions won't work with missing responsibilities
|
|
63
|
+
console.log('Loading platform...');
|
|
63
64
|
await Platform.getSharedStruct();
|
|
64
65
|
|
|
65
66
|
const router = new Router();
|
|
66
67
|
|
|
68
|
+
console.log('Loading endpoints...');
|
|
69
|
+
|
|
67
70
|
// Note: we should load endpoints one by once to have a reliable order of url matching
|
|
68
71
|
await router.loadAllEndpoints(__dirname + '/src/endpoints/global/*');
|
|
69
72
|
await router.loadAllEndpoints(__dirname + '/src/endpoints/admin/*');
|
|
@@ -76,6 +79,7 @@ const start = async () => {
|
|
|
76
79
|
|
|
77
80
|
router.endpoints.push(new CORSPreflightEndpoint());
|
|
78
81
|
|
|
82
|
+
console.log('Creating router...');
|
|
79
83
|
const routerServer = new RouterServer(router);
|
|
80
84
|
routerServer.verbose = false;
|
|
81
85
|
|
|
@@ -104,6 +108,8 @@ const start = async () => {
|
|
|
104
108
|
// Add CORS headers
|
|
105
109
|
routerServer.addResponseMiddleware(CORSMiddleware);
|
|
106
110
|
|
|
111
|
+
console.log('Loading loaders...');
|
|
112
|
+
|
|
107
113
|
// Register Excel loaders
|
|
108
114
|
await import('./src/excel-loaders/members');
|
|
109
115
|
await import('./src/excel-loaders/payments');
|
|
@@ -115,6 +121,7 @@ const start = async () => {
|
|
|
115
121
|
await import('./src/email-recipient-loaders/orders');
|
|
116
122
|
await import('./src/email-recipient-loaders/receivable-balances');
|
|
117
123
|
|
|
124
|
+
console.log('Opening port...');
|
|
118
125
|
routerServer.listen(STAMHOOFD.PORT ?? 9090);
|
|
119
126
|
|
|
120
127
|
const hrend = process.hrtime(bootTime);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.78.
|
|
3
|
+
"version": "2.78.4",
|
|
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.1",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.78.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.78.
|
|
42
|
-
"@stamhoofd/email": "2.78.
|
|
43
|
-
"@stamhoofd/models": "2.78.
|
|
44
|
-
"@stamhoofd/queues": "2.78.
|
|
45
|
-
"@stamhoofd/sql": "2.78.
|
|
46
|
-
"@stamhoofd/structures": "2.78.
|
|
47
|
-
"@stamhoofd/utility": "2.78.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.78.4",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.78.4",
|
|
42
|
+
"@stamhoofd/email": "2.78.4",
|
|
43
|
+
"@stamhoofd/models": "2.78.4",
|
|
44
|
+
"@stamhoofd/queues": "2.78.4",
|
|
45
|
+
"@stamhoofd/sql": "2.78.4",
|
|
46
|
+
"@stamhoofd/structures": "2.78.4",
|
|
47
|
+
"@stamhoofd/utility": "2.78.4",
|
|
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": "e8c207be8fd75320024f670d5c663eaa8306ba29"
|
|
68
68
|
}
|
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { Database } from '@simonbackx/simple-database';
|
|
2
|
+
import { PatchableArray, PatchMap } from '@simonbackx/simple-encoding';
|
|
3
|
+
import { Endpoint, Request } from '@simonbackx/simple-endpoints';
|
|
4
|
+
import { GroupFactory, MemberFactory, OrganizationFactory, OrganizationTagFactory, Platform, RegistrationFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
5
|
+
import { MemberDetails, MemberWithRegistrationsBlob, OrganizationMetaData, OrganizationRecordsConfiguration, Parent, PatchAnswers, PermissionLevel, Permissions, PermissionsResourceType, RecordCategory, RecordSettings, RecordTextAnswer, ResourcePermissions, UserPermissions } from '@stamhoofd/structures';
|
|
6
|
+
import { TestUtils } from '@stamhoofd/test-utils';
|
|
7
|
+
import { testServer } from '../../../../tests/helpers/TestServer';
|
|
8
|
+
import { PatchOrganizationMembersEndpoint } from './PatchOrganizationMembersEndpoint';
|
|
9
|
+
|
|
10
|
+
const baseUrl = `/organization/members`;
|
|
11
|
+
const endpoint = new PatchOrganizationMembersEndpoint();
|
|
12
|
+
type EndpointType = typeof endpoint;
|
|
13
|
+
type Body = EndpointType extends Endpoint<any, any, infer B, any> ? B : never;
|
|
14
|
+
|
|
15
|
+
const firstName = 'John';
|
|
16
|
+
const lastName = 'Doe';
|
|
17
|
+
const birthDay = { year: 1993, month: 4, day: 5 };
|
|
18
|
+
|
|
19
|
+
const errorWithCode = (code: string) => expect.objectContaining({ code }) as jest.Constructable;
|
|
20
|
+
|
|
21
|
+
describe('Endpoint.PatchOrganizationMembersEndpoint', () => {
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
TestUtils.setEnvironment('userMode', 'platform');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
// Delete all members (so the duplicate checks work as expected)
|
|
28
|
+
await Database.delete('DELETE FROM `members`');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Duplicate members', () => {
|
|
32
|
+
test('The security code should be a requirement', async () => {
|
|
33
|
+
const organization = await new OrganizationFactory({ }).create();
|
|
34
|
+
const user = await new UserFactory({
|
|
35
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
36
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
37
|
+
}).create();
|
|
38
|
+
|
|
39
|
+
const existingMember = await new MemberFactory({
|
|
40
|
+
firstName,
|
|
41
|
+
lastName,
|
|
42
|
+
birthDay,
|
|
43
|
+
generateData: true,
|
|
44
|
+
}).create();
|
|
45
|
+
|
|
46
|
+
const token = await Token.createToken(user);
|
|
47
|
+
|
|
48
|
+
const arr: Body = new PatchableArray();
|
|
49
|
+
const put = MemberWithRegistrationsBlob.create({
|
|
50
|
+
details: MemberDetails.create({
|
|
51
|
+
firstName,
|
|
52
|
+
lastName,
|
|
53
|
+
birthDay: new Date(existingMember.details.birthDay!.getTime() + 1),
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
arr.addPut(put);
|
|
57
|
+
|
|
58
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
59
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
60
|
+
await expect(testServer.test(endpoint, request))
|
|
61
|
+
.rejects
|
|
62
|
+
.toThrow(errorWithCode('known_member_missing_rights'));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('The security code is not a requirement for members without additional data', async () => {
|
|
66
|
+
const organization = await new OrganizationFactory({ }).create();
|
|
67
|
+
const user = await new UserFactory({
|
|
68
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
69
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
70
|
+
}).create();
|
|
71
|
+
|
|
72
|
+
const existingMember = await new MemberFactory({
|
|
73
|
+
firstName,
|
|
74
|
+
lastName,
|
|
75
|
+
birthDay,
|
|
76
|
+
generateData: false,
|
|
77
|
+
}).create();
|
|
78
|
+
|
|
79
|
+
const token = await Token.createToken(user);
|
|
80
|
+
|
|
81
|
+
const arr: Body = new PatchableArray();
|
|
82
|
+
const put = MemberWithRegistrationsBlob.create({
|
|
83
|
+
details: MemberDetails.create({
|
|
84
|
+
firstName,
|
|
85
|
+
lastName,
|
|
86
|
+
birthDay: new Date(existingMember.details.birthDay!.getTime() + 1),
|
|
87
|
+
email: 'anewemail@example.com',
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
arr.addPut(put);
|
|
91
|
+
|
|
92
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
93
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
94
|
+
const response = await testServer.test(endpoint, request);
|
|
95
|
+
expect(response.status).toBe(200);
|
|
96
|
+
|
|
97
|
+
// Check id of the returned memebr matches the existing member
|
|
98
|
+
expect(response.body.members.length).toBe(1);
|
|
99
|
+
expect(response.body.members[0].id).toBe(existingMember.id);
|
|
100
|
+
|
|
101
|
+
// Check data matches the original data + changes from the put
|
|
102
|
+
const member = response.body.members[0];
|
|
103
|
+
expect(member.details.firstName).toBe(firstName);
|
|
104
|
+
expect(member.details.lastName).toBe(lastName);
|
|
105
|
+
expect(member.details.birthDay).toEqual(existingMember.details.birthDay);
|
|
106
|
+
expect(member.details.email).toBe('anewemail@example.com'); // this has been merged
|
|
107
|
+
expect(member.details.alternativeEmails).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('A duplicate member with existing registrations returns those registrations after a merge', async () => {
|
|
111
|
+
const organization = await new OrganizationFactory({ }).create();
|
|
112
|
+
const user = await new UserFactory({
|
|
113
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
114
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
115
|
+
}).create();
|
|
116
|
+
|
|
117
|
+
const details = MemberDetails.create({
|
|
118
|
+
firstName,
|
|
119
|
+
lastName,
|
|
120
|
+
securityCode: 'ABC-123',
|
|
121
|
+
email: 'original@example.com',
|
|
122
|
+
parents: [
|
|
123
|
+
Parent.create({
|
|
124
|
+
firstName: 'Jane',
|
|
125
|
+
lastName: 'Doe',
|
|
126
|
+
email: 'jane.doe@example.com',
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const existingMember = await new MemberFactory({
|
|
132
|
+
birthDay,
|
|
133
|
+
details,
|
|
134
|
+
}).create();
|
|
135
|
+
|
|
136
|
+
// Create a registration for this member
|
|
137
|
+
const group = await new GroupFactory({ organization }).create();
|
|
138
|
+
const registration = await new RegistrationFactory({
|
|
139
|
+
member: existingMember,
|
|
140
|
+
group,
|
|
141
|
+
}).create();
|
|
142
|
+
|
|
143
|
+
const token = await Token.createToken(user);
|
|
144
|
+
|
|
145
|
+
const arr: Body = new PatchableArray();
|
|
146
|
+
const put = MemberWithRegistrationsBlob.create({
|
|
147
|
+
details: MemberDetails.create({
|
|
148
|
+
firstName,
|
|
149
|
+
lastName,
|
|
150
|
+
birthDay: new Date(existingMember.details.birthDay!.getTime() + 1),
|
|
151
|
+
securityCode: existingMember.details.securityCode,
|
|
152
|
+
email: 'anewemail@example.com',
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
arr.addPut(put);
|
|
156
|
+
|
|
157
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
158
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
159
|
+
const response = await testServer.test(endpoint, request);
|
|
160
|
+
expect(response.status).toBe(200);
|
|
161
|
+
|
|
162
|
+
// Check id of the returned memebr matches the existing member
|
|
163
|
+
expect(response.body.members.length).toBe(1);
|
|
164
|
+
expect(response.body.members[0].id).toBe(existingMember.id);
|
|
165
|
+
|
|
166
|
+
// Check data matches the original data + changes from the put
|
|
167
|
+
const member = response.body.members[0];
|
|
168
|
+
expect(member.details.firstName).toBe(firstName);
|
|
169
|
+
expect(member.details.lastName).toBe(lastName);
|
|
170
|
+
expect(member.details.birthDay).toEqual(existingMember.details.birthDay);
|
|
171
|
+
expect(member.details.email).toBe('original@example.com'); // this has been merged
|
|
172
|
+
expect(member.details.alternativeEmails).toEqual(['anewemail@example.com']); // this has been merged
|
|
173
|
+
|
|
174
|
+
// Check the registration is still there
|
|
175
|
+
expect(member.registrations.length).toBe(1);
|
|
176
|
+
expect(member.registrations[0].id).toBe(registration.id);
|
|
177
|
+
|
|
178
|
+
// Check parent is still there
|
|
179
|
+
expect(member.details.parents.length).toBe(1);
|
|
180
|
+
expect(member.details.parents[0]).toEqual(existingMember.details.parents[0]);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('Permission checking', () => {
|
|
185
|
+
test('An admin cannot edit members of a different organization', async () => {
|
|
186
|
+
const organization = await new OrganizationFactory({}).create();
|
|
187
|
+
const otherOrganization = await new OrganizationFactory({}).create();
|
|
188
|
+
|
|
189
|
+
const user = await new UserFactory({
|
|
190
|
+
permissions: Permissions.create({
|
|
191
|
+
level: PermissionLevel.Full,
|
|
192
|
+
}),
|
|
193
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
194
|
+
}).create();
|
|
195
|
+
|
|
196
|
+
const member = await new MemberFactory({
|
|
197
|
+
firstName,
|
|
198
|
+
lastName,
|
|
199
|
+
birthDay,
|
|
200
|
+
generateData: false,
|
|
201
|
+
}).create();
|
|
202
|
+
|
|
203
|
+
// Register this member
|
|
204
|
+
await new RegistrationFactory({
|
|
205
|
+
member,
|
|
206
|
+
organization: otherOrganization,
|
|
207
|
+
}).create();
|
|
208
|
+
|
|
209
|
+
const token = await Token.createToken(user);
|
|
210
|
+
|
|
211
|
+
const arr: Body = new PatchableArray();
|
|
212
|
+
const patch = MemberWithRegistrationsBlob.patch({
|
|
213
|
+
id: member.id,
|
|
214
|
+
details: MemberDetails.patch({
|
|
215
|
+
firstName: 'Changed',
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
arr.addPatch(patch);
|
|
219
|
+
|
|
220
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
221
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
222
|
+
await expect(testServer.test(endpoint, request)).rejects.toThrow(errorWithCode('not_found'));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('An admin can edit members registered in its own organization', async () => {
|
|
226
|
+
const organization = await new OrganizationFactory({}).create();
|
|
227
|
+
|
|
228
|
+
const user = await new UserFactory({
|
|
229
|
+
permissions: Permissions.create({
|
|
230
|
+
level: PermissionLevel.Full,
|
|
231
|
+
}),
|
|
232
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
233
|
+
}).create();
|
|
234
|
+
|
|
235
|
+
const member = await new MemberFactory({
|
|
236
|
+
firstName,
|
|
237
|
+
lastName,
|
|
238
|
+
birthDay,
|
|
239
|
+
generateData: false,
|
|
240
|
+
}).create();
|
|
241
|
+
|
|
242
|
+
// Register this member
|
|
243
|
+
await new RegistrationFactory({
|
|
244
|
+
member,
|
|
245
|
+
organization,
|
|
246
|
+
}).create();
|
|
247
|
+
|
|
248
|
+
const token = await Token.createToken(user);
|
|
249
|
+
|
|
250
|
+
const arr: Body = new PatchableArray();
|
|
251
|
+
const patch = MemberWithRegistrationsBlob.patch({
|
|
252
|
+
id: member.id,
|
|
253
|
+
details: MemberDetails.patch({
|
|
254
|
+
firstName: 'Changed',
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
arr.addPatch(patch);
|
|
258
|
+
|
|
259
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
260
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
261
|
+
const response = await testServer.test(endpoint, request);
|
|
262
|
+
|
|
263
|
+
// Check returned
|
|
264
|
+
expect(response.status).toBe(200);
|
|
265
|
+
expect(response.body.members.length).toBe(1);
|
|
266
|
+
const memberStruct = response.body.members[0];
|
|
267
|
+
expect(memberStruct.details).toMatchObject({
|
|
268
|
+
firstName: 'Changed',
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('A full platform admin can edit members without registrations', async () => {
|
|
273
|
+
const organization = await new OrganizationFactory({}).create();
|
|
274
|
+
|
|
275
|
+
const user = await new UserFactory({
|
|
276
|
+
globalPermissions: Permissions.create({
|
|
277
|
+
level: PermissionLevel.Full,
|
|
278
|
+
}),
|
|
279
|
+
}).create();
|
|
280
|
+
|
|
281
|
+
const member = await new MemberFactory({
|
|
282
|
+
firstName,
|
|
283
|
+
lastName,
|
|
284
|
+
birthDay,
|
|
285
|
+
generateData: false,
|
|
286
|
+
}).create();
|
|
287
|
+
|
|
288
|
+
const token = await Token.createToken(user);
|
|
289
|
+
|
|
290
|
+
const arr: Body = new PatchableArray();
|
|
291
|
+
const patch = MemberWithRegistrationsBlob.patch({
|
|
292
|
+
id: member.id,
|
|
293
|
+
details: MemberDetails.patch({
|
|
294
|
+
firstName: 'Changed',
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
arr.addPatch(patch);
|
|
298
|
+
|
|
299
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
300
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
301
|
+
const response = await testServer.test(endpoint, request);
|
|
302
|
+
|
|
303
|
+
// Check returned
|
|
304
|
+
expect(response.status).toBe(200);
|
|
305
|
+
expect(response.body.members.length).toBe(1);
|
|
306
|
+
const memberStruct = response.body.members[0];
|
|
307
|
+
expect(memberStruct.details).toMatchObject({
|
|
308
|
+
firstName: 'Changed',
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('[Regression] A platform admin with all tag access can edit members without registrations', async () => {
|
|
313
|
+
const organization = await new OrganizationFactory({}).create();
|
|
314
|
+
|
|
315
|
+
const user = await new UserFactory({
|
|
316
|
+
globalPermissions: Permissions.create({
|
|
317
|
+
level: PermissionLevel.None,
|
|
318
|
+
resources: new Map([
|
|
319
|
+
// All Tags
|
|
320
|
+
[PermissionsResourceType.OrganizationTags, new Map(
|
|
321
|
+
[['', ResourcePermissions.create({ level: PermissionLevel.Full })]],
|
|
322
|
+
)],
|
|
323
|
+
]),
|
|
324
|
+
}),
|
|
325
|
+
}).create();
|
|
326
|
+
|
|
327
|
+
const member = await new MemberFactory({
|
|
328
|
+
firstName,
|
|
329
|
+
lastName,
|
|
330
|
+
birthDay,
|
|
331
|
+
generateData: false,
|
|
332
|
+
}).create();
|
|
333
|
+
|
|
334
|
+
const token = await Token.createToken(user);
|
|
335
|
+
|
|
336
|
+
const arr: Body = new PatchableArray();
|
|
337
|
+
const patch = MemberWithRegistrationsBlob.patch({
|
|
338
|
+
id: member.id,
|
|
339
|
+
details: MemberDetails.patch({
|
|
340
|
+
firstName: 'Changed',
|
|
341
|
+
}),
|
|
342
|
+
});
|
|
343
|
+
arr.addPatch(patch);
|
|
344
|
+
|
|
345
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
346
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
347
|
+
const response = await testServer.test(endpoint, request);
|
|
348
|
+
|
|
349
|
+
// Check returned
|
|
350
|
+
expect(response.status).toBe(200);
|
|
351
|
+
expect(response.body.members.length).toBe(1);
|
|
352
|
+
const memberStruct = response.body.members[0];
|
|
353
|
+
expect(memberStruct.details).toMatchObject({
|
|
354
|
+
firstName: 'Changed',
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('Record answers', () => {
|
|
360
|
+
test('An admin can set records of its own organization', async () => {
|
|
361
|
+
const commentsRecord = RecordSettings.create({
|
|
362
|
+
name: 'Opmerkingen',
|
|
363
|
+
externalPermissionLevel: PermissionLevel.Read, // this should be ignored since we are an admin
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const recordCategory = RecordCategory.create({
|
|
367
|
+
name: 'Medische fiche',
|
|
368
|
+
records: [
|
|
369
|
+
commentsRecord,
|
|
370
|
+
],
|
|
371
|
+
});
|
|
372
|
+
const organization = await new OrganizationFactory({
|
|
373
|
+
meta: OrganizationMetaData.create({
|
|
374
|
+
recordsConfiguration: OrganizationRecordsConfiguration.create({
|
|
375
|
+
recordCategories: [recordCategory],
|
|
376
|
+
}),
|
|
377
|
+
}),
|
|
378
|
+
}).create();
|
|
379
|
+
|
|
380
|
+
const user = await new UserFactory({
|
|
381
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
382
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
383
|
+
}).create();
|
|
384
|
+
|
|
385
|
+
const member = await new MemberFactory({
|
|
386
|
+
firstName,
|
|
387
|
+
lastName,
|
|
388
|
+
birthDay,
|
|
389
|
+
generateData: false,
|
|
390
|
+
}).create();
|
|
391
|
+
|
|
392
|
+
// Register this member
|
|
393
|
+
await new RegistrationFactory({
|
|
394
|
+
member,
|
|
395
|
+
organization,
|
|
396
|
+
}).create();
|
|
397
|
+
|
|
398
|
+
const token = await Token.createToken(user);
|
|
399
|
+
|
|
400
|
+
const recordAnswers = new PatchMap() as PatchAnswers;
|
|
401
|
+
|
|
402
|
+
recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
|
|
403
|
+
settings: commentsRecord,
|
|
404
|
+
value: 'Some comments',
|
|
405
|
+
}));
|
|
406
|
+
|
|
407
|
+
const arr: Body = new PatchableArray();
|
|
408
|
+
const patch = MemberWithRegistrationsBlob.patch({
|
|
409
|
+
id: member.id,
|
|
410
|
+
details: MemberDetails.patch({
|
|
411
|
+
recordAnswers,
|
|
412
|
+
}),
|
|
413
|
+
});
|
|
414
|
+
arr.addPatch(patch);
|
|
415
|
+
|
|
416
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
417
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
418
|
+
const response = await testServer.test(endpoint, request);
|
|
419
|
+
|
|
420
|
+
// Check returned
|
|
421
|
+
expect(response.status).toBe(200);
|
|
422
|
+
expect(response.body.members.length).toBe(1);
|
|
423
|
+
const struct = response.body.members[0];
|
|
424
|
+
expect(struct.details.recordAnswers.get(commentsRecord.id)).toMatchObject({
|
|
425
|
+
value: 'Some comments',
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('An admin with read only record category permission cannot set the records in that category', async () => {
|
|
430
|
+
const commentsRecord = RecordSettings.create({
|
|
431
|
+
name: 'Opmerkingen',
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const recordCategory = RecordCategory.create({
|
|
435
|
+
name: 'Medische fiche',
|
|
436
|
+
records: [
|
|
437
|
+
commentsRecord,
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
const organization = await new OrganizationFactory({
|
|
441
|
+
meta: OrganizationMetaData.create({
|
|
442
|
+
recordsConfiguration: OrganizationRecordsConfiguration.create({
|
|
443
|
+
recordCategories: [recordCategory],
|
|
444
|
+
}),
|
|
445
|
+
}),
|
|
446
|
+
}).create();
|
|
447
|
+
|
|
448
|
+
const group = await new GroupFactory({ organization }).create();
|
|
449
|
+
|
|
450
|
+
const user = await new UserFactory({
|
|
451
|
+
permissions: Permissions.create({
|
|
452
|
+
level: PermissionLevel.None,
|
|
453
|
+
resources: new Map([
|
|
454
|
+
[PermissionsResourceType.RecordCategories, new Map([
|
|
455
|
+
[recordCategory.id, ResourcePermissions.create({ level: PermissionLevel.Read })],
|
|
456
|
+
])],
|
|
457
|
+
[PermissionsResourceType.Groups, new Map([
|
|
458
|
+
[group.id, ResourcePermissions.create({ level: PermissionLevel.Full })],
|
|
459
|
+
])],
|
|
460
|
+
]),
|
|
461
|
+
}),
|
|
462
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
463
|
+
}).create();
|
|
464
|
+
|
|
465
|
+
const member = await new MemberFactory({
|
|
466
|
+
firstName,
|
|
467
|
+
lastName,
|
|
468
|
+
birthDay,
|
|
469
|
+
generateData: false,
|
|
470
|
+
}).create();
|
|
471
|
+
|
|
472
|
+
// Register this member
|
|
473
|
+
await new RegistrationFactory({
|
|
474
|
+
member,
|
|
475
|
+
group,
|
|
476
|
+
}).create();
|
|
477
|
+
|
|
478
|
+
const token = await Token.createToken(user);
|
|
479
|
+
|
|
480
|
+
const recordAnswers = new PatchMap() as PatchAnswers;
|
|
481
|
+
|
|
482
|
+
recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
|
|
483
|
+
settings: commentsRecord,
|
|
484
|
+
value: 'Some comments',
|
|
485
|
+
}));
|
|
486
|
+
|
|
487
|
+
const arr: Body = new PatchableArray();
|
|
488
|
+
const patch = MemberWithRegistrationsBlob.patch({
|
|
489
|
+
id: member.id,
|
|
490
|
+
details: MemberDetails.patch({
|
|
491
|
+
recordAnswers,
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
arr.addPatch(patch);
|
|
495
|
+
|
|
496
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
497
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
498
|
+
await expect(testServer.test(endpoint, request)).rejects.toThrow(errorWithCode('permission_denied'));
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('An admin without record category permission cannot set the records in that category', async () => {
|
|
502
|
+
const commentsRecord = RecordSettings.create({
|
|
503
|
+
name: 'Opmerkingen',
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const recordCategory = RecordCategory.create({
|
|
507
|
+
name: 'Medische fiche',
|
|
508
|
+
records: [
|
|
509
|
+
commentsRecord,
|
|
510
|
+
],
|
|
511
|
+
});
|
|
512
|
+
const organization = await new OrganizationFactory({
|
|
513
|
+
meta: OrganizationMetaData.create({
|
|
514
|
+
recordsConfiguration: OrganizationRecordsConfiguration.create({
|
|
515
|
+
recordCategories: [recordCategory],
|
|
516
|
+
}),
|
|
517
|
+
}),
|
|
518
|
+
}).create();
|
|
519
|
+
|
|
520
|
+
const group = await new GroupFactory({ organization }).create();
|
|
521
|
+
|
|
522
|
+
const user = await new UserFactory({
|
|
523
|
+
permissions: Permissions.create({
|
|
524
|
+
level: PermissionLevel.None,
|
|
525
|
+
resources: new Map([
|
|
526
|
+
[PermissionsResourceType.Groups, new Map([
|
|
527
|
+
[group.id, ResourcePermissions.create({ level: PermissionLevel.Full })],
|
|
528
|
+
])],
|
|
529
|
+
]),
|
|
530
|
+
}),
|
|
531
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
532
|
+
}).create();
|
|
533
|
+
|
|
534
|
+
const member = await new MemberFactory({
|
|
535
|
+
firstName,
|
|
536
|
+
lastName,
|
|
537
|
+
birthDay,
|
|
538
|
+
generateData: false,
|
|
539
|
+
}).create();
|
|
540
|
+
|
|
541
|
+
// Register this member
|
|
542
|
+
await new RegistrationFactory({
|
|
543
|
+
member,
|
|
544
|
+
group,
|
|
545
|
+
}).create();
|
|
546
|
+
|
|
547
|
+
const token = await Token.createToken(user);
|
|
548
|
+
|
|
549
|
+
const recordAnswers = new PatchMap() as PatchAnswers;
|
|
550
|
+
|
|
551
|
+
recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
|
|
552
|
+
settings: commentsRecord,
|
|
553
|
+
value: 'Some comments',
|
|
554
|
+
}));
|
|
555
|
+
|
|
556
|
+
const arr: Body = new PatchableArray();
|
|
557
|
+
const patch = MemberWithRegistrationsBlob.patch({
|
|
558
|
+
id: member.id,
|
|
559
|
+
details: MemberDetails.patch({
|
|
560
|
+
recordAnswers,
|
|
561
|
+
}),
|
|
562
|
+
});
|
|
563
|
+
arr.addPatch(patch);
|
|
564
|
+
|
|
565
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
566
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
567
|
+
await expect(testServer.test(endpoint, request)).rejects.toThrow(errorWithCode('permission_denied'));
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('An admin can set records of the platform', async () => {
|
|
571
|
+
const commentsRecord = RecordSettings.create({
|
|
572
|
+
name: 'Opmerkingen',
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const recordCategory = RecordCategory.create({
|
|
576
|
+
name: 'Medische fiche',
|
|
577
|
+
records: [
|
|
578
|
+
commentsRecord,
|
|
579
|
+
],
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const platform = await Platform.getShared();
|
|
583
|
+
platform.config.recordsConfiguration.recordCategories.push(recordCategory);
|
|
584
|
+
await platform.save();
|
|
585
|
+
|
|
586
|
+
const organization = await new OrganizationFactory({}).create();
|
|
587
|
+
const group = await new GroupFactory({ organization }).create();
|
|
588
|
+
|
|
589
|
+
const user = await new UserFactory({
|
|
590
|
+
permissions: Permissions.create({
|
|
591
|
+
level: PermissionLevel.None,
|
|
592
|
+
resources: new Map([
|
|
593
|
+
[PermissionsResourceType.RecordCategories, new Map([
|
|
594
|
+
[recordCategory.id, ResourcePermissions.create({ level: PermissionLevel.Write })],
|
|
595
|
+
])],
|
|
596
|
+
[PermissionsResourceType.Groups, new Map([
|
|
597
|
+
[group.id, ResourcePermissions.create({ level: PermissionLevel.Full })],
|
|
598
|
+
])],
|
|
599
|
+
]),
|
|
600
|
+
}),
|
|
601
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
602
|
+
}).create();
|
|
603
|
+
|
|
604
|
+
const member = await new MemberFactory({
|
|
605
|
+
firstName,
|
|
606
|
+
lastName,
|
|
607
|
+
birthDay,
|
|
608
|
+
generateData: false,
|
|
609
|
+
}).create();
|
|
610
|
+
|
|
611
|
+
// Register this member
|
|
612
|
+
await new RegistrationFactory({
|
|
613
|
+
member,
|
|
614
|
+
group,
|
|
615
|
+
}).create();
|
|
616
|
+
|
|
617
|
+
const token = await Token.createToken(user);
|
|
618
|
+
|
|
619
|
+
const recordAnswers = new PatchMap() as PatchAnswers;
|
|
620
|
+
|
|
621
|
+
recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
|
|
622
|
+
settings: commentsRecord,
|
|
623
|
+
value: 'Some comments',
|
|
624
|
+
}));
|
|
625
|
+
|
|
626
|
+
const arr: Body = new PatchableArray();
|
|
627
|
+
const patch = MemberWithRegistrationsBlob.patch({
|
|
628
|
+
id: member.id,
|
|
629
|
+
details: MemberDetails.patch({
|
|
630
|
+
recordAnswers,
|
|
631
|
+
}),
|
|
632
|
+
});
|
|
633
|
+
arr.addPatch(patch);
|
|
634
|
+
|
|
635
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
636
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
637
|
+
const response = await testServer.test(endpoint, request);
|
|
638
|
+
|
|
639
|
+
// Check returned
|
|
640
|
+
expect(response.status).toBe(200);
|
|
641
|
+
expect(response.body.members.length).toBe(1);
|
|
642
|
+
const struct = response.body.members[0];
|
|
643
|
+
expect(struct.details.recordAnswers.get(commentsRecord.id)).toMatchObject({
|
|
644
|
+
value: 'Some comments',
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test('[Regression] A platform admin with tag-access to an organization can change platform records', async () => {
|
|
649
|
+
const commentsRecord = RecordSettings.create({
|
|
650
|
+
name: 'Opmerkingen',
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const recordCategory = RecordCategory.create({
|
|
654
|
+
name: 'Medische fiche',
|
|
655
|
+
records: [
|
|
656
|
+
commentsRecord,
|
|
657
|
+
],
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const platform = await Platform.getShared();
|
|
661
|
+
platform.config.recordsConfiguration.recordCategories.push(recordCategory);
|
|
662
|
+
await platform.save();
|
|
663
|
+
|
|
664
|
+
const tag = await new OrganizationTagFactory({}).create();
|
|
665
|
+
const organization = await new OrganizationFactory({
|
|
666
|
+
tags: [tag.id],
|
|
667
|
+
}).create();
|
|
668
|
+
const group = await new GroupFactory({ organization }).create();
|
|
669
|
+
|
|
670
|
+
const user = await new UserFactory({
|
|
671
|
+
globalPermissions: Permissions.create({
|
|
672
|
+
level: PermissionLevel.None,
|
|
673
|
+
resources: new Map([
|
|
674
|
+
[PermissionsResourceType.OrganizationTags, new Map([
|
|
675
|
+
[tag.id, ResourcePermissions.create({ level: PermissionLevel.Full })],
|
|
676
|
+
])],
|
|
677
|
+
]),
|
|
678
|
+
}),
|
|
679
|
+
organization, // since we are in platform mode, this will only set the permissions for this organization
|
|
680
|
+
}).create();
|
|
681
|
+
|
|
682
|
+
const member = await new MemberFactory({
|
|
683
|
+
firstName,
|
|
684
|
+
lastName,
|
|
685
|
+
birthDay,
|
|
686
|
+
generateData: false,
|
|
687
|
+
}).create();
|
|
688
|
+
|
|
689
|
+
// Register this member
|
|
690
|
+
await new RegistrationFactory({
|
|
691
|
+
member,
|
|
692
|
+
group,
|
|
693
|
+
}).create();
|
|
694
|
+
|
|
695
|
+
const token = await Token.createToken(user);
|
|
696
|
+
|
|
697
|
+
const recordAnswers = new PatchMap() as PatchAnswers;
|
|
698
|
+
|
|
699
|
+
recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
|
|
700
|
+
settings: commentsRecord,
|
|
701
|
+
value: 'Some comments',
|
|
702
|
+
}));
|
|
703
|
+
|
|
704
|
+
const arr: Body = new PatchableArray();
|
|
705
|
+
const patch = MemberWithRegistrationsBlob.patch({
|
|
706
|
+
id: member.id,
|
|
707
|
+
details: MemberDetails.patch({
|
|
708
|
+
recordAnswers,
|
|
709
|
+
}),
|
|
710
|
+
});
|
|
711
|
+
arr.addPatch(patch);
|
|
712
|
+
|
|
713
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
714
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
715
|
+
const response = await testServer.test(endpoint, request);
|
|
716
|
+
|
|
717
|
+
// Check returned
|
|
718
|
+
expect(response.status).toBe(200);
|
|
719
|
+
expect(response.body.members.length).toBe(1);
|
|
720
|
+
const struct = response.body.members[0];
|
|
721
|
+
expect(struct.details.recordAnswers.get(commentsRecord.id)).toMatchObject({
|
|
722
|
+
value: 'Some comments',
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
});
|
|
@@ -16,8 +16,7 @@ import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
|
|
|
16
16
|
import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
|
|
17
17
|
import { PlatformMembershipService } from '../../../services/PlatformMembershipService';
|
|
18
18
|
import { RegistrationService } from '../../../services/RegistrationService';
|
|
19
|
-
import { shouldCheckIfMemberIsDuplicateForPatch
|
|
20
|
-
import { AuditLogService } from '../../../services/AuditLogService';
|
|
19
|
+
import { shouldCheckIfMemberIsDuplicateForPatch } from './shouldCheckIfMemberIsDuplicate';
|
|
21
20
|
|
|
22
21
|
type Params = Record<string, never>;
|
|
23
22
|
type Query = undefined;
|
|
@@ -109,12 +108,10 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
109
108
|
struct.details.cleanData();
|
|
110
109
|
member.details = struct.details;
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (duplicate) {
|
|
111
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode, 'put');
|
|
112
|
+
if (duplicate) {
|
|
115
113
|
// Merge data
|
|
116
|
-
|
|
117
|
-
}
|
|
114
|
+
member = duplicate;
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
// We risk creating a new member without being able to access it manually afterwards
|
|
@@ -159,12 +156,11 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
159
156
|
const securityCode = patch.details?.securityCode; // will get cleared after the filter
|
|
160
157
|
|
|
161
158
|
if (!member) {
|
|
162
|
-
throw Context.auth.
|
|
159
|
+
throw Context.auth.memberNotFoundOrNoAccess();
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
if (!await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
|
|
166
|
-
|
|
167
|
-
await PatchOrganizationMembersEndpoint.checkSecurityCode(member, securityCode);
|
|
162
|
+
if (!(await Context.auth.canAccessMember(member, PermissionLevel.Write))) {
|
|
163
|
+
await PatchOrganizationMembersEndpoint.checkSecurityCode(member, securityCode, 'patch');
|
|
168
164
|
}
|
|
169
165
|
|
|
170
166
|
patch = await Context.auth.filterMemberPatch(member, patch);
|
|
@@ -194,7 +190,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
194
190
|
}
|
|
195
191
|
|
|
196
192
|
if (shouldCheckDuplicate) {
|
|
197
|
-
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
|
|
193
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode, 'patch');
|
|
198
194
|
|
|
199
195
|
if (duplicate) {
|
|
200
196
|
// Remove the member from the list
|
|
@@ -759,8 +755,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
759
755
|
}
|
|
760
756
|
}
|
|
761
757
|
|
|
762
|
-
static async checkSecurityCode(member: MemberWithRegistrations, securityCode: string | null | undefined) {
|
|
763
|
-
if (await member.isSafeToMergeDuplicateWithoutSecurityCode() || await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
|
|
758
|
+
static async checkSecurityCode(member: MemberWithRegistrations, securityCode: string | null | undefined, type: 'put' | 'patch') {
|
|
759
|
+
if ((type === 'put' && await member.isSafeToMergeDuplicateWithoutSecurityCode()) || await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
|
|
764
760
|
console.log('checkSecurityCode: without security code: allowed for ' + member.id);
|
|
765
761
|
}
|
|
766
762
|
else if (securityCode) {
|
|
@@ -802,8 +798,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
802
798
|
const log = new AuditLog();
|
|
803
799
|
|
|
804
800
|
// a member has multiple organizations, so this is difficult to determine - for now it is only visible in the admin panel
|
|
805
|
-
log.organizationId = member.organizationId;
|
|
806
|
-
|
|
801
|
+
log.organizationId = member.organizationId;
|
|
802
|
+
|
|
807
803
|
log.type = AuditLogType.MemberSecurityCodeUsed;
|
|
808
804
|
log.source = AuditLogSource.Anonymous;
|
|
809
805
|
|
|
@@ -823,6 +819,9 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
823
819
|
await log.save();
|
|
824
820
|
}
|
|
825
821
|
else {
|
|
822
|
+
if (type === 'patch') {
|
|
823
|
+
throw Context.auth.memberNotFoundOrNoAccess();
|
|
824
|
+
}
|
|
826
825
|
throw new SimpleError({
|
|
827
826
|
code: 'known_member_missing_rights',
|
|
828
827
|
message: 'Creating known member without sufficient access rights',
|
|
@@ -832,11 +831,25 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
832
831
|
}
|
|
833
832
|
}
|
|
834
833
|
|
|
835
|
-
static
|
|
834
|
+
static shouldCheckIfMemberIsDuplicate(put: Member): boolean {
|
|
835
|
+
if (put.details.firstName.length <= 3 && put.details.lastName.length <= 3) {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const age = put.details.age;
|
|
840
|
+
// do not check if member is duplicate for historical members
|
|
841
|
+
return age !== null && age < 81;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
static async checkDuplicate(member: Member, securityCode: string | null | undefined, type: 'put' | 'patch') {
|
|
845
|
+
if (!this.shouldCheckIfMemberIsDuplicate(member)) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
836
849
|
// Check for duplicates and prevent creating a duplicate member by a user
|
|
837
850
|
const duplicate = await this.findExistingMember(member);
|
|
838
851
|
if (duplicate) {
|
|
839
|
-
await this.checkSecurityCode(duplicate, securityCode);
|
|
852
|
+
await this.checkSecurityCode(duplicate, securityCode, type);
|
|
840
853
|
|
|
841
854
|
// Merge data
|
|
842
855
|
// NOTE: We use mergeTwoMembers instead of mergeMultipleMembers, because we should never safe 'member' , because that one does not exist in the database
|
|
@@ -1,34 +1,22 @@
|
|
|
1
1
|
import { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { MemberDetails, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Returns true when either the firstname, lastname or birthday has changed
|
|
6
|
+
*/
|
|
4
7
|
export function shouldCheckIfMemberIsDuplicateForPatch(patch: { details: MemberDetails | AutoEncoderPatchType<MemberDetails> | undefined }, originalDetails: MemberDetails): boolean {
|
|
5
8
|
if (patch.details === undefined) {
|
|
6
9
|
return false;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
((patch.details.firstName !== undefined && patch.details.firstName.length > 3) || (patch.details.firstName === undefined && originalDetails.firstName.length > 3))
|
|
12
|
-
// or has long last name
|
|
13
|
-
|| ((patch.details.lastName !== undefined && patch.details.lastName.length > 3) || (patch.details.lastName === undefined && originalDetails.lastName.length > 3))
|
|
14
|
-
)
|
|
15
|
-
// has name change or birthday change
|
|
16
|
-
&& (
|
|
17
|
-
// has first name change
|
|
12
|
+
// name or birthday has changed
|
|
13
|
+
if (
|
|
18
14
|
(patch.details.firstName !== undefined && patch.details.firstName !== originalDetails.firstName)
|
|
19
|
-
// has last name change
|
|
20
15
|
|| (patch.details.lastName !== undefined && patch.details.lastName !== originalDetails.lastName)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function shouldCheckIfMemberIsDuplicateForPut(put: MemberWithRegistrationsBlob): boolean {
|
|
27
|
-
if (put.details.firstName.length <= 3 && put.details.lastName.length <= 3) {
|
|
28
|
-
return false;
|
|
16
|
+
|| (patch.details.birthDay !== undefined && patch.details.birthDay !== originalDetails.birthDay)
|
|
17
|
+
) {
|
|
18
|
+
return true;
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
// do not check if member is duplicate for historical members
|
|
33
|
-
return age !== null && age < 81;
|
|
21
|
+
return false;
|
|
34
22
|
}
|
|
@@ -8,7 +8,7 @@ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructure
|
|
|
8
8
|
import { Context } from '../../../helpers/Context';
|
|
9
9
|
import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
|
|
10
10
|
import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint';
|
|
11
|
-
import { shouldCheckIfMemberIsDuplicateForPatch
|
|
11
|
+
import { shouldCheckIfMemberIsDuplicateForPatch } from '../members/shouldCheckIfMemberIsDuplicate';
|
|
12
12
|
type Params = Record<string, never>;
|
|
13
13
|
type Query = undefined;
|
|
14
14
|
type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
@@ -61,12 +61,10 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
61
61
|
|
|
62
62
|
this.throwIfInvalidDetails(member.details);
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
64
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode, 'put');
|
|
65
|
+
if (duplicate) {
|
|
66
|
+
addedMembers.push(duplicate);
|
|
67
|
+
continue;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
await member.save();
|
|
@@ -79,12 +77,9 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
79
77
|
for (let struct of request.body.getPatches()) {
|
|
80
78
|
const member = members.find(m => m.id === struct.id);
|
|
81
79
|
if (!member) {
|
|
82
|
-
throw
|
|
83
|
-
code: 'invalid_member',
|
|
84
|
-
message: "This member does not exist or you don't have permissions to modify this member",
|
|
85
|
-
human: 'Je probeert een lid aan te passen die niet (meer) bestaat. Er ging ergens iets mis.',
|
|
86
|
-
});
|
|
80
|
+
throw Context.auth.memberNotFoundOrNoAccess();
|
|
87
81
|
}
|
|
82
|
+
|
|
88
83
|
const securityCode = struct.details?.securityCode; // will get cleared after the filter
|
|
89
84
|
struct = await Context.auth.filterMemberPatch(member, struct);
|
|
90
85
|
|
|
@@ -117,7 +112,7 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
117
112
|
}
|
|
118
113
|
|
|
119
114
|
if (shouldCheckDuplicate) {
|
|
120
|
-
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
|
|
115
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode, 'patch');
|
|
121
116
|
if (duplicate) {
|
|
122
117
|
// Remove the member from the list
|
|
123
118
|
members.splice(members.findIndex(m => m.id === member.id), 1);
|
|
@@ -250,7 +250,7 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
250
250
|
if (!member || !(await Context.auth.canLinkBalanceItemToMember(member))) {
|
|
251
251
|
throw new SimpleError({
|
|
252
252
|
code: 'permission_denied',
|
|
253
|
-
message: 'No permission to link
|
|
253
|
+
message: 'No permission to link balance items to this member',
|
|
254
254
|
human: 'Je hebt geen toegang om aanrekeningen te maken verbonden met dit lid',
|
|
255
255
|
field: 'memberId',
|
|
256
256
|
});
|
|
@@ -264,7 +264,7 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
264
264
|
if (!user || !await Context.auth.canLinkBalanceItemToUser(balanceItem, user)) {
|
|
265
265
|
throw new SimpleError({
|
|
266
266
|
code: 'permission_denied',
|
|
267
|
-
message: 'No permission to link
|
|
267
|
+
message: 'No permission to link balance items to this user',
|
|
268
268
|
human: 'Je hebt geen toegang om aanrekeningen te maken verbonden met deze gebruiker',
|
|
269
269
|
field: 'userId',
|
|
270
270
|
});
|
|
@@ -91,6 +91,10 @@ export class AdminPermissionChecker {
|
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
memberNotFoundOrNoAccess(): SimpleError {
|
|
95
|
+
return this.notFoundOrNoAccess($t('Je hebt geen toegang tot dit lid of het bestaat niet'));
|
|
96
|
+
}
|
|
97
|
+
|
|
94
98
|
notFoundOrNoAccess(message?: string): SimpleError {
|
|
95
99
|
return new SimpleError({
|
|
96
100
|
code: 'not_found',
|
|
@@ -1043,6 +1047,20 @@ export class AdminPermissionChecker {
|
|
|
1043
1047
|
};
|
|
1044
1048
|
}
|
|
1045
1049
|
|
|
1050
|
+
// It is possible that this is a platform admin, and inherits automatic permissions for tags. So'll need to loop all the organizations where this member has an active registration for
|
|
1051
|
+
if (!record.organizationId && this.platformPermissions) {
|
|
1052
|
+
const organizations = Formatter.uniqueArray(member.registrations.map(r => r.organizationId));
|
|
1053
|
+
for (const organizationId of organizations) {
|
|
1054
|
+
const organizationPermissions = await this.getOrganizationPermissions(organizationId);
|
|
1055
|
+
if (organizationPermissions && organizationPermissions.hasResourceAccess(PermissionsResourceType.RecordCategories, record.rootCategoryId, level)) {
|
|
1056
|
+
return {
|
|
1057
|
+
canAccess: true,
|
|
1058
|
+
record: record.record,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1046
1064
|
return {
|
|
1047
1065
|
canAccess: false,
|
|
1048
1066
|
record: record.record,
|
|
@@ -1088,8 +1106,8 @@ export class AdminPermissionChecker {
|
|
|
1088
1106
|
if (isUserManager) {
|
|
1089
1107
|
// For a user manager without an organization, we don't delete data, because when registering a new member, it doesn't have any organizations yet...
|
|
1090
1108
|
if (!(await this.canAccessMember(member, PermissionLevel.Full))) {
|
|
1091
|
-
cloned.details.securityCode = null;
|
|
1092
1109
|
cloned.details.notes = null;
|
|
1110
|
+
// a user manager can see the security codes
|
|
1093
1111
|
}
|
|
1094
1112
|
|
|
1095
1113
|
return cloned;
|
|
@@ -22,7 +22,7 @@ export class FileSignService {
|
|
|
22
22
|
const alg = STAMHOOFD.FILE_SIGNING_ALG || 'ES256';
|
|
23
23
|
|
|
24
24
|
if (!STAMHOOFD.FILE_SIGNING_PUBLIC_KEY || !STAMHOOFD.FILE_SIGNING_PRIVATE_KEY) {
|
|
25
|
-
if (STAMHOOFD.environment !== 'development') {
|
|
25
|
+
if (STAMHOOFD.environment !== 'development' && STAMHOOFD.environment !== 'test') {
|
|
26
26
|
throw new Error('Missing environment variables for file signing. Please make sure FILE_SIGNING_PUBLIC_KEY, FILE_SIGNING_PRIVATE_KEY and FILE_SIGNING_ALG are set.');
|
|
27
27
|
}
|
|
28
28
|
|