@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.
Files changed (99) hide show
  1. package/package.json +19 -19
  2. package/src/crons/mollie-chargebacks.ts +31 -29
  3. package/src/crons/mollie-refunds.ts +61 -59
  4. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
  5. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
  6. package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
  7. package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
  8. package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
  9. package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
  10. package/src/endpoints/auth/MFA.security.test.ts +9 -7
  11. package/src/endpoints/auth/MFA.test.ts +42 -15
  12. package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
  13. package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
  14. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
  15. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
  16. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
  17. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
  18. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
  19. package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
  20. package/src/endpoints/global/files/UploadFile.test.ts +2 -1
  21. package/src/endpoints/global/files/UploadImage.test.ts +2 -1
  22. package/src/endpoints/global/files/upload-security.test.ts +3 -2
  23. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
  24. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
  25. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
  26. package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
  27. package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
  28. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
  29. package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
  30. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
  31. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
  32. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
  33. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
  34. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
  35. package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
  36. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
  37. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
  38. package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
  39. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
  40. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
  41. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
  42. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
  43. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
  44. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
  45. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
  46. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
  47. package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
  48. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
  49. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
  50. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
  51. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
  52. package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
  53. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
  54. package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
  55. package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
  56. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
  57. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
  58. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
  59. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
  60. package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
  61. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
  62. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
  63. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
  64. package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
  65. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
  66. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
  67. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
  68. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
  69. package/src/helpers/Context.ts +13 -6
  70. package/src/helpers/ContextStorage.ts +5 -0
  71. package/src/helpers/StripeSettlementSync.ts +10 -7
  72. package/src/helpers/TwoFactorHelper.ts +21 -22
  73. package/src/services/AdminSessionService.ts +5 -3
  74. package/src/services/InvoiceService.ts +2 -2
  75. package/src/services/MollieService.ts +168 -126
  76. package/src/services/PaymentService.ts +2 -2
  77. package/src/services/SSOService.test.ts +25 -1
  78. package/src/services/SSOService.ts +17 -6
  79. package/src/services/SessionService.test.ts +319 -0
  80. package/src/services/SessionService.ts +357 -0
  81. package/src/services/WebshopCrowdfundingService.test.ts +4 -3
  82. package/src/sql-filters/registrations.ts +59 -1
  83. package/tests/actions/patchUserMember.ts +2 -1
  84. package/tests/e2e/api-rate-limits.test.ts +2 -1
  85. package/tests/e2e/bundle-discounts.test.ts +4 -3
  86. package/tests/e2e/charge-members.test.ts +6 -5
  87. package/tests/e2e/documents.test.ts +4 -3
  88. package/tests/e2e/private-files.test.ts +9 -8
  89. package/tests/e2e/register.test.ts +4 -3
  90. package/tests/e2e/stock.test.ts +4 -3
  91. package/tests/e2e/tickets.test.ts +4 -3
  92. package/tests/helpers/ExportSlice.ts +2 -1
  93. package/tests/helpers/MollieMocker.ts +85 -23
  94. package/tests/init/initAdmin.ts +2 -1
  95. package/tests/init/initPlatformAdmin.ts +2 -1
  96. package/src/helpers/ViesHelper.test.ts +0 -401
  97. package/src/helpers/ViesHelper.ts +0 -222
  98. package/src/services/PeppolDirectoryService.test.ts +0 -139
  99. package/src/services/PeppolDirectoryService.ts +0 -111
@@ -1,5 +1,6 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
3
+ import { SessionService } from '../../../../services/SessionService.js';
3
4
 
4
5
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
5
6
  import { PatchApiUserEndpoint } from './PatchApiUserEndpoint.js';
@@ -28,7 +29,7 @@ describe('Endpoint.CreateApiUserEndpoint', () => {
28
29
  }).create();
29
30
 
30
31
  // A token as produced by a refresh_token rotation (never authenticatedAt).
31
- const token = await Token.createToken(user);
32
+ const token = await SessionService.createSession(user);
32
33
 
33
34
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
34
35
  permissions: UserPermissions.create({
@@ -51,7 +52,7 @@ describe('Endpoint.CreateApiUserEndpoint', () => {
51
52
  level: PermissionLevel.Full,
52
53
  }),
53
54
  }).create();
54
- const token = await Token.createToken(user, new Date());
55
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
55
56
 
56
57
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
57
58
  permissions: UserPermissions.create({
@@ -78,7 +79,7 @@ describe('Endpoint.CreateApiUserEndpoint', () => {
78
79
  }),
79
80
  organization,
80
81
  }).create();
81
- const token = await Token.createToken(user, new Date());
82
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
82
83
 
83
84
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
84
85
  permissions: UserPermissions.create({
@@ -105,7 +106,7 @@ describe('Endpoint.CreateApiUserEndpoint', () => {
105
106
  }),
106
107
  organization,
107
108
  }).create();
