@stamhoofd/backend 2.143.1 → 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 (111) hide show
  1. package/package.json +19 -19
  2. package/src/crons/fake-settlements.test.ts +13 -11
  3. package/src/crons/fake-settlements.ts +22 -38
  4. package/src/crons/mollie-chargebacks.ts +31 -29
  5. package/src/crons/mollie-refunds.ts +61 -59
  6. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
  7. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
  8. package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
  9. package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
  10. package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
  11. package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
  12. package/src/endpoints/auth/MFA.security.test.ts +9 -7
  13. package/src/endpoints/auth/MFA.test.ts +42 -15
  14. package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
  15. package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
  16. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
  17. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
  18. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
  19. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
  20. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
  21. package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
  22. package/src/endpoints/global/files/UploadFile.test.ts +2 -1
  23. package/src/endpoints/global/files/UploadFile.ts +53 -25
  24. package/src/endpoints/global/files/UploadImage.test.ts +2 -1
  25. package/src/endpoints/global/files/UploadImage.ts +32 -6
  26. package/src/endpoints/global/files/upload-security.test.ts +3 -2
  27. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
  28. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
  29. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
  30. package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
  31. package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
  32. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
  33. package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
  34. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
  35. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
  36. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
  37. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
  38. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
  39. package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
  40. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
  41. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
  42. package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
  43. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
  44. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
  45. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
  46. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
  47. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
  48. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
  49. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
  50. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
  51. package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
  52. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
  53. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
  54. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
  55. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
  56. package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
  57. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
  58. package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
  59. package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
  60. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
  61. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
  62. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
  63. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
  64. package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
  65. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
  66. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
  67. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
  68. package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
  69. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
  70. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
  71. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
  72. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
  73. package/src/helpers/ApplicationFeeInvoicer.ts +1 -1
  74. package/src/helpers/Context.ts +14 -21
  75. package/src/helpers/ContextStorage.ts +5 -0
  76. package/src/helpers/MollieSettlementSync.test.ts +94 -41
  77. package/src/helpers/MollieSettlementSync.ts +174 -78
  78. package/src/helpers/StripeSettlementSync.test.ts +0 -85
  79. package/src/helpers/StripeSettlementSync.ts +47 -70
  80. package/src/helpers/TwoFactorHelper.ts +21 -22
  81. package/src/helpers/streamForBreakdown.ts +1 -1
  82. package/src/services/AdminSessionService.ts +5 -3
  83. package/src/services/InvoiceService.ts +2 -2
  84. package/src/services/MollieService.ts +168 -126
  85. package/src/services/PaymentService.ts +2 -2
  86. package/src/services/SSOService.test.ts +25 -1
  87. package/src/services/SSOService.ts +17 -6
  88. package/src/services/SessionService.test.ts +319 -0
  89. package/src/services/SessionService.ts +357 -0
  90. package/src/services/SettlementService.test.ts +13 -85
  91. package/src/services/SettlementService.ts +5 -113
  92. package/src/services/WebshopCrowdfundingService.test.ts +4 -3
  93. package/src/sql-filters/balance-items.ts +10 -0
  94. package/src/sql-filters/registrations.ts +59 -1
  95. package/tests/actions/patchUserMember.ts +2 -1
  96. package/tests/e2e/api-rate-limits.test.ts +2 -1
  97. package/tests/e2e/bundle-discounts.test.ts +4 -3
  98. package/tests/e2e/charge-members.test.ts +6 -5
  99. package/tests/e2e/documents.test.ts +4 -3
  100. package/tests/e2e/private-files.test.ts +9 -8
  101. package/tests/e2e/register.test.ts +4 -3
  102. package/tests/e2e/stock.test.ts +4 -3
  103. package/tests/e2e/tickets.test.ts +4 -3
  104. package/tests/helpers/ExportSlice.ts +2 -1
  105. package/tests/helpers/MollieMocker.ts +173 -38
  106. package/tests/init/initAdmin.ts +2 -1
  107. package/tests/init/initPlatformAdmin.ts +2 -1
  108. package/src/helpers/ViesHelper.test.ts +0 -401
  109. package/src/helpers/ViesHelper.ts +0 -215
  110. package/src/services/PeppolDirectoryService.test.ts +0 -139
  111. package/src/services/PeppolDirectoryService.ts +0 -111
