@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.145.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -58,27 +58,27 @@
|
|
|
58
58
|
"@aws-sdk/client-sqs": "3.1090.0",
|
|
59
59
|
"@aws-sdk/s3-request-presigner": "3.1090.0",
|
|
60
60
|
"@bwip-js/node": "4.11.2",
|
|
61
|
-
"@mollie/api-client": "4.6.0",
|
|
62
61
|
"@simonbackx/simple-database": "1.37.1",
|
|
63
62
|
"@simonbackx/simple-encoding": "2.26.11",
|
|
64
63
|
"@simonbackx/simple-endpoints": "1.22.0",
|
|
65
64
|
"@simonbackx/simple-errors": "1.5.0",
|
|
66
65
|
"@simonbackx/simple-logging": "1.0.1",
|
|
67
66
|
"@simplewebauthn/server": "13.3.2",
|
|
68
|
-
"@stamhoofd/backend-env": "2.
|
|
69
|
-
"@stamhoofd/backend-i18n": "2.
|
|
70
|
-
"@stamhoofd/backend-middleware": "2.
|
|
71
|
-
"@stamhoofd/crons": "2.
|
|
72
|
-
"@stamhoofd/email": "2.
|
|
73
|
-
"@stamhoofd/excel-writer": "2.
|
|
74
|
-
"@stamhoofd/logging": "2.
|
|
75
|
-
"@stamhoofd/models": "2.
|
|
76
|
-
"@stamhoofd/object-differ": "2.
|
|
77
|
-
"@stamhoofd/queues": "2.
|
|
78
|
-
"@stamhoofd/sql": "2.
|
|
79
|
-
"@stamhoofd/structures": "2.
|
|
80
|
-
"@stamhoofd/types": "2.
|
|
81
|
-
"@stamhoofd/utility": "2.
|
|
67
|
+
"@stamhoofd/backend-env": "2.145.0",
|
|
68
|
+
"@stamhoofd/backend-i18n": "2.145.0",
|
|
69
|
+
"@stamhoofd/backend-middleware": "2.145.0",
|
|
70
|
+
"@stamhoofd/crons": "2.145.0",
|
|
71
|
+
"@stamhoofd/email": "2.145.0",
|
|
72
|
+
"@stamhoofd/excel-writer": "2.145.0",
|
|
73
|
+
"@stamhoofd/logging": "2.145.0",
|
|
74
|
+
"@stamhoofd/models": "2.145.0",
|
|
75
|
+
"@stamhoofd/object-differ": "2.145.0",
|
|
76
|
+
"@stamhoofd/queues": "2.145.0",
|
|
77
|
+
"@stamhoofd/sql": "2.145.0",
|
|
78
|
+
"@stamhoofd/structures": "2.145.0",
|
|
79
|
+
"@stamhoofd/types": "2.145.0",
|
|
80
|
+
"@stamhoofd/utility": "2.145.0",
|
|
81
|
+
"@stamhoofd/vies": "2.145.0",
|
|
82
82
|
"archiver": "7.0.1",
|
|
83
83
|
"argon2": "0.44.0",
|
|
84
84
|
"axios": "1.18.1",
|
|
@@ -89,11 +89,11 @@
|
|
|
89
89
|
"handlebars": "4.7.9",
|
|
90
90
|
"jose": "5.10.0",
|
|
91
91
|
"jsonwebtoken": "9.0.3",
|
|
92
|
-
"jsvat-next": "3.0.4",
|
|
93
92
|
"libphonenumber-js": "1.13.9",
|
|
94
93
|
"luxon": "3.7.2",
|
|
95
94
|
"mailparser": "3.9.14",
|
|
96
95
|
"mockdate": "3.0.5",
|
|
96
|
+
"mollie-api-typescript": "1.12.23",
|
|
97
97
|
"mysql2": "3.23.1",
|
|
98
98
|
"node-rsa": "2.0.0",
|
|
99
99
|
"openid-client": "5.7.1",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"uuid": "14.0.1"
|
|
103
103
|
},
|
|
104
104
|
"devDependencies": {
|
|
105
|
-
"@stamhoofd/test-utils": "2.
|
|
105
|
+
"@stamhoofd/test-utils": "2.145.0",
|
|
106
106
|
"@types/cookie": "0.6.0",
|
|
107
107
|
"@types/luxon": "3.7.2",
|
|
108
108
|
"@types/mailparser": "3.4.6",
|
|
@@ -117,5 +117,5 @@
|
|
|
117
117
|
"publishConfig": {
|
|
118
118
|
"access": "public"
|
|
119
119
|
},
|
|
120
|
-
"gitHead": "
|
|
120
|
+
"gitHead": "ee58cd6383c03df39218d8589679638019241e15"
|
|
121
121
|
}
|
|
@@ -42,40 +42,42 @@ export async function checkMollieChargebacksFor(service: MollieService, checkAll
|
|
|
42
42
|
// Check last 3 days
|
|
43
43
|
const offset = new Date(Date.now() - 1000 * 60 * 60 * 24 * 4);
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
47
|
-
for await (const chargeback of service.client.chargebacks.iterate({ sort: 'desc', testmode: service.testMode } as any)) {
|
|
48
|
-
if (!checkAll && new Date(chargeback.createdAt) < offset) {
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
45
|
+
const pages = await service.client.chargebacks.all({ sort: 'desc', limit: 250 });
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
pagesLoop: for await (const page of pages) {
|
|
48
|
+
for (const chargeback of page.result.embedded.chargebacks) {
|
|
49
|
+
if (!checkAll && new Date(chargeback.createdAt) < offset) {
|
|
50
|
+
break pagesLoop;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if this chargeback has been handled
|
|
54
|
+
const existingChargeback = await MolliePayment.select().where('mollieId', chargeback.id).first(false);
|
|
55
|
+
if (existingChargeback) {
|
|
56
|
+
// We can break because all older chargebacks will also be handled
|
|
57
|
+
break pagesLoop;
|
|
58
|
+
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
if (chargeback.paymentId) {
|
|
61
|
+
const molliePayment = await MolliePayment.select().where('mollieId', chargeback.paymentId).first(false);
|
|
62
|
+
if (molliePayment) {
|
|
63
|
+
const payment = await Payment.getByID(molliePayment.paymentId);
|
|
64
|
+
if (payment) {
|
|
65
|
+
try {
|
|
66
|
+
const amount = Math.round(parseFloat(chargeback.amount.value) * 100) * 100;
|
|
67
|
+
const createdPayment = await PaymentService.registerChargeback(payment, amount, new Date(chargeback.createdAt));
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
// Link Mollie chargeback ID (so we can set settlement later in the settlements cron)
|
|
70
|
+
const molliePayment = new MolliePayment();
|
|
71
|
+
molliePayment.paymentId = createdPayment.id;
|
|
72
|
+
molliePayment.mollieId = chargeback.id;
|
|
73
|
+
await molliePayment.save();
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('Failed to register chargeback ' + chargeback.id, e);
|
|
76
|
+
}
|
|
75
77
|
}
|
|
78
|
+
} else {
|
|
79
|
+
console.error('Invalid chargeback payment id ' + chargeback.paymentId + ', not found');
|
|
76
80
|
}
|
|
77
|
-
} else {
|
|
78
|
-
console.error('Invalid chargeback payment id ' + chargeback.paymentId + ', not found');
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { RefundStatus } from '@mollie/api-client';
|
|
2
1
|
import { registerCron } from '@stamhoofd/crons';
|
|
3
2
|
import { MolliePayment, MollieToken, Organization, Payment } from '@stamhoofd/models';
|
|
4
3
|
import { PaymentStatus, PaymentType } from '@stamhoofd/structures';
|
|
4
|
+
import type { ListEntityRefundStatus } from 'mollie-api-typescript/models';
|
|
5
5
|
import { MollieService } from '../services/MollieService.js';
|
|
6
6
|
import { PaymentService } from '../services/PaymentService.js';
|
|
7
7
|
|
|
@@ -49,73 +49,75 @@ export async function checkMollieRefundsFor(service: MollieService, checkAll = f
|
|
|
49
49
|
// Check last 7 days
|
|
50
50
|
const offset = new Date(Date.now() - 1000 * 60 * 60 * 24 * 7);
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
54
|
-
for await (const refund of service.client.refunds.iterate({ sort: 'desc', testmode: service.testMode } as any)) {
|
|
55
|
-
const createdAt = new Date(refund.createdAt);
|
|
52
|
+
const pages = await service.client.refunds.all({ sort: 'desc', limit: 250 });
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
pagesLoop: for await (const page of pages) {
|
|
55
|
+
for (const refund of page.result.embedded.refunds) {
|
|
56
|
+
const createdAt = new Date(refund.createdAt);
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
if (createdAt < MOLLIE_REFUNDS_MINIMUM_DATE) {
|
|
59
|
+
break pagesLoop;
|
|
60
|
+
}
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// refunds can still fail or be canceled until they are actually executed.
|
|
69
|
-
const existingRefund = await MolliePayment.select().where('mollieId', refund.id).first(false);
|
|
70
|
-
if (existingRefund) {
|
|
71
|
-
await reconcileRefundStatus(existingRefund.paymentId, refund.status, createdAt);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
62
|
+
if (!checkAll && createdAt < offset) {
|
|
63
|
+
break pagesLoop;
|
|
64
|
+
}
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
// Check if this refund is already registered (refunds created via the API are linked on
|
|
67
|
+
// creation, so we cannot break here: an older refund created in the Mollie UI could still
|
|
68
|
+
// be unhandled). Registered refunds that are still pending locally are reconciled: Mollie
|
|
69
|
+
// refunds can still fail or be canceled until they are actually executed.
|
|
70
|
+
const existingRefund = await MolliePayment.select().where('mollieId', refund.id).first(false);
|
|
71
|
+
if (existingRefund) {
|
|
72
|
+
await reconcileRefundStatus(existingRefund.paymentId, refund.status, createdAt);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// the refund twice (the metadata is set in PaymentService.createRefund).
|
|
83
|
-
const metadata = refund.metadata as { refundPaymentId?: string } | null;
|
|
84
|
-
if (metadata?.refundPaymentId) {
|
|
85
|
-
const localRefund = await Payment.getByID(metadata.refundPaymentId);
|
|
86
|
-
if (localRefund && localRefund.type === PaymentType.Refund) {
|
|
87
|
-
console.error('Restoring missing link for Mollie refund ' + refund.id + ' to payment ' + localRefund.id);
|
|
88
|
-
const link = new MolliePayment();
|
|
89
|
-
link.paymentId = localRefund.id;
|
|
90
|
-
link.mollieId = refund.id;
|
|
91
|
-
await link.save();
|
|
92
|
-
|
|
93
|
-
await reconcileRefundStatus(localRefund.id, refund.status, createdAt);
|
|
76
|
+
// Refunds that were canceled or failed before we ever registered them can be ignored
|
|
77
|
+
if (refund.status === 'canceled' || refund.status === 'failed') {
|
|
94
78
|
continue;
|
|
95
79
|
}
|
|
96
|
-
}
|
|
97
80
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
81
|
+
// Safety net: when creating a refund via the API succeeded, but saving the link afterwards
|
|
82
|
+
// failed, the local refund payment already exists. Restore the link instead of registering
|
|
83
|
+
// the refund twice (the metadata is set in PaymentService.createRefund).
|
|
84
|
+
const metadata = refund.metadata as { refundPaymentId?: string } | null;
|
|
85
|
+
if (metadata?.refundPaymentId) {
|
|
86
|
+
const localRefund = await Payment.getByID(metadata.refundPaymentId);
|
|
87
|
+
if (localRefund && localRefund.type === PaymentType.Refund) {
|
|
88
|
+
console.error('Restoring missing link for Mollie refund ' + refund.id + ' to payment ' + localRefund.id);
|
|
89
|
+
const link = new MolliePayment();
|
|
90
|
+
link.paymentId = localRefund.id;
|
|
91
|
+
link.mollieId = refund.id;
|
|
92
|
+
await link.save();
|
|
93
|
+
|
|
94
|
+
await reconcileRefundStatus(localRefund.id, refund.status, createdAt);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (refund.paymentId) {
|
|
100
|
+
const molliePayment = await MolliePayment.select().where('mollieId', refund.paymentId).first(false);
|
|
101
|
+
if (molliePayment) {
|
|
102
|
+
const payment = await Payment.getByID(molliePayment.paymentId);
|
|
103
|
+
if (payment) {
|
|
104
|
+
try {
|
|
105
|
+
const amount = Math.round(parseFloat(refund.amount.value) * 100) * 100;
|
|
106
|
+
const status = MollieService.refundStatusToPaymentStatus(refund.status) === PaymentStatus.Succeeded ? PaymentStatus.Succeeded : PaymentStatus.Pending;
|
|
107
|
+
const createdPayment = await PaymentService.registerRefund(payment, amount, createdAt, status);
|
|
108
|
+
|
|
109
|
+
// Link Mollie refund ID (so we don't register it twice and can set the settlement later)
|
|
110
|
+
const molliePayment = new MolliePayment();
|
|
111
|
+
molliePayment.paymentId = createdPayment.id;
|
|
112
|
+
molliePayment.mollieId = refund.id;
|
|
113
|
+
await molliePayment.save();
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error('Failed to register refund ' + refund.id, e);
|
|
116
|
+
}
|
|
115
117
|
}
|
|
118
|
+
} else {
|
|
119
|
+
console.error('Invalid refund payment id ' + refund.paymentId + ', not found');
|
|
116
120
|
}
|
|
117
|
-
} else {
|
|
118
|
-
console.error('Invalid refund payment id ' + refund.paymentId + ', not found');
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
}
|
|
@@ -129,7 +131,7 @@ export async function checkMollieRefundsFor(service: MollieService, checkAll = f
|
|
|
129
131
|
* Update a locally registered refund that is still pending when Mollie reports a final status
|
|
130
132
|
* (refunded, failed or canceled).
|
|
131
133
|
*/
|
|
132
|
-
async function reconcileRefundStatus(paymentId: string, mollieStatus:
|
|
134
|
+
async function reconcileRefundStatus(paymentId: string, mollieStatus: ListEntityRefundStatus, createdAt: Date) {
|
|
133
135
|
const payment = await Payment.getByID(paymentId);
|
|
134
136
|
if (!payment || !payment.organizationId) {
|
|
135
137
|
return;
|
|
@@ -10,6 +10,7 @@ import { Organization as OrganizationStruct } from '@stamhoofd/structures';
|
|
|
10
10
|
import { Formatter } from '@stamhoofd/utility';
|
|
11
11
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
|
|
12
12
|
import { Context } from '../../../helpers/Context.js';
|
|
13
|
+
import { SQL } from '@stamhoofd/sql';
|
|
13
14
|
|
|
14
15
|
type Params = Record<string, never>;
|
|
15
16
|
type Query = undefined;
|
|
@@ -64,13 +65,23 @@ export class PatchOrganizationsEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
64
65
|
|
|
65
66
|
// Financial records may not disappear together with the organization that had to pay them
|
|
66
67
|
const payerRecordCounts = await Promise.all([
|
|
67
|
-
BalanceItem.select().where('payingOrganizationId', id).
|
|
68
|
-
Payment.select().where('payingOrganizationId', id).
|
|
69
|
-
ApplicationFee.select().where('payingOrganizationId', id).
|
|
70
|
-
Invoice.select().where('payingOrganizationId', id).
|
|
68
|
+
BalanceItem.select().where('payingOrganizationId', id).first(false),
|
|
69
|
+
Payment.select().where('payingOrganizationId', id).first(false),
|
|
70
|
+
ApplicationFee.select().where('payingOrganizationId', id).first(false),
|
|
71
|
+
Invoice.select().where('payingOrganizationId', id).first(false),
|
|
72
|
+
Payment.select()
|
|
73
|
+
.where('organizationId', id)
|
|
74
|
+
.where(
|
|
75
|
+
SQL.where('serviceFeePayout', '!=', 0)
|
|
76
|
+
.or('serviceFeeManual', '!=', 0)
|
|
77
|
+
.or('serviceFeeManualCharged', '!=', 0)
|
|
78
|
+
.or('transferFeeManual', '!=', 0)
|
|
79
|
+
.or('transferFeeManualCharged', '!=', 0),
|
|
80
|
+
)
|
|
81
|
+
.first(false),
|
|
71
82
|
]);
|
|
72
83
|
|
|
73
|
-
if (payerRecordCounts.some(
|
|
84
|
+
if (payerRecordCounts.some(hasSome => hasSome !== null)) {
|
|
74
85
|
throw new SimpleError({
|
|
75
86
|
code: 'organization_has_financial_records',
|
|
76
87
|
message: 'Organization is still referenced as paying organization',
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import {
|
|
3
|
-
import { Token
|
|
2
|
+
import type { Organization } from '@stamhoofd/models';
|
|
3
|
+
import { OrganizationFactory, Token, User, UserFactory, UserSession } from '@stamhoofd/models';
|
|
4
|
+
import { SESSION_DURATIONS } from '@stamhoofd/models/constants/sessions.js';
|
|
5
|
+
import { PermissionLevel, Permissions, SessionClientType, Token as TokenStruct } from '@stamhoofd/structures';
|
|
6
|
+
import { SessionService } from '../../services/SessionService.js';
|
|
4
7
|
|
|
5
8
|
import { testServer } from '../../../tests/helpers/TestServer.js';
|
|
6
9
|
import { CreateTokenEndpoint } from './CreateTokenEndpoint.js';
|
|
@@ -41,7 +44,7 @@ describe('Endpoint.CreateToken', () => {
|
|
|
41
44
|
// Also check UTF8 passwords
|
|
42
45
|
const password = '54😂test👌🏾86s&é';
|
|
43
46
|
const user = await new UserFactory({ organization, password }).create();
|
|
44
|
-
const token = await
|
|
47
|
+
const token = await SessionService.createSession(user);
|
|
45
48
|
|
|
46
49
|
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
47
50
|
grant_type: 'refresh_token',
|
|
@@ -94,7 +97,7 @@ describe('Endpoint.CreateToken', () => {
|
|
|
94
97
|
test('refreshing a token marks the user as active again', async () => {
|
|
95
98
|
const organization = await new OrganizationFactory({}).create();
|
|
96
99
|
const user = await new UserFactory({ organization, password }).create();
|
|
97
|
-
const token = await
|
|
100
|
+
const token = await SessionService.createSession(user);
|
|
98
101
|
|
|
99
102
|
const monthsAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
100
103
|
monthsAgo.setMilliseconds(0);
|
|
@@ -125,4 +128,207 @@ describe('Endpoint.CreateToken', () => {
|
|
|
125
128
|
expect(await getLastActiveAt(user)).toBeNull();
|
|
126
129
|
});
|
|
127
130
|
});
|
|
131
|
+
|
|
132
|
+
describe('session length', () => {
|
|
133
|
+
const password = 'test-password-1234';
|
|
134
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
135
|
+
|
|
136
|
+
function expectValidFor(date: Date, duration: number) {
|
|
137
|
+
expect(date.getTime()).toBeGreaterThan(Date.now() + duration - 5000);
|
|
138
|
+
expect(date.getTime()).toBeLessThanOrEqual(Date.now() + duration);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function login(organization: Organization, user: User, platform: string | null = null): Promise<Token> {
|
|
142
|
+
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
143
|
+
grant_type: 'password',
|
|
144
|
+
username: user.email,
|
|
145
|
+
password,
|
|
146
|
+
});
|
|
147
|
+
if (platform) {
|
|
148
|
+
r.headers['x-platform'] = platform;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const response = await testServer.test(endpoint, r);
|
|
152
|
+
if (!(response.body instanceof TokenStruct)) {
|
|
153
|
+
throw new Error('Expected TokenStruct');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const token = await Token.getByAccessToken(response.body.accessToken);
|
|
157
|
+
expect(token).toBeDefined();
|
|
158
|
+
return token!;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function refresh(organization: Organization, refreshToken: string): Promise<Token> {
|
|
162
|
+
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
163
|
+
grant_type: 'refresh_token',
|
|
164
|
+
refresh_token: refreshToken,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const response = await testServer.test(endpoint, r);
|
|
168
|
+
if (!(response.body instanceof TokenStruct)) {
|
|
169
|
+
throw new Error('Expected TokenStruct');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const token = await Token.getByAccessToken(response.body.accessToken);
|
|
173
|
+
expect(token).toBeDefined();
|
|
174
|
+
return token!;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function getSession(token: Token): Promise<UserSession> {
|
|
178
|
+
const session = await UserSession.getByID(token.sessionId);
|
|
179
|
+
if (!session) throw new Error('Expected user session');
|
|
180
|
+
return session;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test('an administrator that signs in in a browser gets a limited session', async () => {
|
|
184
|
+
const organization = await new OrganizationFactory({}).create();
|
|
185
|
+
const admin = await new UserFactory({
|
|
186
|
+
organization,
|
|
187
|
+
password,
|
|
188
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
189
|
+
}).create();
|
|
190
|
+
|
|
191
|
+
const token = await login(organization, admin, 'web');
|
|
192
|
+
|
|
193
|
+
expect((await getSession(token)).clientType).toBe(SessionClientType.Browser);
|
|
194
|
+
expectValidFor(token.refreshTokenValidUntil, SESSION_DURATIONS.admin[SessionClientType.Browser].refreshToken);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('an administrator that signs in in the native app has no maximum session length', async () => {
|
|
198
|
+
const organization = await new OrganizationFactory({}).create();
|
|
199
|
+
const admin = await new UserFactory({
|
|
200
|
+
organization,
|
|
201
|
+
password,
|
|
202
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
203
|
+
}).create();
|
|
204
|
+
|
|
205
|
+
const token = await login(organization, admin, 'ios');
|
|
206
|
+
|
|
207
|
+
expect((await getSession(token)).clientType).toBe(SessionClientType.iOS);
|
|
208
|
+
expectValidFor(token.refreshTokenValidUntil, SESSION_DURATIONS.admin[SessionClientType.iOS].refreshToken);
|
|
209
|
+
|
|
210
|
+
const browserSessionDuration = SESSION_DURATIONS.admin[SessionClientType.Browser].session;
|
|
211
|
+
if (browserSessionDuration === null) {
|
|
212
|
+
throw new Error('Expected browser administrator sessions to have a maximum length');
|
|
213
|
+
}
|
|
214
|
+
const userSession = await getSession(token);
|
|
215
|
+
userSession.startedAt = new Date(Date.now() - 2 * browserSessionDuration);
|
|
216
|
+
await userSession.save();
|
|
217
|
+
|
|
218
|
+
const renewed = await refresh(organization, token.refreshToken);
|
|
219
|
+
expectValidFor(renewed.refreshTokenValidUntil, SESSION_DURATIONS.admin[SessionClientType.iOS].refreshToken);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('renewing the same active token twice revokes the first replacement', async () => {
|
|
223
|
+
const organization = await new OrganizationFactory({}).create();
|
|
224
|
+
const user = await new UserFactory({ organization, password }).create();
|
|
225
|
+
const original = await login(organization, user);
|
|
226
|
+
|
|
227
|
+
const firstReplacement = await refresh(organization, original.refreshToken);
|
|
228
|
+
const secondReplacement = await refresh(organization, original.refreshToken);
|
|
229
|
+
|
|
230
|
+
expect(secondReplacement.sessionId).toBe(original.sessionId);
|
|
231
|
+
expect(await Token.getByAccessToken(firstReplacement.accessToken)).toBeUndefined();
|
|
232
|
+
expect(await Token.getByAccessToken(original.accessToken)).toBeDefined();
|
|
233
|
+
expect(await Token.getByAccessToken(secondReplacement.accessToken)).toBeDefined();
|
|
234
|
+
|
|
235
|
+
const sessionTokens = await Token.where({ sessionId: original.sessionId });
|
|
236
|
+
expect(sessionTokens.map(token => token.id).sort()).toEqual([original.id, secondReplacement.id].sort());
|
|
237
|
+
|
|
238
|
+
const forkRequest = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
239
|
+
grant_type: 'refresh_token',
|
|
240
|
+
refresh_token: firstReplacement.refreshToken,
|
|
241
|
+
});
|
|
242
|
+
await expect(testServer.test(endpoint, forkRequest)).rejects.toMatchObject({ code: 'invalid_refresh_token' });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('an SSO handoff refresh token can only be exchanged once', async () => {
|
|
246
|
+
const organization = await new OrganizationFactory({}).create();
|
|
247
|
+
const user = await new UserFactory({ organization }).create();
|
|
248
|
+
const handoff = await SessionService.createSSOHandoff(user);
|
|
249
|
+
|
|
250
|
+
const sessionToken = await refresh(organization, handoff.refreshToken);
|
|
251
|
+
expect(sessionToken.sessionId).toBe(handoff.sessionId);
|
|
252
|
+
|
|
253
|
+
await expect(refresh(organization, handoff.refreshToken)).rejects.toMatchObject({
|
|
254
|
+
code: 'invalid_refresh_token',
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('concurrent exchanges cannot fork an SSO handoff', async () => {
|
|
259
|
+
const organization = await new OrganizationFactory({}).create();
|
|
260
|
+
const user = await new UserFactory({ organization }).create();
|
|
261
|
+
const handoff = await SessionService.createSSOHandoff(user);
|
|
262
|
+
|
|
263
|
+
const exchanges = await Promise.allSettled([
|
|
264
|
+
refresh(organization, handoff.refreshToken),
|
|
265
|
+
refresh(organization, handoff.refreshToken),
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
expect(exchanges.filter(result => result.status === 'fulfilled')).toHaveLength(1);
|
|
269
|
+
const rejected = exchanges.filter(result => result.status === 'rejected');
|
|
270
|
+
expect(rejected).toHaveLength(1);
|
|
271
|
+
expect(rejected[0].reason).toMatchObject({ code: 'invalid_refresh_token' });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('renewing an access token does not extend the session past its maximum length', async () => {
|
|
275
|
+
const organization = await new OrganizationFactory({}).create();
|
|
276
|
+
const admin = await new UserFactory({
|
|
277
|
+
organization,
|
|
278
|
+
password,
|
|
279
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
280
|
+
}).create();
|
|
281
|
+
|
|
282
|
+
const token = await login(organization, admin, 'web');
|
|
283
|
+
|
|
284
|
+
const sessionDuration = SESSION_DURATIONS.admin[SessionClientType.Browser].session;
|
|
285
|
+
if (sessionDuration === null) {
|
|
286
|
+
throw new Error('Expected browser administrator sessions to have a maximum length');
|
|
287
|
+
}
|
|
288
|
+
const sessionStartedAt = new Date(Date.now() - sessionDuration + DAY);
|
|
289
|
+
sessionStartedAt.setMilliseconds(0);
|
|
290
|
+
const userSession = await getSession(token);
|
|
291
|
+
userSession.startedAt = sessionStartedAt;
|
|
292
|
+
await userSession.save();
|
|
293
|
+
|
|
294
|
+
const renewed = await refresh(organization, token.refreshToken);
|
|
295
|
+
|
|
296
|
+
expect((await getSession(renewed)).startedAt).toEqual(sessionStartedAt);
|
|
297
|
+
expect(renewed.refreshTokenValidUntil.getTime()).toBeLessThanOrEqual(sessionStartedAt.getTime() + sessionDuration);
|
|
298
|
+
expect(renewed.refreshTokenValidUntil.getTime()).toBeGreaterThan(Date.now());
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('a session that reached its maximum length cannot be renewed', async () => {
|
|
302
|
+
const organization = await new OrganizationFactory({}).create();
|
|
303
|
+
const admin = await new UserFactory({
|
|
304
|
+
organization,
|
|
305
|
+
password,
|
|
306
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
307
|
+
}).create();
|
|
308
|
+
|
|
309
|
+
const token = await login(organization, admin, 'web');
|
|
310
|
+
const sessionDuration = SESSION_DURATIONS.admin[SessionClientType.Browser].session;
|
|
311
|
+
if (sessionDuration === null) {
|
|
312
|
+
throw new Error('Expected browser administrator sessions to have a maximum length');
|
|
313
|
+
}
|
|
314
|
+
const userSession = await getSession(token);
|
|
315
|
+
userSession.startedAt = new Date(Date.now() - sessionDuration - DAY);
|
|
316
|
+
await userSession.save();
|
|
317
|
+
token.refreshTokenValidUntil = new Date(Date.now() + DAY);
|
|
318
|
+
await token.save();
|
|
319
|
+
|
|
320
|
+
const otherSession = await login(organization, admin, 'web');
|
|
321
|
+
|
|
322
|
+
const r = Request.buildJson('POST', '/oauth/token', organization.getApiHost(), {
|
|
323
|
+
grant_type: 'refresh_token',
|
|
324
|
+
refresh_token: token.refreshToken,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await expect(testServer.test(endpoint, r)).rejects.toMatchObject({ code: 'invalid_refresh_token' });
|
|
328
|
+
expect(await Token.getByAccessToken(otherSession.accessToken)).toBeDefined();
|
|
329
|
+
|
|
330
|
+
await expect(testServer.test(endpoint, r)).rejects.toMatchObject({ code: 'invalid_refresh_token' });
|
|
331
|
+
expect(await Token.getByAccessToken(otherSession.accessToken)).toBeDefined();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
128
334
|
});
|