@stamhoofd/backend 2.71.0 → 2.73.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 (44) hide show
  1. package/index.ts +1 -0
  2. package/package.json +10 -10
  3. package/src/audit-logs/OrganizationLogger.ts +1 -1
  4. package/src/audit-logs/PlatformLogger.ts +1 -0
  5. package/src/email-recipient-loaders/orders.ts +1 -1
  6. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
  7. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +7 -7
  8. package/src/endpoints/auth/CreateTokenEndpoint.ts +11 -1
  9. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +26 -2
  10. package/src/endpoints/auth/PatchUserEndpoint.ts +24 -2
  11. package/src/endpoints/auth/SignupEndpoint.ts +1 -1
  12. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +23 -3
  13. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +3 -3
  14. package/src/endpoints/global/events/GetEventsEndpoint.ts +6 -6
  15. package/src/endpoints/global/events/PatchEventsEndpoint.ts +36 -4
  16. package/src/endpoints/global/members/GetMembersEndpoint.ts +9 -7
  17. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +24 -14
  18. package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +34 -0
  19. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -1
  20. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +20 -12
  21. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -3
  22. package/src/endpoints/global/sso/GetSSOEndpoint.ts +8 -1
  23. package/src/endpoints/global/sso/SetSSOEndpoint.ts +4 -0
  24. package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
  25. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +6 -6
  26. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -1
  27. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +5 -5
  28. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +51 -9
  29. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
  30. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +6 -6
  31. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +6 -6
  32. package/src/excel-loaders/members.ts +8 -0
  33. package/src/excel-loaders/receivable-balances.ts +294 -0
  34. package/src/helpers/AdminPermissionChecker.ts +4 -3
  35. package/src/helpers/AuthenticatedStructures.ts +32 -6
  36. package/src/helpers/SetupStepUpdater.ts +10 -4
  37. package/src/helpers/xlsxAddressTransformerColumnFactory.ts +8 -0
  38. package/src/services/PaymentReallocationService.ts +3 -2
  39. package/src/services/PaymentService.ts +17 -1
  40. package/src/services/SSOService.ts +68 -4
  41. package/src/sql-filters/members.ts +20 -1
  42. package/src/sql-filters/organizations.ts +1 -0
  43. package/src/sql-filters/receivable-balances.ts +53 -1
  44. package/src/sql-sorters/organizations.ts +11 -0
