@stamhoofd/backend 2.144.0 → 2.145.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 (99) hide show
  1. package/package.json +19 -19
  2. package/src/crons/mollie-chargebacks.ts +31 -29
  3. package/src/crons/mollie-refunds.ts +61 -59
  4. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
  5. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
  6. package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
  7. package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
  8. package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
  9. package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
  10. package/src/endpoints/auth/MFA.security.test.ts +9 -7
  11. package/src/endpoints/auth/MFA.test.ts +42 -15
  12. package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
  13. package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
  14. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
  15. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
  16. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
  17. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
  18. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
  19. package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
  20. package/src/endpoints/global/files/UploadFile.test.ts +2 -1
  21. package/src/endpoints/global/files/UploadImage.test.ts +2 -1
  22. package/src/endpoints/global/files/upload-security.test.ts +3 -2
  23. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
  24. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
  25. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
  26. package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
  27. package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
  28. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
  29. package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
  30. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
  31. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
  32. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
  33. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
  34. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
  35. package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
  36. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
  37. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
  38. package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
  39. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
  40. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
  41. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
  42. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
  43. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
  44. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
  45. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
  46. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
  47. package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
  48. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
  49. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
  50. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
  51. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
  52. package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
  53. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
  54. package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
  55. package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
  56. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
  57. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
  58. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
  59. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
  60. package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
  61. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
  62. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
  63. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
  64. package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
  65. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
  66. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
  67. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
  68. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
  69. package/src/helpers/Context.ts +13 -6
  70. package/src/helpers/ContextStorage.ts +5 -0
  71. package/src/helpers/StripeSettlementSync.ts +10 -7
  72. package/src/helpers/TwoFactorHelper.ts +21 -22
  73. package/src/services/AdminSessionService.ts +5 -3
  74. package/src/services/InvoiceService.ts +2 -2
  75. package/src/services/MollieService.ts +168 -126
  76. package/src/services/PaymentService.ts +2 -2
  77. package/src/services/SSOService.test.ts +25 -1
  78. package/src/services/SSOService.ts +17 -6
  79. package/src/services/SessionService.test.ts +319 -0
  80. package/src/services/SessionService.ts +357 -0
  81. package/src/services/WebshopCrowdfundingService.test.ts +4 -3
  82. package/src/sql-filters/registrations.ts +59 -1
  83. package/tests/actions/patchUserMember.ts +2 -1
  84. package/tests/e2e/api-rate-limits.test.ts +2 -1
  85. package/tests/e2e/bundle-discounts.test.ts +4 -3
  86. package/tests/e2e/charge-members.test.ts +6 -5
  87. package/tests/e2e/documents.test.ts +4 -3
  88. package/tests/e2e/private-files.test.ts +9 -8
  89. package/tests/e2e/register.test.ts +4 -3
  90. package/tests/e2e/stock.test.ts +4 -3
  91. package/tests/e2e/tickets.test.ts +4 -3
  92. package/tests/helpers/ExportSlice.ts +2 -1
  93. package/tests/helpers/MollieMocker.ts +85 -23
  94. package/tests/init/initAdmin.ts +2 -1
  95. package/tests/init/initPlatformAdmin.ts +2 -1
  96. package/src/helpers/ViesHelper.test.ts +0 -401
  97. package/src/helpers/ViesHelper.ts +0 -222
  98. package/src/services/PeppolDirectoryService.test.ts +0 -139
  99. package/src/services/PeppolDirectoryService.ts +0 -111
