@stamhoofd/backend 2.137.5 → 2.138.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 (91) hide show
  1. package/package.json +20 -17
  2. package/src/boot.ts +5 -0
  3. package/src/crons/balance-emails.ts +2 -1
  4. package/src/crons/delete-expired-mfa-tokens.ts +35 -0
  5. package/src/crons/index.ts +1 -0
  6. package/src/crons/invoices.ts +5 -3
  7. package/src/email-recipient-loaders/orders.ts +2 -1
  8. package/src/email-recipient-loaders/organizations.ts +3 -2
  9. package/src/endpoints/auth/ConfirmTOTPEndpoint.ts +84 -0
  10. package/src/endpoints/auth/CreateAdminEndpoint.ts +2 -1
  11. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +61 -1
  12. package/src/endpoints/auth/CreateTokenEndpoint.ts +133 -6
  13. package/src/endpoints/auth/DeletePasskeyEndpoint.ts +61 -0
  14. package/src/endpoints/auth/DeleteTOTPEndpoint.ts +62 -0
  15. package/src/endpoints/auth/DeleteUserEndpoint.ts +1 -1
  16. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +2 -1
  17. package/src/endpoints/auth/GetMFAChallengeEndpoint.ts +56 -0
  18. package/src/endpoints/auth/GetMFAStatusEndpoint.ts +31 -0
  19. package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -1
  20. package/src/endpoints/auth/MFA.security.test.ts +688 -0
  21. package/src/endpoints/auth/MFA.test.ts +1398 -0
  22. package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +2 -2
  23. package/src/endpoints/auth/RegenerateRecoveryCodesEndpoint.ts +51 -0
  24. package/src/endpoints/auth/RegisterPasskeyEndpoint.ts +99 -0
  25. package/src/endpoints/auth/RegisterPasskeyOptionsEndpoint.ts +49 -0
  26. package/src/endpoints/auth/SetupTOTPEndpoint.ts +54 -0
  27. package/src/endpoints/auth/SignupEndpoint.ts +13 -2
  28. package/src/endpoints/auth/VerifyEmailEndpoint.ts +16 -0
  29. package/src/endpoints/global/email/CreateEmailEndpoint.ts +3 -2
  30. package/src/endpoints/global/email/PatchEmailEndpoint.ts +3 -2
  31. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +2 -1
  32. package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -1
  33. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +2 -1
  34. package/src/endpoints/global/files/UploadFile.ts +1 -1
  35. package/src/endpoints/global/files/UploadImage.ts +1 -1
  36. package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.ts +2 -1
  37. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +4 -1
  38. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +35 -1
  39. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +8 -0
  40. package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +92 -0
  41. package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.ts +43 -0
  42. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
  43. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +3 -2
  44. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +32 -3
  45. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +4 -1
  46. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +100 -0
  47. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +4 -1
  48. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +5 -5
  49. package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +160 -0
  50. package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.ts +44 -0
  51. package/src/helpers/AuthenticatedStructures.ts +9 -1
  52. package/src/helpers/Context.ts +114 -4
  53. package/src/helpers/EmailBuilder.test.ts +287 -0
  54. package/src/helpers/EmailBuilder.ts +811 -0
  55. package/src/helpers/EmailResumer.ts +2 -1
  56. package/src/helpers/ForwardHandler.ts +2 -1
  57. package/src/helpers/MFAEncryption.ts +34 -0
  58. package/src/helpers/RecoveryCodeHelper.ts +81 -0
  59. package/src/helpers/TOTPHelper.ts +79 -0
  60. package/src/helpers/TenantContext.test.ts +96 -0
  61. package/src/helpers/TenantContext.ts +67 -0
  62. package/src/helpers/TwoFactorHelper.ts +356 -0
  63. package/src/helpers/WebauthnHelper.ts +211 -0
  64. package/src/helpers/data/aaguids.json +1006 -0
  65. package/src/middleware/TenantScopeMiddleware.test.ts +41 -0
  66. package/src/middleware/TenantScopeMiddleware.ts +24 -0
  67. package/src/seeds/1785417302-fill-user-last-active-at.sql +6 -0
  68. package/src/services/AdminSessionService.ts +41 -0
  69. package/src/services/EmailPreviewService.ts +1 -1
  70. package/src/services/EmailSendService.test.ts +1218 -0
  71. package/src/services/EmailSendService.ts +705 -0
  72. package/src/services/EventNotificationService.ts +2 -1
  73. package/src/services/InvoicePdfService.ts +3 -3
  74. package/src/services/InvoiceService.ts +4 -2
  75. package/src/services/InvoiceXMLService.ts +2 -1
  76. package/src/services/OrderService.ts +2 -1
  77. package/src/services/OrganizationAdminService.test.ts +47 -0
  78. package/src/services/OrganizationAdminService.ts +139 -0
  79. package/src/services/OrganizationEmailService.ts +3 -2
  80. package/src/services/PaymentService.ts +5 -3
  81. package/src/services/ReferralService.ts +3 -2
  82. package/src/services/RegistrationService.ts +2 -1
  83. package/src/services/SSOService.ts +106 -14
  84. package/src/services/STPackageService.ts +4 -2
  85. package/src/services/TwoFactorAuditLogService.ts +64 -0
  86. package/src/services/VerificationCodeService.ts +2 -1
  87. package/tests/e2e/api-rate-limits.test.ts +1 -1
  88. package/tests/helpers/MFATestHelper.ts +42 -0
  89. package/tests/helpers/TestServer.ts +4 -0
  90. package/tests/helpers/index.ts +1 -0
  91. package/tsconfig.build.json +2 -1
