@stamhoofd/backend 2.137.5 → 2.138.1
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 +20 -17
- package/src/boot.ts +5 -0
- package/src/crons/balance-emails.ts +2 -1
- package/src/crons/delete-expired-mfa-tokens.ts +35 -0
- package/src/crons/index.ts +1 -0
- package/src/crons/invoices.ts +5 -3
- package/src/email-recipient-loaders/orders.ts +2 -1
- package/src/email-recipient-loaders/organizations.ts +3 -2
- package/src/endpoints/auth/ConfirmTOTPEndpoint.ts +84 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +2 -1
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +61 -1
- package/src/endpoints/auth/CreateTokenEndpoint.ts +133 -6
- package/src/endpoints/auth/DeletePasskeyEndpoint.ts +61 -0
- package/src/endpoints/auth/DeleteTOTPEndpoint.ts +62 -0
- package/src/endpoints/auth/DeleteUserEndpoint.ts +1 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +2 -1
- package/src/endpoints/auth/GetMFAChallengeEndpoint.ts +56 -0
- package/src/endpoints/auth/GetMFAStatusEndpoint.ts +31 -0
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -1
- package/src/endpoints/auth/MFA.security.test.ts +688 -0
- package/src/endpoints/auth/MFA.test.ts +1398 -0
- package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +2 -2
- package/src/endpoints/auth/RegenerateRecoveryCodesEndpoint.ts +51 -0
- package/src/endpoints/auth/RegisterPasskeyEndpoint.ts +99 -0
- package/src/endpoints/auth/RegisterPasskeyOptionsEndpoint.ts +49 -0
- package/src/endpoints/auth/SetupTOTPEndpoint.ts +54 -0
- package/src/endpoints/auth/SignupEndpoint.ts +13 -2
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +16 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +3 -2
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +3 -2
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +2 -1
- package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -1
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +2 -1
- package/src/endpoints/global/files/UploadFile.ts +1 -1
- package/src/endpoints/global/files/UploadImage.ts +1 -1
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.ts +2 -1
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +4 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +35 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +8 -0
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +92 -0
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +32 -3
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +100 -0
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +5 -5
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +160 -0
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.ts +44 -0
- package/src/helpers/AuthenticatedStructures.ts +9 -1
- package/src/helpers/Context.ts +114 -4
- package/src/helpers/EmailBuilder.test.ts +287 -0
- package/src/helpers/EmailBuilder.ts +811 -0
- package/src/helpers/EmailResumer.ts +2 -1
- package/src/helpers/ForwardHandler.ts +2 -1
- package/src/helpers/MFAEncryption.ts +34 -0
- package/src/helpers/RecoveryCodeHelper.ts +81 -0
- package/src/helpers/TOTPHelper.ts +79 -0
- package/src/helpers/TenantContext.test.ts +96 -0
- package/src/helpers/TenantContext.ts +67 -0
- package/src/helpers/TwoFactorHelper.ts +356 -0
- package/src/helpers/WebauthnHelper.test.ts +144 -0
- package/src/helpers/WebauthnHelper.ts +239 -0
- package/src/helpers/data/aaguids.json +1006 -0
- package/src/middleware/TenantScopeMiddleware.test.ts +41 -0
- package/src/middleware/TenantScopeMiddleware.ts +24 -0
- package/src/seeds/1785417302-fill-user-last-active-at.sql +6 -0
- package/src/services/AdminSessionService.ts +41 -0
- package/src/services/EmailPreviewService.ts +1 -1
- package/src/services/EmailSendService.test.ts +1218 -0
- package/src/services/EmailSendService.ts +705 -0
- package/src/services/EventNotificationService.ts +2 -1
- package/src/services/InvoicePdfService.ts +3 -3
- package/src/services/InvoiceService.ts +4 -2
- package/src/services/InvoiceXMLService.ts +2 -1
- package/src/services/OrderService.ts +2 -1
- package/src/services/OrganizationAdminService.test.ts +47 -0
- package/src/services/OrganizationAdminService.ts +139 -0
- package/src/services/OrganizationEmailService.ts +3 -2
- package/src/services/PaymentService.ts +5 -3
- package/src/services/ReferralService.ts +3 -2
- package/src/services/RegistrationService.ts +2 -1
- package/src/services/SSOService.ts +106 -14
- package/src/services/STPackageService.ts +4 -2
- package/src/services/TwoFactorAuditLogService.ts +64 -0
- package/src/services/VerificationCodeService.ts +2 -1
- package/tests/e2e/api-rate-limits.test.ts +1 -1
- package/tests/helpers/MFATestHelper.ts +42 -0
- package/tests/helpers/TestServer.ts +4 -0
- package/tests/helpers/index.ts +1 -0
- package/tsconfig.build.json +2 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DecodedRequest } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { ROOT_TENANT_ID } from '@stamhoofd/models';
|
|
4
|
+
import { testServer } from '../../tests/helpers/TestServer.js';
|
|
5
|
+
import { TenantContext } from '../helpers/TenantContext.js';
|
|
6
|
+
|
|
7
|
+
type Params = Record<string, never>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reports the tenant scope it was reached in, so a request can assert what the middleware set up.
|
|
11
|
+
*/
|
|
12
|
+
class TenantProbeEndpoint extends Endpoint<Params, undefined, undefined, string> {
|
|
13
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
14
|
+
if (request.method !== 'GET' || request.url !== '/tenant-probe') {
|
|
15
|
+
return [false];
|
|
16
|
+
}
|
|
17
|
+
return [true, {}];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async handle(_request: DecodedRequest<Params, undefined, undefined>) {
|
|
21
|
+
return new Response(TenantContext.optional?.tenantId ?? 'no-tenant');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('TenantScopeMiddleware', () => {
|
|
26
|
+
test('a request runs in a tenant scope', async () => {
|
|
27
|
+
const endpoint = new TenantProbeEndpoint();
|
|
28
|
+
|
|
29
|
+
const response = await testServer.test(endpoint, Request.buildJson('GET', '/tenant-probe', 'localhost'));
|
|
30
|
+
|
|
31
|
+
expect(response.body).toBe(ROOT_TENANT_ID);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('the scope does not outlive the request', async () => {
|
|
35
|
+
const endpoint = new TenantProbeEndpoint();
|
|
36
|
+
|
|
37
|
+
await testServer.test(endpoint, Request.buildJson('GET', '/tenant-probe', 'localhost'));
|
|
38
|
+
|
|
39
|
+
expect(TenantContext.optional).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Request, RequestMiddleware } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { ROOT_TENANT_ID } from '@stamhoofd/models';
|
|
3
|
+
|
|
4
|
+
import { TenantContext } from '../helpers/TenantContext.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Puts every request in a tenant scope.
|
|
8
|
+
*
|
|
9
|
+
* Every request resolves to the deployment's root tenant for now. Resolving it from the host is a
|
|
10
|
+
* later step; this exists so the scope is already there when it starts to matter.
|
|
11
|
+
*
|
|
12
|
+
* Register this *after* ContextMiddleware: wrapRun wrappers are applied in registration order, so
|
|
13
|
+
* the last one registered is the outermost. The tenant has to be in scope before the context that
|
|
14
|
+
* reads it.
|
|
15
|
+
*/
|
|
16
|
+
export const TenantScopeMiddleware: RequestMiddleware = {
|
|
17
|
+
wrapRun<T>(run: () => Promise<T>, _request: Request) {
|
|
18
|
+
return TenantContext.run(ROOT_TENANT_ID, run);
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
handleRequest() {
|
|
22
|
+
// Noop
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Token, User } from '@stamhoofd/models';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Signs out administrators in bulk.
|
|
5
|
+
*
|
|
6
|
+
* Making two-factor authentication required only affects new logins: it is evaluated when a
|
|
7
|
+
* session is created, so admins that are already signed in keep their session and never get
|
|
8
|
+
* asked to enroll. An administrator can therefore ask us to end those sessions, which forces
|
|
9
|
+
* every other admin through the login flow again — and with that through the forced
|
|
10
|
+
* enrollment.
|
|
11
|
+
*
|
|
12
|
+
* API users are never signed out: their tokens are machine credentials with a long lifetime
|
|
13
|
+
* that cannot complete an interactive login. User.getAdmins()/getPlatformAdmins() already
|
|
14
|
+
* leave them out.
|
|
15
|
+
*/
|
|
16
|
+
export class AdminSessionService {
|
|
17
|
+
/**
|
|
18
|
+
* Sign out every administrator of an organization.
|
|
19
|
+
*
|
|
20
|
+
* `keepAccessToken` is the session of the administrator making the request: signing
|
|
21
|
+
* themselves out would interrupt what they are doing, and they are forced to enroll on
|
|
22
|
+
* their next login anyway.
|
|
23
|
+
*
|
|
24
|
+
* Returns the number of ended sessions.
|
|
25
|
+
*/
|
|
26
|
+
static async signOutOrganizationAdmins(organizationId: string, keepAccessToken: string | null): Promise<number> {
|
|
27
|
+
const admins = await User.getAdmins(organizationId);
|
|
28
|
+
return await Token.deleteForUsers(admins.map(a => a.id), keepAccessToken);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sign out every administrator of the platform. Administrators that only have
|
|
33
|
+
* permissions within an organization are not affected.
|
|
34
|
+
*
|
|
35
|
+
* Returns the number of ended sessions.
|
|
36
|
+
*/
|
|
37
|
+
static async signOutPlatformAdmins(keepAccessToken: string | null): Promise<number> {
|
|
38
|
+
const admins = await User.getPlatformAdmins();
|
|
39
|
+
return await Token.deleteForUsers(admins.map(a => a.id), keepAccessToken);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { I18n } from '@stamhoofd/backend-i18n/I18n';
|
|
2
2
|
import type { Email } from '@stamhoofd/models';
|
|
3
3
|
import { EmailRecipient, Organization, User } from '@stamhoofd/models';
|
|
4
|
-
import { fillRecipientReplacements, mergeReplacementsIfEqual, removeUnusedReplacements, runWithRecipientLocale, stripRecipientReplacementsForWebDisplay, stripSensitiveRecipientReplacements } from '
|
|
4
|
+
import { fillRecipientReplacements, mergeReplacementsIfEqual, removeUnusedReplacements, runWithRecipientLocale, stripRecipientReplacementsForWebDisplay, stripSensitiveRecipientReplacements } from '../helpers/EmailBuilder.js';
|
|
5
5
|
import type { BaseOrganization, EmailRecipient as EmailRecipientStruct, Replacement, User as UserStruct } from '@stamhoofd/structures';
|
|
6
6
|
import { EmailPreview, EmailRecipientFilter, EmailWithRecipients, getExampleRecipient } from '@stamhoofd/structures';
|
|
7
7
|
import { ExampleReplacements } from '@stamhoofd/structures/email/exampleReplacements.js';
|