@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
@@ -1,10 +1,11 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
- import { GroupPrivateSettings, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from "@stamhoofd/structures";
2
+ import { Group as GroupStruct, GroupPrivateSettings, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version, GroupType } from "@stamhoofd/structures";
3
3
 
4
4
  import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from "@simonbackx/simple-encoding";
5
5
  import { Context } from "../../../../helpers/Context";
6
- import { Group, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from "@stamhoofd/models";
6
+ import { Group, Member, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from "@stamhoofd/models";
7
7
  import { SimpleError } from "@simonbackx/simple-errors";
8
+ import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
8
9
 
9
10
  type Params = Record<string, never>;
10
11
  type Query = undefined;
@@ -15,7 +16,7 @@ type ResponseBody = OrganizationRegistrationPeriodStruct[]
15
16
  * One endpoint to create, patch and delete members and their registrations and payments
16
17
  */
17
18
 
18
- export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
20
  bodyDecoder = new PatchableArrayDecoder(OrganizationRegistrationPeriodStruct as Decoder<OrganizationRegistrationPeriodStruct>, OrganizationRegistrationPeriodStruct.patchType() as Decoder<AutoEncoderPatchType<OrganizationRegistrationPeriodStruct>>, StringDecoder)
20
21
 
21
22
  protected doesMatch(request: Request): [true, Params] | [false] {
@@ -33,32 +34,13 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
33
34
 
34
35
  async handle(request: DecodedRequest<Params, Query, Body>) {
35
36
  const organization = await Context.setOrganizationScope();
36
- const {user} = await Context.authenticate()
37
+ await Context.authenticate()
37
38
 
38
39
  if (!await Context.auth.hasFullAccess(organization.id)) {
39
40
  throw Context.auth.error()
40
41
  }
41
42
 
42
- const platform = await Platform.getShared()
43
-
44
- function validateDefaultGroupId(id: string|null): string|null {
45
- if (id === null) {
46
- return id;
47
- }
48
-
49
- if (platform.config.defaultAgeGroups.find(g => g.id === id)) {
50
- return id;
51
- }
52
-
53
- throw new SimpleError({
54
- code: "invalid_default_age_group",
55
- message: "Invalid default age group",
56
- human: "De standaard leeftijdsgroep is ongeldig",
57
- statusCode: 400
58
- })
59
- }
60
-
61
- const structs: OrganizationRegistrationPeriodStruct[] = [];
43
+ const periods: OrganizationRegistrationPeriod[] = [];
62
44
 
63
45
  for (const {put} of request.body.getPuts()) {
64
46
  if (!await Context.auth.hasFullAccess(organization.id)) {
@@ -82,23 +64,14 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
82
64
  await organizationPeriod.save();
83
65
 
84
66
  for (const struct of put.groups) {
85
- const model = new Group()
86
- model.id = struct.id
87
- model.organizationId = organization.id
88
- model.defaultAgeGroupId = validateDefaultGroupId(struct.defaultAgeGroupId)
89
- model.periodId = organizationPeriod.periodId
90
- model.settings = struct.settings
91
- model.privateSettings = struct.privateSettings ?? GroupPrivateSettings.create({})
92
- model.status = struct.status
93
- await model.updateOccupancy()
94
- await model.save();
67
+ await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(struct, organization.id, organizationPeriod.periodId)
95
68
  }
96
69
  const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
97
70
 
98
71
  // Delete unreachable categories first
99
72
  await organizationPeriod.cleanCategories(groups);
100
73
  await Group.deleteUnreachable(organization.id, organizationPeriod, groups)
101
- structs.push(organizationPeriod.getStructure(period, groups));
74
+ periods.push(organizationPeriod);
102
75
  }
103
76
 
104
77
  for (const patch of request.body.getPatches()) {
@@ -152,122 +125,238 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
152
125
  const deleteGroups = patch.groups.getDeletes()
153
126
  if (deleteGroups.length > 0) {
154
127
  for (const id of deleteGroups) {
155
- const model = await Group.getByID(id)
156
- if (!model || !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
157
- throw Context.auth.error('Je hebt geen toegangsrechten om deze groep te verwijderen')
158
- }
159
-
160
- model.deletedAt = new Date()
161
- await model.save()
128
+ await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(id)
162
129
  deleteUnreachable = true
163
130
  }
164
131
  }
165
132
 
166
133
  for (const groupPut of patch.groups.getPuts()) {
167
- if (!await Context.auth.hasFullAccess(organization.id) && !allowedIds.includes(groupPut.put.id)) {
168
- throw Context.auth.error('Je hebt geen toegangsrechten om groepen toe te voegen')
169
- }
134
+ await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(groupPut.put, organization.id, organizationPeriod.periodId, {allowedIds})
135
+ }
170
136
 
171
- const struct = groupPut.put
172
- const model = new Group()
173
- model.id = struct.id
174
- model.organizationId = organization.id
175
- model.defaultAgeGroupId = validateDefaultGroupId(struct.defaultAgeGroupId)
176
- model.periodId = organizationPeriod.periodId
177
- model.settings = struct.settings
178
- model.privateSettings = struct.privateSettings ?? GroupPrivateSettings.create({})
179
- model.status = struct.status
180
-
181
- if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
182
- // Create a temporary permission role for this user
183
- const organizationPermissions = user.permissions?.organizationPermissions?.get(organization.id)
184
- if (!organizationPermissions) {
185
- throw new Error('Unexpected missing permissions')
186
- }
187
- const resourcePermissions = ResourcePermissions.create({
188
- resourceName: model.settings.name,
189
- level: PermissionLevel.Full
190
- })
191
- const patch = resourcePermissions.createInsertPatch(PermissionsResourceType.Groups, model.id, organizationPermissions)
192
- user.permissions!.organizationPermissions.set(organization.id, organizationPermissions.patch(patch))
193
- console.log('Automatically granted author full permissions to resource', 'group', model.id, 'user', user.id, 'patch', patch.encode({version: Version}))
194
- await user.save()
195
- }
137
+ for (const struct of patch.groups.getPatches()) {
138
+ await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(struct)
139
+ }
196
140
 
197
- // Check if current user has permissions to this new group -> else fail with error
198
- if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
199
- throw new SimpleError({
200
- code: "missing_permissions",
201
- message: "You cannot restrict your own permissions",
202
- human: "Je kan geen inschrijvingsgroep maken zonder dat je zelf volledige toegang hebt tot de nieuwe groep"
203
- })
204
- continue;
205
- }
206
141
 
207
- await model.updateOccupancy()
208
- await model.save();
142
+ if (deleteUnreachable) {
143
+ const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
144
+
145
+ // Delete unreachable categories first
146
+ await organizationPeriod.cleanCategories(groups);
147
+ await Group.deleteUnreachable(organization.id, organizationPeriod, groups)
209
148
  }
210
149
 
211
- for (const struct of patch.groups.getPatches()) {
212
- const model = await Group.getByID(struct.id)
150
+ periods.push(organizationPeriod);
151
+ }
213
152
 
214
- if (!model || !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
215
- throw Context.auth.error('Je hebt geen toegangsrechten om deze groep te wijzigen')
216
- }
153
+ return new Response(
154
+ await AuthenticatedStructures.organizationRegistrationPeriods(periods),
155
+ );
156
+ }
157
+
158
+ static async validateDefaultGroupId(id: string|null): Promise<string|null> {
159
+ if (id === null) {
160
+ return id;
161
+ }
162
+ const platform = await Platform.getSharedStruct()
163
+
164
+ if (platform.config.defaultAgeGroups.find(g => g.id === id)) {
165
+ return id;
166
+ }
167
+
168
+ throw new SimpleError({
169
+ code: "invalid_default_age_group",
170
+ message: "Invalid default age group",
171
+ human: "De standaard leeftijdsgroep is ongeldig",
172
+ statusCode: 400
173
+ })
174
+ }
175
+
176
+ static async deleteGroup(id: string) {
177
+ const model = await Group.getByID(id)
178
+ if (!model || !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
179
+ throw Context.auth.error('Je hebt geen toegangsrechten om deze groep te verwijderen')
180
+ }
181
+
182
+ model.deletedAt = new Date()
183
+ await model.save()
184
+ Member.updateMembershipsForGroupId(id)
185
+ }
186
+
187
+ static async patchGroup(struct: AutoEncoderPatchType<GroupStruct>) {
188
+ const model = await Group.getByID(struct.id)
189
+
190
+ if (!model || !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
191
+ throw Context.auth.error('Je hebt geen toegangsrechten om deze groep te wijzigen')
192
+ }
193
+
194
+ if (struct.settings) {
195
+ model.settings.patchOrPut(struct.settings)
196
+ }
217
197
 
218
- if (struct.settings) {
219
- model.settings.patchOrPut(struct.settings)
198
+ if (struct.status) {
199
+ model.status = struct.status
200
+ }
201
+
202
+ if (struct.privateSettings) {
203
+ model.privateSettings.patchOrPut(struct.privateSettings)
204
+
205
+ if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
206
+ throw new SimpleError({
207
+ code: "missing_permissions",
208
+ message: "You cannot restrict your own permissions",
209
+ human: "Je kan je eigen volledige toegang tot deze inschrijvingsgroep niet verwijderen. Vraag aan een hoofdbeheerder om jouw toegang te verwijderen."
210
+ })
211
+ }
212
+ }
213
+
214
+ if (struct.cycle !== undefined) {
215
+ model.cycle = struct.cycle
216
+ }
217
+
218
+ if (struct.deletedAt !== undefined) {
219
+ model.deletedAt = struct.deletedAt
220
+ }
221
+
222
+ if (struct.defaultAgeGroupId !== undefined) {
223
+ model.defaultAgeGroupId = await this.validateDefaultGroupId(struct.defaultAgeGroupId)
224
+ }
225
+
226
+ const patch = struct;
227
+ if (patch.waitingList !== undefined) {
228
+ if (patch.waitingList === null) {
229
+ // delete
230
+ if (model.waitingListId) {
231
+ // for now don't delete, as waiting lists can be shared between multiple groups
232
+ // await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
233
+ model.waitingListId = null;
220
234
  }
221
235
 
222
- if (struct.status) {
223
- model.status = struct.status
236
+ } else if (patch.waitingList.isPatch()) {
237
+ if (!model.waitingListId) {
238
+ throw new SimpleError({
239
+ code: 'invalid_field',
240
+ field: 'waitingList',
241
+ message: 'Cannot patch waiting list before it is created'
242
+ })
224
243
  }
225
-
226
- if (struct.privateSettings) {
227
- model.privateSettings.patchOrPut(struct.privateSettings)
244
+ patch.waitingList.id = model.waitingListId
245
+ patch.waitingList.type = GroupType.WaitingList
246
+ await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.waitingList)
247
+ } else {
248
+ if (model.waitingListId) {
249
+ // for now don't delete, as waiting lists can be shared between multiple groups
250
+ // await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
251
+ model.waitingListId = null;
252
+ }
253
+ patch.waitingList.type = GroupType.WaitingList
228
254
 
229
- if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
255
+ const existing = await Group.getByID(patch.waitingList.id)
256
+ if (existing) {
257
+ if (existing.organizationId !== model.organizationId) {
230
258
  throw new SimpleError({
231
- code: "missing_permissions",
232
- message: "You cannot restrict your own permissions",
233
- human: "Je kan je eigen volledige toegang tot deze inschrijvingsgroep niet verwijderen. Vraag aan een hoofdbeheerder om jouw toegang te verwijderen."
259
+ code: 'invalid_field',
260
+ field: 'waitingList',
261
+ message: 'Waiting list group is already used in another organization'
234
262
  })
235
263
  }
236
- }
237
264
 
238
- if (struct.cycle !== undefined) {
239
- model.cycle = struct.cycle
265
+ model.waitingListId = existing.id
266
+ } else {
267
+ const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
268
+ patch.waitingList,
269
+ model.organizationId,
270
+ model.periodId
271
+ )
272
+ model.waitingListId = group.id
240
273
  }
274
+ }
275
+ }
276
+
277
+ await model.updateOccupancy()
278
+ await model.save();
241
279
 
242
- if (struct.deletedAt !== undefined) {
243
- model.deletedAt = struct.deletedAt
244
- }
280
+ if (struct.deletedAt !== undefined || struct.defaultAgeGroupId !== undefined) {
281
+ Member.updateMembershipsForGroupId(model.id)
282
+ }
283
+ }
245
284
 
246
- if (struct.defaultAgeGroupId !== undefined) {
247
- model.defaultAgeGroupId = validateDefaultGroupId(struct.defaultAgeGroupId)
248
- }
249
-
250
- await model.updateOccupancy()
251
- await model.save();
252
- }
253
285
 
254
- const period = await RegistrationPeriod.getByID(organizationPeriod.periodId);
255
- const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
286
+ static async createGroup(struct: GroupStruct, organizationId: string, periodId: string, options?: {allowedIds?: string[]}): Promise<Group> {
287
+ const allowedIds = options?.allowedIds ?? []
256
288
 
257
- if (deleteUnreachable) {
258
- // Delete unreachable categories first
259
- await organizationPeriod.cleanCategories(groups);
260
- await Group.deleteUnreachable(organization.id, organizationPeriod, groups)
289
+ if (!await Context.auth.hasFullAccess(organizationId)) {
290
+ if (allowedIds.includes(struct.id)) {
291
+ // Ok
292
+ } else {
293
+ throw Context.auth.error('Je hebt geen toegangsrechten om groepen toe te voegen')
261
294
  }
295
+ }
262
296
 
263
- if (period) {
264
- structs.push(organizationPeriod.getStructure(period, groups));
297
+ const user = Context.auth.user
298
+
299
+ const model = new Group()
300
+ model.id = struct.id
301
+ model.organizationId = organizationId
302
+ model.defaultAgeGroupId = await this.validateDefaultGroupId(struct.defaultAgeGroupId)
303
+ model.periodId = periodId
304
+ model.settings = struct.settings
305
+ model.privateSettings = struct.privateSettings ?? GroupPrivateSettings.create({})
306
+ model.status = struct.status
307
+ model.type = struct.type
308
+
309
+ if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
310
+ // Create a temporary permission role for this user
311
+ const organizationPermissions = user.permissions?.organizationPermissions?.get(organizationId)
312
+ if (!organizationPermissions) {
313
+ throw new Error('Unexpected missing permissions')
265
314
  }
315
+ const resourcePermissions = ResourcePermissions.create({
316
+ resourceName: model.settings.name,
317
+ level: PermissionLevel.Full
318
+ })
319
+ const patch = resourcePermissions.createInsertPatch(PermissionsResourceType.Groups, model.id, organizationPermissions)
320
+ user.permissions!.organizationPermissions.set(organizationId, organizationPermissions.patch(patch))
321
+ console.log('Automatically granted author full permissions to resource', 'group', model.id, 'user', user.id, 'patch', patch.encode({version: Version}))
322
+ await user.save()
266
323
  }
267
324
 
268
- return new Response(
269
- structs
270
- );
325
+ // Check if current user has permissions to this new group -> else fail with error
326
+ if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
327
+ throw new SimpleError({
328
+ code: "missing_permissions",
329
+ message: "You cannot restrict your own permissions",
330
+ human: "Je kan geen inschrijvingsgroep maken zonder dat je zelf volledige toegang hebt tot de nieuwe groep"
331
+ })
332
+ }
333
+
334
+ if (struct.waitingList) {
335
+ const existing = await Group.getByID(struct.waitingList.id)
336
+ if (existing) {
337
+ if (existing.organizationId !== model.organizationId) {
338
+ throw new SimpleError({
339
+ code: 'invalid_field',
340
+ field: 'waitingList',
341
+ message: 'Waiting list group is already used in another organization'
342
+ })
343
+ }
344
+
345
+ model.waitingListId = existing.id
346
+ } else {
347
+ struct.waitingList.type = GroupType.WaitingList
348
+ const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
349
+ struct.waitingList,
350
+ model.organizationId,
351
+ model.periodId
352
+ )
353
+ model.waitingListId = group.id
354
+ }
355
+ }
356
+
357
+ await model.updateOccupancy()
358
+ await model.save();
359
+ return model;
271
360
  }
272
361
 
273
362
  }
@@ -1,10 +1,10 @@
1
1
 
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { SimpleError } from '@simonbackx/simple-errors';
4
3
  import { StripeAccount } from '@stamhoofd/models';
5
4
  import { PermissionLevel } from '@stamhoofd/structures';
6
5
 
7
6
  import { Context } from '../../../../helpers/Context';
7
+ import { StripeHelper } from '../../../../helpers/StripeHelper';
8
8
 
9
9
  type Params = { id: string };
10
10
  type Body = undefined;
@@ -42,13 +42,13 @@ export class DeleteStripeAccountEndpoint extends Endpoint<Params, Query, Body, R
42
42
  }
