@stamhoofd/backend 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/index.ts +3 -0
  2. package/package.json +4 -4
  3. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +63 -2
  4. package/src/endpoints/auth/CreateAdminEndpoint.ts +6 -3
  5. package/src/endpoints/auth/GetOtherUserEndpoint.ts +41 -0
  6. package/src/endpoints/auth/GetUserEndpoint.ts +6 -28
  7. package/src/endpoints/auth/PatchUserEndpoint.ts +25 -6
  8. package/src/endpoints/auth/SignupEndpoint.ts +2 -2
  9. package/src/endpoints/global/email/CreateEmailEndpoint.ts +120 -0
  10. package/src/endpoints/global/email/GetEmailEndpoint.ts +51 -0
  11. package/src/endpoints/global/email/PatchEmailEndpoint.ts +108 -0
  12. package/src/endpoints/global/events/GetEventsEndpoint.ts +223 -0
  13. package/src/endpoints/global/events/PatchEventsEndpoint.ts +319 -0
  14. package/src/endpoints/global/members/GetMembersEndpoint.ts +124 -48
  15. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +86 -109
  16. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +2 -1
  17. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -0
  18. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -2
  19. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +1 -1
  20. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +43 -25
  21. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +26 -7
  22. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +23 -22
  23. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +136 -123
  24. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +8 -8
  25. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +2 -1
  26. package/src/helpers/AdminPermissionChecker.ts +54 -3
  27. package/src/helpers/AuthenticatedStructures.ts +88 -23
  28. package/src/helpers/Context.ts +4 -0
  29. package/src/helpers/EmailResumer.ts +17 -0
  30. package/src/helpers/MemberUserSyncer.ts +221 -0
  31. package/src/seeds/1722256498-group-update-occupancy.ts +52 -0
@@ -2,9 +2,9 @@
2
2
  import { Decoder } from '@simonbackx/simple-encoding';
3
3
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
4
4
  import { SimpleError } from '@simonbackx/simple-errors';
5
- import { Member, MemberWithRegistrations, Platform } from '@stamhoofd/models';
6
- import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions, SQLOrderBy, SQLOrderByDirection, SQLScalar, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler, joinSQLQuery } from "@stamhoofd/sql";
7
- import { CountFilteredRequest, GroupStatus, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
5
+ import { Email, Member, MemberWithRegistrations, Platform } from '@stamhoofd/models';
6
+ import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions, SQLJSONValue, SQLOrderBy, SQLOrderByDirection, SQLScalar, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler, joinSQLQuery } from "@stamhoofd/sql";
7
+ import { CountFilteredRequest, EmailRecipientFilterType, GroupStatus, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter, mergeFilters } from '@stamhoofd/structures';
8
8
  import { DataValidator, Formatter } from '@stamhoofd/utility';
9
9
 
10
10
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -16,6 +16,44 @@ type Query = LimitedFilteredRequest;
16
16
  type Body = undefined;
17
17
  type ResponseBody = PaginatedResponse<MembersBlob, LimitedFilteredRequest>
18
18
 