@@ -0,0 +1,294 @@
1
+ import { XlsxBuiltInNumberFormat, XlsxTransformerColumn, XlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
2
+ import { BalanceItemRelationType, BalanceItemWithPayments, DetailedReceivableBalance, ExcelExportType, getBalanceItemRelationTypeName, getBalanceItemStatusName, getBalanceItemTypeName, getReceivableBalanceTypeNameNotTranslated, PaginatedResponse, ReceivableBalance } from '@stamhoofd/structures';
3
+ import { Formatter } from '@stamhoofd/utility';
4
+ import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
5
+ import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint';
6
+
7
+ type ReceivableBalanceWithItem = {
8
+ receivableBalance: DetailedReceivableBalance;
9
+ balanceItem: BalanceItemWithPayments;
10
+ };
11
+
12
+ ExportToExcelEndpoint.loaders.set(ExcelExportType.ReceivableBalances, {
13
+ fetch: async (requestQuery) => {
14
+ const data = await GetReceivableBalancesEndpoint.buildDetailedData(requestQuery);
15
+
16
+ return new PaginatedResponse({
17
+ ...data,
18
+ });
19
+ },
20
+ sheets: [
21
+ {
22
+ id: 'receivableBalances',
23
+ name: 'Te ontvangen bedragen',
24
+ columns: getGeneralColumns(),
25
+ },
26
+ {
27
+ id: 'balanceItems',
28
+ name: 'Lijnen',
29
+ transform: (data: DetailedReceivableBalance): ReceivableBalanceWithItem[] => data.balanceItems.map(balanceItem => ({
30
+ receivableBalance: data,
31
+ balanceItem,
32
+ })),
33
+ columns: [
34
+ ...getBalanceItemColumns(),
35
+
36
+ // Repeating columns need to de-transform again
37
+ ...getGeneralColumns()
38
+ .map((c) => {
39
+ return {
40
+ ...c,
41
+ id: `receivableBalance.${c.id}`,
42
+ getValue: (object: ReceivableBalanceWithItem) => {
43
+ return c.getValue(object.receivableBalance);
44
+ },
45
+ };
46
+ }),
47
+ ],
48
+ },
49
+ ],
50
+ });
51
+
52
+ function getBalanceItemColumns(): XlsxTransformerColumn<ReceivableBalanceWithItem>[] {
53
+ return [
54
+ {
55
+ id: 'id',
56
+ name: 'ID',
57
+ width: 40,
58
+ getValue: (object: ReceivableBalanceWithItem) => ({
59
+ value: object.balanceItem.id,
60
+ style: {
61
+ font: {
62
+ bold: true,
63
+ },
64
+ },
65
+ }),
66
+ },
67
+ {
68
+ id: 'type',
69
+ name: 'Type',
70
+ width: 30,
71
+ getValue: (object: ReceivableBalanceWithItem) => ({
72
+ value: getBalanceItemTypeName(object.balanceItem.type),
73
+ }),
74
+ },
75
+ {
76
+ id: 'category',
77
+ name: 'Categorie',
78
+ width: 30,
79
+ getValue: (object: ReceivableBalanceWithItem) => {
80
+ return {
81
+ value: Formatter.capitalizeFirstLetter(object.balanceItem.category),
82
+ };
83
+ },
84
+ },
85
+ {
86
+ id: 'description',
87
+ name: 'Beschrijving',
88
+ width: 40,
89
+ getValue: (object: ReceivableBalanceWithItem) => ({
90
+ value: object.balanceItem.description,
91
+ }),
92
+ },
93
+ {
94
+ id: 'amount',
95
+ name: 'Aantal',
96
+ width: 20,
97
+ getValue: (object: ReceivableBalanceWithItem) => ({
98
+ value: object.balanceItem.amount,
99
+ style: {
100
+ numberFormat: {
101
+ id: XlsxBuiltInNumberFormat.Number,
102
+ },
103
+ },
104
+ }),
105
+ },
106
+ {
107
+ id: 'unitPrice',
108
+ name: 'Eenheidsprijs',
109
+ width: 20,
110
+ getValue: (object: ReceivableBalanceWithItem) => ({
111
+ value: object.balanceItem.unitPrice / 100,
112
+ style: {
113
+ numberFormat: {
114
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
115
+ },
116
+ },
117
+ }),
118
+ },
119
+ {
120
+ id: 'price',
121
+ name: 'Prijs',
122
+ width: 20,
123
+ getValue: (object: ReceivableBalanceWithItem) => ({
124
+ value: object.balanceItem.price / 100,
125
+ style: {
126
+ numberFormat: {
127
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
128
+ },
129
+ },
130
+ }),
131
+ },
132
+ {
133
+ id: 'pricePaid',
134
+ name: 'Betaald bedrag',
135
+ width: 20,
136
+ getValue: (object: ReceivableBalanceWithItem) => ({
137
+ value: object.balanceItem.pricePaid / 100,
138
+ style: {
139
+ numberFormat: {
140
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
141
+ },
142
+ },
143
+ }),
144
+ },
145
+ {
146
+ id: 'pricePending',
147
+ name: 'In verwerking',
148
+ width: 20,
149
+ getValue: (object: ReceivableBalanceWithItem) => ({
150
+ value: object.balanceItem.pricePending / 100,
151
+ style: {
152
+ numberFormat: {
153
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
154
+ },
155
+ },
156
+ }),
157
+ },
158
+ {
159
+ id: 'priceOpen',
160
+ name: 'Openstaand bedrag',
161
+ width: 20,
162
+ getValue: (object: ReceivableBalanceWithItem) => ({
163
+ value: object.balanceItem.priceOpen / 100,
164
+ style: {
165
+ numberFormat: {
166
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
167
+ },
168
+ },
169
+ }),
170
+ },
171
+ {
172
+ id: 'createdAt',
173
+ name: 'Aangemaakt op',
174
+ width: 20,
175
+ getValue: (object: ReceivableBalanceWithItem) => ({
176
+ value: object.balanceItem.createdAt,
177
+ style: {
178
+ numberFormat: {
179
+ id: XlsxBuiltInNumberFormat.DateSlash,
180
+ },
181
+ },
182
+ }),
183
+ },
184
+ {
185
+ id: 'dueAt',
186
+ name: 'Verschuldigd vanaf',
187
+ width: 20,
188
+ getValue: (object: ReceivableBalanceWithItem) => ({
189
+ value: object.balanceItem.dueAt,
190
+ style: {
191
+ numberFormat: {
192
+ id: XlsxBuiltInNumberFormat.DateSlash,
193
+ },
194
+ },
195
+ }),
196
+ },
197
+ {
198
+ id: 'status',
199
+ name: 'Status',
200
+ width: 20,
201
+ getValue: (object: ReceivableBalanceWithItem) => ({
202
+ value: getBalanceItemStatusName(object.balanceItem.status),
203
+ }),
204
+ },
205
+
206
+ {
207
+ match: (id) => {
208
+ if (id.startsWith('relations.')) {
209
+ const type = id.split('.')[1] as BalanceItemRelationType;
210
+ if (Object.values(BalanceItemRelationType).includes(type)) {
211
+ return [
212
+ {
213
+ id: `relations.${type}`,
214
+ name: getBalanceItemRelationTypeName(type),
215
+ width: 35,
216
+ getValue: (object: ReceivableBalanceWithItem) => ({
217
+ value: object.balanceItem.relations.get(type)?.name || '',
218
+ }),
219
+ },
220
+ ];
221
+ }
222
+ }
223
+ },
224
+ },
225
+ ];
226
+ }
227
+
228
+ function getGeneralColumns(): XlsxTransformerConcreteColumn<ReceivableBalance>[] {
229
+ return [
230
+ {
231
+ id: 'id',
232
+ name: 'ID schuldenaar',
233
+ width: 40,
234
+ getValue: (object: ReceivableBalance) => ({
235
+ value: object.id,
236
+ style: {
237
+ font: {
238
+ bold: true,
239
+ },
240
+ },
241
+ }),
242
+ },
243
+ {
244
+ id: 'name',
245
+ name: 'Schuldenaar',
246
+ width: 40,
247
+ getValue: (object: ReceivableBalance) => ({
248
+ value: object.object.name,
249
+ }),
250
+ },
251
+ {
252
+ id: 'uri',
253
+ name: 'Groepsnummer',
254
+ width: 16,
255
+ getValue: (object: ReceivableBalance) => ({
256
+ value: object.object.uri,
257
+ }),
258
+ },
259
+ {
260
+ id: 'amountOpen',
261
+ name: 'Openstaand bedrag',
262
+ width: 10,
263
+ getValue: (object: ReceivableBalance) => ({
264
+ value: object.amountOpen / 100,
265
+ style: {
266
+ numberFormat: {
267
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
268
+ },
269
+ },
270
+ }),
271
+ },
272
+ {
273
+ id: 'amountPending',
274
+ name: 'In verwerking',
275
+ width: 18,
276
+ getValue: (object: ReceivableBalance) => ({
277
+ value: object.amountPending / 100,
278
+ style: {
279
+ numberFormat: {
280
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
281
+ },
282
+ },
283
+ }),
284
+ },
285
+ {
286
+ id: 'objectType',
287
+ name: 'type',
288
+ width: 10,
289
+ getValue: (object: ReceivableBalance) => ({
290
+ value: getReceivableBalanceTypeNameNotTranslated(object.objectType),
291
+ }),
292
+ },
293
+ ];
294
+ }
@@ -538,6 +538,10 @@ export class AdminPermissionChecker {
538
538
  }
539
539
 
540
540
  async canEditUserName(user: User) {
541
+ if (user.hasAccount() && !user.hasPasswordBasedAccount()) {
542
+ return false;
543
+ }
544
+
541
545
  if (user.id === this.user.id) {
542
546
  return true;
543
547
  }
@@ -556,9 +560,6 @@ export class AdminPermissionChecker {
556
560
  }
557
561
 
558
562
  async canEditUserEmail(user: User) {
559
- if (user.meta?.loginProviderIds?.size) {
560
- return false;
561
- }
562
563
  return this.canEditUserName(user);
563
564
  }
564
565
 
@@ -1,6 +1,6 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import { AuditLog, BalanceItem, CachedBalance, Document, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
3
- import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, Document as DocumentStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
 
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { Context } from './Context';
@@ -399,7 +399,7 @@ export class AuthenticatedStructures {
399
399
  const platformMemberships = members.length > 0 ? await MemberPlatformMembership.where({ deletedAt: null, memberId: { sign: 'IN', value: members.map(m => m.id) } }) : [];
400
400
 
401
401
  // Load missing organizations
402
- const organizationIds = Formatter.uniqueArray(responsibilities.map(r => r.organizationId).filter(id => id !== null));
402
+ const organizationIds = Formatter.uniqueArray(responsibilities.map(r => r.organizationId).concat(platformMemberships.map(r => r.organizationId)).filter(id => id !== null));
403
403
  for (const id of organizationIds) {
404
404
  if (includeContextOrganization || id !== Context.auth.organization?.id) {
405
405
  const found = organizations.get(id);
@@ -575,6 +575,10 @@ export class AuthenticatedStructures {
575
575
  }
576
576
 
577
577
  static async receivableBalances(balances: CachedBalance[]): Promise<ReceivableBalanceStruct[]> {
578
+ return (await this.receivableBalancesHelper(balances)).map(({ balance, object }) => ReceivableBalanceStruct.create({ ...balance, object }));
579
+ }
580
+
581
+ private static async receivableBalancesHelper(balances: CachedBalance[]): Promise< { balance: CachedBalance; object: ReceivableBalanceObject }[]> {
578
582
  if (balances.length === 0) {
579
583
  return [];
580
584
  }
@@ -608,7 +612,8 @@ export class AuthenticatedStructures {
608
612
  ]);
609
613
  const users = userIds.length > 0 ? await User.getByIDs(...userIds) : [];
610
614
 
611
- const result: ReceivableBalanceStruct[] = [];
615
+ const result: { balance: CachedBalance; object: ReceivableBalanceObject }[] = [];
616
+
612
617
  for (const balance of balances) {
613
618
  let object = ReceivableBalanceObject.create({
614
619
  id: balance.objectId,
@@ -632,6 +637,7 @@ export class AuthenticatedStructures {
632
637
  object = ReceivableBalanceObject.create({
633
638
  id: balance.objectId,
634
639
  name: organization.name,
640
+ uri: organization.uri,
635
641
  contacts: thisMembers.map(({ member, responsibilities }) => ReceivableBalanceObjectContact.create({
636
642
  firstName: member.firstName ?? '',
637
643
  lastName: member.lastName ?? '',
@@ -749,15 +755,35 @@ export class AuthenticatedStructures {
749
755
  }
750
756
  }
751
757
 
752
- const struct = ReceivableBalanceStruct.create({
758
+ result.push({
759
+ balance,
760
+ object,
761
+ });
762
+ }
763
+
764
+ return result;
765
+ }
766
+
767
+ static async detailedReceivableBalances(organizationId: string, balances: CachedBalance[]): Promise<DetailedReceivableBalance[]> {
768
+ const items = await this.receivableBalancesHelper(balances);
769
+ const results: DetailedReceivableBalance[] = [];
770
+
771
+ for (const { balance, object } of items) {
772
+ const balanceItems = await CachedBalance.balanceForObjects(organizationId, [balance.objectId], balance.objectType, true);
773
+ const balanceItemsWithPayments = await BalanceItem.getStructureWithPayments(balanceItems);
774
+
775
+ const result = DetailedReceivableBalance.create({
753
776
  ...balance,
754
777
  object,
778
+ balanceItems: balanceItemsWithPayments,
779
+ // todo!!!
780
+ payments: [],
755
781
  });
756
782
 
757
- result.push(struct);
783
+ results.push(result);
758
784
  }
759
785
 
760
- return result;
786
+ return results;
761
787
  }
762
788
 
763
789
  static async auditLogs(logs: AuditLog[]): Promise<AuditLogStruct[]> {
@@ -283,10 +283,16 @@ export class SetupStepUpdater {
283
283
  finishedSteps++;
284
284
  }
285
285
 
286
- setupSteps.update(SetupStepType.Premises, {
287
- totalSteps,
288
- finishedSteps,
289
- });
286
+ if (premiseTypes.length > 0) {
287
+ setupSteps.update(SetupStepType.Premises, {
288
+ totalSteps,
289
+ finishedSteps,
290
+ });
291
+ }
292
+ else {
293
+ // if no premise types, remove step
294
+ setupSteps.remove(SetupStepType.Premises);
295
+ }
290
296
  }
291
297
 
292
298
  private static updateStepGroups(
@@ -90,6 +90,14 @@ export class XlsxTransformerColumnHelper {
90
90
  value: getParent(member)?.email ?? '',
91
91
  }),
92
92
  },
93
+ {
94
+ id: getId('nationalRegisterNumber'),
95
+ name: getName('Rijksregisternummer'),
96
+ width: 20,
97
+ getValue: (member: PlatformMember) => ({
98
+ value: getParent(member)?.nationalRegisterNumber?.toString() ?? '',
99
+ }),
100
+ },
93
101
  XlsxTransformerColumnHelper.createAddressColumns<PlatformMember>({
94
102
  matchId: getId('address'),
95
103
  getAddress: member => getParent(member)?.address,
@@ -40,13 +40,13 @@ export const PaymentReallocationService = {
40
40
  continue;
41
41
  }
42
42
 
43
- const similarDueItems = balanceItems.filter(b => b.id !== balanceItem.id && b.status === BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
43
+ const similarDueItems = balanceItems.filter(b => b.id !== balanceItem.id && b.type === balanceItem.type && b.status === BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
44
44
 
45
45
  if (similarDueItems.length) {
46
46
  // Not possible to merge into one: there are 2 due items
47
47
  continue;
48
48
  }
49
- const similarCanceledItems = balanceItems.filter(b => b.id !== balanceItem.id && b.status !== BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
49
+ const similarCanceledItems = balanceItems.filter(b => b.id !== balanceItem.id && b.type === balanceItem.type && b.status !== BalanceItemStatus.Due && doBalanceItemRelationsMatch(b.relations, balanceItem.relations, 0));
50
50
 
51
51
  if (similarCanceledItems.length) {
52
52
  await this.mergeBalanceItems(balanceItem, similarCanceledItems);
@@ -190,6 +190,7 @@ export const PaymentReallocationService = {
190
190
  payment.type = PaymentType.Reallocation;
191
191
  payment.method = PaymentMethod.Unknown;
192
192
  payment.status = PaymentStatus.Succeeded;
193
+ payment.paidAt = new Date();
193
194
  await payment.save();
194
195
 
195
196
  // Create balance item payments
@@ -161,7 +161,7 @@ export const PaymentService = {
161
161
  if (token) {
162
162
  try {
163
163
  const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
164
- const mollieData = await mollieClient.payments.get(molliePayment.mollieId, {
164
+ let mollieData = await mollieClient.payments.get(molliePayment.mollieId, {
165
165
  testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
166
166
  });
167
167
 
@@ -187,6 +187,22 @@ export const PaymentService = {
187
187
  else if (mollieData.status === MolliePaymentStatus.failed || mollieData.status === MolliePaymentStatus.expired || mollieData.status === MolliePaymentStatus.canceled) {
188
188
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
189
189
  }
190
+ else if ((cancel || this.shouldTryToCancel(payment.status, payment)) && mollieData.isCancelable) {
191
+ console.log('Cancelling Mollie payment on request', payment.id);
192
+ mollieData = await mollieClient.payments.cancel(molliePayment.mollieId);
193
+
194
+ if (mollieData.status === MolliePaymentStatus.paid) {
195
+ await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Succeeded);
196
+ }
197
+ else if (mollieData.status === MolliePaymentStatus.failed || mollieData.status === MolliePaymentStatus.expired || mollieData.status === MolliePaymentStatus.canceled) {
198
+ await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
199
+ }
200
+ else if (this.isManualExpired(payment.status, payment)) {
201
+ // Mollie still returning pending after 1 day: mark as failed
202
+ console.error('Manually marking Mollie payment as expired', payment.id);
203
+ await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
204
+ }
205
+ }
190
206
  else if (this.isManualExpired(payment.status, payment)) {
191
207
  // Mollie still returning pending after 1 day: mark as failed
192
208
  console.error('Manually marking Mollie payment as expired', payment.id);
@@ -1,7 +1,7 @@
1
1
  import { DecodedRequest, Response } from '@simonbackx/simple-endpoints';
2
2
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
3
  import { Organization, Platform, Token, User, Webshop } from '@stamhoofd/models';
4
- import { LoginProviderType, OpenIDClientConfiguration, StartOpenIDFlowStruct, Token as TokenStruct } from '@stamhoofd/structures';
4
+ import { LoginMethod, LoginProviderType, OpenIDClientConfiguration, StartOpenIDFlowStruct, Token as TokenStruct } from '@stamhoofd/structures';
5
5
  import crypto from 'crypto';
6
6
  import { generators, Issuer } from 'openid-client';
7
7
  import { Context } from '../helpers/Context';
@@ -81,8 +81,8 @@ export class SSOService {
81
81
  const platform = await Platform.getShared();
82
82
 
83
83
  const service = new SSOService({ provider, platform, organization, user: Context.user });
84
- // Validate configuration
85
- const _ = service.configuration;
84
+ service.validate();
85
+
86
86
  return service;
87
87
  }
88
88
 
@@ -117,6 +117,51 @@ export class SSOService {
117
117
  return configuration;
118
118
  }
119
119
 
120
+ get loginConfiguration() {
121
+ if (this.organization) {
122
+ throw new SimpleError({
123
+ code: 'invalid_client',
124
+ message: 'Login configuration not yet supported for organization users',
125
+ statusCode: 400,
126
+ });
127
+ }
128
+
129
+ const loginConfiguration = this.platform.config.loginMethods.get(this.provider as unknown as LoginMethod);
130
+ if (!loginConfiguration) {
131
+ throw new SimpleError({
132
+ code: 'invalid_client',
133
+ message: 'SSO not configured (correctly)',
134
+ statusCode: 400,
135
+ });
136
+ }
137
+
138
+ return loginConfiguration;
139
+ }
140
+
141
+ validate() {
142
+ // Validate configuration exists
143
+ const _ = this.configuration;
144
+ const __ = this.loginConfiguration;
145
+
146
+ if (this.user) {
147
+ this.validateEmail(this.user.email);
148
+ }
149
+ }
150
+
151
+ validateEmail(email: string) {
152
+ // Validate configuration
153
+ const loginConfiguration = this.loginConfiguration;
154
+
155
+ if (!loginConfiguration.isEnabledForEmail(email)) {
156
+ throw new SimpleError({
157
+ code: 'invalid_user',
158
+ message: 'User not allowed to use this login method',
159
+ human: 'Je kan deze inlogmethode niet gebruiken',
160
+ statusCode: 400,
161
+ });
162
+ }
163
+ }
164
+
120
165
  async setConfiguration(configuration: OpenIDClientConfiguration) {
121
166
  if (this.provider === LoginProviderType.SSO) {
122
167
  if (this.organization) {
@@ -289,8 +334,15 @@ export class SSOService {
289
334
  const client = await this.getClient();
290
335
  await SSOService.storeSession(response, session);
291
336
 
337
+ const scopes = ['openid', 'email', 'profile'];
338
+
339
+ if (this.provider === LoginProviderType.SSO) {
340
+ // Google doesn't support this scope
341
+ scopes.push('offline_access');
342
+ }
343
+
292
344
  const redirect = client.authorizationUrl({
293
- scope: 'openid email profile',
345
+ scope: scopes.join(' '),
294
346
  code_challenge,
295
347
  code_challenge_method: 'S256',
296
348
  response_mode: 'form_post',
@@ -300,6 +352,9 @@ export class SSOService {
300
352
  prompt: prompt ?? undefined,
301
353
  login_hint: this.user?.email ?? undefined,
302
354
  redirect_uri: this.externalRedirectUri,
355
+
356
+ // Google has this instead of the offline_access scope
357
+ access_type: this.provider === LoginProviderType.Google ? 'offline' : undefined,
303
358
  });
304
359
 
305
360
  response.headers['location'] = redirect;
@@ -387,6 +442,13 @@ export class SSOServiceWithSession {
387
442
  }
388
443
  }
389
444
 
445
+ if (tokenSet.refresh_token) {
446
+ console.log('OK. Refresh token received!');
447
+ }
448
+ else {
449
+ console.log('No refresh token');
450
+ }
451
+
390
452
  if (!claims.email) {
391
453
  throw new SimpleError({
392
454
  code: 'invalid_user',
@@ -403,6 +465,8 @@ export class SSOServiceWithSession {
403
465
  });
404
466
  }
405
467
 
468
+ this.service.validateEmail(claims.email);
469
+
406
470
  // Get user from database
407
471
  let user = await User.getForRegister(this.service.organization?.id ?? null, claims.email);
408
472
  if (!user) {
@@ -1,5 +1,8 @@
1
+ import { SimpleError } from '@simonbackx/simple-errors';
1
2
  import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions, SQLScalar, SQLValueType, baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler } from '@stamhoofd/sql';
3
+ import { AccessRight } from '@stamhoofd/structures';
2
4
  import { Formatter } from '@stamhoofd/utility';
5
+ import { Context } from '../helpers/Context';
3
6
  import { organizationFilterCompilers } from './organizations';
4
7
  import { registrationFilterCompilers } from './registrations';
5
8
 
@@ -44,7 +47,23 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
44
47
 
45
48
  'details.requiresFinancialSupport': createSQLExpressionFilterCompiler(
46
49
  SQL.jsonValue(SQL.column('details'), '$.value.requiresFinancialSupport.value'),
47
- { isJSONValue: true, type: SQLValueType.JSONBoolean },
50
+ { isJSONValue: true, type: SQLValueType.JSONBoolean, checkPermission: async () => {
51
+ const organization = Context.organization;
52
+ if (!organization) {
53
+ return;
54
+ }
55
+
56
+ const permissions = await Context.auth.getOrganizationPermissions(organization);
57
+
58
+ if (!permissions || !permissions.hasAccessRight(AccessRight.MemberReadFinancialData)) {
59
+ throw new SimpleError({
60
+ code: 'permission_denied',
61
+ message: 'No permissions for financial support filter (organization scope).',
62
+ human: 'Je hebt geen toegangsrechten om deze filter te gebruiken.',
63
+ statusCode: 400,
64
+ });
65
+ }
66
+ } },
48
67
  ),
49
68
 
50
69
  'email': createSQLExpressionFilterCompiler(
@@ -22,6 +22,7 @@ import { SetupStepType } from '@stamhoofd/structures';
22
22
  export const organizationFilterCompilers: SQLFilterDefinitions = {
23
23
  ...baseSQLFilterCompilers,
24
24
  id: createSQLExpressionFilterCompiler(SQL.column('organizations', 'id')),
25
+ uriPadded: createSQLExpressionFilterCompiler(SQL.lpad(SQL.column('organizations', 'uri'), 100, '0')),
25
26
  uri: createSQLExpressionFilterCompiler(SQL.column('organizations', 'uri')),
26
27
  name: createSQLExpressionFilterCompiler(
27
28
  SQL.column('organizations', 'name'),