@stamhoofd/backend 2.78.2 → 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.
Files changed (28) hide show
  1. package/.env.ci.json +32 -16
  2. package/index.ts +7 -0
  3. package/jest.config.cjs +17 -0
  4. package/package.json +10 -10
  5. package/src/endpoints/auth/GetUserEndpoint.test.ts +0 -10
  6. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +726 -0
  7. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +31 -18
  8. package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +9 -21
  9. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +1 -1
  10. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +0 -4
  11. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +0 -4
  12. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +288 -8
  13. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -13
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +7 -7
  15. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +2 -217
  16. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +2 -2
  17. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +6 -3
  18. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -6
  19. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +2 -20
  20. package/src/helpers/AdminPermissionChecker.ts +88 -140
  21. package/src/helpers/GlobalHelper.ts +6 -1
  22. package/src/services/FileSignService.ts +3 -3
  23. package/src/services/MemberRecordStore.ts +155 -0
  24. package/src/services/PlatformMembershipService.ts +17 -8
  25. package/tests/e2e/register.test.ts +49 -21
  26. package/tests/helpers/StripeMocker.ts +7 -2
  27. package/tests/jest.global.setup.ts +6 -1
  28. package/tests/jest.setup.ts +10 -2
@@ -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, shouldCheckIfMemberIsDuplicateForPut } from './shouldCheckIfMemberIsDuplicate';
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
- if (shouldCheckIfMemberIsDuplicateForPut(struct)) {
113
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
114
- if (duplicate) {
111
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode, 'put');
112
+ if (duplicate) {
115
113
  // Merge data
116
- member = duplicate;
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.notFoundOrNoAccess('Je hebt geen toegang tot dit lid of het bestaat niet');
159
+ throw Context.auth.memberNotFoundOrNoAccess();
163
160
  }
164
161
 
165
- if (!await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
166
- // Still allowed if you provide a security code
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 async checkDuplicate(member: Member, securityCode: string | null | undefined) {
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
- return (
10
- // has long first name
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
- // has birth day change
22
- || (patch.details.birthDay !== undefined && patch.details.birthDay?.getTime() !== originalDetails.birthDay?.getTime())
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
- const age = put.details.age;
32
- // do not check if member is duplicate for historical members
33
- return age !== null && age < 81;
21
+ return false;
34
22
  }
@@ -4,7 +4,7 @@ import { Address, Country, CreateOrganization, NewUser, Organization as Organiza
4
4
  import { testServer } from '../../../../tests/helpers/TestServer';
5
5
  import { CreateOrganizationEndpoint } from './CreateOrganizationEndpoint';
6
6
 
7
- describe('Endpoint.CreateOrganization', () => {
7
+ describe.skip('Endpoint.CreateOrganization', () => {
8
8
  // Test endpoint
9
9
  const endpoint = new CreateOrganizationEndpoint();
10
10
 
@@ -11,7 +11,6 @@ describe('Endpoint.GetOrganizationFromDomain', () => {
11
11
 
12
12
  test('Get organization from default uri', async () => {
13
13
  const organization = await new OrganizationFactory({}).create();
14
- const groups = await new GroupFactory({ organization }).createMultiple(2);
15
14
 
16
15
  const r = Request.buildJson('GET', '/v2/organization-from-domain');
17
16
  r.query = {
@@ -26,12 +25,10 @@ describe('Endpoint.GetOrganizationFromDomain', () => {
26
25
  }
27
26
 
28
27
  expect(response.body.id).toEqual(organization.id);
29
- expect(response.body.groups.map(g => g.id).sort()).toEqual(groups.map(g => g.id).sort());
30
28
  });
31
29
 
32
30
  test('Get organization from custom domain', async () => {
33
31
  const organization = await new OrganizationFactory({ domain: 'inschrijven.mijnscouts.be' }).create();
34
- const groups = await new GroupFactory({ organization }).createMultiple(2);
35
32
 
36
33
  const r = Request.buildJson('GET', '/v2/organization-from-domain');
37
34
  r.query = {
@@ -46,6 +43,5 @@ describe('Endpoint.GetOrganizationFromDomain', () => {
46
43
  }
47
44
 
48
45
  expect(response.body.id).toEqual(organization.id);
49
- expect(response.body.groups.map(g => g.id).sort()).toEqual(groups.map(g => g.id).sort());
50
46
  });
51
47
  });
@@ -1,6 +1,5 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { OrganizationFactory } from '@stamhoofd/models';
3
- import { OrganizationSimple } from '@stamhoofd/structures';
4
3
  import { v4 as uuidv4 } from 'uuid';
5
4
 
6
5
  import { testServer } from '../../../../tests/helpers/TestServer';
@@ -25,7 +24,6 @@ describe('Endpoint.SearchOrganization', () => {
25
24
  expect(response.body).toHaveLength(1);
26
25
 
27
26
  // Access token should be expired
28
- expect(response.body[0]).toBeInstanceOf(OrganizationSimple);
29
27
  expect(response.status).toEqual(200);
30
28
  expect(response.body[0]).toMatchObject({
31
29
  id: organization.id,
@@ -50,8 +48,6 @@ describe('Endpoint.SearchOrganization', () => {
50
48
  expect(response.body).toHaveLength(2);
51
49
 
52
50
  // Access token should be expired
53
- expect(response.body[0]).toBeInstanceOf(OrganizationSimple);
54
- expect(response.body[1]).toBeInstanceOf(OrganizationSimple);
55
51
  expect(response.status).toEqual(200);
56
52
  expect(response.body.map(o => o.id).sort()).toEqual(organizations.map(o => o.id).sort());
57
53
  });
@@ -1,9 +1,11 @@
1
- import { PatchableArray } from '@simonbackx/simple-encoding';
1
+ import { PatchableArray, PatchMap } from '@simonbackx/simple-encoding';
2
2
  import { Endpoint, Request } from '@simonbackx/simple-endpoints';
3
- import { GroupFactory, MemberFactory, OrganizationFactory, RegistrationFactory, Token, UserFactory } from '@stamhoofd/models';
4
- import { MemberDetails, MemberWithRegistrationsBlob, Parent } from '@stamhoofd/structures';
3
+ import { GroupFactory, MemberFactory, OrganizationFactory, Platform, RegistrationFactory, Token, UserFactory } from '@stamhoofd/models';
4
+ import { MemberDetails, MemberWithRegistrationsBlob, OrganizationMetaData, OrganizationRecordsConfiguration, Parent, PatchAnswers, PermissionLevel, RecordCategory, RecordSettings, RecordTextAnswer } from '@stamhoofd/structures';
5
5
  import { testServer } from '../../../../tests/helpers/TestServer';
6
6
  import { PatchUserMembersEndpoint } from './PatchUserMembersEndpoint';
7
+ import { Database } from '@simonbackx/simple-database';
8
+ import { TestUtils } from '@stamhoofd/test-utils';
7
9
 
8
10
  const baseUrl = `/members`;
9
11
  const endpoint = new PatchUserMembersEndpoint();
@@ -17,12 +19,20 @@ const birthDay = { year: 1993, month: 4, day: 5 };
17
19
  const errorWithCode = (code: string) => expect.objectContaining({ code }) as jest.Constructable;
18
20
 
19
21
  describe('Endpoint.PatchUserMembersEndpoint', () => {
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
+
20
31
  describe('Duplicate members', () => {
21
32
  test('The security code should be a requirement', async () => {
22
33
  const organization = await new OrganizationFactory({ }).create();
23
- const user = await new UserFactory({ organization }).create();
34
+ const user = await new UserFactory({ }).create();
24
35
  const existingMember = await new MemberFactory({
25
- organization,
26
36
  firstName,
27
37
  lastName,
28
38
  birthDay,
@@ -50,9 +60,8 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
50
60
 
51
61
  test('The security code is not a requirement for members without additional data', async () => {
52
62
  const organization = await new OrganizationFactory({ }).create();
53
- const user = await new UserFactory({ organization }).create();
63
+ const user = await new UserFactory({ }).create();
54
64
  const existingMember = await new MemberFactory({
55
- organization,
56
65
  firstName,
57
66
  lastName,
58
67
  birthDay,
@@ -108,7 +117,6 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
108
117
  });
109
118
 
110
119
  const existingMember = await new MemberFactory({
111
- organization,
112
120
  birthDay,
113
121
  details,
114
122
  }).create();
@@ -160,4 +168,276 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
160
168
  expect(member.details.parents[0]).toEqual(existingMember.details.parents[0]);
161
169
  });
162
170
  });
171
+
172
+ describe('Record answers', () => {
173
+ test('A user can save answers of records of an organization it has not yet registered for', async () => {
174
+ const commentsRecord = RecordSettings.create({
175
+ name: 'Opmerkingen',
176
+ });
177
+
178
+ const recordCategory = RecordCategory.create({
179
+ name: 'Medische fiche',
180
+ records: [
181
+ commentsRecord,
182
+ ],
183
+ });
184
+ const organization = await new OrganizationFactory({
185
+ meta: OrganizationMetaData.create({
186
+ recordsConfiguration: OrganizationRecordsConfiguration.create({
187
+ recordCategories: [recordCategory],
188
+ }),
189
+ }),
190
+ }).create();
191
+
192
+ const user = await new UserFactory({ }).create();
193
+ const existingMember = await new MemberFactory({
194
+ firstName,
195
+ lastName,
196
+ birthDay,
197
+ generateData: false,
198
+ // Give user access to this member
199
+ user,
200
+ }).create();
201
+
202
+ const token = await Token.createToken(user);
203
+
204
+ const recordAnswers = new PatchMap() as PatchAnswers;
205
+
206
+ recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
207
+ settings: commentsRecord,
208
+ value: 'Some comments',
209
+ }));
210
+
211
+ const arr: Body = new PatchableArray();
212
+ const patch = MemberWithRegistrationsBlob.patch({
213
+ id: existingMember.id,
214
+ details: MemberDetails.patch({
215
+ recordAnswers,
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
+ const response = await testServer.test(endpoint, request);
223
+
224
+ // Check returned
225
+ expect(response.status).toBe(200);
226
+ expect(response.body.members.length).toBe(1);
227
+ const member = response.body.members[0];
228
+ expect(member.details.recordAnswers.get(commentsRecord.id)).toMatchObject({
229
+ value: 'Some comments',
230
+ });
231
+ });
232
+
233
+ test('A user cannot save answers to organization read-only records', async () => {
234
+ const commentsRecord = RecordSettings.create({
235
+ name: 'Opmerkingen',
236
+ externalPermissionLevel: PermissionLevel.Read,
237
+ });
238
+
239
+ const recordCategory = RecordCategory.create({
240
+ name: 'Medische fiche',
241
+ records: [
242
+ commentsRecord,
243
+ ],
244
+ });
245
+ const organization = await new OrganizationFactory({
246
+ meta: OrganizationMetaData.create({
247
+ recordsConfiguration: OrganizationRecordsConfiguration.create({
248
+ recordCategories: [recordCategory],
249
+ }),
250
+ }),
251
+ }).create();
252
+
253
+ const user = await new UserFactory({ }).create();
254
+ const existingMember = await new MemberFactory({
255
+ firstName,
256
+ lastName,
257
+ birthDay,
258
+ generateData: false,
259
+ // Give user access to this member
260
+ user,
261
+ }).create();
262
+
263
+ const token = await Token.createToken(user);
264
+
265
+ const recordAnswers = new PatchMap() as PatchAnswers;
266
+
267
+ recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
268
+ settings: commentsRecord,
269
+ value: 'Some comments',
270
+ }));
271
+
272
+ const arr: Body = new PatchableArray();
273
+ const patch = MemberWithRegistrationsBlob.patch({
274
+ id: existingMember.id,
275
+ details: MemberDetails.patch({
276
+ recordAnswers,
277
+ }),
278
+ });
279
+ arr.addPatch(patch);
280
+
281
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
282
+ request.headers.authorization = 'Bearer ' + token.accessToken;
283
+ await expect(testServer.test(endpoint, request)).rejects.toThrow(errorWithCode('permission_denied'));
284
+ });
285
+
286
+ test('A user can save answers of records of the platform', async () => {
287
+ const commentsRecord = RecordSettings.create({
288
+ name: 'Opmerkingen',
289
+ });
290
+
291
+ const recordCategory = RecordCategory.create({
292
+ name: 'Medische fiche',
293
+ records: [
294
+ commentsRecord,
295
+ ],
296
+ });
297
+
298
+ const platform = await Platform.getShared();
299
+ platform.config.recordsConfiguration.recordCategories.push(recordCategory);
300
+ await platform.save();
301
+
302
+ const organization = await new OrganizationFactory({}).create();
303
+
304
+ const user = await new UserFactory({ }).create();
305
+ const existingMember = await new MemberFactory({
306
+ firstName,
307
+ lastName,
308
+ birthDay,
309
+ generateData: false,
310
+ // Give user access to this member
311
+ user,
312
+ }).create();
313
+
314
+ const token = await Token.createToken(user);
315
+
316
+ const recordAnswers = new PatchMap() as PatchAnswers;
317
+
318
+ recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
319
+ settings: commentsRecord,
320
+ value: 'Some comments',
321
+ }));
322
+
323
+ const arr: Body = new PatchableArray();
324
+ const patch = MemberWithRegistrationsBlob.patch({
325
+ id: existingMember.id,
326
+ details: MemberDetails.patch({
327
+ recordAnswers,
328
+ }),
329
+ });
330
+ arr.addPatch(patch);
331
+
332
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
333
+ request.headers.authorization = 'Bearer ' + token.accessToken;
334
+ const response = await testServer.test(endpoint, request);
335
+
336
+ // Check returned
337
+ expect(response.status).toBe(200);
338
+ expect(response.body.members.length).toBe(1);
339
+ const member = response.body.members[0];
340
+ expect(member.details.recordAnswers.get(commentsRecord.id)).toMatchObject({
341
+ value: 'Some comments',
342
+ });
343
+ });
344
+
345
+ test('A user cannot save answers to platform read-only records', async () => {
346
+ const commentsRecord = RecordSettings.create({
347
+ name: 'Opmerkingen',
348
+ externalPermissionLevel: PermissionLevel.Read,
349
+ });
350
+
351
+ const recordCategory = RecordCategory.create({
352
+ name: 'Medische fiche',
353
+ records: [
354
+ commentsRecord,
355
+ ],
356
+ });
357
+
358
+ const platform = await Platform.getShared();
359
+ platform.config.recordsConfiguration.recordCategories.push(recordCategory);
360
+ await platform.save();
361
+
362
+ const organization = await new OrganizationFactory({}).create();
363
+
364
+ const user = await new UserFactory({ }).create();
365
+ const existingMember = await new MemberFactory({
366
+ firstName,
367
+ lastName,
368
+ birthDay,
369
+ generateData: false,
370
+ // Give user access to this member
371
+ user,
372
+ }).create();
373
+
374
+ const token = await Token.createToken(user);
375
+
376
+ const recordAnswers = new PatchMap() as PatchAnswers;
377
+
378
+ recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
379
+ settings: commentsRecord,
380
+ value: 'Some comments',
381
+ }));
382
+
383
+ const arr: Body = new PatchableArray();
384
+ const patch = MemberWithRegistrationsBlob.patch({
385
+ id: existingMember.id,
386
+ details: MemberDetails.patch({
387
+ recordAnswers,
388
+ }),
389
+ });
390
+ arr.addPatch(patch);
391
+
392
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
393
+ request.headers.authorization = 'Bearer ' + token.accessToken;
394
+ await expect(testServer.test(endpoint, request)).rejects.toThrow(errorWithCode('permission_denied'));
395
+ });
396
+
397
+ test('A user can not save anwers to inexisting records', async () => {
398
+ const commentsRecord = RecordSettings.create({
399
+ name: 'Opmerkingen',
400
+ });
401
+
402
+ const organization = await new OrganizationFactory({
403
+ meta: OrganizationMetaData.create({
404
+ recordsConfiguration: OrganizationRecordsConfiguration.create({
405
+ recordCategories: [],
406
+ }),
407
+ }),
408
+ }).create();
409
+
410
+ const user = await new UserFactory({ }).create();
411
+ const existingMember = await new MemberFactory({
412
+ firstName,
413
+ lastName,
414
+ birthDay,
415
+ generateData: false,
416
+ // Give user access to this member
417
+ user,
418
+ }).create();
419
+
420
+ const token = await Token.createToken(user);
421
+
422
+ const recordAnswers = new PatchMap() as PatchAnswers;
423
+
424
+ recordAnswers.set(commentsRecord.id, RecordTextAnswer.create({
425
+ settings: commentsRecord,
426
+ value: 'Some comments',
427
+ }));
428
+
429
+ const arr: Body = new PatchableArray();
430
+ const patch = MemberWithRegistrationsBlob.patch({
431
+ id: existingMember.id,
432
+ details: MemberDetails.patch({
433
+ recordAnswers,
434
+ }),
435
+ });
436
+ arr.addPatch(patch);
437
+
438
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
439
+ request.headers.authorization = 'Bearer ' + token.accessToken;
440
+ await expect(testServer.test(endpoint, request)).rejects.toThrow(errorWithCode('permission_denied'));
441
+ });
442
+ });
163
443
  });
@@ -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, shouldCheckIfMemberIsDuplicateForPut } from '../members/shouldCheckIfMemberIsDuplicate';
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
- if (shouldCheckIfMemberIsDuplicateForPut(struct)) {
65
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
66
- if (duplicate) {
67
- addedMembers.push(duplicate);
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 new SimpleError({
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);
@@ -770,7 +770,7 @@ describe('Endpoint.RegisterMembers', () => {
770
770
  administrationFee: 0,
771
771
  freeContribution: 0,
772
772
  paymentMethod: PaymentMethod.PointOfSale,
773
- totalPrice: 0,
773
+ totalPrice: 25,
774
774
  asOrganizationId: organization.id,
775
775
  });
776
776
 
@@ -1649,7 +1649,7 @@ describe('Endpoint.RegisterMembers', () => {
1649
1649
  administrationFee: 0,
1650
1650
  freeContribution: 0,
1651
1651
  paymentMethod: PaymentMethod.PointOfSale,
1652
- totalPrice: 5,
1652
+ totalPrice: 30,
1653
1653
  asOrganizationId: organization.id,
1654
1654
  customer: null,
1655
1655
  });
@@ -1927,7 +1927,7 @@ describe('Endpoint.RegisterMembers', () => {
1927
1927
  administrationFee: 0,
1928
1928
  freeContribution: 0,
1929
1929
  paymentMethod: PaymentMethod.PointOfSale,
1930
- totalPrice: 5,
1930
+ totalPrice: 30,
1931
1931
  asOrganizationId: organization1.id,
1932
1932
  customer: null,
1933
1933
  });
@@ -2049,7 +2049,7 @@ describe('Endpoint.RegisterMembers', () => {
2049
2049
  administrationFee: 0,
2050
2050
  freeContribution: 0,
2051
2051
  paymentMethod: PaymentMethod.PointOfSale,
2052
- totalPrice: 5,
2052
+ totalPrice: 30,
2053
2053
  asOrganizationId: organization.id,
2054
2054
  customer: null,
2055
2055
  });
@@ -2226,7 +2226,7 @@ describe('Endpoint.RegisterMembers', () => {
2226
2226
  administrationFee: 0,
2227
2227
  freeContribution: 0,
2228
2228
  paymentMethod: PaymentMethod.PointOfSale,
2229
- totalPrice: 5,
2229
+ totalPrice: 30,
2230
2230
  customer: null,
2231
2231
  asOrganizationId: organization.id,
2232
2232
  });
@@ -2331,7 +2331,7 @@ describe('Endpoint.RegisterMembers', () => {
2331
2331
  administrationFee: 0,
2332
2332
  freeContribution: 0,
2333
2333
  paymentMethod: PaymentMethod.PointOfSale,
2334
- totalPrice: 5,
2334
+ totalPrice: 30,
2335
2335
  customer: null,
2336
2336
  asOrganizationId: organization.id,
2337
2337
  });
@@ -2355,7 +2355,7 @@ describe('Endpoint.RegisterMembers', () => {
2355
2355
  administrationFee: 0,
2356
2356
  freeContribution: 0,
2357
2357
  paymentMethod: PaymentMethod.PointOfSale,
2358
- totalPrice: 5,
2358
+ totalPrice: 30,
2359
2359
  customer: null,
2360
2360
  asOrganizationId: organization.id,
2361
2361
  });