@stamhoofd/backend 2.137.4 → 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.
- package/package.json +26 -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/drip-emails.ts +2 -1
- package/src/crons/index.ts +1 -0
- package/src/crons/invoices.ts +5 -3
- package/src/crons.ts +4 -2
- package/src/email-recipient-loaders/orders.ts +2 -1
- package/src/email-recipient-loaders/organizations.ts +3 -2
- package/src/email-recipient-loaders/payments.ts +3 -2
- package/src/endpoints/auth/ConfirmTOTPEndpoint.ts +84 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -2
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +61 -1
- package/src/endpoints/auth/CreateTokenEndpoint.ts +135 -7
- 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 +4 -2
- 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/PatchUserEndpoint.ts +6 -3
- 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/RetryEmailVerificationEndpoint.ts +2 -1
- package/src/endpoints/auth/SetupTOTPEndpoint.ts +54 -0
- package/src/endpoints/auth/SignupEndpoint.ts +17 -4
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +16 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +5 -3
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +5 -3
- 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 +11 -6
- package/src/endpoints/global/files/UploadFile.test.ts +206 -0
- package/src/endpoints/global/files/UploadFile.ts +31 -6
- package/src/endpoints/global/files/UploadImage.test.ts +177 -0
- package/src/endpoints/global/files/UploadImage.ts +23 -4
- package/src/endpoints/global/files/upload-security.test.ts +837 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.ts +2 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.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/documents/GetDocumentTemplateXML.ts +2 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
- 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/PatchApiUserEndpoint.ts +5 -3
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +160 -0
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.ts +44 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
- package/src/excel-loaders/balance-items.ts +1 -1
- package/src/excel-loaders/event-notifications.ts +6 -7
- package/src/excel-loaders/members.test.ts +2 -2
- package/src/excel-loaders/members.ts +9 -10
- package/src/excel-loaders/organizations.ts +14 -20
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/platform-memberships.ts +7 -7
- package/src/excel-loaders/platform-sheets.test.ts +113 -0
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +9 -9
- package/src/helpers/AdminPermissionChecker.ts +1 -1
- package/src/helpers/AuthenticatedStructures.ts +21 -10
- 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/MembershipCharger.ts +3 -2
- 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.ts +211 -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/BalanceItemService.ts +2 -1
- package/src/services/DocumentRenderService.test.ts +229 -0
- package/src/services/DocumentRenderService.ts +180 -0
- package/src/services/EmailPreviewService.test.ts +300 -0
- package/src/services/EmailPreviewService.ts +239 -0
- 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/FileSignService.test.ts +85 -0
- package/src/services/FileSignService.ts +23 -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.test.ts +308 -0
- package/src/services/OrderService.ts +215 -0
- package/src/services/OrganizationAdminService.test.ts +47 -0
- package/src/services/OrganizationAdminService.ts +139 -0
- package/src/services/OrganizationDNSService.test.ts +177 -0
- package/src/services/OrganizationDNSService.ts +282 -0
- package/src/services/OrganizationEmailService.test.ts +57 -0
- package/src/services/OrganizationEmailService.ts +212 -0
- package/src/services/PasswordForgotService.test.ts +99 -0
- package/src/services/PasswordForgotService.ts +38 -0
- package/src/services/PaymentService.ts +5 -3
- package/src/services/PlatformMembershipService.test.ts +180 -0
- package/src/services/PlatformMembershipService.ts +281 -4
- package/src/services/ReferralService.ts +3 -2
- package/src/services/RegistrationService.ts +4 -2
- package/src/services/SSOService.ts +106 -14
- package/src/services/STPackageService.test.ts +191 -0
- package/src/services/STPackageService.ts +115 -1
- package/src/services/TwoFactorAuditLogService.ts +64 -0
- package/src/services/VerificationCodeService.test.ts +71 -0
- package/src/services/VerificationCodeService.ts +101 -0
- package/tests/e2e/api-rate-limits.test.ts +1 -1
- package/tests/e2e/documents.test.ts +2 -1
- package/tests/e2e/private-files.test.ts +77 -14
- 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
|
@@ -32,10 +32,10 @@ export class OpenIDConnectAuthTokenEndpoint extends Endpoint<Params, Query, Body
|
|
|
32
32
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
33
33
|
// Check webshop and/or organization
|
|
34
34
|
await Context.setUserOrganizationScope();
|
|
35
|
-
await Context.authenticate({ allowWithoutAccount: false });
|
|
35
|
+
const { token: sessionToken } = await Context.authenticate({ allowWithoutAccount: false });
|
|
36
36
|
|
|
37
37
|
// Create a SSO auth token that can only be used once
|
|
38
|
-
const token = await SSOService.createToken();
|
|
38
|
+
const token = await SSOService.createToken(sessionToken);
|
|
39
39
|
|
|
40
40
|
return new Response(OpenIDAuthTokenResponse.create({
|
|
41
41
|
ssoAuthToken: token,
|
|
@@ -10,6 +10,7 @@ import { LoginMethod, NewUser, PermissionLevel, SignupResponse, UserPermissions
|
|
|
10
10
|
import { Context } from '../../helpers/Context.js';
|
|
11
11
|
import { MemberUserSyncer } from '../../helpers/MemberUserSyncer.js';
|
|
12
12
|
import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures.js';
|
|
13
|
+
import { VerificationCodeService } from '../../services/VerificationCodeService.js';
|
|
13
14
|
|
|
14
15
|
type Params = { id: string };
|
|
15
16
|
type Query = undefined;
|
|
@@ -79,10 +80,12 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
if (request.body.permissions) {
|
|
83
|
+
const platform = await Platform.getSharedStruct();
|
|
84
|
+
|
|
82
85
|
if (organization) {
|
|
83
86
|
editUser.permissions = UserPermissions.limitedPatch(editUser.permissions, request.body.permissions, organization.id);
|
|
84
87
|
|
|
85
|
-
if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization)?.hasFullAccess()) && STAMHOOFD.environment !== 'development') {
|
|
88
|
+
if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization, platform, { inheritFromPlatform: false })?.hasFullAccess()) && STAMHOOFD.environment !== 'development') {
|
|
86
89
|
throw new SimpleError({
|
|
87
90
|
code: 'permission_denied',
|
|
88
91
|
message: $t(`%DG`),
|
|
@@ -99,7 +102,7 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
99
102
|
editUser.permissions = null;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
if (editUser.id === user.id && !editUser.permissions?.platform?.hasFullAccess() && STAMHOOFD.environment !== 'development') {
|
|
105
|
+
if (editUser.id === user.id && !editUser.permissions?.forPlatform(platform)?.hasFullAccess() && STAMHOOFD.environment !== 'development') {
|
|
103
106
|
throw new SimpleError({
|
|
104
107
|
code: 'permission_denied',
|
|
105
108
|
message: $t(`%DG`),
|
|
@@ -187,7 +190,7 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
187
190
|
// Create an validation code
|
|
188
191
|
// We always need the code, to return it. Also on password recovery -> may not be visible to the client whether the user exists or not
|
|
189
192
|
const code = await EmailVerificationCode.createFor(editUser, request.body.email);
|
|
190
|
-
|
|
193
|
+
VerificationCodeService.send(code, editUser, organization, request.i18n, editUser.id === user.id).catch(console.error);
|
|
191
194
|
|
|
192
195
|
throw new SimpleError({
|
|
193
196
|
code: 'verify_email',
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { RecoveryCodes } from '@stamhoofd/structures';
|
|
5
|
+
|
|
6
|
+
import { Context } from '../../helpers/Context.js';
|
|
7
|
+
import { RecoveryCodeHelper } from '../../helpers/RecoveryCodeHelper.js';
|
|
8
|
+
import { TwoFactorHelper } from '../../helpers/TwoFactorHelper.js';
|
|
9
|
+
import { TwoFactorAuditLogService } from '../../services/TwoFactorAuditLogService.js';
|
|
10
|
+
|
|
11
|
+
type Params = Record<string, never>;
|
|
12
|
+
type Query = undefined;
|
|
13
|
+
type Body = undefined;
|
|
14
|
+
type ResponseBody = RecoveryCodes;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Regenerate the user's recovery codes (replacing the previous batch). Returns the new
|
|
18
|
+
* plaintext codes once. Requires the user to already have a factor enrolled.
|
|
19
|
+
*/
|
|
20
|
+
export class RegenerateRecoveryCodesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
21
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
22
|
+
if (request.method !== 'POST') {
|
|
23
|
+
return [false];
|
|
24
|
+
}
|
|
25
|
+
const params = Endpoint.parseParameters(request.url, '/mfa/recovery-codes', {});
|
|
26
|
+
if (params) {
|
|
27
|
+
return [true, params as Params];
|
|
28
|
+
}
|
|
29
|
+
return [false];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
33
|
+
await Context.setOptionalOrganizationScope();
|
|
34
|
+
const { user } = await Context.authenticateFresh({ allowWithoutAccount: true, allowUnscoped: true });
|
|
35
|
+
|
|
36
|
+
if (!(await TwoFactorHelper.userHasFactors(user.id))) {
|
|
37
|
+
throw new SimpleError({
|
|
38
|
+
code: 'no_factors',
|
|
39
|
+
message: 'You need at least one two-factor method before generating recovery codes',
|
|
40
|
+
human: $t('%Zgz'),
|
|
41
|
+
statusCode: 400,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const codes = await RecoveryCodeHelper.regenerateForUser(user.id);
|
|
46
|
+
|
|
47
|
+
await TwoFactorAuditLogService.logRecoveryCodesRegenerated(user);
|
|
48
|
+
|
|
49
|
+
return new Response(RecoveryCodes.create({ codes }));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
4
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
|
+
import { WebauthnChallenge, WebauthnCredential } from '@stamhoofd/models';
|
|
6
|
+
import type { MFAEnrollmentResult } from '@stamhoofd/structures';
|
|
7
|
+
import { MFAMethodType, WebauthnRegistrationRequest } from '@stamhoofd/structures';
|
|
8
|
+
|
|
9
|
+
import { Context } from '../../helpers/Context.js';
|
|
10
|
+
import { TwoFactorHelper } from '../../helpers/TwoFactorHelper.js';
|
|
11
|
+
import { WebauthnHelper } from '../../helpers/WebauthnHelper.js';
|
|
12
|
+
import { TwoFactorAuditLogService } from '../../services/TwoFactorAuditLogService.js';
|
|
13
|
+
|
|
14
|
+
type Params = Record<string, never>;
|
|
15
|
+
type Query = undefined;
|
|
16
|
+
type Body = WebauthnRegistrationRequest;
|
|
17
|
+
type ResponseBody = MFAEnrollmentResult;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Verify and store a newly registered passkey. On the user's first factor this also
|
|
21
|
+
* returns recovery codes, and during forced enrollment it issues a full session token.
|
|
22
|
+
*/
|
|
23
|
+
export class RegisterPasskeyEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
24
|
+
bodyDecoder = WebauthnRegistrationRequest as Decoder<WebauthnRegistrationRequest>;
|
|
25
|
+
|
|
26
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
27
|
+
if (request.method !== 'POST') {
|
|
28
|
+
return [false];
|
|
29
|
+
}
|
|
30
|
+
const params = Endpoint.parseParameters(request.url, '/mfa/passkeys', {});
|
|
31
|
+
if (params) {
|
|
32
|
+
return [true, params as Params];
|
|
33
|
+
}
|
|
34
|
+
return [false];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
38
|
+
await Context.setOptionalOrganizationScope();
|
|
39
|
+
const { user, setupToken, token } = await Context.authenticateMFAEnrollment();
|
|
40
|
+
|
|
41
|
+
if (!user.canUsePasskeys()) {
|
|
42
|
+
throw new SimpleError({
|
|
43
|
+
code: 'passkeys_not_available',
|
|
44
|
+
message: 'Passkeys are not available for this account',
|
|
45
|
+
human: $t('%ZhE'),
|
|
46
|
+
statusCode: 400,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const challenge = await WebauthnChallenge.consumeForUser(user.id);
|
|
51
|
+
if (!challenge) {
|
|
52
|
+
throw new SimpleError({
|
|
53
|
+
code: 'invalid_challenge',
|
|
54
|
+
message: 'No valid registration challenge found',
|
|
55
|
+
human: $t('%Zi9'),
|
|
56
|
+
statusCode: 400,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = await WebauthnHelper.verifyRegistration(request.body.response, challenge);
|
|
61
|
+
if (!result) {
|
|
62
|
+
throw new SimpleError({
|
|
63
|
+
code: 'invalid_passkey',
|
|
64
|
+
message: 'Could not verify the passkey',
|
|
65
|
+
human: $t('%Zhr'),
|
|
66
|
+
statusCode: 400,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (await WebauthnCredential.getByCredentialId(result.credentialId)) {
|
|
71
|
+
throw new SimpleError({
|
|
72
|
+
code: 'passkey_already_registered',
|
|
73
|
+
message: 'This passkey is already registered',
|
|
74
|
+
human: $t('%Zhf'),
|
|
75
|
+
statusCode: 400,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const wasFirstFactor = !(await TwoFactorHelper.userHasFactors(user.id));
|
|
80
|
+
|
|
81
|
+
const credential = new WebauthnCredential();
|
|
82
|
+
credential.userId = user.id;
|
|
83
|
+
credential.credentialId = result.credentialId;
|
|
84
|
+
credential.rpId = result.rpId;
|
|
85
|
+
credential.publicKey = result.publicKey;
|
|
86
|
+
credential.counter = result.counter;
|
|
87
|
+
credential.transports = result.transports ? JSON.stringify(result.transports) : null;
|
|
88
|
+
credential.backedUp = result.backedUp;
|
|
89
|
+
credential.backupEligible = result.backupEligible;
|
|
90
|
+
credential.name = request.body.name?.trim() || '';
|
|
91
|
+
credential.providerId = result.providerId;
|
|
92
|
+
credential.providerName = result.providerName;
|
|
93
|
+
await credential.save();
|
|
94
|
+
|
|
95
|
+
await TwoFactorAuditLogService.logMethodAdded(user, MFAMethodType.Passkey, credential.name || credential.providerName || '');
|
|
96
|
+
|
|
97
|
+
return new Response(await TwoFactorHelper.completeEnrollment(user, setupToken, wasFirstFactor, token));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { WebauthnChallenge, WebauthnCredential } from '@stamhoofd/models';
|
|
5
|
+
import { WebauthnRegistrationOptions } from '@stamhoofd/structures';
|
|
6
|
+
|
|
7
|
+
import { Context } from '../../helpers/Context.js';
|
|
8
|
+
import { WebauthnHelper } from '../../helpers/WebauthnHelper.js';
|
|
9
|
+
|
|
10
|
+
type Params = Record<string, never>;
|
|
11
|
+
type Query = undefined;
|
|
12
|
+
type Body = undefined;
|
|
13
|
+
type ResponseBody = WebauthnRegistrationOptions;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate WebAuthn registration options (a challenge) for enrolling a new passkey.
|
|
17
|
+
*/
|
|
18
|
+
export class RegisterPasskeyOptionsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
19
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
20
|
+
if (request.method !== 'POST') {
|
|
21
|
+
return [false];
|
|
22
|
+
}
|
|
23
|
+
const params = Endpoint.parseParameters(request.url, '/mfa/passkeys/options', {});
|
|
24
|
+
if (params) {
|
|
25
|
+
return [true, params as Params];
|
|
26
|
+
}
|
|
27
|
+
return [false];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
|
+
await Context.setOptionalOrganizationScope();
|
|
32
|
+
const { user } = await Context.authenticateMFAEnrollment();
|
|
33
|
+
|
|
34
|
+
if (!user.canUsePasskeys()) {
|
|
35
|
+
throw new SimpleError({
|
|
36
|
+
code: 'passkeys_not_available',
|
|
37
|
+
message: 'Passkeys are not available for this account',
|
|
38
|
+
human: $t('%ZhE'),
|
|
39
|
+
statusCode: 400,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const existing = await WebauthnCredential.getForUser(user.id);
|
|
44
|
+
const options = await WebauthnHelper.generateRegistration(user, existing);
|
|
45
|
+
await WebauthnChallenge.createFor(user.id, options.challenge);
|
|
46
|
+
|
|
47
|
+
return new Response(WebauthnRegistrationOptions.create({ options }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -5,6 +5,7 @@ import { EmailVerificationCode } from '@stamhoofd/models';
|
|
|
5
5
|
import { PollEmailVerificationRequest, PollEmailVerificationResponse } from '@stamhoofd/structures';
|
|
6
6
|
|
|
7
7
|
import { Context } from '../../helpers/Context.js';
|
|
8
|
+
import { VerificationCodeService } from '../../services/VerificationCodeService.js';
|
|
8
9
|
|
|
9
10
|
type Params = Record<string, never>;
|
|
10
11
|
type Query = undefined;
|
|
@@ -32,7 +33,7 @@ export class PollEmailVerificationEndpoint extends Endpoint<Params, Query, Body,
|
|
|
32
33
|
const valid = await EmailVerificationCode.poll(organization?.id ?? null, request.body.token);
|
|
33
34
|
|
|
34
35
|
if (valid) {
|
|
35
|
-
|
|
36
|
+
VerificationCodeService.resend(organization, request.body.token, request.i18n).catch(console.error);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
return new Response(PollEmailVerificationResponse.create({
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { MFATOTP } from '@stamhoofd/models';
|
|
4
|
+
import { TOTPSetupResponse } from '@stamhoofd/structures';
|
|
5
|
+
|
|
6
|
+
import { Context } from '../../helpers/Context.js';
|
|
7
|
+
import { TOTPHelper } from '../../helpers/TOTPHelper.js';
|
|
8
|
+
|
|
9
|
+
type Params = Record<string, never>;
|
|
10
|
+
type Query = undefined;
|
|
11
|
+
type Body = undefined;
|
|
12
|
+
type ResponseBody = TOTPSetupResponse;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Start enrolling a new TOTP authenticator. Creates an unconfirmed row and returns the
|
|
16
|
+
* secret (shown once). Confirm it with ConfirmTOTPEndpoint.
|
|
17
|
+
*/
|
|
18
|
+
export class SetupTOTPEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
19
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
20
|
+
if (request.method !== 'POST') {
|
|
21
|
+
return [false];
|
|
22
|
+
}
|
|
23
|
+
const params = Endpoint.parseParameters(request.url, '/mfa/totp', {});
|
|
24
|
+
if (params) {
|
|
25
|
+
return [true, params as Params];
|
|
26
|
+
}
|
|
27
|
+
return [false];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
|
+
await Context.setOptionalOrganizationScope();
|
|
32
|
+
const { user } = await Context.authenticateMFAEnrollment();
|
|
33
|
+
|
|
34
|
+
// Drop any unconfirmed authenticators from earlier, abandoned setup attempts so
|
|
35
|
+
// they don't pile up as orphaned rows holding an (encrypted) unused secret.
|
|
36
|
+
await MFATOTP.deleteUnconfirmedForUser(user.id);
|
|
37
|
+
|
|
38
|
+
const secret = TOTPHelper.generateSecret();
|
|
39
|
+
const totp = new MFATOTP();
|
|
40
|
+
totp.userId = user.id;
|
|
41
|
+
totp.name = '';
|
|
42
|
+
totp.secret = TOTPHelper.encrypt(secret);
|
|
43
|
+
totp.confirmedAt = null;
|
|
44
|
+
await totp.save();
|
|
45
|
+
|
|
46
|
+
const otpauthUri = TOTPHelper.keyuri(user.email, secret);
|
|
47
|
+
|
|
48
|
+
return new Response(TOTPSetupResponse.create({
|
|
49
|
+
totpId: totp.id,
|
|
50
|
+
secret,
|
|
51
|
+
otpauthUri,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -2,10 +2,13 @@ import type { Decoder } from '@simonbackx/simple-encoding';
|
|
|
2
2
|
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
|
-
import { EmailVerificationCode, PasswordToken, Platform,
|
|
5
|
+
import { EmailVerificationCode, PasswordToken, Platform, User } from '@stamhoofd/models';
|
|
6
|
+
import { sendEmailTemplate } from '../../helpers/EmailBuilder.js';
|
|
6
7
|
import { EmailTemplateType, LoginMethod, NewUser, Recipient, Replacement, SignupResponse } from '@stamhoofd/structures';
|
|
7
8
|
|
|
8
9
|
import { Context } from '../../helpers/Context.js';
|
|
10
|
+
import { PasswordForgotService } from '../../services/PasswordForgotService.js';
|
|
11
|
+
import { VerificationCodeService } from '../../services/VerificationCodeService.js';
|
|
9
12
|
|
|
10
13
|
type Params = Record<string, never>;
|
|
11
14
|
type Query = undefined;
|
|
@@ -31,7 +34,17 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
|
|
|
31
34
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
32
35
|
const organization = await Context.setUserOrganizationScope({ willAuthenticate: false });
|
|
33
36
|
|
|
34
|
-
if (
|
|
37
|
+
if (!organization) {
|
|
38
|
+
if (STAMHOOFD.userMode === 'organization') {
|
|
39
|
+
// Not supported for now
|
|
40
|
+
throw new SimpleError({
|
|
41
|
+
code: 'not_supported',
|
|
42
|
+
message: 'This platform does not support password login',
|
|
43
|
+
human: $t(`%D7`),
|
|
44
|
+
statusCode: 400,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
const platform = await Platform.getShared();
|
|
36
49
|
if (!platform.config.loginMethods.has(LoginMethod.Password)) {
|
|
37
50
|
throw new SimpleError({
|
|
@@ -77,7 +90,7 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
|
|
|
77
90
|
// We don't await this block to avoid user enumeration attack using request response time
|
|
78
91
|
(async () => {
|
|
79
92
|
// Send an e-mail to say you already have an account + follow password forgot flow
|
|
80
|
-
const recoveryUrl = await
|
|
93
|
+
const recoveryUrl = await PasswordForgotService.getPasswordRecoveryUrl(user, organization, request.i18n);
|
|
81
94
|
|
|
82
95
|
// Create e-mail builder
|
|
83
96
|
await sendEmailTemplate(organization, {
|
|
@@ -118,7 +131,7 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
|
|
|
118
131
|
const code = await EmailVerificationCode.createFor(user, user.email);
|
|
119
132
|
|
|
120
133
|
if (sendCode) {
|
|
121
|
-
|
|
134
|
+
VerificationCodeService.send(code, user, organization, request.i18n).catch(console.error);
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
return new Response(SignupResponse.create({
|
|
@@ -6,6 +6,7 @@ import { EmailVerificationCode, Token, User } from '@stamhoofd/models';
|
|
|
6
6
|
import { Token as TokenStruct, VerifyEmailRequest } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../helpers/Context.js';
|
|
9
|
+
import { TwoFactorHelper } from '../../helpers/TwoFactorHelper.js';
|
|
9
10
|
import { BalanceItemService } from '../../services/BalanceItemService.js';
|
|
10
11
|
|
|
11
12
|
type Params = Record<string, never>;
|
|
@@ -50,6 +51,11 @@ export class VerifyEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
50
51
|
});
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// Read the (optional) session of the caller before consuming the code: a user that
|
|
55
|
+
// is already signed in is verifying a new email address from their account
|
|
56
|
+
// settings, and already passed their second factor for that session.
|
|
57
|
+
const authenticatedUser = await Context.optionalAuthenticatedUser();
|
|
58
|
+
|
|
53
59
|
const code = await EmailVerificationCode.verify(organization?.id ?? null, request.body.token, request.body.code);
|
|
54
60
|
|
|
55
61
|
if (!code) {
|
|
@@ -105,7 +111,17 @@ export class VerifyEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
105
111
|
throw e;
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
// Verifying an email is a single primary credential: if this user already has a
|
|
115
|
+
// second factor (or must enroll one), don't hand out a session without it. Unless
|
|
116
|
+
// the request already comes from a session of that same user (changing your email
|
|
117
|
+
// address while signed in): the second factor was checked when it was created.
|
|
118
|
+
if (authenticatedUser?.id !== user.id) {
|
|
119
|
+
await TwoFactorHelper.assertSecondFactorOrThrow(user, organization, request.request.getVersion());
|
|
120
|
+
}
|
|
121
|
+
|
|
108
122
|
const token = await Token.createToken(user);
|
|
123
|
+
await user.markActive();
|
|
124
|
+
|
|
109
125
|
const st = new TokenStruct(token);
|
|
110
126
|
return new Response(st);
|
|
111
127
|
}
|
|
@@ -7,6 +7,8 @@ import { EmailStatus, Email as EmailStruct, EmailTemplate as EmailTemplateStruct
|
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../../helpers/Context.js';
|
|
9
9
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
10
|
+
import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
|
|
11
|
+
import { EmailSendService } from '../../../services/EmailSendService.js';
|
|
10
12
|
|
|
11
13
|
type Params = Record<string, never>;
|
|
12
14
|
type Query = undefined;
|
|
@@ -131,7 +133,7 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
await model.save();
|
|
134
|
-
await
|
|
136
|
+
await EmailSendService.buildExampleRecipient(model, true);
|
|
135
137
|
await model.updateCount();
|
|
136
138
|
|
|
137
139
|
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
|
|
@@ -141,7 +143,7 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
141
143
|
human: $t('%1Cw'),
|
|
142
144
|
});
|
|
143
145
|
}
|
|
144
|
-
await
|
|
146
|
+
await EmailSendService.queueForSending(model);
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
// Delete open drafts with the same content, from the same user
|
|
@@ -162,6 +164,6 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
162
164
|
await duplicate.save();
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
return new Response(await
|
|
167
|
+
return new Response(await EmailPreviewService.getPreviewStructure(model, { allLanguages: true }));
|
|
166
168
|
}
|
|
167
169
|
}
|
|
@@ -157,7 +157,7 @@ describe('Endpoint.getAdminEmails', () => {
|
|
|
157
157
|
expect(unsubscribeUrlReplacement!.value).toMatch(/^https:\/\//); // Should still be a valid URL
|
|
158
158
|
expect(unsubscribeUrlReplacement!.value).not.toContain('secret-token-12345'); // Original sensitive token should be gone
|
|
159
159
|
|
|
160
|
-
// This tests that
|
|
160
|
+
// This tests that EmailPreviewService.getStructureForUser properly handles sensitive data by:
|
|
161
161
|
// 1. Removing original sensitive replacements from other users' data
|
|
162
162
|
// 2. Creating fresh, appropriate replacements for the current viewer
|
|
163
163
|
// 3. Ensuring web safety of all replacement values
|
|
@@ -9,6 +9,7 @@ import { Email, Platform } from '@stamhoofd/models';
|
|
|
9
9
|
import type { SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
10
10
|
import { applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
11
11
|
import { Context } from '../../../helpers/Context.js';
|
|
12
|
+
import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
|
|
12
13
|
import { emailFilterCompilers } from '../../../sql-filters/emails.js';
|
|
13
14
|
import { emailSorters } from '../../../sql-sorters/emails.js';
|
|
14
15
|
|
|
@@ -158,7 +159,7 @@ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, Respon
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
return new PaginatedResponse<EmailPreview[], LimitedFilteredRequest>({
|
|
161
|
-
results: await Promise.all(emails.map(email =>
|
|
162
|
+
results: await Promise.all(emails.map(email => EmailPreviewService.getPreviewStructure(email, { allLanguages: true }))),
|
|
162
163
|
next,
|
|
163
164
|
});
|
|
164
165
|
}
|
|
@@ -5,6 +5,7 @@ import type { EmailPreview } from '@stamhoofd/structures';
|
|
|
5
5
|
|
|
6
6
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
7
|
import { Context } from '../../../helpers/Context.js';
|
|
8
|
+
import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
|
|
8
9
|
|
|
9
10
|
type Params = { id: string };
|
|
10
11
|
type Query = undefined;
|
|
@@ -51,6 +52,6 @@ export class GetEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody
|
|
|
51
52
|
throw Context.auth.error();
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
return new Response(await
|
|
55
|
+
return new Response(await EmailPreviewService.getPreviewStructure(model, { allLanguages: true }));
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -537,7 +537,7 @@ describe('Endpoint.GetUserEmails', () => {
|
|
|
537
537
|
expect(unsubscribeUrlReplacement!.value).toMatch(/^https:\/\//); // Should still be a valid URL
|
|
538
538
|
expect(unsubscribeUrlReplacement!.value).not.toContain('secret-token-12345'); // Original sensitive token should be gone
|
|
539
539
|
|
|
540
|
-
// This tests that
|
|
540
|
+
// This tests that EmailPreviewService.getStructureForUser properly handles sensitive data by:
|
|
541
541
|
// 1. Removing original sensitive replacements from other users' data
|
|
542
542
|
// 2. Creating fresh, appropriate replacements for the current viewer
|
|
543
543
|
// 3. Ensuring web safety of all replacement values
|
|
@@ -11,6 +11,7 @@ import type { Language } from '@stamhoofd/types/Language';
|
|
|
11
11
|
import type { SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
12
12
|
import { applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
13
13
|
import { Context } from '../../../helpers/Context.js';
|
|
14
|
+
import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
|
|
14
15
|
import { emailFilterCompilers, userEmailFilterCompilers } from '../../../sql-filters/emails.js';
|
|
15
16
|
import { emailSorters } from '../../../sql-sorters/emails.js';
|
|
16
17
|
|
|
@@ -135,7 +136,7 @@ export class GetUserEmailsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
return new PaginatedResponse<EmailWithRecipients[], LimitedFilteredRequest>({
|
|
138
|
-
results: await Promise.all(emails.map(email =>
|
|
139
|
+
results: await Promise.all(emails.map(email => EmailPreviewService.getStructureForUser(email, user, memberIds, options))),
|
|
139
140
|
next,
|
|
140
141
|
});
|
|
141
142
|
}
|
|
@@ -8,6 +8,8 @@ import type { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding'
|
|
|
8
8
|
import { patchObject } from '@simonbackx/simple-encoding';
|
|
9
9
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
10
10
|
import { Context } from '../../../helpers/Context.js';
|
|
11
|
+
import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
|
|
12
|
+
import { EmailSendService } from '../../../services/EmailSendService.js';
|
|
11
13
|
|
|
12
14
|
type Params = { id: string };
|
|
13
15
|
type Query = undefined;
|
|
@@ -195,7 +197,7 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
195
197
|
await model.save();
|
|
196
198
|
|
|
197
199
|
if (rebuild) {
|
|
198
|
-
await
|
|
200
|
+
await EmailSendService.buildExampleRecipient(model);
|
|
199
201
|
await model.updateCount();
|
|
200
202
|
}
|
|
201
203
|
|
|
@@ -208,9 +210,9 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
// Preview the sending status
|
|
211
|
-
await
|
|
213
|
+
await EmailSendService.queueForSending(model);
|
|
212
214
|
}
|
|
213
215
|
|
|
214
|
-
return new Response(await
|
|
216
|
+
return new Response(await EmailPreviewService.getPreviewStructure(model, { allLanguages: true }));
|
|
215
217
|
}
|
|
216
218
|
}
|
|
@@ -5,7 +5,8 @@ import { assertSort, getSortFilter, LimitedFilteredRequest, PaginatedResponse, P
|
|
|
5
5
|
|
|
6
6
|
import type { Decoder } from '@simonbackx/simple-encoding';
|
|
7
7
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
8
|
-
import { EmailRecipient
|
|
8
|
+
import { EmailRecipient } from '@stamhoofd/models';
|
|
9
|
+
import { fillRecipientReplacements } from '../../../helpers/EmailBuilder.js';
|
|
9
10
|
import type { SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
10
11
|
import { applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
11
12
|
import { Context } from '../../../helpers/Context.js';
|
|
@@ -6,6 +6,7 @@ import { EmailStatus, PermissionLevel } from '@stamhoofd/structures';
|
|
|
6
6
|
|
|
7
7
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
8
8
|
import { Context } from '../../../helpers/Context.js';
|
|
9
|
+
import { EmailSendService } from '../../../services/EmailSendService.js';
|
|
9
10
|
|
|
10
11
|
type Params = { id: string };
|
|
11
12
|
type Query = undefined;
|
|
@@ -76,7 +77,7 @@ export class RetryEmailRecipientEndpoint extends Endpoint<Params, Query, Body, R
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
// Retry
|
|
79
|
-
await
|
|
80
|
+
await EmailSendService.resumeSending(model, emailRecipient.id);
|
|
80
81
|
|
|
81
82
|
await emailRecipient.refresh();
|
|
82
83
|
|
|
@@ -4,9 +4,10 @@ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import type { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
|
|
6
6
|
import { ArchiverWriterAdapter, exportToExcel, XlsxWriter } from '@stamhoofd/excel-writer';
|
|
7
|
-
import { Platform, RateLimiter
|
|
7
|
+
import { Platform, RateLimiter } from '@stamhoofd/models';
|
|
8
|
+
import { sendEmailTemplate } from '../../../helpers/EmailBuilder.js';
|
|
8
9
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
9
|
-
import type { ExcelExportType, IPaginatedResponse, LimitedFilteredRequest } from '@stamhoofd/structures';
|
|
10
|
+
import type { ExcelExportType, IPaginatedResponse, LimitedFilteredRequest, Platform as PlatformStruct } from '@stamhoofd/structures';
|
|
10
11
|
import { EmailTemplateType, ExcelExportRequest, ExcelExportResponse, Replacement, Version } from '@stamhoofd/structures';
|
|
11
12
|
import { sleep } from '@stamhoofd/utility';
|
|
12
13
|
import { Context } from '../../../helpers/Context.js';
|
|
@@ -20,7 +21,12 @@ type ResponseBody = ExcelExportResponse;
|
|
|
20
21
|
|
|
21
22
|
type ExcelExporter<T> = {
|
|
22
23
|
fetch(request: LimitedFilteredRequest): Promise<IPaginatedResponse<T[], LimitedFilteredRequest>>;
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The sheet definitions depend on the platform: they are built per export request instead of
|
|
27
|
+
* once at module load, so the callbacks never have to read a process wide platform singleton.
|
|
28
|
+
*/
|
|
29
|
+
getSheets(platform: PlatformStruct): XlsxTransformerSheet<T, unknown>[];
|
|
24
30
|
};
|
|
25
31
|
|
|
26
32
|
export const limiter = new RateLimiter({
|
|
@@ -86,8 +92,6 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
86
92
|
limiter.track(user.id, 1);
|
|
87
93
|
let sendEmail = false;
|
|
88
94
|
|
|
89
|
-
await Platform.getSharedStruct();
|
|
90
|
-
|
|
91
95
|
const result = await Promise.race([
|
|
92
96
|
this.job(loader, request.body, request.params.type, user.id).then(async (url: string) => {
|
|
93
97
|
if (sendEmail) {
|
|
@@ -146,6 +150,7 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
146
150
|
// Estimate how long it will take.
|
|
147
151
|
// If too long, we'll schedule it and write it to Digitalocean Spaces
|
|
148
152
|
// Otherwise we'll just return the file directly
|
|
153
|
+
const platform = await Platform.getSharedStruct();
|
|
149
154
|
const { file, stream } = await FileCache.getWriteStream('.xlsx');
|
|
150
155
|
|
|
151
156
|
const zipWriterAdapter = new ArchiverWriterAdapter(stream);
|
|
@@ -155,7 +160,7 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
155
160
|
request.filter.limit = STAMHOOFD.environment === 'development' ? 1000 : 100; // in development, we need to check if total count matches and pagination is working correctly
|
|
156
161
|
|
|
157
162
|
await exportToExcel({
|
|
158
|
-
definitions: loader.
|
|
163
|
+
definitions: loader.getSheets(platform),
|
|
159
164
|
writer,
|
|
160
165
|
dataGenerator: fetchToAsyncIterator(request.filter, loader, { signFiles: true }),
|
|
161
166
|
filter: request.workbookFilter,
|