@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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { I18n } from '@stamhoofd/backend-i18n';
|
|
2
|
+
import type { Organization } from '@stamhoofd/models';
|
|
3
|
+
import { EmailVerificationCode, Platform, User } from '@stamhoofd/models';
|
|
4
|
+
import { sendEmailTemplate } from '../helpers/EmailBuilder.js';
|
|
5
|
+
import { EmailTemplateType, getAppHost, Recipient, Replacement } from '@stamhoofd/structures';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sends the verification emails for an EmailVerificationCode. The code itself is generated, stored
|
|
9
|
+
* and verified on the model.
|
|
10
|
+
*/
|
|
11
|
+
export class VerificationCodeService {
|
|
12
|
+
static getEmailVerificationUrl(code: EmailVerificationCode, user: User, organization: Organization | null, i18n: I18n) {
|
|
13
|
+
const host = getAppHost('verify-email', organization, !!user.permissions, i18n);
|
|
14
|
+
return 'https://' + host + '?code=' + encodeURIComponent(code.code) + '&token=' + encodeURIComponent(code.token) + '&email=' + encodeURIComponent(code.email);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static async send(code: EmailVerificationCode, user: User, organization: Organization | null, i18n: I18n, withCode = true) {
|
|
18
|
+
const url = this.getEmailVerificationUrl(code, user, organization, i18n);
|
|
19
|
+
|
|
20
|
+
const name = organization?.name ?? (await Platform.getSharedPrivateStruct()).config.name;
|
|
21
|
+
|
|
22
|
+
const replacements: Replacement[] = [
|
|
23
|
+
Replacement.create({
|
|
24
|
+
token: 'organizationName',
|
|
25
|
+
value: name,
|
|
26
|
+
}),
|
|
27
|
+
Replacement.create({
|
|
28
|
+
token: 'confirmEmailUrl',
|
|
29
|
+
value: url,
|
|
30
|
+
}),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
if (withCode) {
|
|
34
|
+
const formattedCode = code.code.substr(0, 3) + ' ' + code.code.substr(3);
|
|
35
|
+
|
|
36
|
+
await sendEmailTemplate(organization, {
|
|
37
|
+
recipients: [
|
|
38
|
+
Recipient.create({
|
|
39
|
+
email: code.email,
|
|
40
|
+
replacements: [
|
|
41
|
+
...replacements,
|
|
42
|
+
Replacement.create({
|
|
43
|
+
token: 'confirmEmailCode',
|
|
44
|
+
value: formattedCode,
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
}),
|
|
48
|
+
],
|
|
49
|
+
template: {
|
|
50
|
+
type: EmailTemplateType.VerifyEmail,
|
|
51
|
+
},
|
|
52
|
+
type: 'transactional',
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
await sendEmailTemplate(organization, {
|
|
56
|
+
recipients: [
|
|
57
|
+
Recipient.create({
|
|
58
|
+
email: code.email,
|
|
59
|
+
replacements,
|
|
60
|
+
}),
|
|
61
|
+
],
|
|
62
|
+
template: {
|
|
63
|
+
type: EmailTemplateType.VerifyEmailWithoutCode,
|
|
64
|
+
},
|
|
65
|
+
type: 'transactional',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static async resend(organization: Organization | null, token: string, i18n: I18n) {
|
|
71
|
+
const verificationCodes = await EmailVerificationCode.where({
|
|
72
|
+
token,
|
|
73
|
+
organizationId: organization
|
|
74
|
+
? {
|
|
75
|
+
sign: 'IN',
|
|
76
|
+
value: [organization.id, null],
|
|
77
|
+
}
|
|
78
|
+
: null,
|
|
79
|
+
}, { limit: 1 });
|
|
80
|
+
|
|
81
|
+
if (verificationCodes.length == 0) {
|
|
82
|
+
console.log("Can't resend code, no coded found for token", token);
|
|
83
|
+
// TODO: maybe send a note via email
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const verificationCode = verificationCodes[0];
|
|
88
|
+
|
|
89
|
+
if (verificationCode.expiresAt < new Date()) {
|
|
90
|
+
// Don't report error, could be brute forced
|
|
91
|
+
console.log("Can't resend code, token is expired", token);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const user = await User.getByID(verificationCode.userId);
|
|
96
|
+
if (!user) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await this.send(verificationCode, user, organization, i18n);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -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({
|
|
@@ -12,6 +12,7 @@ import { testServer } from '../helpers/TestServer.js';
|
|
|
12
12
|
import { initAdmin } from '../init/initAdmin.js';
|
|
13
13
|
import { initStripe } from '../init/initStripe.js';
|
|
14
14
|
import { registrationUpdateQueue } from '../../src/services/BalanceItemService.js';
|
|
15
|
+
import { DocumentRenderService } from '../../src/services/DocumentRenderService.js';
|
|
15
16
|
import { initMembershipOrganization } from '../init/initMembershipOrganization.js';
|
|
16
17
|
|
|
17
18
|
const baseUrl = `/members/register`;
|
|
@@ -119,7 +120,7 @@ describe('E2E.Documents', () => {
|
|
|
119
120
|
const document = await Document.select().where('registrationId', registration.id).first(false);
|
|
120
121
|
expect(document).not.toBeNull();
|
|
121
122
|
|
|
122
|
-
const html = await
|
|
123
|
+
const html = await DocumentRenderService.getRenderedHtml(document!, organization);
|
|
123
124
|
|
|
124
125
|
const registrationModel = (await Registration.getByID(registration.id))!;
|
|
125
126
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { File, MemberDetails, MemberWithRegistrationsBlob, RecordCategory, RecordFileAnswer, RecordSettings, RecordType, TranslatedString, Version } from '@stamhoofd/structures';
|
|
2
2
|
|
|
3
3
|
import type { PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
|
|
4
|
-
import { PatchableArray } from '@simonbackx/simple-encoding';
|
|
4
|
+
import { encodeObject, EncodeMedium, PatchableArray } from '@simonbackx/simple-encoding';
|
|
5
5
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
6
6
|
import { Member, OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
7
7
|
import { PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
@@ -62,7 +62,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
62
62
|
// access to the signed URL. This should not be possible without a valid signature
|
|
63
63
|
const privateFile = new File({
|
|
64
64
|
id: 'test',
|
|
65
|
-
server: 'test.com',
|
|
65
|
+
server: 'https://test.com',
|
|
66
66
|
path: 'test.txt',
|
|
67
67
|
size: 100,
|
|
68
68
|
isPrivate: true,
|
|
@@ -105,7 +105,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
105
105
|
// access to the signed URL. This should not be possible without a valid signature
|
|
106
106
|
const privateFile = new File({
|
|
107
107
|
id: 'test',
|
|
108
|
-
server: 'test.com',
|
|
108
|
+
server: 'https://test.com',
|
|
109
109
|
path: 'test.txt',
|
|
110
110
|
size: 100,
|
|
111
111
|
isPrivate: true,
|
|
@@ -149,7 +149,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
149
149
|
// access to the signed URL. This should not be possible without a valid signature
|
|
150
150
|
const privateFile = new File({
|
|
151
151
|
id: 'test',
|
|
152
|
-
server: 'test.com',
|
|
152
|
+
server: 'https://test.com',
|
|
153
153
|
path: 'test.txt',
|
|
154
154
|
size: 100,
|
|
155
155
|
isPrivate: true,
|
|
@@ -188,6 +188,69 @@ describe('E2E.PrivateFiles', () => {
|
|
|
188
188
|
expect(answer.file!.signedUrl).toBeTruthy();
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
test('Cannot set a public file with a url a browser would run as code', async () => {
|
|
192
|
+
const organization = await createOrganization();
|
|
193
|
+
|
|
194
|
+
const user = await new UserFactory({
|
|
195
|
+
organization,
|
|
196
|
+
permissions: Permissions.create({
|
|
197
|
+
level: PermissionLevel.Full,
|
|
198
|
+
}),
|
|
199
|
+
}).create();
|
|
200
|
+
const token = await Token.createToken(user);
|
|
201
|
+
|
|
202
|
+
// Public files are not signed, so a malicious user can put anything in them. The url of a file ends up
|
|
203
|
+
// in a link (and in the source of an image), so a javascript: url would run on our own domain.
|
|
204
|
+
const buildBody = (server: string) => {
|
|
205
|
+
const member = MemberWithRegistrationsBlob.create({
|
|
206
|
+
details: MemberDetails.create({
|
|
207
|
+
firstName: 'John',
|
|
208
|
+
lastName: 'Doe',
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
member.details.recordAnswers.set(recordSettings.id, RecordFileAnswer.create({
|
|
213
|
+
file: new File({
|
|
214
|
+
id: 'test',
|
|
215
|
+
server: 'https://replace-me.example.com',
|
|
216
|
+
path: 'test.pdf',
|
|
217
|
+
size: 100,
|
|
218
|
+
isPrivate: false,
|
|
219
|
+
}),
|
|
220
|
+
settings: recordSettings,
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
const arr = new PatchableArray() as PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
224
|
+
arr.addPut(member);
|
|
225
|
+
|
|
226
|
+
// An attacker doesn't use our client, so build the request body they would send instead
|
|
227
|
+
const encoded = encodeObject(arr, { version: Version, medium: EncodeMedium.Network });
|
|
228
|
+
return JSON.parse(JSON.stringify(encoded).replaceAll('https://replace-me.example.com', server));
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const attacks = [
|
|
232
|
+
{ server: 'javascript:alert(document.domain)', error: /can only be http or https/i },
|
|
233
|
+
{ server: 'data:text/html,<script>alert(1)</script>', error: /can only be http or https/i },
|
|
234
|
+
{ server: 'not-a-url', error: /invalid url for a file/i },
|
|
235
|
+
{ server: '', error: /invalid url for a file/i },
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
for (const attack of attacks) {
|
|
239
|
+
const r = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), buildBody(attack.server));
|
|
240
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
241
|
+
|
|
242
|
+
await expect(testServer.test(endpoint, r), attack.server).rejects.toThrow(attack.error);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ... and our own code can never store one either
|
|
246
|
+
expect(() => Request.buildJson('PATCH', baseUrl, organization.getApiHost(), new File({
|
|
247
|
+
id: 'test',
|
|
248
|
+
server: 'javascript:alert(document.domain)',
|
|
249
|
+
path: 'test.pdf',
|
|
250
|
+
size: 100,
|
|
251
|
+
}))).toThrow(/can only be http or https/i);
|
|
252
|
+
});
|
|
253
|
+
|
|
191
254
|
test('Can set public files', async () => {
|
|
192
255
|
const organization = await createOrganization();
|
|
193
256
|
|
|
@@ -211,7 +274,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
211
274
|
// access to the signed URL. This should not be possible without a valid signature
|
|
212
275
|
const publicFile = new File({
|
|
213
276
|
id: 'test',
|
|
214
|
-
server: 'test.com',
|
|
277
|
+
server: 'https://test.com',
|
|
215
278
|
path: 'test.txt',
|
|
216
279
|
size: 100,
|
|
217
280
|
isPrivate: false,
|
|
@@ -275,7 +338,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
275
338
|
// access to the signed URL. This should not be possible without a valid signature
|
|
276
339
|
const maliciousFile = new File({
|
|
277
340
|
id: 'test',
|
|
278
|
-
server: 'test.com',
|
|
341
|
+
server: 'https://test.com',
|
|
279
342
|
path: 'test.txt',
|
|
280
343
|
size: 100,
|
|
281
344
|
isPrivate: true,
|
|
@@ -355,7 +418,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
355
418
|
// access to the signed URL. This should not be possible without a valid signature
|
|
356
419
|
const maliciousFile = new File({
|
|
357
420
|
id: 'test',
|
|
358
|
-
server: 'test.com',
|
|
421
|
+
server: 'https://test.com',
|
|
359
422
|
path: 'test.txt',
|
|
360
423
|
size: 100,
|
|
361
424
|
isPrivate: true,
|
|
@@ -418,7 +481,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
418
481
|
// access to the signed URL. This should not be possible without a valid signature
|
|
419
482
|
const privateFile = new File({
|
|
420
483
|
id: 'test',
|
|
421
|
-
server: 'test.com',
|
|
484
|
+
server: 'https://test.com',
|
|
422
485
|
path: 'test.txt',
|
|
423
486
|
size: 100,
|
|
424
487
|
isPrivate: true,
|
|
@@ -447,7 +510,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
447
510
|
// access to the signed URL. This should not be possible without a valid signature
|
|
448
511
|
const privateFile = new File({
|
|
449
512
|
id: 'test',
|
|
450
|
-
server: 'test.com',
|
|
513
|
+
server: 'https://test.com',
|
|
451
514
|
path: 'test.txt',
|
|
452
515
|
size: 100,
|
|
453
516
|
isPrivate: true,
|
|
@@ -481,7 +544,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
481
544
|
// access to the signed URL. This should not be possible without a valid signature
|
|
482
545
|
const privateFile = new File({
|
|
483
546
|
id: 'test',
|
|
484
|
-
server: 'test.com',
|
|
547
|
+
server: 'https://test.com',
|
|
485
548
|
path: 'test.txt',
|
|
486
549
|
size: 100,
|
|
487
550
|
isPrivate: true,
|
|
@@ -509,7 +572,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
509
572
|
// access to the signed URL. This should not be possible without a valid signature
|
|
510
573
|
const privateFile = new File({
|
|
511
574
|
id: 'test',
|
|
512
|
-
server: 'test.com',
|
|
575
|
+
server: 'https://test.com',
|
|
513
576
|
path: 'test.txt',
|
|
514
577
|
size: 100,
|
|
515
578
|
isPrivate: true,
|
|
@@ -537,7 +600,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
537
600
|
// access to the signed URL. This should not be possible without a valid signature
|
|
538
601
|
const privateFile = new File({
|
|
539
602
|
id: 'test',
|
|
540
|
-
server: 'test.com',
|
|
603
|
+
server: 'https://test.com',
|
|
541
604
|
path: 'test.txt',
|
|
542
605
|
size: 100,
|
|
543
606
|
isPrivate: true,
|
|
@@ -564,7 +627,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
564
627
|
// access to the signed URL. This should not be possible without a valid signature
|
|
565
628
|
const privateFile = new File({
|
|
566
629
|
id: 'test',
|
|
567
|
-
server: 'test.com',
|
|
630
|
+
server: 'https://test.com',
|
|
568
631
|
path: 'test.txt',
|
|
569
632
|
size: 100,
|
|
570
633
|
isPrivate: true,
|
|
@@ -613,7 +676,7 @@ describe('E2E.PrivateFiles', () => {
|
|
|
613
676
|
// access to the signed URL. This should not be possible without a valid signature
|
|
614
677
|
const maliciousFile = new File({
|
|
615
678
|
id: 'test',
|
|
616
|
-
server: 'test.com',
|
|
679
|
+
server: 'https://test.com',
|
|
617
680
|
path: 'test.txt',
|
|
618
681
|
size: 100,
|
|
619
682
|
isPrivate: true,
|
|
@@ -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
|
|
package/tests/helpers/index.ts
CHANGED