@stamhoofd/backend 2.129.2 → 2.129.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.129.2",
3
+ "version": "2.129.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -57,20 +57,20 @@
57
57
  "@simonbackx/simple-endpoints": "1.21.1",
58
58
  "@simonbackx/simple-errors": "1.5.0",
59
59
  "@simonbackx/simple-logging": "1.0.1",
60
- "@stamhoofd/backend-env": "2.129.2",
61
- "@stamhoofd/backend-i18n": "2.129.2",
62
- "@stamhoofd/backend-middleware": "2.129.2",
63
- "@stamhoofd/crons": "2.129.2",
64
- "@stamhoofd/email": "2.129.2",
65
- "@stamhoofd/excel-writer": "2.129.2",
66
- "@stamhoofd/logging": "2.129.2",
67
- "@stamhoofd/models": "2.129.2",
68
- "@stamhoofd/object-differ": "2.129.2",
69
- "@stamhoofd/queues": "2.129.2",
70
- "@stamhoofd/sql": "2.129.2",
71
- "@stamhoofd/structures": "2.129.2",
72
- "@stamhoofd/types": "2.129.2",
73
- "@stamhoofd/utility": "2.129.2",
60
+ "@stamhoofd/backend-env": "2.129.3",
61
+ "@stamhoofd/backend-i18n": "2.129.3",
62
+ "@stamhoofd/backend-middleware": "2.129.3",
63
+ "@stamhoofd/crons": "2.129.3",
64
+ "@stamhoofd/email": "2.129.3",
65
+ "@stamhoofd/excel-writer": "2.129.3",
66
+ "@stamhoofd/logging": "2.129.3",
67
+ "@stamhoofd/models": "2.129.3",
68
+ "@stamhoofd/object-differ": "2.129.3",
69
+ "@stamhoofd/queues": "2.129.3",
70
+ "@stamhoofd/sql": "2.129.3",
71
+ "@stamhoofd/structures": "2.129.3",
72
+ "@stamhoofd/types": "2.129.3",
73
+ "@stamhoofd/utility": "2.129.3",
74
74
  "archiver": "7.0.1",
75
75
  "axios": "1.16.0",
76
76
  "base-x": "3.0.11",
@@ -91,7 +91,7 @@
91
91
  "stripe": "16.12.0"
92
92
  },
93
93
  "devDependencies": {
94
- "@stamhoofd/test-utils": "2.129.2",
94
+ "@stamhoofd/test-utils": "2.129.3",
95
95
  "@types/cookie": "0.6.0",
96
96
  "@types/luxon": "3.7.1",
97
97
  "@types/mailparser": "3.4.6",
@@ -107,5 +107,5 @@
107
107
  "publishConfig": {
108
108
  "access": "public"
109
109
  },
110
- "gitHead": "099a48481fdbd3d661793675ab9e2c24a594b130"
110
+ "gitHead": "982b7912b141d0149eb2ad89cd2120e073251e66"
111
111
  }
@@ -44,8 +44,11 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
44
44
  const organization = Context.organization;
45
45
  let scopeFilter: StamhoofdFilter | undefined = undefined;
46
46
 
47
- // First do a quick validation of the groups, so that prevents the backend from having to add a scope filter
48
- if (!Context.auth.canAccessAllPlatformMembers(permissionLevel) && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: 'registrations' })) {
47
+ if (organization && STAMHOOFD.userMode === 'organization' && await Context.auth.hasFullAccess(organization.id)) {
48
+ // Don't add any scoping. Organization id filter is added automatically.
49
+ } else if (!Context.auth.canAccessAllPlatformMembers(permissionLevel) && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: 'registrations' })) {
50
+ // First do a quick validation of the groups, so that prevents the backend from having to add a scope filter
51
+
49
52
  if (!organization) {
50
53
  const tags = Context.auth.getPlatformAccessibleOrganizationTags(permissionLevel);
51
54
  if (tags !== 'all' && tags.length === 0) {
@@ -69,9 +72,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
69
72
  },
70
73
  };
71
74
  }
