@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.138.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
"import": "./dist/src/boot.js",
|
|
20
20
|
"require": "./dist/src/boot.js"
|
|
21
21
|
},
|
|
22
|
+
"./services/*": {
|
|
23
|
+
"types": "./dist/src/services/*.d.ts",
|
|
24
|
+
"import": "./dist/src/services/*.js",
|
|
25
|
+
"require": "./dist/src/services/*.js",
|
|
26
|
+
"@stamhoofd/source": "./src/services/*.ts"
|
|
27
|
+
},
|
|
22
28
|
"./tests/helpers": {
|
|
23
29
|
"types": "./dist/tests/helpers/index.d.ts",
|
|
24
30
|
"import": "./dist/tests/helpers/index.js",
|
|
@@ -58,21 +64,23 @@
|
|
|
58
64
|
"@simonbackx/simple-endpoints": "1.22.0",
|
|
59
65
|
"@simonbackx/simple-errors": "1.5.0",
|
|
60
66
|
"@simonbackx/simple-logging": "1.0.1",
|
|
61
|
-
"@
|
|
62
|
-
"@stamhoofd/backend-
|
|
63
|
-
"@stamhoofd/backend-
|
|
64
|
-
"@stamhoofd/
|
|
65
|
-
"@stamhoofd/
|
|
66
|
-
"@stamhoofd/
|
|
67
|
-
"@stamhoofd/
|
|
68
|
-
"@stamhoofd/
|
|
69
|
-
"@stamhoofd/
|
|
70
|
-
"@stamhoofd/
|
|
71
|
-
"@stamhoofd/
|
|
72
|
-
"@stamhoofd/
|
|
73
|
-
"@stamhoofd/
|
|
74
|
-
"@stamhoofd/
|
|
67
|
+
"@simplewebauthn/server": "13.3.2",
|
|
68
|
+
"@stamhoofd/backend-env": "2.138.0",
|
|
69
|
+
"@stamhoofd/backend-i18n": "2.138.0",
|
|
70
|
+
"@stamhoofd/backend-middleware": "2.138.0",
|
|
71
|
+
"@stamhoofd/crons": "2.138.0",
|
|
72
|
+
"@stamhoofd/email": "2.138.0",
|
|
73
|
+
"@stamhoofd/excel-writer": "2.138.0",
|
|
74
|
+
"@stamhoofd/logging": "2.138.0",
|
|
75
|
+
"@stamhoofd/models": "2.138.0",
|
|
76
|
+
"@stamhoofd/object-differ": "2.138.0",
|
|
77
|
+
"@stamhoofd/queues": "2.138.0",
|
|
78
|
+
"@stamhoofd/sql": "2.138.0",
|
|
79
|
+
"@stamhoofd/structures": "2.138.0",
|
|
80
|
+
"@stamhoofd/types": "2.138.0",
|
|
81
|
+
"@stamhoofd/utility": "2.138.0",
|
|
75
82
|
"archiver": "7.0.1",
|
|
83
|
+
"argon2": "0.44.0",
|
|
76
84
|
"axios": "1.18.1",
|
|
77
85
|
"base-x": "3.0.11",
|
|
78
86
|
"chalk": "5.6.2",
|
|
@@ -89,11 +97,12 @@
|
|
|
89
97
|
"mysql2": "3.23.1",
|
|
90
98
|
"node-rsa": "2.0.0",
|
|
91
99
|
"openid-client": "5.7.1",
|
|
100
|
+
"otplib": "12.0.1",
|
|
92
101
|
"stripe": "16.12.0",
|
|
93
102
|
"uuid": "14.0.1"
|
|
94
103
|
},
|
|
95
104
|
"devDependencies": {
|
|
96
|
-
"@stamhoofd/test-utils": "2.
|
|
105
|
+
"@stamhoofd/test-utils": "2.138.0",
|
|
97
106
|
"@types/cookie": "0.6.0",
|
|
98
107
|
"@types/luxon": "3.7.2",
|
|
99
108
|
"@types/mailparser": "3.4.6",
|
|
@@ -108,5 +117,5 @@
|
|
|
108
117
|
"publishConfig": {
|
|
109
118
|
"access": "public"
|
|
110
119
|
},
|
|
111
|
-
"gitHead": "
|
|
120
|
+
"gitHead": "781877acb024516e384b4119af7e86f847a9d237"
|
|
112
121
|
}
|
package/src/boot.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { resumeEmails } from './helpers/EmailResumer.js';
|
|
|
18
18
|
import { GlobalHelper } from './helpers/GlobalHelper.js';
|
|
19
19
|
import { SetupStepUpdater } from './helpers/SetupStepUpdater.js';
|
|
20
20
|
import { ContextMiddleware } from './middleware/ContextMiddleware.js';
|
|
21
|
+
import { TenantScopeMiddleware } from './middleware/TenantScopeMiddleware.js';
|
|
21
22
|
import { AuditLogService } from './services/AuditLogService.js';
|
|
22
23
|
import { BalanceItemService } from './services/BalanceItemService.js';
|
|
23
24
|
import { BootChecksService } from './services/BootChecksService.js';
|
|
@@ -137,6 +138,10 @@ export const boot = async (options: { killProcess: boolean }) => {
|
|
|
137
138
|
// Contexts
|
|
138
139
|
routerServer.addRequestMiddleware(ContextMiddleware);
|
|
139
140
|
|
|
141
|
+
// After ContextMiddleware on purpose: the last registered wrapRun is the outermost, and the
|
|
142
|
+
// tenant has to be in scope before the context that reads it.
|
|
143
|
+
routerServer.addRequestMiddleware(TenantScopeMiddleware);
|
|
144
|
+
|
|
140
145
|
// Add version headers and minimum version
|
|
141
146
|
const versionMiddleware = new VersionMiddleware({
|
|
142
147
|
latestVersions: {
|
|
@@ -6,6 +6,7 @@ import type { OrganizationEmail, StamhoofdFilter } from '@stamhoofd/structures';
|
|
|
6
6
|
import { EmailRecipientFilter, EmailRecipientSubfilter, EmailTemplateType, ReceivableBalanceType } from '@stamhoofd/structures';
|
|
7
7
|
import { ContextInstance } from '../helpers/Context.js';
|
|
8
8
|
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
9
|
+
import { EmailSendService } from '../services/EmailSendService.js';
|
|
9
10
|
|
|
10
11
|
registerCron('balanceEmails', balanceEmails);
|
|
11
12
|
|
|
@@ -201,7 +202,7 @@ async function sendTemplate({
|
|
|
201
202
|
|
|
202
203
|
try {
|
|
203
204
|
const upToDate = await ContextInstance.startForUser(systemUser, organization, async () => {
|
|
204
|
-
return await
|
|
205
|
+
return await EmailSendService.queueForSending(model, true);
|
|
205
206
|
});
|
|
206
207
|
|
|
207
208
|
if (!upToDate) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { registerCron } from '@stamhoofd/crons';
|
|
2
|
+
import { MFAToken, WebauthnChallenge } from '@stamhoofd/models';
|
|
3
|
+
|
|
4
|
+
let lastRunDate: number | null = null;
|
|
5
|
+
|
|
6
|
+
registerCron('deleteExpiredMFATokens', deleteExpiredMFATokens);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sweep the short-lived tokens of the two-factor flows.
|
|
10
|
+
*
|
|
11
|
+
* Both are also cleaned up while they are used (a new challenge supersedes the previous
|
|
12
|
+
* one, and an expired token is dropped when it is looked up), but a token of a login that
|
|
13
|
+
* was simply abandoned has nothing left to trigger that. Run every night at 5 AM.
|
|
14
|
+
*/
|
|
15
|
+
export async function deleteExpiredMFATokens() {
|
|
16
|
+
const now = new Date();
|
|
17
|
+
|
|
18
|
+
if (now.getDate() === lastRunDate) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hour = now.getHours();
|
|
23
|
+
|
|
24
|
+
// between 5 and 6 AM
|
|
25
|
+
if (hour !== 5 && STAMHOOFD.environment !== 'development') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tokens = await MFAToken.deleteExpired();
|
|
30
|
+
const challenges = await WebauthnChallenge.deleteExpired();
|
|
31
|
+
|
|
32
|
+
console.log(`Deleted ${tokens} expired MFA tokens and ${challenges} expired WebAuthn challenges.`);
|
|
33
|
+
|
|
34
|
+
lastRunDate = now.getDate();
|
|
35
|
+
}
|
package/src/crons/drip-emails.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerCron } from '@stamhoofd/crons';
|
|
2
2
|
import { Organization } from '@stamhoofd/models';
|
|
3
|
+
import { OrganizationEmailService } from '../services/OrganizationEmailService.js';
|
|
3
4
|
import { isOutside } from './helpers/isOutside.js';
|
|
4
5
|
import { useSavedIterator } from './helpers/useSavedIterator.js';
|
|
5
6
|
|
|
@@ -30,7 +31,7 @@ async function checkDrips() {
|
|
|
30
31
|
// Get the next x organization to send e-mails for
|
|
31
32
|
for await (const organization of iterate()) {
|
|
32
33
|
try {
|
|
33
|
-
await
|
|
34
|
+
await OrganizationEmailService.checkDrips(organization);
|
|
34
35
|
} catch (e) {
|
|
35
36
|
console.error(e);
|
|
36
37
|
}
|
package/src/crons/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import './update-cached-balances.js';
|
|
|
5
5
|
import './cleanup-orphaned-cached-balances.js';
|
|
6
6
|
import './balance-emails.js';
|
|
7
7
|
import './delete-old-email-drafts.js';
|
|
8
|
+
import './delete-expired-mfa-tokens.js';
|
|
8
9
|
import './delete-archived-data.js';
|
|
9
10
|
import './mollie-chargebacks.js';
|
|
10
11
|
import './mollie-refunds.js';
|
package/src/crons/invoices.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { isSimpleError, isSimpleErrors } from '@simonbackx/simple-errors';
|
|
2
2
|
import { registerCron } from '@stamhoofd/crons';
|
|
3
3
|
import type { Invoice } from '@stamhoofd/models';
|
|
4
|
-
import { Organization, Payment
|
|
4
|
+
import { Organization, Payment } from '@stamhoofd/models';
|
|
5
|
+
import { sendEmailTemplate } from '../helpers/EmailBuilder.js';
|
|
5
6
|
import { EmailTemplateType, InvoiceStruct, PaymentStatus, Replacement } from '@stamhoofd/structures';
|
|
6
7
|
import { Formatter, Sorter } from '@stamhoofd/utility';
|
|
7
8
|
import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
|
|
8
9
|
import { InvoiceService } from '../services/InvoiceService.js';
|
|
9
10
|
import { useSavedIterator } from './helpers/useSavedIterator.js';
|
|
10
11
|
import { isOutside } from './helpers/isOutside.js';
|
|
12
|
+
import { OrganizationAdminService } from '../services/OrganizationAdminService.js';
|
|
11
13
|
|
|
12
14
|
registerCron('invoices', invoices);
|
|
13
15
|
|
|
@@ -50,7 +52,7 @@ async function createInvoicesFor(organization: Organization) {
|
|
|
50
52
|
|
|
51
53
|
// Belgian rules: allowed to invoice up to the 15th day of the next month. We extend it with one month to fix mistakes.
|
|
52
54
|
const today = Formatter.luxon();
|
|
53
|
-
const startDate = today.day <= 15 ? today.minus({ month:
|
|
55
|
+
const startDate = today.day <= 15 ? today.minus({ month: 3 }).startOf('month') : today.minus({ month: 2 }).startOf('month');
|
|
54
56
|
|
|
55
57
|
// Don't invoice below 4 euro - unless we reached the timeout date for invoices (end of month + 15 days - 5 days margin) OR + 15 day offset
|
|
56
58
|
const invoiceLimit = STAMHOOFD.environment === 'development' ? 0 : 4_0000;
|
|
@@ -147,7 +149,7 @@ async function createInvoicesFor(organization: Organization) {
|
|
|
147
149
|
template: {
|
|
148
150
|
type: EmailTemplateType.InvoiceGenerationErrors,
|
|
149
151
|
},
|
|
150
|
-
recipients: await
|
|
152
|
+
recipients: await OrganizationAdminService.getAdminRecipients(organization),
|
|
151
153
|
type: 'transactional',
|
|
152
154
|
fromStamhoofd: true,
|
|
153
155
|
defaultReplacements: [
|
package/src/crons.ts
CHANGED
|
@@ -4,8 +4,10 @@ import { Group, Organization, Payment, Registration, STPackage, Webshop } from '
|
|
|
4
4
|
import { SQL } from '@stamhoofd/sql';
|
|
5
5
|
import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
6
6
|
import { checkSettlements } from './helpers/CheckSettlements.js';
|
|
7
|
+
import { OrganizationDNSService } from './services/OrganizationDNSService.js';
|
|
7
8
|
import { PaymentService } from './services/PaymentService.js';
|
|
8
9
|
import { RegistrationService } from './services/RegistrationService.js';
|
|
10
|
+
import { STPackageService } from './services/STPackageService.js';
|
|
9
11
|
|
|
10
12
|
let lastDNSCheck: Date | null = null;
|
|
11
13
|
let lastDNSId = '';
|
|
@@ -39,7 +41,7 @@ async function checkDNS() {
|
|
|
39
41
|
console.log('[DNS] ' + organization.name);
|
|
40
42
|
}
|
|
41
43
|
try {
|
|
42
|
-
await
|
|
44
|
+
await OrganizationDNSService.updateDNSRecords(organization);
|
|
43
45
|
} catch (e) {
|
|
44
46
|
console.error(e);
|
|
45
47
|
}
|
|
@@ -76,7 +78,7 @@ async function checkExpirationEmails() {
|
|
|
76
78
|
console.log('[EXPIRATION EMAILS] Sending expiration emails...');
|
|
77
79
|
|
|
78
80
|
for (const pack of packages) {
|
|
79
|
-
await
|
|
81
|
+
await STPackageService.sendExpiryEmail(pack);
|
|
80
82
|
}
|
|
81
83
|
lastExpirationCheck = new Date();
|
|
82
84
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Email,
|
|
1
|
+
import { Email, Webshop } from '@stamhoofd/models';
|
|
2
|
+
import { runWithRecipientLocale } from '../helpers/EmailBuilder.js';
|
|
2
3
|
import type { EmailRecipient, LimitedFilteredRequest, WebshopPreview } from '@stamhoofd/structures';
|
|
3
4
|
import { mergeFilters, PaginatedResponse } from '@stamhoofd/structures';
|
|
4
5
|
import { GetWebshopOrdersEndpoint } from '../endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.js';
|
|
@@ -6,6 +6,7 @@ import { baseInMemoryFilterCompilers, compileToInMemoryFilter, createInMemoryFil
|
|
|
6
6
|
import { PaginatedResponse } from '@stamhoofd/structures';
|
|
7
7
|
import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
|
|
8
8
|
import { GetOrganizationsEndpoint } from '../endpoints/admin/organizations/GetOrganizationsEndpoint.js';
|
|
9
|
+
import { OrganizationAdminService } from '../services/OrganizationAdminService.js';
|
|
9
10
|
|
|
10
11
|
function userToFilterableAdmin(user: User, platform: PlatformStruct, organization: Organization) {
|
|
11
12
|
return {
|
|
@@ -49,14 +50,14 @@ async function fetchRecipients(query: LimitedFilteredRequest, subfilter: Stamhoo
|
|
|
49
50
|
const recipients: EmailRecipient[] = [];
|
|
50
51
|
for (const organization of result.results) {
|
|
51
52
|
// todo: filter admins
|
|
52
|
-
const users = await
|
|
53
|
+
const users = await OrganizationAdminService.getAdmins(organization);
|
|
53
54
|
const filteredUsers = users.filter((user) => {
|
|
54
55
|
const filterable = userToFilterableAdmin(user, platform, organization);
|
|
55
56
|
return compiledFilter(filterable);
|
|
56
57
|
});
|
|
57
58
|
|
|
58
59
|
recipients.push(
|
|
59
|
-
...
|
|
60
|
+
...OrganizationAdminService.adminsToRecipients(filteredUsers).map((r) => {
|
|
60
61
|
return EmailRecipient.create({
|
|
61
62
|
...r,
|
|
62
63
|
replacements: [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RecipientLoader } from '@stamhoofd/models';
|
|
2
|
-
import { BalanceItem, BalanceItemPayment, Email, Member, MemberResponsibilityRecord, Organization, Payment, User } from '@stamhoofd/models';
|
|
2
|
+
import { BalanceItem, BalanceItemPayment, Email, Member, MemberResponsibilityRecord, Organization, Payment, Platform, User } from '@stamhoofd/models';
|
|
3
3
|
import { compileToSQLFilter, SQL } from '@stamhoofd/sql';
|
|
4
4
|
import type { LimitedFilteredRequest, PaymentGeneral, StamhoofdFilter } from '@stamhoofd/structures';
|
|
5
5
|
import { CountFilteredRequest, EmailRecipient, PaginatedResponse, PaymentMethod } from '@stamhoofd/structures';
|
|
@@ -301,12 +301,13 @@ async function getOrganizationRecipients(ids: { organizationId: string; payment:
|
|
|
301
301
|
if (subFilter === null) {
|
|
302
302
|
// Use full admins instead
|
|
303
303
|
const admins = await User.getAdmins(allOrganizationIds, { verified: true });
|
|
304
|
+
const platform = await Platform.getSharedStruct();
|
|
304
305
|
for (const { organizationId, payment } of ids) {
|
|
305
306
|
const organization = organizationMap.get(organizationId);
|
|
306
307
|
if (!organization) {
|
|
307
308
|
continue;
|
|
308
309
|
}
|
|
309
|
-
const users = admins.filter(a => a.permissions?.forOrganization(organization)?.hasFullAccess());
|
|
310
|
+
const users = admins.filter(a => a.permissions?.forOrganization(organization, platform, { inheritFromPlatform: false })?.hasFullAccess());
|
|
310
311
|
|
|
311
312
|
if (users.length === 0) {
|
|
312
313
|
console.warn('No admins found for organization with id ', organizationId, ' while fetching email recipients for payment with id ', payment.id);
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { MFATOTP } from '@stamhoofd/models';
|
|
6
|
+
import type { MFAEnrollmentResult } from '@stamhoofd/structures';
|
|
7
|
+
import { MFAMethodType, TOTPConfirmRequest } from '@stamhoofd/structures';
|
|
8
|
+
|
|
9
|
+
import { Context } from '../../helpers/Context.js';
|
|
10
|
+
import { TOTPHelper } from '../../helpers/TOTPHelper.js';
|
|
11
|
+
import { TwoFactorHelper } from '../../helpers/TwoFactorHelper.js';
|
|
12
|
+
import { TwoFactorAuditLogService } from '../../services/TwoFactorAuditLogService.js';
|
|
13
|
+
|
|
14
|
+
type Params = { id: string };
|
|
15
|
+
type Query = undefined;
|
|
16
|
+
type Body = TOTPConfirmRequest;
|
|
17
|
+
type ResponseBody = MFAEnrollmentResult;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Confirm a pending TOTP authenticator by verifying the first code. On the user's first
|
|
21
|
+
* factor this also returns recovery codes, and during forced enrollment it issues a
|
|
22
|
+
* full session token.
|
|
23
|
+
*/
|
|
24
|
+
export class ConfirmTOTPEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
25
|
+
bodyDecoder = TOTPConfirmRequest as Decoder<TOTPConfirmRequest>;
|
|
26
|
+
|
|
27
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
28
|
+
if (request.method !== 'POST') {
|
|
29
|
+
return [false];
|
|
30
|
+
}
|
|
31
|
+
const params = Endpoint.parseParameters(request.url, '/mfa/totp/@id/confirm', { id: String });
|
|
32
|
+
if (params) {
|
|
33
|
+
return [true, params as Params];
|
|
34
|
+
}
|
|
35
|
+
return [false];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
39
|
+
await Context.setOptionalOrganizationScope();
|
|
40
|
+
const { user, setupToken, token } = await Context.authenticateMFAEnrollment();
|
|
41
|
+
|
|
42
|
+
const totp = await MFATOTP.getByID(request.params.id);
|
|
43
|
+
if (!totp || totp.userId !== user.id) {
|
|
44
|
+
throw new SimpleError({
|
|
45
|
+
code: 'not_found',
|
|
46
|
+
message: 'TOTP authenticator not found',
|
|
47
|
+
human: $t('%ZgJ'),
|
|
48
|
+
statusCode: 404,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (totp.confirmedAt) {
|
|
53
|
+
throw new SimpleError({
|
|
54
|
+
code: 'already_confirmed',
|
|
55
|
+
message: 'This authenticator is already confirmed',
|
|
56
|
+
human: $t('%ZgM'),
|
|
57
|
+
statusCode: 400,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const counter = TOTPHelper.verify(request.body.code, totp.secret);
|
|
62
|
+
if (counter === null) {
|
|
63
|
+
throw new SimpleError({
|
|
64
|
+
code: 'invalid_mfa_code',
|
|
65
|
+
message: 'Invalid code',
|
|
66
|
+
human: $t('%Zgp'),
|
|
67
|
+
field: 'code',
|
|
68
|
+
statusCode: 400,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const wasFirstFactor = !(await TwoFactorHelper.userHasFactors(user.id));
|
|
73
|
+
|
|
74
|
+
totp.confirmedAt = new Date();
|
|
75
|
+
totp.name = request.body.name.trim() || $t('%ZiG');
|
|
76
|
+
// Record the counter so the confirmation code can't be replayed as a login code.
|
|
77
|
+
totp.lastUsedCounter = counter;
|
|
78
|
+
await totp.save();
|
|
79
|
+
|
|
80
|
+
await TwoFactorAuditLogService.logMethodAdded(user, MFAMethodType.TOTP, totp.name);
|
|
81
|
+
|
|
82
|
+
return new Response(await TwoFactorHelper.completeEnrollment(user, setupToken, wasFirstFactor, token));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -2,13 +2,15 @@ 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 {
|
|
5
|
+
import { Platform, User } from '@stamhoofd/models';
|
|
6
|
+
import { sendEmailTemplate } from '../../helpers/EmailBuilder.js';
|
|
6
7
|
import type { UserWithMembers } from '@stamhoofd/structures';
|
|
7
8
|
import { EmailTemplateType, Recipient, Replacement, UserPermissions, User as UserStruct } from '@stamhoofd/structures';
|
|
8
9
|
import { Formatter } from '@stamhoofd/utility';
|
|
9
10
|
|
|
10
11
|
import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures.js';
|
|
11
12
|
import { Context } from '../../helpers/Context.js';
|
|
13
|
+
import { PasswordForgotService } from '../../services/PasswordForgotService.js';
|
|
12
14
|
type Params = Record<string, never>;
|
|
13
15
|
type Query = undefined;
|
|
14
16
|
type Body = UserStruct;
|
|
@@ -117,7 +119,7 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
117
119
|
validUntil.setTime(validUntil.getTime() + 7 * 24 * 3600 * 1000);
|
|
118
120
|
|
|
119
121
|
const dateTime = Formatter.dateTime(validUntil);
|
|
120
|
-
const recoveryUrl = await
|
|
122
|
+
const recoveryUrl = await PasswordForgotService.getPasswordRecoveryUrl(admin, organization, request.i18n, validUntil);
|
|
121
123
|
const platformName = ((await Platform.getSharedStruct()).config.name);
|
|
122
124
|
|
|
123
125
|
const name = organization?.name ?? platformName;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
2
|
+
import { OrganizationFactory, Token, User, UserFactory } from '@stamhoofd/models';
|
|
3
3
|
import { Token as TokenStruct } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { testServer } from '../../../tests/helpers/TestServer.js';
|
|
@@ -65,4 +65,64 @@ describe('Endpoint.CreateToken', () => {
|
|
|
65
65
|
const byRefresh = await Token.getByRefreshToken(response.body.refreshToken);
|
|
66
66
|
expect(byRefresh).toBeDefined();
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
describe('last activity', () => {
|
|
70
|
+
const password = 'test-password-1234';
|
|
71
|
+
|
|
72
|
+
async function getLastActiveAt(user: User): Promise<Date | null> {
|
|
73
|
+
const updated = await User.getByID(user.id);
|
|
74
|
+
return updated!.lastActiveAt;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test('a password login marks the user as active', async () => {
|
|
78
|
+
const organization = await new OrganizationFactory({}).create();
|
|
79
|
+
const user = await new UserFactory({ organization, password }).create();
|
|
80
|
+
expect(user.lastActiveAt).toBeNull();
|
|
81
|
+
|
|
82
|
+
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
83
|
+
grant_type: 'password',
|
|
84
|
+
username: user.email,
|
|
85
|
+
password,
|
|
86
|
+
});
|
|
87
|
+
await testServer.test(endpoint, r);
|
|
88
|
+
|
|
89
|
+
const lastActiveAt = await getLastActiveAt(user);
|
|
90
|
+
expect(lastActiveAt).not.toBeNull();
|
|
91
|
+
expect(lastActiveAt!.getTime()).toBeGreaterThan(Date.now() - 60 * 1000);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('refreshing a token marks the user as active again', async () => {
|
|
95
|
+
const organization = await new OrganizationFactory({}).create();
|
|
96
|
+
const user = await new UserFactory({ organization, password }).create();
|
|
97
|
+
const token = await Token.createToken(user);
|
|
98
|
+
|
|
99
|
+
const monthsAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
100
|
+
monthsAgo.setMilliseconds(0);
|
|
101
|
+
user.lastActiveAt = monthsAgo;
|
|
102
|
+
await user.save();
|
|
103
|
+
|
|
104
|
+
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
105
|
+
grant_type: 'refresh_token',
|
|
106
|
+
refresh_token: token.refreshToken,
|
|
107
|
+
});
|
|
108
|
+
await testServer.test(endpoint, r);
|
|
109
|
+
|
|
110
|
+
const lastActiveAt = await getLastActiveAt(user);
|
|
111
|
+
expect(lastActiveAt!.getTime()).toBeGreaterThan(Date.now() - 60 * 1000);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a failed login does not mark the user as active', async () => {
|
|
115
|
+
const organization = await new OrganizationFactory({}).create();
|
|
116
|
+
const user = await new UserFactory({ organization, password }).create();
|
|
117
|
+
|
|
118
|
+
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
119
|
+
grant_type: 'password',
|
|
120
|
+
username: user.email,
|
|
121
|
+
password: 'wrong-password',
|
|
122
|
+
});
|
|
123
|
+
await expect(testServer.test(endpoint, r)).rejects.toThrow();
|
|
124
|
+
|
|
125
|
+
expect(await getLastActiveAt(user)).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
68
128
|
});
|