@stamhoofd/backend 2.3.1 → 2.5.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 (39) hide show
  1. package/index.ts +3 -0
  2. package/package.json +4 -4
  3. package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +1 -1
  4. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +63 -2
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +6 -3
  6. package/src/endpoints/auth/GetOtherUserEndpoint.ts +41 -0
  7. package/src/endpoints/auth/GetUserEndpoint.ts +6 -28
  8. package/src/endpoints/auth/PatchUserEndpoint.ts +25 -6
  9. package/src/endpoints/auth/SignupEndpoint.ts +2 -2
  10. package/src/endpoints/global/email/CreateEmailEndpoint.ts +120 -0
  11. package/src/endpoints/global/email/GetEmailEndpoint.ts +51 -0
  12. package/src/endpoints/global/email/PatchEmailEndpoint.ts +108 -0
  13. package/src/endpoints/global/events/GetEventsEndpoint.ts +223 -0
  14. package/src/endpoints/global/events/PatchEventsEndpoint.ts +319 -0
  15. package/src/endpoints/global/members/GetMembersEndpoint.ts +124 -48
  16. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +107 -117
  17. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +3 -2
  18. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -1
  19. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +2 -1
  20. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +2 -1
  21. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -0
  22. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -2
  23. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +345 -176
  24. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +5 -5
  25. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +1 -1
  26. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +43 -25
  27. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +26 -7
  28. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +23 -22
  29. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -121
  30. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +8 -8
  31. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +2 -1
  32. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +2 -1
  33. package/src/helpers/AdminPermissionChecker.ts +60 -3
  34. package/src/helpers/AuthenticatedStructures.ts +164 -37
  35. package/src/helpers/Context.ts +4 -0
  36. package/src/helpers/EmailResumer.ts +17 -0
  37. package/src/helpers/MemberUserSyncer.ts +221 -0
  38. package/src/seeds/1722256498-group-update-occupancy.ts +52 -0
  39. package/src/seeds/1722344160-update-membership.ts +57 -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;
@@ -105,16 +106,29 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
105
106
  duplicate.details.merge(member.details)
106
107
  member = duplicate
107
108
 
108
- // Only save after checking permissions
109
+ // You need write permissions, because a user can potentially earn write permissions on a member
110
+ // by registering it
111
+ if (!await Context.auth.canAccessMember(duplicate, PermissionLevel.Write)) {
112
+ throw new SimpleError({
113
+ code: "known_member_missing_rights",
114
+ message: "Creating known member without sufficient access rights",
115
+ human: "Dit lid is al bekend in het systeem, maar je hebt er geen toegang tot. Vraag iemand met de juiste toegangsrechten om dit lid voor jou toe te voegen, of vraag het lid om zelf in te schrijven via het ledenportaal.",
116
+ statusCode: 400
117
+ })
118
+ }
109
119
  }
110
120
 
111
121
  if (struct.registrations.length === 0) {
112
- throw new SimpleError({
113
- code: "missing_group",
114
- message: "Missing group",
115
- human: "Schrijf een nieuw lid altijd in voor minstens één groep",
116
- statusCode: 400
117
- })
122
+ // We risk creating a new member without being able to access it manually afterwards
123
+
124
+ if ((organization && !await Context.auth.hasFullAccess(organization.id)) || (!organization && !Context.auth.hasPlatformFullAccess())) {
125
+ throw new SimpleError({
126
+ code: "missing_group",
127
+ message: "Missing group",
128
+ human: "Je moet hoofdbeheerder zijn om een lid toe te voegen zonder inschrijving in het systeem",
129
+ statusCode: 400
130
+ })
131
+ }
118
132
  }
119
133
 
120
134
  // Throw early
@@ -188,13 +202,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
188
202
  updateGroups.set(group.id, group)
189
203
  }
190
204
 
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
205
  // Auto link users based on data
197
- await PatchOrganizationMembersEndpoint.updateManagers(member)
206
+ await MemberUserSyncer.onChangeMember(member)
198
207
  }
199
208
 
200
209
  // Loop all members one by one
@@ -227,7 +236,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
227
236
  // Update registrations