@@ -10,6 +10,7 @@ import { LoginMethod, NewUser, PermissionLevel, SignupResponse, UserPermissions
10
10
  import { Context } from '../../helpers/Context.js';
11
11
  import { MemberUserSyncer } from '../../helpers/MemberUserSyncer.js';
12
12
  import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures.js';
13
+ import { SessionService } from '../../services/SessionService.js';
13
14
  import { VerificationCodeService } from '../../services/VerificationCodeService.js';
14
15
 
15
16
  type Params = { id: string };
@@ -163,7 +164,7 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
163
164
  // password changes
164
165
  await editUser.changePassword(request.body.password);
165
166
  await PasswordToken.clearFor(editUser.id);
166
- await Token.clearFor(editUser.id, token.accessToken);
167
+ await SessionService.clearFor({ userId: editUser.id, keepAccessToken: token.accessToken });
167
168
  }
168
169
 
169
170
  if (request.body.hasPassword === false) {
@@ -179,7 +180,7 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
179
180
  }
180
181
  editUser.password = null;
181
182
  await PasswordToken.clearFor(editUser.id);
182
- await Token.clearFor(editUser.id, token.accessToken);
183
+ await SessionService.clearFor({ userId: editUser.id, keepAccessToken: token.accessToken });
183
184
  }
184
185
  }
185
186
 
@@ -2,12 +2,13 @@ import type { Decoder } from '@simonbackx/simple-encoding';
2
2
  import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
3
3
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
4
4
  import { SimpleError } from '@simonbackx/simple-errors';
5
- import { EmailVerificationCode, Token, User } from '@stamhoofd/models';
6
- import { Token as TokenStruct, VerifyEmailRequest } from '@stamhoofd/structures';
5
+ import { EmailVerificationCode, User } from '@stamhoofd/models';
6
+ import { SessionLoginMethod, Token as TokenStruct, VerifyEmailRequest } from '@stamhoofd/structures';
7
7
 
8
8
  import { Context } from '../../helpers/Context.js';
9
9
  import { TwoFactorHelper } from '../../helpers/TwoFactorHelper.js';
10
10
  import { BalanceItemService } from '../../services/BalanceItemService.js';
11
+ import { SessionService } from '../../services/SessionService.js';
11
12
 
12
13
  type Params = Record<string, never>;
13
14
  type Query = undefined;
@@ -130,10 +131,10 @@ export class VerifyEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
130
131
  // the request already comes from a session of that same user (changing your email
131
132
  // address while signed in): the second factor was checked when it was created.
132
133
  if (authenticatedUser?.id !== user.id) {
133
- await TwoFactorHelper.assertSecondFactorOrThrow(user, organization, request.request.getVersion(), { loginMethod: 'email', i18n: request.i18n });
134
+ await TwoFactorHelper.assertSecondFactorOrThrow(user, organization, request.request.getVersion(), { loginMethod: SessionLoginMethod.Email, i18n: request.i18n });
134
135
  }
135
136
 
136
- const token = await Token.createToken(user);
137
+ const token = await SessionService.createSession(user, { loginMethod: SessionLoginMethod.Email });
137
138
  await user.markActive();
138
139
 
139
140
  const st = new TokenStruct(token);