@@ -0,0 +1,64 @@
1
+ import type { User } from '@stamhoofd/models';
2
+ import { AuditLog } from '@stamhoofd/models';
3
+ import type { MFAMethodType } from '@stamhoofd/structures';
4
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
5
+
6
+ /**
7
+ * Records the changes a user makes to their own two-factor authentication.
8
+ *
9
+ * These are security-relevant events that are not visible anywhere else: the credentials
10
+ * themselves are not audit-logged models, and an attacker who took over a session would
11
+ * use exactly these endpoints to lock the owner out. So they are logged explicitly, at the
12
+ * moment the change is effective.
13
+ */
14
+ export class TwoFactorAuditLogService {
15
+ static async logMethodAdded(user: User, method: MFAMethodType, name: string) {
16
+ await this.save(AuditLogType.UserTwoFactorMethodAdded, user, method, name);
17
+ }
18
+
19
+ static async logMethodDeleted(user: User, method: MFAMethodType, name: string) {
20
+ await this.save(AuditLogType.UserTwoFactorMethodDeleted, user, method, name);
21
+ }
22
+
23
+ /**
24
+ * Only for an explicit regeneration by the user. The batch that is generated together
25
+ * with a first factor is already covered by the method-added log.
26
+ */
27
+ static async logRecoveryCodesRegenerated(user: User) {
28
+ await this.save(AuditLogType.UserRecoveryCodesRegenerated, user, null, '');
29
+ }
30
+
31
+ private static async save(type: AuditLogType, user: User, method: MFAMethodType | null, name: string) {
32
+ const log = new AuditLog();
33
+ log.type = type;
34
+ log.source = AuditLogSource.User;
35
+
36
+ // A user changes their own factors, so the account is both the actor and the subject.
37
+ log.userId = user.id;
38
+ log.objectId = user.id;
39
+
40
+ // Deliberately the organization of the account itself, not the one the request was
41
+ // scoped to: a platform administrator enrolling a factor while working in an
42
+ // organization is not that organization's business.
43
+ log.organizationId = user.organizationId;
44
+
45
+ log.replacements = new Map([
46
+ ['u', AuditLogReplacement.create({
47
+ id: user.id,
48
+ value: user.email,
49
+ type: AuditLogReplacementType.User,
50
+ })],
51
+ ]);
52
+
53
+ const methodReplacement = AuditLogReplacement.enum('MFAMethodType', method);
54
+ if (methodReplacement) {
55
+ log.replacements.set('method', methodReplacement);
56
+ }
57
+
58
+ if (name) {
59
+ log.replacements.set('name', AuditLogReplacement.string(name));
60
+ }
61
+
62
+ await log.save();
63
+ }
64
+ }
@@ -1,6 +1,7 @@
1
1
  import type { I18n } from '@stamhoofd/backend-i18n';
