@stamhoofd/backend 2.144.0 → 2.145.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 +19 -19
- package/src/crons/mollie-chargebacks.ts +31 -29
- package/src/crons/mollie-refunds.ts +61 -59
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
- package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
- package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
- package/src/endpoints/auth/MFA.security.test.ts +9 -7
- package/src/endpoints/auth/MFA.test.ts +42 -15
- package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
- package/src/endpoints/global/files/UploadFile.test.ts +2 -1
- package/src/endpoints/global/files/UploadImage.test.ts +2 -1
- package/src/endpoints/global/files/upload-security.test.ts +3 -2
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
- package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
- package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
- package/src/helpers/Context.ts +13 -6
- package/src/helpers/ContextStorage.ts +5 -0
- package/src/helpers/StripeSettlementSync.ts +10 -7
- package/src/helpers/TwoFactorHelper.ts +21 -22
- package/src/services/AdminSessionService.ts +5 -3
- package/src/services/InvoiceService.ts +2 -2
- package/src/services/MollieService.ts +168 -126
- package/src/services/PaymentService.ts +2 -2
- package/src/services/SSOService.test.ts +25 -1
- package/src/services/SSOService.ts +17 -6
- package/src/services/SessionService.test.ts +319 -0
- package/src/services/SessionService.ts +357 -0
- package/src/services/WebshopCrowdfundingService.test.ts +4 -3
- package/src/sql-filters/registrations.ts +59 -1
- package/tests/actions/patchUserMember.ts +2 -1
- package/tests/e2e/api-rate-limits.test.ts +2 -1
- package/tests/e2e/bundle-discounts.test.ts +4 -3
- package/tests/e2e/charge-members.test.ts +6 -5
- package/tests/e2e/documents.test.ts +4 -3
- package/tests/e2e/private-files.test.ts +9 -8
- package/tests/e2e/register.test.ts +4 -3
- package/tests/e2e/stock.test.ts +4 -3
- package/tests/e2e/tickets.test.ts +4 -3
- package/tests/helpers/ExportSlice.ts +2 -1
- package/tests/helpers/MollieMocker.ts +85 -23
- package/tests/init/initAdmin.ts +2 -1
- package/tests/init/initPlatformAdmin.ts +2 -1
- package/src/helpers/ViesHelper.test.ts +0 -401
- package/src/helpers/ViesHelper.ts +0 -222
- package/src/services/PeppolDirectoryService.test.ts +0 -139
- package/src/services/PeppolDirectoryService.ts +0 -111
|
@@ -512,7 +512,7 @@ export class StripeSettlementSync {
|
|
|
512
512
|
case 'payment': {
|
|
513
513
|
const payment = await this.#resolvePayment(transaction);
|
|
514
514
|
|
|
515
|
-
if (payment.organizationId !== settlement.organizationId) {
|
|
515
|
+
if (!payment || payment.organizationId !== settlement.organizationId) {
|
|
516
516
|
// Destination charge via the platform
|
|
517
517
|
await this.#storePaidFeesForTransaction(transaction, settlement, { paymentId: null });
|
|
518
518
|
return;
|
|
@@ -692,7 +692,7 @@ export class StripeSettlementSync {
|
|
|
692
692
|
continue;
|
|
693
693
|
}
|
|
694
694
|
|
|
695
|
-
|
|
695
|
+
await SettlementService.upsertCharge({
|
|
696
696
|
type: getFeeDetailType(detail, transaction),
|
|
697
697
|
externalId: transaction.id + ':fee:' + index,
|
|
698
698
|
amount: -detail.amount * 100,
|
|
@@ -826,7 +826,7 @@ export class StripeSettlementSync {
|
|
|
826
826
|
await payment.save();
|
|
827
827
|
}
|
|
828
828
|
|
|
829
|
-
async #resolvePayment(transaction: Stripe.BalanceTransaction): Promise<Payment> {
|
|
829
|
+
async #resolvePayment(transaction: Stripe.BalanceTransaction): Promise<Payment | null> {
|
|
830
830
|
const source = transaction.source;
|
|
831
831
|
if (!source || typeof source === 'string' || source.object !== 'charge') {
|
|
832
832
|
throw new SimpleError({
|
|
@@ -848,10 +848,13 @@ export class StripeSettlementSync {
|
|
|
848
848
|
|
|
849
849
|
const payment = await Payment.getByID(paymentId);
|
|
850
850
|
if (!payment) {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
851
|
+
// Probably deleted
|
|
852
|
+
|
|
853
|
+
return null;
|
|
854
|
+
// throw new SimpleError({
|
|
855
|
+
// code: 'payment_not_found',
|
|
856
|
+
// message: 'Payment ' + paymentId + ' referenced by charge ' + source.id + ' does not exist',
|
|
857
|
+
// });
|
|
855
858
|
}
|
|
856
859
|
|
|
857
860
|
this.#assertPaymentScope(payment, source.id);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
2
|
import type { I18n } from '@stamhoofd/backend-i18n/I18n';
|
|
3
|
-
import type { User } from '@stamhoofd/models';
|
|
4
|
-
import { MFARecoveryCode, MFATOTP, MFAToken, Organization, Platform, RateLimiter,
|
|
3
|
+
import type { User, Token } from '@stamhoofd/models';
|
|
4
|
+
import { MFARecoveryCode, MFATOTP, MFAToken, Organization, Platform, RateLimiter, WebauthnCredential } from '@stamhoofd/models';
|
|
5
5
|
import type { User as UserStruct } from '@stamhoofd/structures';
|
|
6
|
-
import { MFAChallengeResponse, MFAEnrollmentResult, MFAMethodType, MFASetupResponse, MFAStatus, PasskeyCredential, RecoveryCodes, TOTPCredential, Token as TokenStruct } from '@stamhoofd/structures';
|
|
6
|
+
import { MFAChallengeResponse, MFAEnrollmentResult, MFAMethodType, MFASetupResponse, MFAStatus, PasskeyCredential, RecoveryCodes, SessionLoginMethod, TOTPCredential, Token as TokenStruct } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
|
|
9
9
|
|
|
10
10
|
import { PasswordForgotService } from '../services/PasswordForgotService.js';
|
|
11
|
+
import { SessionService } from '../services/SessionService.js';
|
|
11
12
|
import { RecoveryCodeHelper } from './RecoveryCodeHelper.js';
|
|
12
13
|
import { WebauthnHelper } from './WebauthnHelper.js';
|
|
13
14
|
import { Formatter, Sorter } from '@stamhoofd/utility';
|
|
@@ -133,33 +134,33 @@ export class TwoFactorHelper {
|
|
|
133
134
|
* credential was accepted.
|
|
134
135
|
*
|
|
135
136
|
* `loginMethod` describes the credential that was just verified:
|
|
136
|
-
* -
|
|
137
|
+
* - Password: the account password. A single credential that says nothing about
|
|
137
138
|
* whether the user still reads the email address on the account, so a required
|
|
138
139
|
* second factor must be set up here if the user does not have one yet — and a
|
|
139
140
|
* long-inactive admin has to confirm their email address before they may.
|
|
140
|
-
* -
|
|
141
|
+
* - Email: a password token or email verification code. Also a single credential,
|
|
141
142
|
* but one that only reaches someone who reads the account's email, so it is the way
|
|
142
143
|
* out of that email confirmation.
|
|
143
|
-
* -
|
|
144
|
+
* - SSO: an external identity provider already authenticated the user, and is
|
|
144
145
|
* trusted to apply its own second factor. An enrolled factor is still verified
|
|
145
146
|
* (the user asked us to protect their account), but we do not force enrollment —
|
|
146
147
|
* unless the account ALSO has a password, because then the password remains a way
|
|
147
148
|
* in that bypasses whatever the provider enforces.
|
|
148
149
|
*/
|
|
149
|
-
static async getSecondFactorRequirement(user: User, organization: Organization | null, { loginMethod }: { loginMethod:
|
|
150
|
+
static async getSecondFactorRequirement(user: User, organization: Organization | null, { loginMethod }: { loginMethod: SessionLoginMethod }): Promise<SecondFactorRequirement> {
|
|
150
151
|
if (await TwoFactorHelper.userHasFactors(user.id)) {
|
|
151
|
-
return { type: 'challenge', challenge: await TwoFactorHelper.createLoginChallenge(user) };
|
|
152
|
+
return { type: 'challenge', challenge: await TwoFactorHelper.createLoginChallenge(user, { loginMethod }) };
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
if (loginMethod ===
|
|
155
|
+
if (loginMethod === SessionLoginMethod.SSO && !user.hasPasswordBasedAccount()) {
|
|
155
156
|
return { type: 'none' };
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
if (await TwoFactorHelper.isTwoFactorRequired(user, organization)) {
|
|
159
|
-
if (loginMethod ===
|
|
160
|
+
if (loginMethod === SessionLoginMethod.Password && TwoFactorHelper.isInactiveForEnrollment(user)) {
|
|
160
161
|
return { type: 'confirm-email' };
|
|
161
162
|
}
|
|
162
|
-
return { type: 'setup', setupToken: await MFAToken.createFor(user.id, 'setup') };
|
|
163
|
+
return { type: 'setup', setupToken: await MFAToken.createFor(user.id, 'setup', { loginMethod }) };
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
return { type: 'none' };
|
|
@@ -173,16 +174,14 @@ export class TwoFactorHelper {
|
|
|
173
174
|
static async sendEnrollmentConfirmationEmail(user: User, organization: Organization | null, i18n: I18n): Promise<void> {
|
|
174
175
|
try {
|
|
175
176
|
enrollmentConfirmationRateLimiter.track(user.id);
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
177
|
+
} catch {
|
|
178
178
|
// Sent often enough already.
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
try {
|
|
183
183
|
await PasswordForgotService.sendPasswordRecoveryEmail(user, organization, i18n);
|
|
184
|
-
}
|
|
185
|
-
catch (e) {
|
|
184
|
+
} catch (e) {
|
|
186
185
|
console.error('Could not send the two-factor enrollment confirmation email', e);
|
|
187
186
|
}
|
|
188
187
|
}
|
|
@@ -203,7 +202,7 @@ export class TwoFactorHelper {
|
|
|
203
202
|
* whoever holds the link could enroll one and get a session anyway, and the client
|
|
204
203
|
* needs a session to let the user choose a password before enrolling.
|
|
205
204
|
*/
|
|
206
|
-
static async assertSecondFactorOrThrow(user: User, organization: Organization | null, version: number, { loginMethod, i18n, allowTemporarySession = false }: { loginMethod:
|
|
205
|
+
static async assertSecondFactorOrThrow(user: User, organization: Organization | null, version: number, { loginMethod, i18n, allowTemporarySession = false }: { loginMethod: SessionLoginMethod.Password | SessionLoginMethod.Email; i18n: I18n; allowTemporarySession?: boolean }): Promise<void> {
|
|
207
206
|
const requirement = await TwoFactorHelper.getSecondFactorRequirement(user, organization, { loginMethod });
|
|
208
207
|
|
|
209
208
|
if (requirement.type === 'confirm-email') {
|
|
@@ -228,7 +227,7 @@ export class TwoFactorHelper {
|
|
|
228
227
|
}
|
|
229
228
|
|
|
230
229
|
if (requirement.type === 'setup') {
|
|
231
|
-
const temporaryToken = allowTemporarySession ? new TokenStruct(await
|
|
230
|
+
const temporaryToken = allowTemporarySession ? new TokenStruct(await SessionService.createSession(user, { loginMethod, authenticatedAt: new Date() })) : null;
|
|
232
231
|
|
|
233
232
|
throw new SimpleError({
|
|
234
233
|
code: 'require_mfa_setup',
|
|
@@ -313,7 +312,7 @@ export class TwoFactorHelper {
|
|
|
313
312
|
/**
|
|
314
313
|
* Create a login MFA session token and the challenge payload returned to the client.
|
|
315
314
|
*/
|
|
316
|
-
static async createLoginChallenge(user: User): Promise<MFAChallengeResponse> {
|
|
315
|
+
static async createLoginChallenge(user: User, { loginMethod }: { loginMethod: SessionLoginMethod }): Promise<MFAChallengeResponse> {
|
|
317
316
|
const { methods, passkeys } = await TwoFactorHelper.getEnrolledMethods(user.id);
|
|
318
317
|
|
|
319
318
|
let webauthnOptions: PublicKeyCredentialRequestOptionsJSON | null = null;
|
|
@@ -323,7 +322,7 @@ export class TwoFactorHelper {
|
|
|
323
322
|
challenge = webauthnOptions.challenge;
|
|
324
323
|
}
|
|
325
324
|
|
|
326
|
-
const mfaToken = await MFAToken.createFor(user.id, 'login', challenge);
|
|
325
|
+
const mfaToken = await MFAToken.createFor(user.id, 'login', { webauthnChallenge: challenge, loginMethod });
|
|
327
326
|
|
|
328
327
|
return MFAChallengeResponse.create({
|
|
329
328
|
token: mfaToken.token,
|
|
@@ -376,12 +375,12 @@ export class TwoFactorHelper {
|
|
|
376
375
|
}
|
|
377
376
|
|
|
378
377
|
// Before minting the new session, so it is never signed out by its own enrollment.
|
|
379
|
-
await
|
|
378
|
+
await SessionService.deleteOtherSessions({ userId: user.id, keepAccessToken: currentToken?.accessToken ?? null });
|
|
380
379
|
|
|
381
380
|
let token: TokenStruct | null = null;
|
|
382
381
|
if (setupToken) {
|
|
383
382
|
await setupToken.consume();
|
|
384
|
-
const t = await
|
|
383
|
+
const t = await SessionService.createSession(user, { loginMethod: setupToken.loginMethod, authenticatedAt: new Date() });
|
|
385
384
|
await user.markActive();
|
|
386
385
|
token = new TokenStruct(t);
|
|
387
386
|
}
|
|
@@ -403,7 +402,7 @@ export class TwoFactorHelper {
|
|
|
403
402
|
await MFARecoveryCode.deleteForUser(user.id);
|
|
404
403
|
}
|
|
405
404
|
|
|
406
|
-
await
|
|
405
|
+
await SessionService.deleteOtherSessions({ userId: user.id, keepAccessToken: currentToken?.accessToken ?? null });
|
|
407
406
|
|
|
408
407
|
return await TwoFactorHelper.buildStatus(user);
|
|
409
408
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { User } from '@stamhoofd/models';
|
|
2
|
+
|
|
3
|
+
import { SessionService } from './SessionService.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Signs out administrators in bulk.
|
|
@@ -25,7 +27,7 @@ export class AdminSessionService {
|
|
|
25
27
|
*/
|
|
26
28
|
static async signOutOrganizationAdmins(organizationId: string, keepAccessToken: string | null): Promise<number> {
|
|
27
29
|
const admins = await User.getAdmins(organizationId);
|
|
28
|
-
return await
|
|
30
|
+
return await SessionService.deleteForUsers({ userIds: admins.map(a => a.id), keepAccessToken });
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -36,6 +38,6 @@ export class AdminSessionService {
|
|
|
36
38
|
*/
|
|
37
39
|
static async signOutPlatformAdmins(keepAccessToken: string | null): Promise<number> {
|
|
38
40
|
const admins = await User.getPlatformAdmins();
|
|
39
|
-
return await
|
|
41
|
+
return await SessionService.deleteForUsers({ userIds: admins.map(a => a.id), keepAccessToken });
|
|
40
42
|
}
|
|
41
43
|
}
|
|
@@ -6,7 +6,7 @@ import { InvoiceCounter } from '@stamhoofd/models/helpers/InvoiceCounter.js';
|
|
|
6
6
|
import type { Invoice as InvoiceStruct } from '@stamhoofd/structures';
|
|
7
7
|
import { EmailTemplateType, PaymentStatus, Recipient, Replacement } from '@stamhoofd/structures';
|
|
8
8
|
import { Formatter } from '@stamhoofd/utility';
|
|
9
|
-
import {
|
|
9
|
+
import { ViesService } from '@stamhoofd/vies';
|
|
10
10
|
import { WebmasterReport } from '../helpers/WebmasterReport.js';
|
|
11
11
|
import { ApplicationFeeService } from './ApplicationFeeService.js';
|
|
12
12
|
import { BalanceItemService } from './BalanceItemService.js';
|
|
@@ -35,7 +35,7 @@ export class InvoiceService {
|
|
|
35
35
|
model.customer = struct.customer;
|
|
36
36
|
|
|
37
37
|
if (model.customer.company) {
|
|
38
|
-
await
|
|
38
|
+
await ViesService.checkCompany(model.customer.company, null, { forceValidation: true });
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
struct.updatePrices();
|