228
237
  for (const patchRegistration of patch.registrations.getPatches()) {
229
238
  const registration = member.registrations.find(r => r.id === patchRegistration.id)
230
- if (!registration || registration.memberId != member.id) {
239
+ if (!registration || registration.memberId != member.id || (!await Context.auth.canAccessRegistration(registration, PermissionLevel.Write))) {
231
240
  throw new SimpleError({
232
241
  code: "permission_denied",
233
242
  message: "You don't have permissions to access this endpoint",
@@ -428,7 +437,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
428
437
  const platform = await Platform.getShared()
429
438
  const responsibility = platform.config.responsibilities.find(r => r.id === patchResponsibility.responsibilityId)
430
439
 
431
- if (responsibility && !responsibility.assignableByOrganizations && !Context.auth.hasPlatformFullAccess()) {
440
+ if (responsibility && !responsibility.organizationBased && !Context.auth.hasPlatformFullAccess()) {
432
441
  throw Context.auth.error("Je hebt niet voldoende rechten om deze functie aan te passen")
433
442
  }
434
443
 
@@ -458,38 +467,88 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
458
467
  await responsibilityRecord.save()
459
468
  }
460
469
 
461
- // Update responsibilities
470
+ // Create responsibilities
462
471
  for (const {put} of patch.responsibilities.getPuts()) {
463
472
  if (!Context.auth.hasPlatformFullAccess() && !(organization && await Context.auth.hasFullAccess(organization.id))) {
464
473
  throw Context.auth.error("Je hebt niet voldoende rechten om functies van leden aan te passen")
465
474
  }
466
475
 
467
476
  const platform = await Platform.getShared()
468
- const responsibility = platform.config.responsibilities.find(r => r.id === put.responsibilityId)
477
+ const platformResponsibility = platform.config.responsibilities.find(r => r.id === put.responsibilityId)
478
+ const org = organization ?? (put.organizationId ? await Organization.getByID(put.organizationId) : null)
479
+
480
+ if (!org && put.organizationId) {
481
+ throw new SimpleError({
482
+ code: "invalid_field",
483
+ message: "Invalid organization",
484
+ human: "Deze vereniging bestaat niet",
485
+ field: "organizationId"
486
+ })
487
+ }
488
+ const responsibility = platformResponsibility ?? org?.privateMeta.responsibilities.find(r => r.id === put.responsibilityId)
469
489
 
470
- if (!responsibility || (!responsibility.assignableByOrganizations && !Context.auth.hasPlatformFullAccess())) {
471
- throw Context.auth.error("Je hebt niet voldoende rechten om deze functie toe te kennen")
490
+ if (!responsibility) {
491
+ throw new SimpleError({
492
+ code: "invalid_field",
493
+ message: "Invalid responsibility",
494
+ human: "Deze functie bestaat niet",
495
+ field: "responsibilityId"
496
+ })
497
+ }
498
+
499
+ if (!org && responsibility.organizationBased) {
500
+ throw new SimpleError({
501
+ code: "invalid_field",
502
+ message: "Invalid organization",
503
+ human: "Deze functie kan niet worden toegewezen aan deze vereniging",
504
+ field: "organizationId"
505
+ })
472
506
  }
473
507
 
474
508
  const model = new MemberResponsibilityRecord()
475
509
  model.memberId = member.id
476
510
  model.responsibilityId = responsibility.id
511
+ model.organizationId = org?.id ?? null
477
512
 
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
513
+ if (responsibility.organizationTagIds !== null && (!org || !org.meta.matchTags(responsibility.organizationTagIds))) {
514
+ throw new SimpleError({
515
+ code: "invalid_field",
516
+ message: "Invalid organization",
517
+ human: "Deze functie is niet beschikbaar voor deze vereniging",
518
+ field: "organizationId"
519
+ })
520
+ }
521
+
522
+ if (responsibility.defaultAgeGroupIds !== null) {
523
+ if (!put.groupId) {
524
+ throw new SimpleError({
525
+ code: "invalid_field",
526
+ message: "Missing groupId",
527
+ human: "Kies een leeftijdsgroep waarvoor je deze functie wilt toekennen",
528
+ field: "groupId"
529
+ })
490
530
  }
491
- } else {
492
- model.organizationId = null
531
+
532
+ const group = await Group.getByID(put.groupId)
533
+ if (!group || group.organizationId !== model.organizationId) {
534
+ throw new SimpleError({
535
+ code: "invalid_field",
536
+ message: "Invalid groupId",
537
+ human: "Deze leeftijdsgroep bestaat niet",
538
+ field: "groupId"
539
+ })
540
+ }
541
+
542
+ if (group.defaultAgeGroupId === null || !responsibility.defaultAgeGroupIds.includes(group.defaultAgeGroupId)) {
543
+ throw new SimpleError({
544
+ code: "invalid_field",
545
+ message: "Invalid groupId",
546
+ human: "Deze leeftijdsgroep komt niet in aanmerking voor deze functie",
547
+ field: "groupId"
548
+ })
549
+ }
550
+
551
+ model.groupId = group.id
493
552
  }
494
553
 
495
554
  // Allow patching begin and end date
@@ -499,25 +558,17 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
499
558
  throw Context.auth.error("Je kan de startdatum van een functie niet in de toekomst zetten")
500
559
  }
501
560
 
561
+ if (put.endDate && put.endDate > new Date(Date.now() + 60*1000)) {
562
+ throw Context.auth.error("Je kan de einddatum van een functie niet in de toekomst zetten - kijk indien nodig je systeemtijd na")
563
+ }
564
+
502
565
  model.startDate = put.startDate
503
566
 
504
567
  await model.save()
505
568
  }
506
569
 
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
570
  // Auto link users based on data
518
- if (patch.users.changes.length || patch.details) {
519
- await PatchOrganizationMembersEndpoint.updateManagers(member)
520
- }
571
+ await MemberUserSyncer.onChangeMember(member)
521
572
 
522
573
  // Add platform memberships
523
574
  for (const {put} of patch.platformMemberships.getPuts()) {
@@ -571,6 +622,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
571
622
 
572
623
  await membership.calculatePrice()
573
624
  await membership.save()
625
+
626
+ updateMembershipMemberIds.add(member.id)
574
627
  }
575
628
 
576
629
  // Delete platform memberships
@@ -599,7 +652,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
599
652
  })
600
653
  }
601
654
 
602
- if (membership.invoiceId || membership.invoiceItemDetailId) {
655
+ if (!membership.canDelete()) {
603
656
  throw new SimpleError({
604
657
  code: "invalid_field",
605
658
  message: "Invalid invoice",
@@ -607,9 +660,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
607
660
  })
608
661
  }
609
662
 
610
- await membership.delete()
663
+ membership.deletedAt = new Date()
664
+ await membership.save()
665
+ updateMembershipMemberIds.add(member.id)
611
666
  }
612
667
 
668
+
613
669
  if (!members.find(m => m.id === member.id)) {
614
670
  members.push(member)
615
671
  }
@@ -622,6 +678,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
622
678
  throw Context.auth.error("Je hebt niet voldoende rechten om dit lid te verwijderen")
623
679
  }
624
680
 
681
+ await MemberUserSyncer.onDeleteMember(member)
625
682
  await User.deleteForDeletedMember(member.id)
626
683
  await BalanceItem.deleteForDeletedMember(member.id)
627
684
  await member.delete()
@@ -804,71 +861,4 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
804
861
  await registration.save()
805
862
  }