19
+ Email.recipientLoaders.set(EmailRecipientFilterType.Members, {
20
+ fetch: async (query: LimitedFilteredRequest) => {
21
+ const result = await GetMembersEndpoint.buildData(query)
22
+
23
+ return new PaginatedResponse({
24
+ results: result.results.members.flatMap(m => m.getEmailRecipients(['member'])),
25
+ next: result.next
26
+ });
27
+ },
28
+
29
+ count: async (query: LimitedFilteredRequest) => {
30
+ query.filter = mergeFilters([query.filter, {
31
+ 'email': {
32
+ $neq: null
33
+ }
34
+ }])
35
+ const q = await GetMembersEndpoint.buildQuery(query)
36
+ return await q.count();
37
+ }
38
+ });
39
+ Email.recipientLoaders.set(EmailRecipientFilterType.MemberParents, {
40
+ fetch: async (query: LimitedFilteredRequest) => {
41
+ const result = await GetMembersEndpoint.buildData(query)
42
+
43
+ return new PaginatedResponse({
44
+ results: result.results.members.flatMap(m => m.getEmailRecipients(['parents'])),
45
+ next: result.next
46
+ });
47
+ },
48
+
49
+ count: async (query: LimitedFilteredRequest) => {
50
+ const q = await GetMembersEndpoint.buildQuery(query)
51
+ return await q.sum(
52
+ SQL.jsonLength(SQL.column('details'), '$.value.parents[*].email')
53
+ );
54
+ }
55
+ });
56
+
19
57
  const registrationFilterCompilers: SQLFilterDefinitions = {
20
58
  ...baseSQLFilterCompilers,
21
59
  "price": createSQLColumnFilterCompiler('price'),
@@ -131,6 +169,14 @@ const filterCompilers: SQLFilterDefinitions = {
131
169
  .from(
132
170
  SQL.table('member_responsibility_records')
133
171
  )
172
+ .join(
173
+ SQL.leftJoin(
174
+ SQL.table('groups')
175
+ ).where(
176
+ SQL.column('groups', 'id'),
177
+ SQL.column('member_responsibility_records', 'groupId')
178
+ )
179
+ )
134
180
  .where(
135
181
  SQL.column('memberId'),
136
182
  SQL.column('members', 'id'),
@@ -143,6 +189,11 @@ const filterCompilers: SQLFilterDefinitions = {
143
189
  "organizationId": createSQLColumnFilterCompiler(SQL.column('member_responsibility_records', 'organizationId')),
144
190
  "startDate": createSQLColumnFilterCompiler(SQL.column('member_responsibility_records', 'startDate')),
145
191
  "endDate": createSQLColumnFilterCompiler(SQL.column('member_responsibility_records', 'endDate')),
192
+ "group": createSQLFilterNamespace({
193
+ ...baseSQLFilterCompilers,
194
+ id: createSQLColumnFilterCompiler(SQL.column('groups', 'id')),
195
+ defaultAgeGroupId: createSQLColumnFilterCompiler(SQL.column('groups', 'defaultAgeGroupId')),
196
+ })
146
197
  }
147
198
  ),
148
199
 
@@ -154,6 +205,10 @@ const filterCompilers: SQLFilterDefinitions = {
154
205
  .where(
155
206
  SQL.column('memberId'),
156
207
  SQL.column('members', 'id'),
208
+ )
209
+ .where(
210
+ SQL.column('deletedAt'),
211
+ null,
157
212
  ),
158
213
  {
159
214
  ...baseSQLFilterCompilers,
@@ -297,7 +352,6 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
297
352
  if (tags != 'all' && tags.length === 0) {
298
353
  throw Context.auth.error()
299
354
  }
300
-
301
355
 
302
356
  if (tags !== 'all') {
303
357
  const platform = await Platform.getShared()
@@ -328,17 +382,35 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
328
382
 
329
383
  if (organization) {
330
384
  // Add organization scope filter
331
- scopeFilter = {
332
- registrations: {
333
- $elemMatch: {
334
- organizationId: organization.id,
335
- periodId: organization.periodId,
336
- registeredAt: {
337
- $neq: null
385
+ const groups = await Context.auth.getAccessibleGroups(organization.id)
386
+
387
+ if (groups === 'all') {
388
+ scopeFilter = {
389
+ registrations: {
390
+ $elemMatch: {
391
+ organizationId: organization.id,
392
+ registeredAt: {
393
+ $neq: null
394
+ }
338
395
  }
339
396
  }
340
- }
341
- };
397
+ };
398
+ } else {
399
+ scopeFilter = {
400
+ registrations: {
401
+ $elemMatch: {
402
+ organizationId: organization.id,
403
+ periodId: organization.periodId,
404
+ groupId: {
405
+ $in: groups
406
+ },
407
+ registeredAt: {
408
+ $neq: null
409
+ }
410
+ }
411
+ }
412
+ };
413
+ }
342
414
  }
343
415
 
344
416
  const query = SQL
@@ -408,29 +480,8 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
408
480
  return query
409
481
  }
410
482
 
411
- async handle(request: DecodedRequest<Params, Query, Body>) {
412
- await Context.setOptionalOrganizationScope();
413
- await Context.authenticate()
414
-
415
- const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
416
-
417
- if (request.query.limit > maxLimit) {
418
- throw new SimpleError({
419
- code: 'invalid_field',
420
- field: 'limit',
421
- message: 'Limit can not be more than ' + maxLimit
422
- })
423
- }
424
-
425
- if (request.query.limit < 1) {
426
- throw new SimpleError({
427
- code: 'invalid_field',
428
- field: 'limit',
429
- message: 'Limit can not be less than 1'
430
- })
431
- }
432
-
433
- const query = await GetMembersEndpoint.buildQuery(request.query)
483
+ static async buildData(requestQuery: LimitedFilteredRequest) {
484
+ const query = await GetMembersEndpoint.buildQuery(requestQuery)
434
485
  const data = await query.fetch()
435
486
 
436
487
  const memberIds = data.map((r) => {
@@ -452,29 +503,54 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
452
503
 
453
504
  let next: LimitedFilteredRequest|undefined;
454
505
 
455
- if (memberIds.length >= request.query.limit) {
506
+ if (memberIds.length >= requestQuery.limit) {
456
507
  const lastObject = members[members.length - 1];
457
- const nextFilter = getSortFilter(lastObject, sorters, request.query.sort);
508
+ const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
458
509
 
459
510
  next = new LimitedFilteredRequest({
460
- filter: request.query.filter,
511
+ filter: requestQuery.filter,
461
512
  pageFilter: nextFilter,
462
- sort: request.query.sort,
463
- limit: request.query.limit,
464
- search: request.query.search
513
+ sort: requestQuery.sort,
514
+ limit: requestQuery.limit,
515
+ search: requestQuery.search
465
516
  })
466
517
 
467
- if (JSON.stringify(nextFilter) === JSON.stringify(request.query.pageFilter)) {
468
- console.error('Found infinite loading loop for', request.query);
518
+ if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
519
+ console.error('Found infinite loading loop for', requestQuery);
469
520
  next = undefined;
470
521
  }
471
522
  }
472
523
 
473
- return new Response(
474
- new PaginatedResponse<MembersBlob, LimitedFilteredRequest>({
475
- results: await AuthenticatedStructures.membersBlob(members),
476
- next
524
+ return new PaginatedResponse<MembersBlob, LimitedFilteredRequest>({
525
+ results: await AuthenticatedStructures.membersBlob(members),
526
+ next
527
+ });
528
+ }
529
+
530
+ async handle(request: DecodedRequest<Params, Query, Body>) {
531
+ await Context.setOptionalOrganizationScope();
532
+ await Context.authenticate()
533
+
534
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
535
+
536
+ if (request.query.limit > maxLimit) {
537
+ throw new SimpleError({
538
+ code: 'invalid_field',
539
+ field: 'limit',
540
+ message: 'Limit can not be more than ' + maxLimit
477
541
  })
542
+ }
543
+
544
+ if (request.query.limit < 1) {
545
+ throw new SimpleError({
546
+ code: 'invalid_field',
547
+ field: 'limit',
548
+ message: 'Limit can not be less than 1'
549
+ })
550
+ }
551
+
552
+ return new Response(
553
+ await GetMembersEndpoint.buildData(request.query)
478
554
  );
479
555
  }
480
556
  }
@@ -2,12 +2,13 @@ import { OneToManyRelation } from '@simonbackx/simple-database';
2
2
  import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
3
3
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
4
  import { SimpleError } from "@simonbackx/simple-errors";
5
- import { BalanceItem, MemberPlatformMembership, BalanceItemPayment, Document, Group, Member, MemberFactory, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Payment, Platform, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
6
- import { BalanceItemStatus, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, PaymentMethod, PaymentStatus, PermissionLevel, Registration as RegistrationStruct, User as UserStruct } from "@stamhoofd/structures";
5
+ import { BalanceItem, BalanceItemPayment, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Payment, Platform, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
6
+ import { BalanceItemStatus, MemberWithRegistrationsBlob, MembersBlob, PaymentMethod, PaymentStatus, PermissionLevel, Registration as RegistrationStruct, User as UserStruct } from "@stamhoofd/structures";
7
7
  import { Formatter } from '@stamhoofd/utility';
8
8
 
9
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
10
  import { Context } from '../../../helpers/Context';
11
+ import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
11
12
 
12
13
  type Params = Record<string, never>;
13
14
  type Query = undefined;
@@ -188,13 +189,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
188
189
  updateGroups.set(group.id, group)
189
190
  }
190
191
 
191
- // Add users if they don't exist (only placeholders allowed)
192
- for (const placeholder of struct.users) {
193
- await PatchOrganizationMembersEndpoint.linkUser(placeholder, member)
194
- }
195
-
196
192
  // Auto link users based on data
197
- await PatchOrganizationMembersEndpoint.updateManagers(member)
193
+ await MemberUserSyncer.onChangeMember(member)
198
194
  }
199
195
 
200
196
  // Loop all members one by one
@@ -428,7 +424,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
428
424
  const platform = await Platform.getShared()
429
425
  const responsibility = platform.config.responsibilities.find(r => r.id === patchResponsibility.responsibilityId)
430
426
 
431
- if (responsibility && !responsibility.assignableByOrganizations && !Context.auth.hasPlatformFullAccess()) {
427
+ if (responsibility && !responsibility.organizationBased && !Context.auth.hasPlatformFullAccess()) {
432
428
  throw Context.auth.error("Je hebt niet voldoende rechten om deze functie aan te passen")
433
429
  }
434
430
 
@@ -458,38 +454,88 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
458
454
  await responsibilityRecord.save()
459
455
  }
460
456
 
461
- // Update responsibilities
457
+ // Create responsibilities
462
458
  for (const {put} of patch.responsibilities.getPuts()) {
463
459
  if (!Context.auth.hasPlatformFullAccess() && !(organization && await Context.auth.hasFullAccess(organization.id))) {
464
460
  throw Context.auth.error("Je hebt niet voldoende rechten om functies van leden aan te passen")
465
461
  }
466
462
 
467
463
  const platform = await Platform.getShared()
468
- const responsibility = platform.config.responsibilities.find(r => r.id === put.responsibilityId)
464
+ const platformResponsibility = platform.config.responsibilities.find(r => r.id === put.responsibilityId)
465
+ const org = organization ?? (put.organizationId ? await Organization.getByID(put.organizationId) : null)
469
466
 
470
- if (!responsibility || (!responsibility.assignableByOrganizations && !Context.auth.hasPlatformFullAccess())) {
471
- throw Context.auth.error("Je hebt niet voldoende rechten om deze functie toe te kennen")
467
+ if (!org && put.organizationId) {
468
+ throw new SimpleError({
469
+ code: "invalid_field",
470
+ message: "Invalid organization",
471
+ human: "Deze vereniging bestaat niet",
472
+ field: "organizationId"
473
+ })
474
+ }
475
+ const responsibility = platformResponsibility ?? org?.privateMeta.responsibilities.find(r => r.id === put.responsibilityId)
476
+
477
+ if (!responsibility) {
478
+ throw new SimpleError({
479
+ code: "invalid_field",
480
+ message: "Invalid responsibility",
481
+ human: "Deze functie bestaat niet",
482
+ field: "responsibilityId"
483
+ })
484
+ }
485
+
486
+ if (!org && responsibility.organizationBased) {
487
+ throw new SimpleError({
488
+ code: "invalid_field",
489
+ message: "Invalid organization",
490
+ human: "Deze functie kan niet worden toegewezen aan deze vereniging",
491
+ field: "organizationId"
492
+ })
472
493
  }
473
494
 
474
495
  const model = new MemberResponsibilityRecord()
475
496
  model.memberId = member.id
476
497
  model.responsibilityId = responsibility.id
498
+ model.organizationId = org?.id ?? null
477
499
 
478
- if (responsibility.assignableByOrganizations) {
479
- if (organization) {
480
- model.organizationId = organization.id
481
- } else {
482
- if (!put.organizationId) {
483
- if (!Context.auth.hasPlatformFullAccess()) {
484
- throw Context.auth.error("Je hebt niet voldoende rechten om deze functie toe te kennen")
485
- }
486
- } else if (!await Context.auth.hasFullAccess(put.organizationId)) {
487
- throw Context.auth.error("Je hebt niet voldoende rechten om functies van leden toe te kennen voor deze vereniging")
488
- }
489
- model.organizationId = put.organizationId
500
+ if (responsibility.organizationTagIds !== null && (!org || !org.meta.matchTags(responsibility.organizationTagIds))) {
501
+ throw new SimpleError({
502
+ code: "invalid_field",
503
+ message: "Invalid organization",
504
+ human: "Deze functie is niet beschikbaar voor deze vereniging",
505
+ field: "organizationId"
506
+ })
507
+ }
508
+
509
+ if (responsibility.defaultAgeGroupIds !== null) {
510
+ if (!put.groupId) {
511
+ throw new SimpleError({
512
+ code: "invalid_field",
513
+ message: "Missing groupId",
514
+ human: "Kies een leeftijdsgroep waarvoor je deze functie wilt toekennen",
515
+ field: "groupId"
516
+ })
490
517
  }
491
- } else {
492
- model.organizationId = null
518
+
519
+ const group = await Group.getByID(put.groupId)
520
+ if (!group || group.organizationId !== model.organizationId) {
521
+ throw new SimpleError({
522
+ code: "invalid_field",
523
+ message: "Invalid groupId",
524
+ human: "Deze leeftijdsgroep bestaat niet",
525
+ field: "groupId"
526
+ })
527
+ }
528
+
529
+ if (group.defaultAgeGroupId === null || !responsibility.defaultAgeGroupIds.includes(group.defaultAgeGroupId)) {
530
+ throw new SimpleError({
531
+ code: "invalid_field",
532
+ message: "Invalid groupId",
533
+ human: "Deze leeftijdsgroep komt niet in aanmerking voor deze functie",
534
+ field: "groupId"
535
+ })
536
+ }
537
+
538
+ model.groupId = group.id
493
539
  }
494
540
 
495
541
  // Allow patching begin and end date
@@ -499,25 +545,17 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
499
545
  throw Context.auth.error("Je kan de startdatum van een functie niet in de toekomst zetten")
500
546
  }
501
547
 
548
+ if (put.endDate && put.endDate > new Date(Date.now() + 60*1000)) {
549
+ throw Context.auth.error("Je kan de einddatum van een functie niet in de toekomst zetten - kijk indien nodig je systeemtijd na")
550
+ }
551
+
502
552
  model.startDate = put.startDate
503
553
 
504
554
  await model.save()
505
555
  }
506
556
 
507
- // Link users
508
- for (const placeholder of patch.users.getPuts()) {
509
- await PatchOrganizationMembersEndpoint.linkUser(placeholder.put, member)
510
- }
511
-
512
- // Unlink users
513
- for (const userId of patch.users.getDeletes()) {
514
- await PatchOrganizationMembersEndpoint.unlinkUser(userId, member)
515
- }
516
-
517
557
  // Auto link users based on data
518
- if (patch.users.changes.length || patch.details) {
519
- await PatchOrganizationMembersEndpoint.updateManagers(member)
520
- }
558
+ await MemberUserSyncer.onChangeMember(member)
521
559
 
522
560
  // Add platform memberships
523
561
  for (const {put} of patch.platformMemberships.getPuts()) {
@@ -571,6 +609,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
571
609
 
572
610
  await membership.calculatePrice()
573
611
  await membership.save()
612
+
613
+ updateMembershipMemberIds.add(member.id)
574
614
  }
575
615
 
576
616
  // Delete platform memberships
@@ -599,7 +639,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
599
639
  })
600
640
  }
601
641
 
602
- if (membership.invoiceId || membership.invoiceItemDetailId) {
642
+ if (!membership.canDelete()) {
603
643
  throw new SimpleError({
604
644
  code: "invalid_field",
605
645
  message: "Invalid invoice",
@@ -607,9 +647,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
607
647
  })
608
648
  }
609
649
 
610
- await membership.delete()
650
+ membership.deletedAt = new Date()
651
+ await membership.save()
652
+ updateMembershipMemberIds.add(member.id)
611
653
  }
612
654
 
655
+
613
656
  if (!members.find(m => m.id === member.id)) {
614
657
  members.push(member)
615
658
  }
@@ -622,6 +665,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
622
665
  throw Context.auth.error("Je hebt niet voldoende rechten om dit lid te verwijderen")
623
666
  }
624
667
 
668
+ await MemberUserSyncer.onDeleteMember(member)
625
669
  await User.deleteForDeletedMember(member.id)
626
670
  await BalanceItem.deleteForDeletedMember(member.id)
627
671
  await member.delete()
@@ -804,71 +848,4 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
804
848
  await registration.save()
805
849
  }
806
850
  }
807
-
808
- static async updateManagers(member: MemberWithRegistrations) {
809
- // Check accounts
810
- const managers = member.details.getManagerEmails()
811
-
812
- for(const email of managers) {
813
- const u = member.users.find(u => u.email.toLocaleLowerCase() === email.toLocaleLowerCase())
814
- if (!u) {
815
- console.log("Linking user "+email+" to member "+member.id)
816
- await PatchOrganizationMembersEndpoint.linkUser(UserStruct.create({
817
- firstName: member.details.parents.find(p => p.email === email)?.firstName,
818
- lastName: member.details.parents.find(p => p.email === email)?.lastName,
819
- email,
820
- }), member)
821
- }
822
- }
823
-
824
- // Delete accounts that should no longer have access
825
- for (const u of member.users) {
826
- if (!u.hasAccount()) {
827
- // And not in managers list (case insensitive)
828
- if (!managers.find(m => m.toLocaleLowerCase() === u.email.toLocaleLowerCase())) {
829
- console.log("Unlinking user "+u.email+" from member "+member.id)
830
- await PatchOrganizationMembersEndpoint.unlinkUser(u.id, member)
831
- }
832
- }
833
- }
834
- }
835
-
836
- static async linkUser(user: UserStruct, member: MemberWithRegistrations) {
837
- const email = user.email
838
- let u = await User.getForAuthentication(member.organizationId, email, {allowWithoutAccount: true});
839
- if (u) {
840
- console.log("Giving an existing user access to a member: "+u.id)
841
- } else {
842
- u = new User()
843
- u.organizationId = member.organizationId
844
- u.email = email
845
- u.firstName = user.firstName
846
- u.lastName = user.lastName
847
- await u.save()
848
-
849
- console.log("Created new (placeholder) user that has access to a member: "+u.id)
850
- }
851
-
852
- await Member.users.reverse("members").link(u, [member])
853
-
854
- // Update model relation to correct response
855
- member.users.push(u)
856
- }
857
-
858
- static async unlinkUser(userId: string, member: MemberWithRegistrations) {
859
- console.log("Removing access for "+ userId +" to member "+member.id)
860
- const existingIndex = member.users.findIndex(u => u.id === userId)
861
- if (existingIndex === -1) {
862
- throw new SimpleError({
863
- code: "user_not_found",
864
- message: "Unlinking a user that doesn't exists anymore",
865
- human: "Je probeert de toegang van een account tot een lid te verwijderen, maar dat account bestaat niet (meer)"
866
- })
867
- }
868
- const existing = member.users[existingIndex]
869
- await Member.users.reverse("members").unlink(existing, member)
870
-
871
- // Update model relation to correct response
872
- member.users.splice(existingIndex, 1)
873
- }
874
851
  }
@@ -3,6 +3,7 @@ import { User } from '@stamhoofd/models';
3
3
  import { User as UserStruct } from "@stamhoofd/structures";
4
4
 
5
5
  import { Context } from "../../../helpers/Context";
6
+ import { AuthenticatedStructures } from "../../../helpers/AuthenticatedStructures";
6
7
  type Params = Record<string, never>;
7
8
  type Query = undefined;
8
9
  type Body = undefined
@@ -38,7 +39,7 @@ export class GetPlatformAdminsEndpoint extends Endpoint<Params, Query, Body, Res
38
39
  admins = admins.filter(a => !!a.permissions?.globalPermissions)
39
40
 
40
41
  return new Response(
41
- admins.map(a => UserStruct.create({...a, hasAccount: a.hasAccount()}))
42
+ await AuthenticatedStructures.usersWithMembers(admins)
42
43
  );
43
44
  }
44
45
  }
@@ -47,6 +47,15 @@ export class PatchPlatformEndpoint extends Endpoint<Params, Query, Body, Respons
47
47
  // Update roles
48
48
  platform.privateConfig.roles = patchObject(platform.privateConfig.roles, request.body.privateConfig.roles)
49
49
  }
50
+
51
+ if (request.body.privateConfig.emails) {
52
+ if (!Context.auth.hasPlatformFullAccess()) {
53
+ throw Context.auth.error()
54
+ }
55
+
56
+ // Update roles
57
+ platform.privateConfig.emails = patchObject(platform.privateConfig.emails, request.body.privateConfig.emails)
58
+ }
50
59
  }
51
60
 
52
61
  if (request.body.config) {
@@ -7,6 +7,7 @@ import { MemberWithRegistrationsBlob, MembersBlob } from "@stamhoofd/structures"
7
7
  import { Context } from '../../../helpers/Context';
8
8
  import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint';
9
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
+ import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
10
11
  type Params = Record<string, never>;
11
12
  type Query = undefined;
12
13
  type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>
@@ -83,7 +84,7 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
83
84
  const updatedMember = members.find(m => m.id === member.id);
84
85
  if (updatedMember) {
85
86
  // Make sure we also give access to other parents
86
- await PatchOrganizationMembersEndpoint.updateManagers(updatedMember)
87
+ await MemberUserSyncer.onChangeMember(updatedMember)
87
88
  await Document.updateForMember(updatedMember.id)
88
89
  }
89
90
  }
@@ -121,7 +122,7 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
121
122
  })
122
123
  }
123
124
  await member.save();
124
- await PatchOrganizationMembersEndpoint.updateManagers(member)
125
+ await MemberUserSyncer.onChangeMember(member)
125
126
 
126
127
  // Update documents
127
128
  await Document.updateForMember(member.id)
@@ -55,7 +55,7 @@ export class EmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
55
55
  return [false];
56
56
  }
57
57
 
58
- const params = Endpoint.parseParameters(request.url, "/email", {});
58
+ const params = Endpoint.parseParameters(request.url, "/email/legacy", {});
59
59
 
60
60
  if (params) {
61
61
  return [true, params as Params];