72
- }
73
-
74
- if (organization) {
75
+ } else {
75
76
  // Add organization scope filter
76
77
  if (await Context.auth.canAccessAllMembers(organization.id, permissionLevel)) {
77
78
  if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
@@ -159,6 +159,10 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
159
159
  .setMaxExecutionTime(15 * 1000)
160
160
  .where('registeredAt', '!=', null);
161
161
 
162
+ if (organization && STAMHOOFD.userMode === 'organization') {
163
+ query.where('organizationId', organization.id);
164
+ }
165
+
162
166
  if (scopeFilter) {
163
167
  query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
164
168
  }
@@ -12,6 +12,7 @@ import { PaymentService } from '../../../../services/PaymentService.js';
12
12
  import { SimpleError } from '@simonbackx/simple-errors';
13
13
  import { PaymentMandateService } from '../../../../services/PaymentMandateService.js';
14
14
  import { BalanceItemService } from '../../../../services/BalanceItemService.js';
15
+ import { QueueHandler } from '@stamhoofd/queues';
15
16
 
16
17
  type Params = Record<string, never>;
17
18
  type Query = CountFilteredRequest;
@@ -37,98 +38,116 @@ export class ChargeReceivableBalancesEndpoint extends Endpoint<Params, Query, Bo
37
38
  async handle(request: DecodedRequest<Params, Query, Body>) {
38
39
  const sellingOrganization = await Context.setOrganizationScope();
39
40
  const { user } = await Context.authenticate();
40
- const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
41
41
 
42
- for await (const cachedBalance of query.all()) {
43
- if (cachedBalance.organizationId !== sellingOrganization.id) {
44
- throw new SimpleError({
45
- code: 'wrong_organization',
46
- message: 'Cannot charge a cached balance from a different organization',
47
- });
48
- }
49
- // todo
50
- const items = await CachedBalance.balanceForObjects(cachedBalance.organizationId, [cachedBalance.objectId], cachedBalance.objectType);
42
+ if (!await Context.auth.canManageFinances(sellingOrganization.id)) {
43
+ throw Context.auth.error();
44
+ }
45
+ const queueId = 'charge-receivable-balances/' + sellingOrganization.id;
51
46
 
52
- if (items.length === 0) {
53
- console.log('Nothing to charge for', cachedBalance.id);
54
- continue;
55
- }
47
+ if (QueueHandler.isRunning(queueId)) {
48
+ throw new SimpleError({
49
+ code: 'charge_pending',
50
+ message: 'Already pending charge',
51
+ human: $t('Er is al een aanrekening bezig, even geduld voor je een tweede aanrekening aanrekend.'),
52
+ });
53
+ }
56
54
 
57
- const map: Map<BalanceItem, number> = new Map();
58
- let total = 0;
59
- for (const i of items) {
60
- map.set(i, i.priceOpen);
61
- total += i.priceOpen;
62
- }
63
- if (total <= 0) {
64
- continue;
65
- }
55
+ await QueueHandler.schedule(queueId, async () => {
56
+ const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
66
57
 
67
- let payingOrganization: Organization | null = null;
68
- let customerUser: User | null = null;
69
- if (cachedBalance.objectType === ReceivableBalanceType.organization) {
70
- const p = await Organization.getByID(cachedBalance.objectId);
71
- if (!p || !(await Context.auth.hasFullAccess(p))) {
72
- console.error('Unexpected missing paying organization id', cachedBalance);
58
+ for await (const cachedBalance of query.all()) {
59
+ if (cachedBalance.organizationId !== sellingOrganization.id) {
60
+ throw new SimpleError({
61
+ code: 'wrong_organization',
62
+ message: 'Cannot charge a cached balance from a different organization',
63
+ });
64
+ }
65
+ // todo
66
+ const items = await CachedBalance.balanceForObjects(cachedBalance.organizationId, [cachedBalance.objectId], cachedBalance.objectType);
67
+
68
+ if (items.length === 0) {
69
+ console.log('Nothing to charge for', cachedBalance.id);
73
70
  continue;
74
71
  }
75
- payingOrganization = p;
76
- }
77
72
 
78
- if (cachedBalance.objectType === ReceivableBalanceType.user || cachedBalance.objectType === ReceivableBalanceType.userWithoutMembers) {
79
- const p = await User.getByID(cachedBalance.objectId);
80
- if (!p || !(Context.auth.checkScope(p.organizationId))) {
81
- console.error('Unexpected missing customer user id', cachedBalance);
73
+ const map: Map<BalanceItem, number> = new Map();
74
+ let total = 0;
75
+ for (const i of items) {
76
+ map.set(i, i.priceOpen);
77
+ total += i.priceOpen;
78
+ }
79
+ if (total <= 0) {
82
80
  continue;
83
81
  }
84
- customerUser = p;
85
- }
86
82
 
87
- const mandates = await PaymentMandateService.getMandates({
88
- sellingOrganization,
89
- user: customerUser,
90
- payingOrganization,
91
- });
83
+ let payingOrganization: Organization | null = null;
84
+ let customerUser: User | null = null;
85
+ if (cachedBalance.objectType === ReceivableBalanceType.organization) {
86
+ const p = await Organization.getByID(cachedBalance.objectId);
87
+ if (!p || !(await Context.auth.hasFullAccess(p))) {
88
+ console.error('Unexpected missing paying organization id', cachedBalance);
89
+ continue;
90
+ }
91
+ payingOrganization = p;
92
+ }
92
93
 
93
- const mandate = mandates.find(m => m.isDefault);
94
+ if (cachedBalance.objectType === ReceivableBalanceType.user || cachedBalance.objectType === ReceivableBalanceType.userWithoutMembers) {
95
+ const p = await User.getByID(cachedBalance.objectId);
96
+ if (!p || !(Context.auth.checkScope(p.organizationId))) {
97
+ console.error('Unexpected missing customer user id', cachedBalance);
98
+ continue;
99
+ }
100
+ customerUser = p;
101
+ }
102
+
103
+ const mandates = await PaymentMandateService.getMandates({
104
+ sellingOrganization,
105
+ user: customerUser,
106
+ payingOrganization,
107
+ });
108
+
109
+ const mandate = mandates.find(m => m.isDefault);
94
110
 
95
- if (!mandate) {
111
+ if (!mandate) {
96
112
  // Not possible
97
- console.error('No mandates found for', cachedBalance.id);
98
- continue;
99
- }
113
+ console.error('No mandates found for', cachedBalance.id);
114
+ continue;
115
+ }
100
116
 
101
- customerUser = customerUser ?? (payingOrganization ? (await payingOrganization.getFullAdmins())[0] : null);
102
- const customer = PaymentCustomer.create({
103
- firstName: customerUser?.firstName,
104
- lastName: customerUser?.lastName,
105
- email: customerUser?.email ?? (payingOrganization ? (await payingOrganization.getReplyEmails())[0].email : null),
106
- company: payingOrganization?.defaultCompanies[0],
107
- });
117
+ customerUser = customerUser ?? (payingOrganization ? (await payingOrganization.getFullAdmins())[0] : null);
118
+ const customer = PaymentCustomer.create({
119
+ firstName: customerUser?.firstName,
120
+ lastName: customerUser?.lastName,
121
+ email: customerUser?.email ?? (payingOrganization ? (await payingOrganization.getReplyEmails())[0].email : null),
122
+ company: payingOrganization?.defaultCompanies[0],
123
+ });
108
124
 
109
- await PaymentService.createPayment({
110
- balanceItems: map,
111
- checkout: {
112
- paymentMethod: PaymentMethod.Unknown,
113
- totalPrice: null,
114
- customer,
115
- cancelUrl: null,
116
- redirectUrl: null,
117
- },
118
- user: customerUser,
119
- adminUserId: user.id,
120
- organization: sellingOrganization,
121
- payingOrganization,
122
- serviceFeeType: 'system',
123
- createMandate: null,
124
- useMandate: mandate,
125
- paymentConfiguration: sellingOrganization.meta.registrationPaymentConfiguration,
126
- privatePaymentConfiguration: sellingOrganization.privateMeta.registrationPaymentConfiguration,
127
- });
128
- }
125
+ await PaymentService.createPayment({
126
+ balanceItems: map,
127
+ checkout: {
128
+ paymentMethod: PaymentMethod.Unknown,
129
+ totalPrice: null,
130
+ customer,
131
+ cancelUrl: null,
132
+ redirectUrl: null,
133
+ },
134
+ user: customerUser,
135
+ adminUserId: user.id,
136
+ organization: sellingOrganization,
137
+ payingOrganization,
138
+ serviceFeeType: 'system',
139
+ createMandate: null,
140
+ useMandate: mandate,
141
+ paymentConfiguration: sellingOrganization.meta.registrationPaymentConfiguration,
142
+ privatePaymentConfiguration: sellingOrganization.privateMeta.registrationPaymentConfiguration,
143
+ });
144
+
145
+ // Clear cache while looping, to update the table while we are charging
146
+ // Make sure the user can refresh data and see the updated cached amounts
147
+ await BalanceItemService.flushCaches(sellingOrganization.id);
148
+ }
149
+ });
129
150
 
130
- // Make sure the user can refresh data and see the updated cached amounts
131
- await BalanceItemService.flushCaches(sellingOrganization.id);
132
151
  return new Response(undefined, 201);
133
152
  }
134
153
  }
@@ -486,9 +486,21 @@ export class AdminPermissionChecker {
486
486
  * Note: only checks admin permissions. Users that 'own' this member can also access it but that does not use the AdminPermissionChecker
487
487
  */
488
488
  async canAccessRegistration(registration: Registration, permissionLevel: PermissionLevel = PermissionLevel.Read, checkMember: boolean | MemberWithUsersRegistrationsAndGroups = true) {
489
+ const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
490
+
491
+ if (!organizationPermissions) {
492
+ return false;
493
+ }
494
+
495
+ if (organizationPermissions.hasAccess(PermissionLevel.Full)) {
496
+ // Only full permissions; because non-full doesn't have access to other periods
497
+ return true;
498
+ }
499
+
500
+ // Non full admin logic
489
501
  if (registration.deactivatedAt || !registration.registeredAt) {
490
502
  if (!checkMember) {
491
- // We can't grant access to a member because of a deactivated registration
503
+ // We can't grant access to a member because of a deactivated registration - unless full permission
492
504
  return false;
493
505
  }
494
506
 
@@ -499,16 +511,6 @@ export class AdminPermissionChecker {
499
511
  }
500
512
  }
501
513
 
502
- const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
503
-
504
- if (!organizationPermissions) {
505
- return false;
506
- }
507
-
508
- if (organizationPermissions.hasAccess(PermissionLevel.Full)) {
509
- // Only full permissions; because non-full doesn't have access to other periods
510
- return true;
511
- }
512
514
  const organization = await this.getOrganization(registration.organizationId);
513
515
 
514
516
  if (registration.periodId !== organization.periodId) {
@@ -251,7 +251,7 @@ export class MollieService {
251
251
  details: PaymentMandateDetails.create({
252
252
  name: ('consumerName' in details ? details.consumerName : details.cardHolder) ?? undefined,
253
253
  cardNumber: 'cardNumber' in details ? details.cardNumber : null,
254
- iban: 'consumerAccount' in details ? details.consumerAccount : null,
254
+ iban: 'consumerAccount' in details ? Formatter.iban(details.consumerAccount) : null,
255
255
  bic: ('consumerBic' in details ? details.consumerBic : undefined),
256
256
  expiryDate: ('cardExpiryDate' in details ? DateTime.fromISO(details.cardExpiryDate, { zone: Formatter.timezone }).toJSDate() : null), // todo: parse date correctly in Brussels timezone!
257
257
  brand: ('cardLabel' in details ? details.cardLabel : null),
@@ -61,6 +61,10 @@ export class SSOService {
61
61
  this.provider = data.provider;
62
62
  this.platform = data.platform;
63
63
  this.organization = data.organization ?? null;
64
+
65
+ if (STAMHOOFD.userMode === 'platform' && this.organization) {
66
+ throw new Error('SSO provided by organization disabled in platform mode');
67
+ }
64
68
  }
65
69
 
66
70
  static async clearExpiredTokensOrFromUser(userId: string | null = null) {
@@ -371,7 +375,7 @@ export class SSOService {
371
375
  if (token) {
372
376
  user = await User.getByID(token.userId);
373
377
 
374
- if (!user) {
378
+ if (!user || user.organizationId !== (this.organization?.id ?? null)) {
375
379
  throw new SimpleError({
376
380
  code: 'invalid_user',
377
381
  message: 'User not found',