806
863
  }
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
864
  }
@@ -4,6 +4,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Organization } from '@stamhoofd/models';
5
5
  import { Organization as OrganizationStruct } from "@stamhoofd/structures";
6
6
  import { GoogleTranslateHelper } from "@stamhoofd/utility";
7
+ import { AuthenticatedStructures } from "../../../helpers/AuthenticatedStructures";
7
8
  type Params = Record<string, never>;
8
9
 
9
10
  class Query extends AutoEncoder {
@@ -60,7 +61,7 @@ export class GetOrganizationFromDomainEndpoint extends Endpoint<Params, Query, B
60
61
  statusCode: 404
61
62
  })
62
63
  }
63
- return new Response(await organization.getStructure());
64
+ return new Response(await AuthenticatedStructures.organization(organization));
64
65
  }
65
66
  }
66
67
 
@@ -75,6 +76,6 @@ export class GetOrganizationFromDomainEndpoint extends Endpoint<Params, Query, B
75
76
  statusCode: 404
76
77
  })
77
78
  }
78
- return new Response(await organization.getStructure());
79
+ return new Response(await AuthenticatedStructures.organization(organization));
79
80
  }
80
81
  }
@@ -4,6 +4,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Organization } from '@stamhoofd/models';
5
5
  import { Organization as OrganizationStruct } from "@stamhoofd/structures";
6
6
  import { GoogleTranslateHelper } from "@stamhoofd/utility";
7
+ import { AuthenticatedStructures } from "../../../helpers/AuthenticatedStructures";
7
8
  type Params = Record<string, never>;
8
9
 
9
10
  class Query extends AutoEncoder {
@@ -44,6 +45,6 @@ export class GetOrganizationFromUriEndpoint extends Endpoint<Params, Query, Body
44
45
  statusCode: 404
45
46
  })
46
47
  }
47
- return new Response(await organization.getStructure());
48
+ return new Response(await AuthenticatedStructures.organization(organization));
48
49
  }
49
50
  }
@@ -2,6 +2,7 @@ import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-e
2
2
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
3
  import { Organization } from "@stamhoofd/models";
4
4
  import { Organization as OrganizationStruct,OrganizationSimple } from "@stamhoofd/structures";
5
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
5
6
 
6
7
  type Params = Record<string, never>;
7
8
 
@@ -57,6 +58,6 @@ export class SearchOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
57
58
  if (request.request.getVersion() < 169) {
58
59
  return new Response(organizations.map(o => OrganizationSimple.create(o)));
59
60
  }
60
- return new Response(await Promise.all(organizations.map(o => o.getStructure())));
61
+ return new Response(await Promise.all(organizations.map(o => AuthenticatedStructures.organization(o))));
61
62
  }
62
63
  }
@@ -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
  }