@@ -1,11 +1,12 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
- import type { Organization, RegistrationPeriod, User } from '@stamhoofd/models';
3
- import { Email, EmailRecipient, MemberFactory, OrganizationFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
2
+ import type { Organization, RegistrationPeriod, User, Token } from '@stamhoofd/models';
3
+ import { Email, EmailRecipient, MemberFactory, OrganizationFactory, RegistrationPeriodFactory, UserFactory } from '@stamhoofd/models';
4
4
  import { EmailStatus, LimitedFilteredRequest, PermissionLevel, Permissions, Replacement } from '@stamhoofd/structures';
5
5
  import { TestUtils } from '@stamhoofd/test-utils';
6
6
  import { testServer } from '../../../../tests/helpers/TestServer.js';
7
7
  import { GetAdminEmailsEndpoint } from './GetAdminEmailsEndpoint.js';
8
8
  import { Formatter } from '@stamhoofd/utility';
9
+ import { SessionService } from '../../../services/SessionService.js';
9
10
 
10
11
  const baseUrl = `/email`;
11
12
 
@@ -39,7 +40,7 @@ describe('Endpoint.getAdminEmails', () => {
39
40
  user,
40
41
  }).create();
41
42
 
42
- userToken = await Token.createToken(user);
43
+ userToken = await SessionService.createSession(user);
43
44
  });
44
45
 
45
46
  const getAdminEmails = async (query: LimitedFilteredRequest = new LimitedFilteredRequest({ limit: 10 }), token: Token = userToken, testOrganization: Organization = organization) => {
@@ -1,12 +1,13 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
- import type { Organization, RegistrationPeriod, User } from '@stamhoofd/models';
3
- import { Email, EmailRecipient, MemberFactory, OrganizationFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
2
+ import type { Organization, RegistrationPeriod, User, Token } from '@stamhoofd/models';
3
+ import { Email, EmailRecipient, MemberFactory, OrganizationFactory, RegistrationPeriodFactory, UserFactory } from '@stamhoofd/models';
4
4
  import { EmailContent, EmailStatus, LimitedFilteredRequest, Replacement } from '@stamhoofd/structures';
5
5
  import { Language } from '@stamhoofd/types/Language';
6
6
  import { TestUtils } from '@stamhoofd/test-utils';
7
7
  import { testServer } from '../../../../tests/helpers/TestServer.js';
8
8
  import { GetUserEmailsEndpoint } from './GetUserEmailsEndpoint.js';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
+ import { SessionService } from '../../../services/SessionService.js';
10
11
 
11
12
  const baseUrl = `/user/email`;
12
13
 
@@ -39,7 +40,7 @@ describe('Endpoint.GetUserEmails', () => {
39
40
  user,
40
41
  }).create();
41
42
 
42
- userToken = await Token.createToken(user);
43
+ userToken = await SessionService.createSession(user);
43
44
  });
44
45
 
