@stamhoofd/backend 2.83.0 → 2.83.2

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.
@@ -1,17 +1,17 @@
1
- import { AutoEncoderPatchType, cloneObject, Decoder, isPatchableArray, ObjectData, PatchableArrayAutoEncoder, patchObject } from '@simonbackx/simple-encoding';
1
+ import { AutoEncoderPatchType, cloneObject, Decoder, isPatchableArray, isPatchMap, ObjectData, PatchableArray, PatchableArrayAutoEncoder, PatchMap, patchObject } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
4
4
  import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, StripeAccount, Webshop } from '@stamhoofd/models';
5
- import { BuckarooSettings, Company, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PlatformConfig } from '@stamhoofd/structures';
5
+ import { BuckarooSettings, Company, MemberResponsibility, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PermissionRoleDetailed, PermissionRoleForResponsibility, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
6
6
  import { Formatter } from '@stamhoofd/utility';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
9
  import { BuckarooHelper } from '../../../../helpers/BuckarooHelper';
10
10
  import { Context } from '../../../../helpers/Context';
11
+ import { MemberUserSyncer } from '../../../../helpers/MemberUserSyncer';
11
12
  import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater';
12
13
  import { TagHelper } from '../../../../helpers/TagHelper';
13
14
  import { ViesHelper } from '../../../../helpers/ViesHelper';
14
- import { MemberUserSyncer } from '../../../../helpers/MemberUserSyncer';
15
15
 
16
16
  type Params = Record<string, never>;
17
17
  type Query = undefined;
@@ -376,6 +376,130 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
376
376
  statusCode: 403,
377
377
  });
378
378
  }
379
+
380
+ // Give users without full access permission to alter responsibilities in order to give other users permissions to resources they also have full permissions to
381
+ if (request.body.privateMeta && request.body.privateMeta.isPatch()) {
382
+ if (request.body.privateMeta.inheritedResponsibilityRoles) {
383
+ const patchableArray: PatchableArrayAutoEncoder<PermissionRoleForResponsibility> = new PatchableArray();
384
+
385
+ for (const patch of request.body.privateMeta.inheritedResponsibilityRoles.getPatches()) {
386
+ const resources: Map<PermissionsResourceType, Map<string, ResourcePermissions>> = patch.resources.applyTo(new Map<PermissionsResourceType, Map<string, ResourcePermissions>>());
387
+
388
+ if (!await Context.auth.hasFullAccessForOrganizationResources(request.body.id, resources)) {
389
+ throw new SimpleError({
390
+ code: 'permission_denied',
391
+ message: 'You do not have permissions to edit inherited responsibility roles',
392
+ statusCode: 403,
393
+ });
394
+ }
395
+
396
+ patchableArray.addPatch(PermissionRoleForResponsibility.patch({
397
+ id: patch.id,
398
+ resources: patch.resources,
399
+ }));
400
+ }
401
+
402
+ for (const { put, afterId } of request.body.privateMeta.inheritedResponsibilityRoles.getPuts()) {
403
+ const resources: Map<PermissionsResourceType, Map<string, ResourcePermissions>> = put.resources;
404
+
405
+ if (!await Context.auth.hasFullAccessForOrganizationResources(request.body.id, resources)) {
406
+ throw new SimpleError({
407
+ code: 'permission_denied',
408
+ message: 'You do not have permissions to add inherited responsibility roles',
409
+ statusCode: 403,
410
+ });
411
+ }
412
+
413
+ const limitedPut = PermissionRoleForResponsibility.create({
414
+ id: put.id,
415
+ name: put.name,
416
+ responsibilityId: put.responsibilityId,
417
+ responsibilityGroupId: put.responsibilityGroupId,
418
+ resources: put.resources,
419
+ });
420
+
421
+ patchableArray.addPut(limitedPut, afterId);
422
+ }
423
+
424
+ organization.privateMeta = organization.privateMeta.patch({
425
+ inheritedResponsibilityRoles: patchableArray,
426
+ });
427
+ }
428
+
429
+ if (request.body.privateMeta.responsibilities) {
430
+ const patchableArray: PatchableArrayAutoEncoder<MemberResponsibility> = new PatchableArray();
431
+
432
+ for (const patch of request.body.privateMeta.responsibilities.getPatches()) {
433
+ if (!patch.permissions) {
434
+ continue;
435
+ };
436
+
437
+ const resources: Map<PermissionsResourceType, Map<string, ResourcePermissions>> = isPatchMap(patch.permissions.resources) ? patch.permissions.resources.applyTo(new Map<PermissionsResourceType, Map<string, ResourcePermissions>>()) : patch.permissions.resources;
438
+
439
+ if (!await Context.auth.hasFullAccessForOrganizationResources(request.body.id, resources)) {
440
+ throw new SimpleError({
441
+ code: 'permission_denied',
442
+ message: 'You do not have permissions to edit responsibilities',
443
+ statusCode: 403,
444
+ });
445
+ }
446
+
447
+ patchableArray.addPatch(MemberResponsibility.patch({
448
+ id: patch.id,
449
+ permissions: PermissionRoleForResponsibility.patch({
450
+ resources: isPatchMap(patch.permissions.resources) ? patch.permissions.resources : new PatchMap(patch.permissions.resources),
451
+ }),
452
+ }));
453
+ }
454
+
455
+ if (request.body.privateMeta.responsibilities.getPuts().length > 0) {
456
+ throw new SimpleError({
457
+ code: 'permission_denied',
458
+ message: 'You do not have permissions to add responsibilities',
459
+ statusCode: 403,
460
+ });
461
+ }
462
+
463
+ organization.privateMeta = organization.privateMeta.patch({
464
+ responsibilities: patchableArray,
465
+ });
466
+ }
467
+
468
+ if (request.body.privateMeta.roles) {
469
+ const patchableArray: PatchableArrayAutoEncoder<PermissionRoleDetailed> = new PatchableArray();
470
+
471
+ for (const patch of request.body.privateMeta.roles.getPatches()) {
472
+ const resources: Map<PermissionsResourceType, Map<string, ResourcePermissions>> = patch.resources.applyTo(new Map<PermissionsResourceType, Map<string, ResourcePermissions>>());
473
+
474
+ if (!await Context.auth.hasFullAccessForOrganizationResources(request.body.id, resources)) {
475
+ throw new SimpleError({
476
+ code: 'permission_denied',
477
+ message: 'You do not have permissions to edit roles',
478
+ statusCode: 403,
479
+ });
480
+ }
481
+
482
+ patchableArray.addPatch(PermissionRoleDetailed.patch({
483
+ id: patch.id,
484
+ resources: patch.resources,
485
+ }));
486
+ }
487
+
488
+ if (request.body.privateMeta.roles.getPuts().length > 0) {
489
+ throw new SimpleError({
490
+ code: 'permission_denied',
491
+ message: 'You do not have permissions to add roles',
492
+ statusCode: 403,
493
+ });
494
+ }
495
+
496
+ organization.privateMeta = organization.privateMeta.patch({
497
+ roles: patchableArray,
498
+ });
499
+ }
500
+
501
+ await organization.save();
502
+ }
379
503
  }