43
43
 
44
44
  // For now we don't delete them in Stripe because this causes issues with data access
45
- // const stripe = StripeHelper.getInstance()
46
- //
47
- // try {
48
- // await stripe.accounts.del(model.accountId);
49
- // } catch (e) {
50
- // console.error('Tried deleting account but failed', e)
51
- // }
45
+ const stripe = StripeHelper.getInstance()
46
+
47
+ try {
48
+ await stripe.accounts.del(model.accountId);
49
+ } catch (e) {
50
+ console.error('Tried deleting account but failed', e)
51
+ }
52
52
 
53
53
  // If that succeeded
54
54
  model.status = "deleted"
@@ -3,6 +3,7 @@ import { User } from '@stamhoofd/models';
3
3
  import { OrganizationAdmins, 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
@@ -35,7 +36,7 @@ export class GetOrganizationAdminsEndpoint extends Endpoint<Params, Query, Body,
35
36
  const admins = await User.getAdmins([organization.id])
36
37
 
37
38
  return new Response(OrganizationAdmins.create({
38
- users: admins.map(a => UserStruct.create({...a, hasAccount: a.hasAccount()})),
39
+ users: await AuthenticatedStructures.usersWithMembers(admins)
39
40
  }));
40
41
  }
41
42
  }
@@ -101,8 +101,9 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
101
101
 