45
46
  afterEach(async () => {
@@ -726,7 +727,7 @@ describe('Endpoint.GetUserEmails', () => {
726
727
  expect(response.body.results.find(e => e.id === email.id)).toBeUndefined();
727
728
 
728
729
  // Test that is IS included if we request the same data via the 'otherUser' token
729
- const otherUserToken = await Token.createToken(otherUser);
730
+ const otherUserToken = await SessionService.createSession(otherUser);
730
731
  const responseForOtherUser = await getUserEmails(new LimitedFilteredRequest({ limit: 10 }), otherUserToken);
731
732
  expect(responseForOtherUser.body.results.find(e => e.id === email.id)).toBeDefined();
732
733
  });
@@ -805,7 +806,7 @@ describe('Endpoint.GetUserEmails', () => {
805
806
  host: organization.getApiHost(),
806
807
  query: new LimitedFilteredRequest({ limit: 10 }),
807
808
  headers: {
808
- 'authorization': 'Bearer ' + userToken.accessToken,
809
+ authorization: 'Bearer ' + userToken.accessToken,
809
810
  ...(language ? { 'accept-language': language } : {}),
810
811
  },
811
812
  });
@@ -1,14 +1,15 @@
1
1
  import type { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
2
2
  import { PatchMap } from '@simonbackx/simple-encoding';
3
3
  import { Request } from '@simonbackx/simple-endpoints';
4
- import type { Organization, RegistrationPeriod, User } from '@stamhoofd/models';
5
- import { Email, EmailRecipient, GroupFactory, MemberFactory, OrganizationFactory, RegistrationFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
4
+ import type { Organization, RegistrationPeriod, User, Token } from '@stamhoofd/models';
5
+ import { Email, EmailRecipient, GroupFactory, MemberFactory, OrganizationFactory, RegistrationFactory, RegistrationPeriodFactory, UserFactory } from '@stamhoofd/models';
6
6
  import { AccessRight, EmailContent, EmailRecipientFilter, EmailRecipientSubfilter, EmailStatus, Email as EmailStruct, OrganizationEmail, Parent, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, UserPermissions, Version } from '@stamhoofd/structures';
7
7
  import { Language } from '@stamhoofd/types/Language';
8
8
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
9
9
  import { testServer } from '../../../../tests/helpers/TestServer.js';
10
10
  import { PatchEmailEndpoint } from './PatchEmailEndpoint.js';
11
11
  import { EmailRecipientFilterType } from '@stamhoofd/structures/email/EmailRecipientFilterType.js';
12
+ import { SessionService } from '../../../services/SessionService.js';
12
13
 
13
14
  // Import recipient loaders to initialize them
14
15
  import { Formatter } from '@stamhoofd/utility';
@@ -82,7 +83,7 @@ describe('Endpoint.PatchEmailEndpoint', () => {
82
83
  })
83
84
  .create();
84
85
 
85
- token = await Token.createToken(user);
86
+ token = await SessionService.createSession(user);
86
87
 
87
88
  user2 = await new UserFactory({
88
89
  organization,
@@ -99,7 +100,7 @@ describe('Endpoint.PatchEmailEndpoint', () => {
99
100
  })
100
101
  .create();
101
102
 
102
- token2 = await Token.createToken(user2);
103
+ token2 = await SessionService.createSession(user2);
103
104
  });
104
105
 
105
106
  test('Should throw for invalid senderId', async () => {
@@ -1,10 +1,11 @@
1
1
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
2
2
  import { GetEmailRecipientsEndpoint } from './GetEmailRecipientsEndpoint.js';
3
3
  import { AccessRight, EmailStatus, LimitedFilteredRequest, OrganizationEmail, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
4
- import type { Organization, RegistrationPeriod, User } from '@stamhoofd/models';
5
- import { Email, EmailRecipient, OrganizationFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
4
+ import type { Organization, RegistrationPeriod, User, Token } from '@stamhoofd/models';
5
+ import { Email, EmailRecipient, OrganizationFactory, RegistrationPeriodFactory, UserFactory } from '@stamhoofd/models';
6
6
  import { Request } from '@simonbackx/simple-endpoints';
7
7
  import { testServer } from '../../../../tests/helpers/TestServer.js';
8
+ import { SessionService } from '../../../services/SessionService.js';
8
9
 
9
10
  const baseUrl = `/email-recipients`;
10
11
 
@@ -57,7 +58,7 @@ describe('Endpoint.GetEmailRecipients', () => {
57
58
  })
58
59
  .create();
59
60
 
60
- token = await Token.createToken(user);
61
+ token = await SessionService.createSession(user);
61
62
 
62
63
  user2 = await new UserFactory({
63
64
  organization,
@@ -73,7 +74,7 @@ describe('Endpoint.GetEmailRecipients', () => {
73
74
  })
74
75
  .create();
75
76
 
76
- token2 = await Token.createToken(user2);
77
+ token2 = await SessionService.createSession(user2);
77
78
  });
78
79
 
79
80
  test('It can request all email recipients if read permission for all senders', async () => {
@@ -10,6 +10,7 @@ import { testServer } from '../../../../tests/helpers/TestServer.js';
10
10
  import '../../../audit-logs/init.js';
11
11
  import { AuditLogService } from '../../../services/AuditLogService.js';
12
12
  import { PatchEventNotificationsEndpoint } from './PatchEventNotificationsEndpoint.js';
13
+ import { SessionService } from '../../../services/SessionService.js';
13
14
 
14
15
  const baseUrl = `/event-notifications`;
15
16
  const endpoint = new PatchEventNotificationsEndpoint();
@@ -34,7 +35,7 @@ const TestRequest = {
34
35
  }) {
35
36
  const request = Request.buildJson('PATCH', baseUrl, options.organization?.getApiHost(), options.body);
36
37
  if (options.user) {
37
- const token = await Token.createToken(options.user);
38
+ const token = await SessionService.createSession(options.user);
38
39
  request.headers.authorization = 'Bearer ' + token.accessToken;
39
40
  }
40
41
  return await testServer.test(endpoint, request);
@@ -8,6 +8,7 @@ import { AccessRight, Event, Group, GroupSettings, GroupType, OrganizationEventT
8
8
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
9
9
  import { testServer } from '../../../../tests/helpers/TestServer.js';
10
10
  import { PatchEventsEndpoint } from './PatchEventsEndpoint.js';
11
+ import { SessionService } from '../../../services/SessionService.js';
11
12
 
12
13
  const baseUrl = `/events`;
13
14
  const endpoint = new PatchEventsEndpoint();
@@ -32,7 +33,7 @@ const TestRequest = {
32
33
  }) {
33
34
  const request = Request.buildJson('PATCH', baseUrl, options.organization?.getApiHost(), options.body);
34
35
  if (options.user) {
35
- const token = await Token.createToken(options.user);
36
+ const token = await SessionService.createSession(options.user);
36
37
  request.headers.authorization = 'Bearer ' + token.accessToken;
37
38
  }
38
39
  return await testServer.test(endpoint, request);
@@ -6,6 +6,7 @@ import { TestUtils } from '@stamhoofd/test-utils';
6
6
  import { promises as fs } from 'fs';
7
7
  import type { IncomingMessage } from 'node:http';
8
8
  import { Readable } from 'node:stream';
9
+ import { SessionService } from '../../../services/SessionService.js';
9
10
 
10
11
  import { testServer } from '../../../../tests/helpers/TestServer.js';
11
12
  import { UploadFile } from './UploadFile.js';
@@ -59,7 +60,7 @@ describe('Endpoint.UploadFile', () => {
59
60
  organization,
60
61
  permissions: Permissions.create({ level: PermissionLevel.Full }),
61
62
  }).create();
62
- const token = await Token.createToken(user);
63
+ const token = await SessionService.createSession(user);
63
64
 
64
65
  const boundary = '--------------------------StamhoofdTest';
65
66
  const body = Buffer.concat([
@@ -1,4 +1,4 @@
1
- import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; // ES Modules import
1
+ import { PutObjectCommand } from '@aws-sdk/client-s3'; // ES Modules import
2
2
  import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
3
3
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
4
4
  import { SimpleError } from '@simonbackx/simple-errors';
@@ -11,7 +11,8 @@ import { v4 as uuidv4 } from 'uuid';
11
11
  import type { Decoder } from '@simonbackx/simple-encoding';
12
12
  import { AutoEncoder, BooleanDecoder, field } from '@simonbackx/simple-encoding';
13
13
  import { Context } from '../../../helpers/Context.js';
14
- import { limiter } from './UploadImage.js';
14
+ import { limiter, organizationLimiter } from './UploadImage.js';
15
+ import type { Organization } from '@stamhoofd/models';
15
16
  import { Image } from '@stamhoofd/models';
16
17
 
17
18
  type Params = Record<string, never>;
@@ -58,11 +59,20 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
58
59
  }
59
60
 
60
61
  async handle(request: DecodedRequest<Params, Query, Body>) {
61
- await Context.setOptionalOrganizationScope();
62
- const { user } = await Context.authenticate();
62
+ const organization = await Context.setOptionalOrganizationScope();
63
+ const { user } = await Context.optionalAuthenticate();
63
64
 
64
- if (!Context.auth.canUpload({ private: request.query.isPrivate })) {
65
- throw Context.auth.error();
65
+ // Webshops allow for file upload, but don't have a user attached
66
+ if (user) {
67
+ if (!Context.auth?.canUpload({ private: request.query.isPrivate })) {
68
+ throw Context.auth.error();
69
+ }
70
+ } else if (!organization) {
71
+ throw new SimpleError({
72
+ code: 'permission_denied',
73
+ message: 'Endpoints needs organization if no user is present',
74
+ human: $t('%ZlM'),
75
+ });
66
76
  }
67
77
 
68
78
  if (!STAMHOOFD.SPACES_BUCKET || !STAMHOOFD.SPACES_ENDPOINT || !STAMHOOFD.SPACES_KEY || !STAMHOOFD.SPACES_SECRET) {
@@ -77,7 +87,14 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
77
87
  throw new Error('Not supported without real request');
78
88
  }
79
89
 
80
- limiter.track(user.id, 1);
90
+ if (user) {
91
+ limiter.track(user.id, 1);
92
+ } else {
93
+ limiter.track(request.request.getIP(), 1);
94
+ if (organization) {
95
+ organizationLimiter.track(organization.id, 1);
96
+ }
97
+ }
81
98
 
82
99
  const form = formidable({ maxFileSize: 20 * 1024 * 1024, maxFields: 1, keepExtensions: true });
83
100
  const file = await new Promise<FormidableFile>((resolve, reject) => {
@@ -89,34 +106,43 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
89
106
  }));
90
107
  return;
91
108
  }
92
- form.parse(request.request.request, (err, fields, files) => {
93
- if (err) {
94
- reject(err);
95
- return;
109
+ try {
110
+ if (request.request.request?.destroyed) {
111
+ throw new Error('Request destroyed before parsing');
96
112
  }
97
-
98
- if (!files.file || !Array.isArray(files.file) || files.file.length !== 1) {
99
- reject(new SimpleError({
100
- code: 'missing_field',
101
- message: 'Missing file',
102
- field: 'file',
103
- }));
104
- return;
105
- }
106
-
107
- resolve(files.file[0]);
108
- });
113
+ form.parse(request.request.request, (err, fields, files) => {
114
+ console.log('form.parse', err, fields, files);
115
+ if (err) {
116
+ reject(err);
117
+ return;
118
+ }
119
+
120
+ if (!files.file || !Array.isArray(files.file) || files.file.length !== 1) {
121
+ reject(new SimpleError({
122
+ code: 'missing_field',
123
+ message: 'Missing file',
124
+ field: 'file',
125
+ }));
126
+ return;
127
+ }
128
+
129
+ resolve(files.file[0]);
130
+ });
131
+ } catch (e) {
132
+ console.error(e);
133
+ reject(e);
134
+ }
109
135
  });
110
136
 
111
137
  try {
112
- return await this.upload(request, file, user);
138
+ return await this.upload(request, file, organization, user);
113
139
  } finally {
114
140
  // Formidable wrote the upload to a temporary file
115
141
  await fs.rm(file.filepath, { force: true }).catch(() => { /* we can't do anything about this */ });
116
142
  }
117
143
  }
118
144
 
119
- private async upload(request: DecodedRequest<Params, Query, Body>, file: FormidableFile, user: { id: string }) {
145
+ private async upload(request: DecodedRequest<Params, Query, Body>, file: FormidableFile, organization: Organization | null, user?: { id: string }) {
120
146
  if (!STAMHOOFD.SPACES_BUCKET || !STAMHOOFD.SPACES_ENDPOINT || !STAMHOOFD.SPACES_KEY || !STAMHOOFD.SPACES_SECRET) {
121
147
  throw new SimpleError({
122
148
  code: 'not_available',
@@ -156,6 +182,8 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
156
182
  if (request.query.isPrivate && user) {
157
183
  // Private files
158
184
  prefix += 'users/' + user.id + '/';
185
+ } else if (request.query.isPrivate && !user && organization) {
186
+ prefix += 'anonymous/' + organization.id + '/';
159
187
  } else {
160
188
  // Public files
161
189
  prefix += 'p/';
@@ -5,6 +5,7 @@ import { PermissionLevel, Permissions } from '@stamhoofd/structures';
5
5
  import { TestUtils } from '@stamhoofd/test-utils';
6
6
  import type { IncomingMessage } from 'node:http';
7
7
  import { Readable } from 'node:stream';
8
+ import { SessionService } from '../../../services/SessionService.js';
8
9
 
9
10
  import { testServer } from '../../../../tests/helpers/TestServer.js';
10
11
  import { UploadImage } from './UploadImage.js';
@@ -52,7 +53,7 @@ describe('Endpoint.UploadImage', () => {
52
53
  organization,
53
54
  permissions: Permissions.create({ level: PermissionLevel.Full }),
54
55
  }).create();
55
- const token = await Token.createToken(user);
56
+ const token = await SessionService.createSession(user);
56
57
 
57
58
  const boundary = '--------------------------StamhoofdTest';
58
59
  const body = Buffer.concat([
@@ -3,6 +3,7 @@ import { AutoEncoder, BooleanDecoder, field, ObjectData } from '@simonbackx/simp
3
3
  import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
4
4
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
6
+ import type { Organization } from '@stamhoofd/models';
6
7
  import { Image, RateLimiter } from '@stamhoofd/models';
7
8
  import { Image as ImageStruct, ResolutionRequest, supportedImageTypes } from '@stamhoofd/structures';
8
9
  import formidable from 'formidable';
@@ -44,6 +45,15 @@ export const limiter = new RateLimiter({
44
45
  },
45
46
  ],
46
47
  });
48
+ export const organizationLimiter = new RateLimiter({
49
+ limits: [
50
+ {
51
+ // Max 200 per day
52
+ limit: 200,
53
+ duration: 60 * 1000 * 60 * 24,
54
+ },
55
+ ],
56
+ });
47
57
 
48
58
  export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
49
59
  queryDecoder = Query as Decoder<Query>;
@@ -63,11 +73,20 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
63
73
  }
64
74
 
65
75
  async handle(request: DecodedRequest<Params, Query, Body>) {
66
- await Context.setOptionalOrganizationScope();
67
- const { user } = await Context.authenticate();
76
+ const organization = await Context.setOptionalOrganizationScope();
77
+ const { user } = await Context.optionalAuthenticate();
68
78
 
69
- if (!Context.auth.canUpload({ private: request.query.isPrivate })) {
70
- throw Context.auth.error();
79
+ // Webshops allow for file upload, but don't have a user attached
80
+ if (user) {
81
+ if (!Context.auth?.canUpload({ private: request.query.isPrivate })) {
82
+ throw Context.auth.error();
83
+ }
84
+ } else if (!organization) {
85
+ throw new SimpleError({
86
+ code: 'permission_denied',
87
+ message: 'Endpoints needs organization if no user is present',
88
+ human: $t('%ZlM'),
89
+ });
71
90
  }
72
91
 
73
92
  if (!STAMHOOFD.SPACES_BUCKET || !STAMHOOFD.SPACES_ENDPOINT || !STAMHOOFD.SPACES_KEY || !STAMHOOFD.SPACES_SECRET) {
@@ -82,7 +101,14 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
82
101
  throw new Error('Not supported without real request');
83
102
  }
84
103
 
85
- limiter.track(user.id, 1);
104
+ if (user) {
105
+ limiter.track(user.id, 1);
106
+ } else {
107
+ limiter.track(request.request.getIP(), 1);
108
+ if (organization) {
109
+ organizationLimiter.track(organization.id, 1);
110
+ }
111
+ }
86
112
 
87
113
  const form = formidable({
88
114
  maxFileSize: 5 * 1024 * 1024,
@@ -147,7 +173,7 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
147
173
  }
148
174
 
149
175
  const fileContent = await fs.readFile(file.filepath);
150
- const image = await Image.create(fileContent, imageType.contentType, resolutions, request.query.isPrivate, user);
176
+ const image = await Image.create(fileContent, imageType.contentType, resolutions, request.query.isPrivate, user, organization as Organization | null | undefined);
151
177
  return new Response(ImageStruct.create(image));
152
178
  } finally {
153
179
  // Formidable wrote the upload to a temporary file
@@ -7,6 +7,7 @@ import { File, PermissionLevel, Permissions } from '@stamhoofd/structures';
7
7
  import { TestUtils } from '@stamhoofd/test-utils';
8
8
  import type { IncomingMessage } from 'node:http';
9
9
  import { Readable } from 'node:stream';
10
+ import { SessionService } from '../../../services/SessionService.js';
10
11
 
11
12
  import { testServer } from '../../../../tests/helpers/TestServer.js';
12
13
  import { FileSignService } from '../../../services/FileSignService.js';
@@ -116,12 +117,12 @@ describe('Upload security', () => {
116
117
  permissions: Permissions.create({ level: PermissionLevel.Full }),
117
118
  }).create();
118
119
  adminUserId = admin.id;
119
- adminToken = (await Token.createToken(admin)).accessToken;
120
+ adminToken = (await SessionService.createSession(admin)).accessToken;
120
121
 
121
122
  // A user without any permissions: it may only upload private files
122
123
  const member = await new UserFactory({ organization }).create();
123
124
  memberUserId = member.id;
124
- memberToken = (await Token.createToken(member)).accessToken;
125
+ memberToken = (await SessionService.createSession(member)).accessToken;
125
126
  });
126
127
 
127
128
  beforeEach(() => {
@@ -1,6 +1,6 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
- import type { Organization, RegistrationPeriod } from '@stamhoofd/models';
3
- import { GroupFactory, OrganizationFactory, OrganizationRegistrationPeriodFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
2
+ import type { Organization, RegistrationPeriod, Token } from '@stamhoofd/models';
3
+ import { GroupFactory, OrganizationFactory, OrganizationRegistrationPeriodFactory, RegistrationPeriodFactory, UserFactory } from '@stamhoofd/models';
4
4
  import type { StamhoofdFilter } from '@stamhoofd/structures';
5
5
  import { BundleDiscountGroupPriceSettings, GroupPrice, GroupType, LimitedFilteredRequest } from '@stamhoofd/structures';
6
6
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
@@ -8,6 +8,7 @@ import { testServer } from '../../../../tests/helpers/TestServer.js';
8
8
  import { initAdmin, initBundleDiscount, initPlatformAdmin } from '../../../../tests/init/index.js';
9
9
  import { GetGroupsEndpoint } from './GetGroupsEndpoint.js';
10
10
  import { Database } from '@simonbackx/simple-database';
11
+ import { SessionService } from '../../../services/SessionService.js';
11
12
 
12
13
  const baseUrl = `/groups`;
13
14
 
@@ -68,7 +69,7 @@ describe('Endpoint.GetGroupsEndpoint', () => {
68
69
  const { organization: otherOrganization } = await initOrganization(registrationPeriod1);
69
70
 
70
71
  const user = await new UserFactory({ organization }).create();
71
- const token = await Token.createToken(user);
72
+ const token = await SessionService.createSession(user);
72
73
 
73
74
  const waitingList = await new GroupFactory({
74
75
  organization,