108
- const token = await Token.createToken(user, new Date());
109
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
109
110
 
110
111
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
111
112
  permissions: UserPermissions.create({
@@ -6,6 +6,7 @@ import { Token, User } from '@stamhoofd/models';
6
6
  import { ApiUser, ApiUserWithToken, UserMeta, UserPermissions } from '@stamhoofd/structures';
7
7
 
8
8
  import { Context } from '../../../../helpers/Context.js';
9
+ import { SessionService } from '../../../../services/SessionService.js';
9
10
  type Params = Record<string, never>;
10
11
  type Query = undefined;
11
12
  type Body = ApiUser;
@@ -79,7 +80,7 @@ export class CreateApiUserEndpoint extends Endpoint<Params, Query, Body, Respons
79
80
  admin.email = admin.id + '.api';
80
81
  await admin.save();
81
82
 
82
- const createdToken = await Token.createApiToken(admin);
83
+ const createdToken = await SessionService.createApiSession(admin);
83
84
 
84
85
  return new Response(ApiUserWithToken.create({
85
86
  ...(await Token.getAPIUserWithToken(admin)),
@@ -3,6 +3,7 @@ import type { Organization, User } from '@stamhoofd/models';
3
3
  import { MFATOTP, OrganizationFactory, Token, UserFactory, WebauthnCredential } from '@stamhoofd/models';
4
4
  import { OrganizationAdmins, PermissionLevel, Permissions } from '@stamhoofd/structures';
5
5
  import crypto from 'crypto';
6
+ import { SessionService } from '../../../../services/SessionService.js';
6
7
 
7
8
  import { MFATestHelper } from '../../../../../tests/helpers/MFATestHelper.js';
8
9
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
@@ -19,7 +20,7 @@ describe('Endpoint.GetOrganizationAdmins', () => {
19
20
  }
20
21
 
21
22
  async function getAdmins(organization: Organization, user: User) {
22
- const token = await Token.createToken(user, new Date());
23
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
23
24
  const request = Request.buildJson('GET', '/organization/admins', organization.getApiHost());
24
25
  request.headers.authorization = 'Bearer ' + token.accessToken;
25
26
 
@@ -45,7 +46,7 @@ describe('Endpoint.GetOrganizationAdmins', () => {
45
46
  inactiveAdmin.lastActiveAt = lastActiveAt;
46
47
  await inactiveAdmin.save();
47
48
 
48
- const token = await Token.createToken(user, new Date());
49
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
49
50
  const request = Request.buildJson('GET', '/organization/admins', organization.getApiHost());
50
51
  request.headers.authorization = 'Bearer ' + token.accessToken;
51
52
 
@@ -1,5 +1,6 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
3
+ import { SessionService } from '../../../../services/SessionService.js';
3
4
 
4
5
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
5
6
  import { PatchApiUserEndpoint } from './PatchApiUserEndpoint.js';
@@ -24,7 +25,7 @@ describe('Endpoint.PatchApiUserEndpoint', () => {
24
25
  level: PermissionLevel.Full,
25
26
  }),
26
27
  }).create();
27
- const token = await Token.createToken(user, new Date());
28
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
28
29
 
29
30
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
30
31
  permissions: UserPermissions.create({
@@ -60,7 +61,7 @@ describe('Endpoint.PatchApiUserEndpoint', () => {
60
61
  }),
61
62
  organization,
62
63
  }).create();
63
- const token = await Token.createToken(user, new Date());
64
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
64
65
 
65
66
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
66
67
  permissions: UserPermissions.create({
@@ -95,7 +96,7 @@ describe('Endpoint.PatchApiUserEndpoint', () => {
95
96
  }),
96
97
  organization,
97
98
  }).create();
98
- const token = await Token.createToken(user, new Date());
99
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
99
100
 
100
101
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
101
102
  permissions: UserPermissions.create({
@@ -143,7 +144,7 @@ describe('Endpoint.PatchApiUserEndpoint', () => {
143
144
  }),
144
145
  organization,
145
146
  }).create();
146
- const token = await Token.createToken(user, new Date());
147
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
147
148
 
148
149
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
149
150
  permissions: UserPermissions.create({
@@ -190,7 +191,7 @@ describe('Endpoint.PatchApiUserEndpoint', () => {
190
191
  }),
191
192
  organization,
192
193
  }).create();
193
- const token = await Token.createToken(user, new Date());
194
+ const token = await SessionService.createSession(user, { authenticatedAt: new Date() });
194
195
 
195
196
  const createRequest = Request.buildJson('POST', '/api-keys', organization.getApiHost(), ApiUser.create({
196
197
  permissions: UserPermissions.create({
@@ -3,6 +3,7 @@ import type { Organization, User } from '@stamhoofd/models';
3
3
  import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
4
4
  import { PermissionLevel, Permissions, UserPermissions } from '@stamhoofd/structures';
5
5
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
6
+ import { SessionService } from '../../../../services/SessionService.js';
6
7
 
7
8
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
8
9
  import { AdminSessionService } from '../../../../services/AdminSessionService.js';
@@ -47,11 +48,11 @@ describe('Endpoint.SignOutOrganizationAdmins', () => {
47
48
  const otherOrganizationAdmin = await createAdmin(otherOrganization);
48
49
  const member = await new UserFactory({ organization }).create();
49
50
 
50
- const currentToken = await Token.createToken(admin, new Date());
51
- const secondDeviceOfAdmin = await Token.createToken(admin, new Date());
52
- const otherAdminToken = await Token.createToken(otherAdmin, new Date());
53
- const otherOrganizationAdminToken = await Token.createToken(otherOrganizationAdmin, new Date());
54
- const memberToken = await Token.createToken(member, new Date());
51
+ const currentToken = await SessionService.createSession(admin, { authenticatedAt: new Date() });
52
+ const secondDeviceOfAdmin = await SessionService.createSession(admin, { authenticatedAt: new Date() });
53
+ const otherAdminToken = await SessionService.createSession(otherAdmin, { authenticatedAt: new Date() });
54
+ const otherOrganizationAdminToken = await SessionService.createSession(otherOrganizationAdmin, { authenticatedAt: new Date() });
55
+ const memberToken = await SessionService.createSession(member, { authenticatedAt: new Date() });
55
56
 
56
57
  const response = await signOut(organization, currentToken);
57
58
 
@@ -71,9 +72,9 @@ describe('Endpoint.SignOutOrganizationAdmins', () => {
71
72
 
72
73
  const admin = await createAdmin(organization, PermissionLevel.Write);
73
74
  const otherAdmin = await createAdmin(organization);
74
- const otherAdminToken = await Token.createToken(otherAdmin, new Date());
75
+ const otherAdminToken = await SessionService.createSession(otherAdmin, { authenticatedAt: new Date() });
75
76
 
76
- await expect(signOut(organization, await Token.createToken(admin, new Date()))).rejects.toThrow(STExpect.simpleError({
77
+ await expect(signOut(organization, await SessionService.createSession(admin, { authenticatedAt: new Date() }))).rejects.toThrow(STExpect.simpleError({
77
78
  code: 'permission_denied',
78
79
  }));
79
80
  expect(await tokenExists(otherAdminToken)).toBe(true);
@@ -89,9 +90,9 @@ describe('Endpoint.SignOutOrganizationAdmins', () => {
89
90
  await apiUser.save();
90
91
 
91
92
  const otherAdmin = await createAdmin(organization);
92
- const otherAdminToken = await Token.createToken(otherAdmin, new Date());
93
+ const otherAdminToken = await SessionService.createSession(otherAdmin, { authenticatedAt: new Date() });
93
94
 
94
- await expect(signOut(organization, await Token.createApiToken(apiUser))).rejects.toThrow(STExpect.simpleError({
95
+ await expect(signOut(organization, await SessionService.createApiSession(apiUser))).rejects.toThrow(STExpect.simpleError({
95
96
  code: 'permission_denied',
96
97
  }));
97
98
  expect(await tokenExists(otherAdminToken)).toBe(true);
@@ -100,7 +101,7 @@ describe('Endpoint.SignOutOrganizationAdmins', () => {
100
101
  test('an organization without admins is a no-op', async () => {
101
102
  const organization = await new OrganizationFactory({}).create();
102
103
  const member = await new UserFactory({ organization }).create();
103
- const memberToken = await Token.createToken(member, new Date());
104
+ const memberToken = await SessionService.createSession(member, { authenticatedAt: new Date() });
104
105
 
105
106
  // Not reachable through the endpoint (the caller is an admin themselves), but the
106
107
  // empty list of users may not turn into a query that deletes everything.
@@ -126,10 +127,10 @@ describe('Endpoint.SignOutOrganizationAdmins', () => {
126
127
  }).create();
127
128
  const member = await new UserFactory({ organization }).create();
128
129
 
129
- const otherAdminToken = await Token.createToken(otherAdmin, new Date());
130
- const memberToken = await Token.createToken(member, new Date());
130
+ const otherAdminToken = await SessionService.createSession(otherAdmin, { authenticatedAt: new Date() });
131
+ const memberToken = await SessionService.createSession(member, { authenticatedAt: new Date() });
131
132
 
132
- const response = await signOut(organization, await Token.createToken(admin, new Date()));
133
+ const response = await signOut(organization, await SessionService.createSession(admin, { authenticatedAt: new Date() }));
133
134
 
134
135
  expect(response.body.count).toBe(1);
135
136
  expect(await tokenExists(otherAdminToken)).toBe(false);
@@ -149,9 +150,9 @@ describe('Endpoint.SignOutOrganizationAdmins', () => {
149
150
  organizationPermissions: new Map([[organization.id, Permissions.create({ level: PermissionLevel.Full })]]),
150
151
  });
151
152
  await apiUser.save();
152
- const apiToken = await Token.createApiToken(apiUser);
153
+ const apiToken = await SessionService.createApiSession(apiUser);
153
154
 
154
- const response = await signOut(organization, await Token.createToken(admin, new Date()));
155
+ const response = await signOut(organization, await SessionService.createSession(admin, { authenticatedAt: new Date() }));
155
156
 
156
157
  expect(response.body.count).toBe(0);
157
158
  expect(await tokenExists(apiToken)).toBe(true);
@@ -5,12 +5,13 @@ import type { PaginatedResponse, PrivateOrder, StamhoofdFilter } from '@stamhoof
5
5
  import { Cart, Customer, LimitedFilteredRequest, OrderData, PermissionLevel, Permissions, RecordDateAnswer, RecordSettings, RecordType } from '@stamhoofd/structures';
6
6
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
7
7
  import { GetWebshopOrdersEndpoint } from './GetWebshopOrdersEndpoint.js';
8
+ import { SessionService } from '../../../../services/SessionService.js';
8
9
 
9
10
  describe('Endpoint.GetWebshopOrdersEndpoint', () => {
10
11
  const endpoint = new GetWebshopOrdersEndpoint();
11
12
 
12
13
  const getOrders = async ({ filter, organization, user }: { filter: StamhoofdFilter | null; organization: Organization; user: User }) => {
13
- const token = await Token.createToken(user);
14
+ const token = await SessionService.createSession(user);
14
15
 
15
16
  const request = Request.get({
16
17
  path: '/webshop/orders',
@@ -3,6 +3,7 @@ import { OrganizationFactory, Token, UserFactory, Webshop, WebshopFactory } from
3
3
  import { PermissionLevel, Permissions, PrivateWebshop, WebshopMetaData } from '@stamhoofd/structures';
4
4
  import { Language } from '@stamhoofd/types/Language';
5
5
  import { TestUtils } from '@stamhoofd/test-utils';
6
+ import { SessionService } from '../../../../services/SessionService.js';
6
7
 
7
8
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
8
9
  import { PatchWebshopEndpoint } from './PatchWebshopEndpoint.js';
@@ -22,7 +23,7 @@ describe('Endpoint.PatchWebshop', () => {
22
23
  level: PermissionLevel.Full,
23
24
  }),
24
25
  }).create();
25
- const token = await Token.createToken(user);
26
+ const token = await SessionService.createSession(user);
26
27
 
27
28
  return { organization, token };
28
29
  }
@@ -6,6 +6,7 @@ import { Cart, CartItem, Customer, OrderData, PaymentMethod, PermissionLevel, Pe
6
6
  import { TestUtils } from '@stamhoofd/test-utils';
7
7
  import { Language } from '@stamhoofd/types/Language';
8
8
  import { v4 as uuidv4 } from 'uuid';
9
+ import { SessionService } from '../../../../services/SessionService.js';
9
10
 
10
11
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
11
12
  import { PatchWebshopOrdersEndpoint } from './PatchWebshopOrdersEndpoint.js';
@@ -25,7 +26,7 @@ describe('Endpoint.PatchWebshopOrders', () => {
25
26
  level: PermissionLevel.Full,
26
27
  }),
27
28
  }).create();
28
- const token = await Token.createToken(user);
29
+ const token = await SessionService.createSession(user);
29
30
 
30
31
  const freeProductPrice = ProductPrice.create({ name: 'Free', price: 0, stock: 100 });
31
32
  const product = Product.create({ name: 'Product', stock: 100, prices: [freeProductPrice] });
@@ -2,6 +2,7 @@ import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { OrganizationFactory, Token, UserFactory, Webshop, WebshopFactory } from '@stamhoofd/models';
3
3
  import { Permissions, PermissionLevel, PrivateWebshop, WebshopMetaData } from '@stamhoofd/structures';
4
4
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
5
+ import { SessionService } from '../../../../services/SessionService.js';
5
6
 
6
7
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
7
8
  import { CreateWebshopEndpoint } from './CreateWebshopEndpoint.js';
@@ -25,7 +26,7 @@ describe('Endpoint.WebshopReservedPathSegments', () => {
25
26
  level: PermissionLevel.Full,
26
27
  }),
27
28
  }).create();
28
- const token = await Token.createToken(user);
29
+ const token = await SessionService.createSession(user);
29
30
 
30
31
  return { organization, token };
31
32
  }
@@ -1,6 +1,7 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
3
3
  import { Organization, PermissionLevel, Permissions } from '@stamhoofd/structures';
4
+ import { SessionService } from '../../../../services/SessionService.js';
4
5
 
5
6
  import { TestUtils } from '@stamhoofd/test-utils';
6
7
  import { testServer } from '../../../../../tests/helpers/TestServer.js';
@@ -17,7 +18,7 @@ describe('Endpoint.GetOrganization', () => {
17
18
  test('Get organization as signed in user', async () => {
18
19
  const organization = await new OrganizationFactory({}).create();
19
20
  const user = await new UserFactory({ organization }).create();
20
- const token = await Token.createToken(user);
21
+ const token = await SessionService.createSession(user);
21
22
 
22
23
  const r = Request.buildJson('GET', '/v3/organization', organization.getApiHost());
23
24
  r.headers.authorization = 'Bearer ' + token.accessToken;
@@ -42,7 +43,7 @@ describe('Endpoint.GetOrganization', () => {
42
43
  }),
43
44
  }).create();
44
45
 
45
- const token = await Token.createToken(user);
46
+ const token = await SessionService.createSession(user);
46
47
 
47
48
  const r = Request.buildJson('GET', '/v3/organization', organization.getApiHost());
48
49
  r.headers.authorization = 'Bearer ' + token.accessToken;
@@ -68,7 +69,7 @@ describe('Endpoint.GetOrganization', () => {
68
69
  }),
69
70
  }).create();
70
71
 
71
- const token = await Token.createToken(user);
72
+ const token = await SessionService.createSession(user);
72
73
 
73
74
  const r = Request.buildJson('GET', '/v3/organization', organization.getApiHost());
74
75
  r.headers.authorization = 'Bearer ' + token.accessToken;
@@ -2,6 +2,7 @@ import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { OrganizationFactory, Token, UserFactory, WebshopFactory } from '@stamhoofd/models';
3
3
  import { PermissionLevel, Permissions, WebshopAuthType, WebshopMetaData } from '@stamhoofd/structures';
4
4
  import { Language } from '@stamhoofd/types/Language';
5
+ import { SessionService } from '../../../services/SessionService.js';
5
6
 
6
7
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
7
8
  import { testServer } from '../../../../tests/helpers/TestServer.js';
@@ -18,7 +19,7 @@ describe('Endpoint.GetWebshop', () => {
18
19
  test('Get webshop as signed in user', async () => {
19
20
  const organization = await new OrganizationFactory({}).create();
20
21
  const user = await new UserFactory({ organization }).create();
21
- const token = await Token.createToken(user);
22
+ const token = await SessionService.createSession(user);
22
23
  const webshop = await new WebshopFactory({ organizationId: organization.id }).create();
23
24
 
24
25
  const r = Request.buildJson('GET', '/v244/webshop/' + webshop.id, organization.getApiHost());
@@ -71,7 +72,7 @@ describe('Endpoint.GetWebshop', () => {
71
72
  test('Allow signed in user access for required-auth webshop', async () => {
72
73
  const organization = await new OrganizationFactory({}).create();
73
74
  const user = await new UserFactory({ organization }).create();
74
- const token = await Token.createToken(user);
75
+ const token = await SessionService.createSession(user);
75
76
  const webshop = await new WebshopFactory({
76
77
  organizationId: organization.id,
77
78
  meta: WebshopMetaData.create({
@@ -94,7 +95,7 @@ describe('Endpoint.GetWebshop', () => {
94
95
  level: PermissionLevel.Read,
95
96
  }),
96
97
  }).create();
97
- const token = await Token.createToken(user);
98
+ const token = await SessionService.createSession(user);
98
99
 
99
100
  const webshop = await new WebshopFactory({ organizationId: organization.id }).create();
100
101
 
@@ -116,7 +117,7 @@ describe('Endpoint.GetWebshop', () => {
116
117
  level: PermissionLevel.None,
117
118
  }),
118
119
  }).create();
119
- const token = await Token.createToken(user);
120
+ const token = await SessionService.createSession(user);
120
121
 
121
122
  const webshop = await new WebshopFactory({ organizationId: organization.id }).create();
122
123
 
@@ -140,7 +141,7 @@ describe('Endpoint.GetWebshop', () => {
140
141
  }),
141
142
  }).create();
142
143
 
143
- const token = await Token.createToken(user);
144
+ const token = await SessionService.createSession(user);
144
145
  const webshop = await new WebshopFactory({ organizationId: organization.id }).create();
145
146
 
146
147
  const r = Request.buildJson('GET', '/v244/webshop/' + webshop.id, organization.getApiHost());
@@ -2,17 +2,19 @@ import type { PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
2
2
  import { PatchableArray } from '@simonbackx/simple-encoding';
3
3
  import type { Response } from '@simonbackx/simple-endpoints';
4
4
  import { Request } from '@simonbackx/simple-endpoints';
5
- import type { Organization, StripeAccount, User } from '@stamhoofd/models';
6
- import { Order, OrganizationFactory, Payment, Token, UserFactory, Webshop, WebshopFactory } from '@stamhoofd/models';
5
+ import type { Organization, StripeAccount, User, Token } from '@stamhoofd/models';
6
+ import { MolliePayment, Order, OrganizationFactory, Payment, UserFactory, Webshop, WebshopFactory } from '@stamhoofd/models';
7
7
  import type { OrderResponse } from '@stamhoofd/structures';
8
- import { Address, Cart, CartItem, CartItemOption, Customer, Option, OptionMenu, OrderData, PaymentConfiguration, PaymentMethod, PermissionLevel, Permissions, PrivateOrder, PrivatePaymentConfiguration, Product, ProductPrice, ProductType, SeatingPlan, SeatingPlanRow, SeatingPlanSeat, SeatingPlanSection, TransferSettings, WebshopAuthType, WebshopDeliveryMethod, WebshopMetaData, WebshopOnSiteMethod, WebshopPrivateMetaData, WebshopTakeoutMethod, WebshopTimeSlot } from '@stamhoofd/structures';
8
+ import { Address, Cart, CartItem, CartItemOption, Customer, MollieOnboarding, MollieStatus, Option, OptionMenu, OrderData, PaymentConfiguration, PaymentMethod, PaymentProvider, PaymentStatus, PermissionLevel, Permissions, PrivateOrder, PrivatePaymentConfiguration, Product, ProductPrice, ProductType, SeatingPlan, SeatingPlanRow, SeatingPlanSeat, SeatingPlanSection, TransferSettings, WebshopAuthType, WebshopDeliveryMethod, WebshopMetaData, WebshopOnSiteMethod, WebshopPrivateMetaData, WebshopTakeoutMethod, WebshopTimeSlot } from '@stamhoofd/structures';
9
9
  import { I18n } from '@stamhoofd/backend-i18n';
10
10
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
11
11
  import { Country } from '@stamhoofd/types/Country';
12
12
  import { Language } from '@stamhoofd/types/Language';
13
13
  import sinon from 'sinon';
14
14
  import { v4 as uuidv4 } from 'uuid';
15
+ import { SessionService } from '../../../services/SessionService.js';
15
16
 
17
+ import { MollieMocker } from '../../../../tests/helpers/MollieMocker.js';
16
18
  import { StripeMocker } from '../../../../tests/helpers/StripeMocker.js';
17
19
  import { testServer } from '../../../../tests/helpers/TestServer.js';
18
20
  import { PatchWebshopOrdersEndpoint } from '../dashboard/webshops/PatchWebshopOrdersEndpoint.js';
@@ -67,6 +69,7 @@ describe('Endpoint.PlaceOrderEndpoint', () => {
67
69
  let radioOption1: Option;
68
70
  let radioOption2: Option;
69
71
  let stripeMocker: StripeMocker;
72
+ let mollieMocker: MollieMocker;
70
73
  let stripeAccount: StripeAccount;
71
74
  let token: Token;
72
75
 
@@ -108,6 +111,8 @@ describe('Endpoint.PlaceOrderEndpoint', () => {
108
111
  beforeAll(async () => {
109
112
  stripeMocker = new StripeMocker();
110
113
  stripeMocker.start();
114
+ mollieMocker = new MollieMocker();
115
+ mollieMocker.start();
111
116
  organization = await new OrganizationFactory({}).create();
112
117
  stripeAccount = await stripeMocker.createStripeAccount(organization.id);
113
118
 
@@ -117,11 +122,12 @@ describe('Endpoint.PlaceOrderEndpoint', () => {
117
122
  level: PermissionLevel.Full,
118
123
  }),
119
124
  }).create();
120
- token = await Token.createToken(user);
125
+ token = await SessionService.createSession(user);
121
126
  });
122
127
 
123
128
  afterAll(() => {
124
129
  stripeMocker.stop();
130
+ mollieMocker.stop();
125
131
  });
126
132
 
127
133
  afterEach(() => {
@@ -130,6 +136,7 @@ describe('Endpoint.PlaceOrderEndpoint', () => {
130
136
 
131
137
  beforeEach(async () => {
132
138
  stripeMocker.reset();
139
+ mollieMocker.reset();
133
140
  let meta = WebshopMetaData.patch({});
134
141
 
135
142
  productPrice1 = ProductPrice.create({
@@ -363,7 +370,7 @@ describe('Endpoint.PlaceOrderEndpoint', () => {
363
370
 
364
371
  test('Placing an order with a signed in user overrides the order customer', async () => {
365
372
  const user = await new UserFactory({ organization, firstName: 'User', lastName: 'Tester' }).create();
366
- const token = await Token.createToken(user);
373
+ const token = await SessionService.createSession(user);
367
374
 
368
375
  const orderData = OrderData.create({
369
376
  paymentMethod: PaymentMethod.Unknown,
@@ -408,9 +415,9 @@ describe('Endpoint.PlaceOrderEndpoint', () => {
408
415
  await webshop.save();
409
416
 
410
417
  const user = await new UserFactory({ organization, firstName: 'User', lastName: 'Tester' }).create();
411
- const token = await Token.createToken(user);
418
+ const token = await SessionService.createSession(user);
412
419
  const otherUser = await new UserFactory({ organization }).create();
413
- const otherToken = await Token.createToken(otherUser);
420
+ const otherToken = await SessionService.createSession(otherUser);
414
421
 
415
422
  const orderData = OrderData.create({
416
423
  paymentMethod: PaymentMethod.Unknown,
@@ -499,7 +506,7 @@ describe('Endpoint.PlaceOrderEndpoint', () => {
499
506
  const pendingResponses: { promise: Promise<Response<OrderResponse>>; user: User }[] = [];
500
507
  for (let i = 0; i < 10; i++) {
501
508
  const user = await new UserFactory({ organization, firstName: 'User', lastName: 'Tester' }).create();
502
- const token = await Token.createToken(user);
509
+ const token = await SessionService.createSession(user);
503
510
 
504
511
  const r = Request.buildJson('POST', `/webshop/${webshop.id}/order`, organization.getApiHost(), orderData);
505
512
  r.headers.authorization = 'Bearer ' + token.accessToken;
@@ -640,4 +647,83 @@ describe('Endpoint.PlaceOrderEndpoint', () => {
640
647
  expect((global as any).$getLanguage()).toEqual(Language.Dutch);
641
648
  });
642
649
  });
650
+
651
+ describe('Mollie payments', () => {
652
+ beforeEach(async () => {
653
+ organization.privateMeta.mollieOnboarding = MollieOnboarding.create({
654
+ canReceivePayments: true,
655
+ canReceiveSettlements: true,
656
+ status: MollieStatus.Completed,
657
+ });
658
+ await organization.save();
659
+ await mollieMocker.setupToken(organization);
660
+
661
+ // Without a Stripe account on the webshop, Bancontact is routed to Mollie
662
+ webshop.privateMeta.paymentConfiguration.stripeAccountId = null;
663
+ await webshop.save();
664
+ });
665
+
666
+ function buildOrderData() {
667
+ return OrderData.create({
668
+ paymentMethod: PaymentMethod.Bancontact,
669
+ checkoutMethod: onSiteMethod,
670
+ timeSlot: slot4,
671
+ cart: Cart.create({
672
+ items: [
673
+ CartItem.create({
674
+ product,
675
+ productPrice: productPrice1,
676
+ amount: 1,
677
+ }),
678
+ ],
679
+ }),
680
+ customer,
681
+ });
682
+ }
683
+
684
+ test('Placing an order via Mollie returns the checkout url and the order is validated once paid', async () => {
685
+ const r = Request.buildJson('POST', `/webshop/${webshop.id}/order`, organization.getApiHost(), buildOrderData());
686
+ const response: Response<OrderResponse> = await testServer.test(endpoint, r);
687
+
688
+ expect(response.body.paymentUrl).toEqual('https://molliecheckout/');
689
+ const orderStruct = response.body.order;
690
+ expect(orderStruct.payment).not.toBeNull();
691
+ expect(orderStruct.payment!.provider).toEqual(PaymentProvider.Mollie);
692
+ expect(orderStruct.payment!.method).toEqual(PaymentMethod.Bancontact);
693
+
694
+ // The payment was created at Mollie with the price and the internal payment id
695
+ const mockPayment = mollieMocker.getLastPayment();
696
+ expect(mockPayment.amount).toEqual({ currency: 'EUR', value: '0.01' });
697
+ expect(mockPayment.internalPaymentId).toEqual(orderStruct.payment!.id);
698
+
699
+ // The Mollie payment id is linked, so webhooks and crons can find the payment back
700
+ const molliePayment = await MolliePayment.select().where('paymentId', orderStruct.payment!.id).first(false);
701
+ expect(molliePayment).not.toBeNull();
702
+ expect(molliePayment!.mollieId).toEqual(mockPayment.id);
703
+
704
+ // Simulate the payment being paid at Mollie (checkout + webhook)
705
+ await mollieMocker.succeedPayment();
706
+
707
+ const payment = (await Payment.getByID(orderStruct.payment!.id))!;
708
+ expect(payment.status).toEqual(PaymentStatus.Succeeded);
709
+
710
+ const orderModel = (await Order.getByID(orderStruct.id))!;
711
+ expect(orderModel.validAt).not.toBeNull();
712
+ expect(orderModel.number).not.toBeNull();
713
+ });
714
+
715
+ test('A failed Mollie payment does not validate the order', async () => {
716
+ const r = Request.buildJson('POST', `/webshop/${webshop.id}/order`, organization.getApiHost(), buildOrderData());
717
+ const response: Response<OrderResponse> = await testServer.test(endpoint, r);
718
+ const orderStruct = response.body.order;
719
+
720
+ await mollieMocker.failPayment();
721
+
722
+ const payment = (await Payment.getByID(orderStruct.payment!.id))!;
723
+ expect(payment.status).toEqual(PaymentStatus.Failed);
724
+
725
+ const orderModel = (await Order.getByID(orderStruct.id))!;
726
+ expect(orderModel.validAt).toBeNull();
727
+ });
728
+ });
643
729
  });
@@ -1,4 +1,3 @@
1
- import { PaymentMethod as molliePaymentMethod } from '@mollie/api-client';
2
1
  import type { Decoder } from '@simonbackx/simple-encoding';
3
2
  import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
4
3
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
@@ -307,26 +306,27 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
307
306
  });
308
307
  }
309
308
  const molliePayment = await mollieService.client.payments.create({
310
- amount: {
311
- currency: 'EUR',
312
- value: (totalPrice / 10000).toFixed(2), // from 4 decimals to 0 decimals
309
+ paymentRequest: {
310
+ amount: {
311
+ currency: 'EUR',
312
+ value: (totalPrice / 10000).toFixed(2), // from 4 decimals to 0 decimals
313
+ },
314
+ method: MollieService.paymentMethodToMollie(payment.method) ?? 'creditcard',
315
+ profileId,
316
+ description,
317
+ redirectUrl,
318
+ webhookUrl: exchangeUrl,
319
+ metadata: {
320
+ order: order.id,
321
+ organization: organization.id,
322
+ webshop: webshop.id,
323
+ payment: payment.id,
324
+ },
325
+ locale: mollieService.locale,
313
326
  },
314
- method: payment.method === PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method === PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
315
- testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
316
- profileId,
317
- description,
318
- redirectUrl,
319
- webhookUrl: exchangeUrl,
320
- metadata: {
321
- order: order.id,
322
- organization: organization.id,
323
- webshop: webshop.id,
324
- payment: payment.id,
325
- },
326
- locale: mollieService.locale,
327
327
  });
328
328
  console.log(molliePayment);
329
- paymentUrl = molliePayment.getCheckoutUrl();
329
+ paymentUrl = MollieService.getCheckoutUrl(molliePayment);
330
330
 
331
331
  // Save payment
332
332
  const dbPayment = new MolliePayment();
@@ -1,13 +1,11 @@
1
- import { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { isSimpleError, SimpleError } from '@simonbackx/simple-errors';
3
3
  import { I18n } from '@stamhoofd/backend-i18n';
4
4
  import { MFAToken, Organization, Platform, RateLimiter, Token, User } from '@stamhoofd/models';
5
- import { AsyncLocalStorage } from 'async_hooks';
6
-
7
- import type { Decoder } from '@simonbackx/simple-encoding';
8
- import { AutoEncoder, field, StringDecoder } from '@simonbackx/simple-encoding';
9
5
  import { ApiUserRateLimits } from '@stamhoofd/structures';
6
+ import { SessionService } from '../services/SessionService.js';
10
7
  import { AdminPermissionChecker } from './AdminPermissionChecker.js';
8
+ import { contextStorage } from './ContextStorage.js';
11
9
  import { TwoFactorHelper } from './TwoFactorHelper.js';
12
10
 
13
11
  export const apiUserRateLimiter = new RateLimiter({
@@ -62,7 +60,7 @@ export class ContextInstance {
62
60
  this.request = request;
63
61
  }
64
62
 
65
- static asyncLocalStorage = new AsyncLocalStorage<ContextInstance>();
63
+ static asyncLocalStorage = contextStorage;
66
64
 
67
65
  static get optional(): ContextInstance | null {
68
66
  const c = this.asyncLocalStorage.getStore();
@@ -297,6 +295,15 @@ export class ContextInstance {
297
295
  });
298
296
  }
299
297
 
298
+ if (!await SessionService.activateToken(token)) {
299
+ throw new SimpleError({
300
+ code: 'invalid_access_token',
301
+ message: 'The access token is invalid',
302
+ human: $t(`%Fi`),
303
+ statusCode: 401,
304
+ });
305
+ }
306
+
300
307
  if (!token.user.hasAccount() && !allowWithoutAccount) {
301
308
  if (token?.user) {
302
309
  console.log(