380
504
 
381
505
  // Only needed for permissions atm, so no put or delete here
@@ -1,7 +1,7 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
2
2
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
3
  import { BalanceItem, CachedBalance, Document, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
4
- import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordSettings } from '@stamhoofd/structures';
4
+ import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordSettings, ResourcePermissions } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { MemberRecordStore } from '../services/MemberRecordStore';
7
7
  import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
@@ -373,6 +373,27 @@ export class AdminPermissionChecker {
373
373
  return false;
374
374
  }
375
375
 
376
+ /**
377
+ Returns true if the user has full access to all resource ids in the provided resources map. The resource permissions in the map are ignored for now.
378
+ */
379
+ async hasFullAccessForOrganizationResources(organizationId: string, resources: Map<PermissionsResourceType, Map<string, ResourcePermissions>>): Promise<boolean> {
380
+ const organizationPermissions = await this.getOrganizationPermissions(organizationId);
381
+
382
+ if (!organizationPermissions) {
383
+ return false;
384
+ }
385
+
386
+ for (const [resourceType, mapForType] of resources.entries()) {
387
+ for (const resourceId of mapForType.keys()) {
388
+ if (!organizationPermissions.hasResourceAccess(resourceType, resourceId, PermissionLevel.Full)) {
389
+ return false;
390
+ }
391
+ }
392
+ }
393
+
394
+ return true;
395
+ }
396
+
376
397
  async canAccessWebshop(webshop: { id: string; organizationId: string }, permissionLevel: PermissionLevel = PermissionLevel.Read) {
377
398
  const organizationPermissions = await this.getOrganizationPermissions(webshop.organizationId);
378
399
 
@@ -214,7 +214,6 @@ export class PlatformMembershipService {
214
214
  membershipTypeId: { sign: 'IN', value: types },
215
215
  deletedAt: null,
216
216
  });
217
- const activeMembershipsUndeletable = activeMemberships.filter(m => !m.canDelete() || !m.generated);
218
217
 