2
2
  import type { Organization } from '@stamhoofd/models';
3
- import { EmailVerificationCode, Platform, sendEmailTemplate, User } from '@stamhoofd/models';
3
+ import { EmailVerificationCode, Platform, User } from '@stamhoofd/models';
4
+ import { sendEmailTemplate } from '../helpers/EmailBuilder.js';
4
5
  import { EmailTemplateType, getAppHost, Recipient, Replacement } from '@stamhoofd/structures';
5
6
 
6
7
  /**
@@ -29,7 +29,7 @@ describe('E2E.APIRateLimits', () => {
29
29
  level: PermissionLevel.Full,
30
30
  }),
31
31
  }).create();
32
- const token = await Token.createToken(user);
32
+ const token = await Token.createToken(user, new Date());
33
33
 
34
34
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
35
35
  permissions: UserPermissions.create({
@@ -0,0 +1,42 @@
1
+ import type { User } from '@stamhoofd/models';
2
+ import { MFATOTP } from '@stamhoofd/models';
3
+ import { authenticator } from 'otplib';
4
+
5
+ import { RecoveryCodeHelper } from '../../src/helpers/RecoveryCodeHelper.js';
6
+ import { TOTPHelper } from '../../src/helpers/TOTPHelper.js';
7
+
8
+ /**
9
+ * Set up two-factor authentication in tests, without going through the enrollment flow.
10
+ * Also used by the Playwright tests, which drive the real UI but need to know the secret
11
+ * of an authenticator app to generate its codes.
12
+ */
13
+ export const MFATestHelper = {
14
+ /**
15
+ * Enroll a confirmed authenticator app and return its plaintext secret.
16
+ */
17
+ async addConfirmedTOTP(user: User, name = 'Test authenticator'): Promise<{ id: string; secret: string }> {
18
+ const totp = new MFATOTP();
19
+ const secret = authenticator.generateSecret();
20
+ totp.userId = user.id;
21
+ totp.name = name;
22
+ totp.secret = TOTPHelper.encrypt(secret);
23
+ totp.confirmedAt = new Date();
24
+ await totp.save();
25
+ return { id: totp.id, secret };
26
+ },
27
+
28
+ /**
29
+ * The code an authenticator app would show right now for this secret.
30
+ */
31
+ generateTOTPCode(secret: string): string {
32
+ return authenticator.generate(secret);
33
+ },
34
+
35
+ /**
36
+ * Whether this recovery code is one of the user's valid codes. Consumes it, just like
37
+ * using it to log in would.
38
+ */
39
+ async useRecoveryCode(userId: string, code: string): Promise<boolean> {
40
+ return await RecoveryCodeHelper.consume(userId, code);
41
+ },
42
+ };
@@ -3,12 +3,16 @@ import { VersionMiddleware } from '@stamhoofd/backend-middleware';
3
3
  import { Version } from '@stamhoofd/structures';
4
4
 
5
5
  import { ContextMiddleware } from '../../src/middleware/ContextMiddleware.js';
6
+ import { TenantScopeMiddleware } from '../../src/middleware/TenantScopeMiddleware.js';
6
7
  import { FileSignService } from '../../src/services/FileSignService.js';
7
8
 
8
9
  export const testServer = new TestServer();
9
10
 
10
11
  // Contexts
11
12
  testServer.addRequestMiddleware(ContextMiddleware);
13
+
14
+ // Registration order mirrors boot.ts
15
+ testServer.addRequestMiddleware(TenantScopeMiddleware);
12
16
  testServer.addResponseMiddleware(FileSignService);
13
17
  testServer.addRequestMiddleware(FileSignService);
14
18
 
@@ -1,3 +1,4 @@
1
+ export * from './MFATestHelper.js';
1
2
  export * from './MollieMocker.js';
2
3
  export * from './PayconiqMocker.js';
3
4
  export * from './StripeMocker.js';
@@ -8,7 +8,8 @@
8
8
  "./index.ts",
9
9
  "./migrations.ts",
10
10
  "./src",
11
- "../../stamhoofd.d.ts"
11
+ "../../stamhoofd.d.ts",
12
+ "./src/**/*.json",
12
13
  ],
13
14
  "exclude": [
14
15
  "./src/**/*.spec.ts",