102
102
  // Prevent concurrency issues
103
103
  await QueueHandler.schedule("balance-item-update/"+organization.id, async () => {
104
+ const unloaded = (await BalanceItemPayment.where({paymentId: payment.id})).map(r => r.setRelation(BalanceItemPayment.payment, payment))
104
105
  const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
105
- (await BalanceItemPayment.where({paymentId: payment.id})).map(r => r.setRelation(BalanceItemPayment.payment, payment))
106
+ unloaded
106
107
  );
107
108
 
108
109
  for (const balanceItemPayment of balanceItemPayments) {
@@ -1,6 +1,6 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from "@simonbackx/simple-encoding"
2
2
  import { SimpleError } from "@simonbackx/simple-errors"
3
- import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
3
+ import { BalanceItem, Document, DocumentTemplate, EmailTemplate, Event, Group, Member, MemberWithRegistrations, Order, Organization, Payment, Registration, User, Webshop } from "@stamhoofd/models"
4
4
  import { AccessRight, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory } from "@stamhoofd/structures"
5
5
  import { Formatter } from "@stamhoofd/utility"
6
6
 
@@ -11,6 +11,10 @@ import { Formatter } from "@stamhoofd/utility"
11
11
  export class AdminPermissionChecker {
12
12
  organization: Organization|null
13
13
  user: User
14
+ /**
15
+ * The member that is linked to this user = is this user
16
+ */
17
+ member: MemberWithRegistrations|null = null
14
18
  platform: PlatformStruct
15
19
 
16
20
  organizationCache: Map<string, Organization|Promise<Organization|undefined>> = new Map()
@@ -143,6 +147,13 @@ export class AdminPermissionChecker {
143
147
  if (!this.checkScope(group.organizationId)) {
144
148
  return false
145
149
  }
150
+ const organization = await this.getOrganization(group.organizationId)
151
+
152
+ if (group.periodId !== organization.periodId) {
153
+ if (!await this.hasFullAccess(group.organizationId)) {
154
+ return false
155
+ }
156
+ }
146
157
 
147
158
  if (group.deletedAt || group.status === GroupStatus.Archived) {
148
159
  return await this.canAccessArchivedGroups(group.organizationId);
@@ -160,7 +171,6 @@ export class AdminPermissionChecker {
160
171
  }
161
172
 
162
173
  // Check parent categories
163
- const organization = await this.getOrganization(group.organizationId)
164
174
  const parentCategories = group.getParentCategories(organization.meta.categories)
165
175
  for (const category of parentCategories) {
166
176
  if (organizationPermissions.hasResourceAccess(PermissionsResourceType.GroupCategories, category.id, permissionLevel)) {
@@ -171,6 +181,28 @@ export class AdminPermissionChecker {
171
181
  return false;
172
182
  }
173
183
 
184
+ async canAccessEvent(event: Event, permissionLevel: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
185
+ // Check permissions aren't scoped to a specific organization, and they mismatch
186
+ if (!this.checkScope(event.organizationId)) {
187
+ return false
188
+ }
189
+
190
+ if (permissionLevel !== PermissionLevel.Read) {
191
+ if (event.organizationId) {
192
+ // Need full access for now
193
+ if (!await this.hasFullAccess(event.organizationId)) {
194
+ return false
195
+ }
196
+ } else {
197
+ if (!this.hasPlatformFullAccess()) {
198
+ return false
199
+ }
200
+ }
201
+ }
202
+
203
+ return true;
204
+ }
205
+
174
206
  async canAccessArchivedGroups(organizationId: string) {
175
207
  return await this.hasFullAccess(organizationId)
176
208
  }
@@ -192,6 +224,12 @@ export class AdminPermissionChecker {
192
224
  return true
193
225
  }
194
226
 
227
+ if (member.registrations.length === 0 && permissionLevel !== PermissionLevel.Full && (this.organization && await this.hasFullAccess(this.organization.id, PermissionLevel.Full))) {
228
+ // Everyone with at least full access to at least one organization can access this member
229
+ // This allows organizations to register new members themselves
230
+ return true;
231
+ }
232
+
195
233
  for (const registration of member.registrations) {
196
234
  if (await this.canAccessRegistration(registration, permissionLevel)) {
197
235
  return true;
@@ -221,7 +259,8 @@ export class AdminPermissionChecker {
221
259
  return false;
222
260
  }
223
261
 
224
- if (organizationPermissions.hasAccess(permissionLevel)) {
262
+ if (organizationPermissions.hasAccess(PermissionLevel.Full)) {
263
+ // Only full permissions; because non-full doesn't have access to other periods
225
264
  return true;
226
265
  }
227
266
 
@@ -853,6 +892,24 @@ export class AdminPermissionChecker {
853
892
  return set
854
893
  }
855
894
 
895
+
896
+ async getAccessibleGroups(organizationId: string, level: PermissionLevel = PermissionLevel.Read): Promise<string[] | 'all'> {
897
+ if (await this.hasFullAccess(organizationId)) {
898
+ return 'all'
899
+ }
900
+
901
+ const groups = await this.getOrganizationGroups(organizationId)
902
+ const accessibleGroups: string[] = []
903
+
904
+ for (const group of groups) {
905
+ if (await this.canAccessGroup(group, level)) {
906
+ accessibleGroups.push(group.id)
907
+ }
908
+ }
909
+ return accessibleGroups
910
+ }
911
+
912
+
856
913
  /**
857
914
  * Changes data inline
858
915
  */