219
218
  if (defaultMemberships.length === 0) {
220
219
  // Stop all active memberships that were added automatically
@@ -233,23 +232,6 @@ export class PlatformMembershipService {
233
232
  continue;
234
233
  }
235
234
 
236
- if (activeMembershipsUndeletable.length) {
237
- // Skip automatic additions
238
- for (const m of activeMembershipsUndeletable) {
239
- try {
240
- await m.calculatePrice(me);
241
- }
242
- catch (e) {
243
- // Ignore error: membership might not be available anymore
244
- if (!silent) {
245
- console.error('Failed to calculate price for undeletable membership', m.id, e);
246
- }
247
- }
248
- await m.save();
249
- }
250
- continue;
251
- }
252
-
253
235
  // Add the cheapest available membership
254
236
  const organizations = await Organization.getByIDs(...Formatter.uniqueArray(defaultMemberships.map(m => m.registration.organizationId)));
255
237
 
@@ -286,28 +268,58 @@ export class PlatformMembershipService {
286
268
  })[0];
287
269
 
288
270
  if (!cheapestMembership) {
271
+ // Technically not possible, but for type checking
289
272
  console.error('No membership found');
290
273
  continue;
291
274
  }
292
275
 
293
276
  // Check if already have the same membership
294
277
  // if that is the case, we'll keep that one and update the price + dates if the organization matches the cheapest/earliest membership
295
- let didFind = false;
278
+ let didFind: MemberPlatformMembership | null = null;
296
279
  for (const m of activeMemberships) {
297
280
  if (m.membershipTypeId === cheapestMembership.membership.id && m.organizationId === cheapestMembership.registration.organizationId) {
298
- // Update the price of this active membership (could have changed)
299
- try {
300
- await m.calculatePrice(me, cheapestMembership.registration);
281
+ if (!m.locked) {
282
+ // Update the price and dates of this active membership (could have changed)
283
+ try {
284
+ await m.calculatePrice(me, cheapestMembership.registration);
285
+ }
286
+ catch (e) {
287
+ // Ignore error: membership might not be available anymore
288
+ if (!silent) {
289
+ console.error('Failed to calculate price for active membership', m.id, e);
290
+ }
291
+ }
292
+ await m.save();
301
293
  }
302
- catch (e) {
303
- // Ignore error: membership might not be available anymore
294
+ didFind = m;
295
+ break;
296
+ }
297
+ }
298
+
299
+ // Delete all other generated memberships that are not the cheapest one
300
+ for (const m of activeMemberships) {
301
+ if (m.id !== didFind?.id) {
302
+ if (!m.locked && (m.generated || m.membershipTypeId === cheapestMembership.membership.id)) {
304
303
  if (!silent) {
305
- console.error('Failed to calculate price for active membership', m.id, e);
304
+ console.log('Removing membership because cheaper membership found or duplicate, for: ' + me.id + ' - membership ' + m.id);
305
+ }
306
+ await m.doDelete();
307
+ }
308
+ else {
309
+ // Update price
310
+ if (!m.locked) {
311
+ try {
312
+ await m.calculatePrice(me);
313
+ }
314
+ catch (e) {
315
+ // Ignore error: membership might not be available anymore
316
+ if (!silent) {
317
+ console.error('Failed to calculate price for undeletable membership', m.id, e);
318
+ }
319
+ }
320
+ await m.save();
306
321
  }
307
322
  }
308
- await m.save();
309
- didFind = true;
310
- break;
311
323
  }
312
324
  }
313
325
 
@@ -315,6 +327,8 @@ export class PlatformMembershipService {
315
327
  continue;
316
328
  }
317
329
 
330
+ // Otherwise make sure we create a new membership
331
+
318
332
  const periodConfig = cheapestMembership.membership.periods.get(period.id);
319
333
  if (!periodConfig) {
320
334
  console.error('Missing membership prices for membership type ' + cheapestMembership.membership.id + ' and period ' + period.id);
@@ -351,16 +365,6 @@ export class PlatformMembershipService {
351
365
 
352
366
  await membership.calculatePrice(me, cheapestMembership.registration);
353
367
  await membership.save();
354
-
355
- // This reasoning allows us to replace an existing membership with a cheaper one (not date based ones, but type based ones)
356
- for (const toDelete of activeMemberships) {
357
- if (toDelete.canDelete() && toDelete.generated) {
358
- if (!silent) {
359
- console.log('Removing membership because cheaper membership found for: ' + me.id + ' - membership ' + toDelete.id);
360
- }
361
- await toDelete.doDelete();
362
- }
363
- }
364
368
  }
365
369
  });
366
370
  });