@@ -0,0 +1,5 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+
3
+ import type { ContextInstance } from './Context.js';
4
+
5
+ export const contextStorage = new AsyncLocalStorage<ContextInstance>();
@@ -512,7 +512,7 @@ export class StripeSettlementSync {
512
512
  case 'payment': {
513
513
  const payment = await this.#resolvePayment(transaction);
514
514
 
515
- if (payment.organizationId !== settlement.organizationId) {
515
+ if (!payment || payment.organizationId !== settlement.organizationId) {
516
516
  // Destination charge via the platform
517
517
  await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
518
518
  return;
@@ -692,7 +692,7 @@ export class StripeSettlementSync {
692
692
  continue;
693
693
  }
694
694
 
695
- const charge = await SettlementService.upsertCharge({
695
+ await SettlementService.upsertCharge({
696
696
  type: getFeeDetailType(detail, transaction),
697
697
  externalId: transaction.id + ':fee:' + index,
698
698
  amount: -detail.amount * 100,
@@ -826,7 +826,7 @@ export class StripeSettlementSync {
826
826
  await payment.save();
827
827
  }
828
828
 
829
- async #resolvePayment(transaction: Stripe.BalanceTransaction): Promise<Payment> {
829
+ async #resolvePayment(transaction: Stripe.BalanceTransaction): Promise<Payment | null> {
830
830
  const source = transaction.source;
831
831
  if (!source || typeof source === 'string' || source.object !== 'charge') {
832
832
  throw new SimpleError({
@@ -848,10 +848,13 @@ export class StripeSettlementSync {
848
848
 
849
849
  const payment = await Payment.getByID(paymentId);
850
850
  if (!payment) {
851
- throw new SimpleError({
852
- code: 'payment_not_found',
853
- message: 'Payment ' + paymentId + ' referenced by charge ' + source.id + ' does not exist',
854
- });
851
+ // Probably deleted
852
+
853
+ return null;
854
+ // throw new SimpleError({
855
+ // code: 'payment_not_found',
856
+ // message: 'Payment ' + paymentId + ' referenced by charge ' + source.id + ' does not exist',
857
+ // });
855
858
  }
856
859
 
857
860
  this.#assertPaymentScope(payment, source.id);
@@ -1,13 +1,14 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import type { I18n } from '@stamhoofd/backend-i18n/I18n';
3
- import type { User } from '@stamhoofd/models';
4
- import { MFARecoveryCode, MFATOTP, MFAToken, Organization, Platform, RateLimiter, Token, WebauthnCredential } from '@stamhoofd/models';
3
+ import type { User, Token } from '@stamhoofd/models';
4
+ import { MFARecoveryCode, MFATOTP, MFAToken, Organization, Platform, RateLimiter, WebauthnCredential } from '@stamhoofd/models';
5
5
  import type { User as UserStruct } from '@stamhoofd/structures';
6
- import { MFAChallengeResponse, MFAEnrollmentResult, MFAMethodType, MFASetupResponse, MFAStatus, PasskeyCredential, RecoveryCodes, TOTPCredential, Token as TokenStruct } from '@stamhoofd/structures';
6
+ import { MFAChallengeResponse, MFAEnrollmentResult, MFAMethodType, MFASetupResponse, MFAStatus, PasskeyCredential, RecoveryCodes, SessionLoginMethod, TOTPCredential, Token as TokenStruct } from '@stamhoofd/structures';
7
7
 
8
8
  import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
9
9
 
10
10
  import { PasswordForgotService } from '../services/PasswordForgotService.js';
11
+ import { SessionService } from '../services/SessionService.js';
11
12
  import { RecoveryCodeHelper } from './RecoveryCodeHelper.js';
12
13
  import { WebauthnHelper } from './WebauthnHelper.js';
13
14
  import { Formatter, Sorter } from '@stamhoofd/utility';
@@ -133,33 +134,33 @@ export class TwoFactorHelper {
133
134
  * credential was accepted.
134
135
  *
135
136
  * `loginMethod` describes the credential that was just verified:
136
- * - 'password': the account password. A single credential that says nothing about
137
+ * - Password: the account password. A single credential that says nothing about
137
138
  * whether the user still reads the email address on the account, so a required
138
139
  * second factor must be set up here if the user does not have one yet — and a
139
140
  * long-inactive admin has to confirm their email address before they may.
140
- * - 'email': a password token or email verification code. Also a single credential,
141
+ * - Email: a password token or email verification code. Also a single credential,
141
142
  * but one that only reaches someone who reads the account's email, so it is the way
142
143
  * out of that email confirmation.
143
- * - 'sso': an external identity provider already authenticated the user, and is
144
+ * - SSO: an external identity provider already authenticated the user, and is
144
145
  * trusted to apply its own second factor. An enrolled factor is still verified
145
146
  * (the user asked us to protect their account), but we do not force enrollment —
146
147
  * unless the account ALSO has a password, because then the password remains a way
147
148
  * in that bypasses whatever the provider enforces.
148
149
  */
149
- static async getSecondFactorRequirement(user: User, organization: Organization | null, { loginMethod }: { loginMethod: 'password' | 'email' | 'sso' }): Promise<SecondFactorRequirement> {
150
+ static async getSecondFactorRequirement(user: User, organization: Organization | null, { loginMethod }: { loginMethod: SessionLoginMethod }): Promise<SecondFactorRequirement> {
150
151
  if (await TwoFactorHelper.userHasFactors(user.id)) {
151
- return { type: 'challenge', challenge: await TwoFactorHelper.createLoginChallenge(user) };
152
+ return { type: 'challenge', challenge: await TwoFactorHelper.createLoginChallenge(user, { loginMethod }) };
152
153
  }
153
154
 
154
- if (loginMethod === 'sso' && !user.hasPasswordBasedAccount()) {
155
+ if (loginMethod === SessionLoginMethod.SSO && !user.hasPasswordBasedAccount()) {
155
156
  return { type: 'none' };
156
157
  }
157
158
 
158
159
  if (await TwoFactorHelper.isTwoFactorRequired(user, organization)) {
159
- if (loginMethod === 'password' && TwoFactorHelper.isInactiveForEnrollment(user)) {
160
+ if (loginMethod === SessionLoginMethod.Password && TwoFactorHelper.isInactiveForEnrollment(user)) {
160
161
  return { type: 'confirm-email' };
161
162
  }
162
- return { type: 'setup', setupToken: await MFAToken.createFor(user.id, 'setup') };
163
+ return { type: 'setup', setupToken: await MFAToken.createFor(user.id, 'setup', { loginMethod }) };
163
164
  }
164
165
 
165
166
  return { type: 'none' };
@@ -173,16 +174,14 @@ export class TwoFactorHelper {
173
174
  static async sendEnrollmentConfirmationEmail(user: User, organization: Organization | null, i18n: I18n): Promise<void> {
174
175
  try {
175
176
  enrollmentConfirmationRateLimiter.track(user.id);
176
- }
177
- catch {
177
+ } catch {
178
178
  // Sent often enough already.
179
179
  return;
180
180
  }
181
181
 
182
182
  try {
183
183
  await PasswordForgotService.sendPasswordRecoveryEmail(user, organization, i18n);
184
- }
185
- catch (e) {
184
+ } catch (e) {
186
185
  console.error('Could not send the two-factor enrollment confirmation email', e);
187
186
  }
188
187
  }
@@ -203,7 +202,7 @@ export class TwoFactorHelper {
203
202
  * whoever holds the link could enroll one and get a session anyway, and the client
204
203
  * needs a session to let the user choose a password before enrolling.
205
204
  */
206
- static async assertSecondFactorOrThrow(user: User, organization: Organization | null, version: number, { loginMethod, i18n, allowTemporarySession = false }: { loginMethod: 'password' | 'email'; i18n: I18n; allowTemporarySession?: boolean }): Promise<void> {
205
+ static async assertSecondFactorOrThrow(user: User, organization: Organization | null, version: number, { loginMethod, i18n, allowTemporarySession = false }: { loginMethod: SessionLoginMethod.Password | SessionLoginMethod.Email; i18n: I18n; allowTemporarySession?: boolean }): Promise<void> {
207
206
  const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod });
208
207
 
209
208
  if (requirement.type === 'confirm-email') {
@@ -228,7 +227,7 @@ export class TwoFactorHelper {
228
227
  }
229
228
 
230
229
  if (requirement.type === 'setup') {
231
- const temporaryToken = allowTemporarySession ? new TokenStruct(await Token.createToken(user, new Date())) : null;
230
+ const temporaryToken = allowTemporarySession ? new TokenStruct(await SessionService.createSession(user, { loginMethod, authenticatedAt: new Date() })) : null;
232
231
 
233
232
  throw new SimpleError({
234
233
  code: 'require_mfa_setup',
@@ -313,7 +312,7 @@ export class TwoFactorHelper {
313
312
  /**
314
313
  * Create a login MFA session token and the challenge payload returned to the client.
315
314
  */
316
- static async createLoginChallenge(user: User): Promise<MFAChallengeResponse> {
315
+ static async createLoginChallenge(user: User, { loginMethod }: { loginMethod: SessionLoginMethod }): Promise<MFAChallengeResponse> {
317
316
  const { methods, passkeys } = await TwoFactorHelper.getEnrolledMethods(user.id);
318
317
 
319
318
  let webauthnOptions: PublicKeyCredentialRequestOptionsJSON | null = null;
@@ -323,7 +322,7 @@ export class TwoFactorHelper {
323
322
  challenge = webauthnOptions.challenge;
324
323
  }
325
324
 
326
- const mfaToken = await MFAToken.createFor(user.id, 'login', challenge);
325
+ const mfaToken = await MFAToken.createFor(user.id, 'login', { webauthnChallenge: challenge, loginMethod });
327
326
 
328
327
  return MFAChallengeResponse.create({
329
328
  token: mfaToken.token,
@@ -376,12 +375,12 @@ export class TwoFactorHelper {
376
375
  }
377
376
 
378
377
  // Before minting the new session, so it is never signed out by its own enrollment.
379
- await Token.deleteOtherSessions(user.id, currentToken?.accessToken ?? null);
378
+ await SessionService.deleteOtherSessions({ userId: user.id, keepAccessToken: currentToken?.accessToken ?? null });
380
379
 
381
380
  let token: TokenStruct | null = null;
382
381
  if (setupToken) {
383
382
  await setupToken.consume();
384
- const t = await Token.createToken(user, new Date());
383
+ const t = await SessionService.createSession(user, { loginMethod: setupToken.loginMethod, authenticatedAt: new Date() });
385
384
  await user.markActive();
386
385
  token = new TokenStruct(t);
387
386
  }
@@ -403,7 +402,7 @@ export class TwoFactorHelper {
403
402
  await MFARecoveryCode.deleteForUser(user.id);
404
403
  }
405
404
 
406
- await Token.deleteOtherSessions(user.id, currentToken?.accessToken ?? null);
405
+ await SessionService.deleteOtherSessions({ userId: user.id, keepAccessToken: currentToken?.accessToken ?? null });
407
406
 
408
407
  return await TwoFactorHelper.buildStatus(user);
409
408
  }
@@ -1,4 +1,6 @@
1
- import { Token, User } from '@stamhoofd/models';
1
+ import { User } from '@stamhoofd/models';
2
+
3
+ import { SessionService } from './SessionService.js';
2
4
 
3
5
  /**
4
6
  * Signs out administrators in bulk.
@@ -25,7 +27,7 @@ export class AdminSessionService {
25
27
  */
26
28
  static async signOutOrganizationAdmins(organizationId: string, keepAccessToken: string | null): Promise<number> {
27
29
  const admins = await User.getAdmins(organizationId);
28
- return await Token.deleteForUsers(admins.map(a => a.id), keepAccessToken);
30
+ return await SessionService.deleteForUsers({ userIds: admins.map(a => a.id), keepAccessToken });
29
31
  }
30
32
 
31
33
  /**
@@ -36,6 +38,6 @@ export class AdminSessionService {
36
38
  */
37
39
  static async signOutPlatformAdmins(keepAccessToken: string | null): Promise<number> {
38
40
  const admins = await User.getPlatformAdmins();
39
- return await Token.deleteForUsers(admins.map(a => a.id), keepAccessToken);
41
+ return await SessionService.deleteForUsers({ userIds: admins.map(a => a.id), keepAccessToken });
40
42
  }
41
43
  }
@@ -6,7 +6,7 @@ import { InvoiceCounter } from '@stamhoofd/models/helpers/InvoiceCounter.js';
6
6
  import type { Invoice as InvoiceStruct } from '@stamhoofd/structures';
7
7
  import { EmailTemplateType, PaymentStatus, Recipient, Replacement } from '@stamhoofd/structures';
8
8
  import { Formatter } from '@stamhoofd/utility';
9
- import { ViesHelper } from '../helpers/ViesHelper.js';
9
+ import { ViesService } from '@stamhoofd/vies';
10
10
  import { WebmasterReport } from '../helpers/WebmasterReport.js';
11
11
  import { ApplicationFeeService } from './ApplicationFeeService.js';
12
12
  import { BalanceItemService } from './BalanceItemService.js';
@@ -35,7 +35,7 @@ export class InvoiceService {
35
35
  model.customer = struct.customer;
36
36
 
37
37
  if (model.customer.company) {
38
- await ViesHelper.checkCompany(model.customer.company, model.customer.company);
38
+ await ViesService.checkCompany(model.customer.company, null, { forceValidation: true });
39
39
  }
40
40
 
41
41
  struct.updatePrices();