@stamhoofd/backend 2.81.0 → 2.83.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 (108) hide show
  1. package/package.json +10 -10
  2. package/src/audit-logs/GroupLogger.ts +3 -3
  3. package/src/audit-logs/MemberResponsibilityRecordLogger.ts +1 -1
  4. package/src/audit-logs/OrderLogger.ts +1 -1
  5. package/src/audit-logs/RegistrationLogger.ts +1 -1
  6. package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +4 -4
  7. package/src/endpoints/admin/memberships/ChargeMembershipsEndpoint.ts +1 -1
  8. package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +5 -5
  9. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +1 -1
  10. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +8 -8
  11. package/src/endpoints/auth/CreateAdminEndpoint.ts +2 -2
  12. package/src/endpoints/auth/CreateTokenEndpoint.ts +10 -10
  13. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +2 -2
  14. package/src/endpoints/auth/PatchUserEndpoint.ts +9 -9
  15. package/src/endpoints/auth/SignupEndpoint.ts +2 -2
  16. package/src/endpoints/auth/VerifyEmailEndpoint.ts +3 -3
  17. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +1 -1
  18. package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +1 -1
  19. package/src/endpoints/global/email/GetEmailEndpoint.ts +1 -1
  20. package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +1 -1
  21. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +139 -0
  22. package/src/endpoints/global/email/PatchEmailEndpoint.ts +30 -7
  23. package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +1 -1
  24. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +16 -35
  25. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +1 -1
  26. package/src/endpoints/global/events/PatchEventsEndpoint.ts +22 -16
  27. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +1 -1
  28. package/src/endpoints/global/files/UploadFile.ts +14 -2
  29. package/src/endpoints/global/files/UploadImage.ts +2 -2
  30. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +2 -2
  31. package/src/endpoints/global/members/GetMembersEndpoint.ts +1 -1
  32. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +19 -19
  33. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +34 -34
  34. package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +1 -1
  35. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +5 -5
  36. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +5 -1
  37. package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +68 -0
  38. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +1 -1
  39. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +2 -2
  40. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +15 -17
  41. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +4 -4
  42. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +37 -37
  43. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -2
  44. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +2 -2
  45. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +1 -1
  46. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +5 -5
  47. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +2 -2
  48. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +1 -1
  49. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +3 -3
  50. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +9 -9
  51. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +4 -4
  52. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +1 -1
  53. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +1 -1
  54. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +11 -11
  55. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +13 -13
  56. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +16 -16
  57. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +1 -1
  58. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -2
  59. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +1 -1
  60. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +1 -1
  61. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +1 -1
  62. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +106 -0
  63. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +16 -3
  64. package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +2 -2
  65. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +247 -0
  66. package/src/endpoints/{auth → organization/dashboard/users}/PatchApiUserEndpoint.ts +25 -6
  67. package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +4 -4
  68. package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +2 -2
  69. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +8 -8
  70. package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +1 -1
  71. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +1 -1
  72. package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -2
  73. package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +1 -1
  74. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +5 -0
  75. package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +1 -1
  76. package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +2 -2
  77. package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +1 -1
  78. package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +3 -3
  79. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -1
  80. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +1 -1
  81. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +10 -8
  82. package/src/excel-loaders/event-notifications.ts +11 -11
  83. package/src/excel-loaders/members.ts +34 -34
  84. package/src/excel-loaders/organizations.ts +23 -23
  85. package/src/excel-loaders/payments.ts +39 -39
  86. package/src/excel-loaders/receivable-balances.ts +21 -21
  87. package/src/helpers/AddressValidator.ts +6 -6
  88. package/src/helpers/AdminPermissionChecker.ts +7 -4
  89. package/src/helpers/AuthenticatedStructures.ts +16 -8
  90. package/src/helpers/BuckarooHelper.ts +1 -1
  91. package/src/helpers/CheckSettlements.ts +1 -1
  92. package/src/helpers/Context.ts +31 -15
  93. package/src/helpers/FileCache.ts +7 -7
  94. package/src/helpers/ForwardHandler.ts +1 -1
  95. package/src/helpers/GlobalHelper.ts +6 -4
  96. package/src/helpers/MembershipCharger.ts +2 -2
  97. package/src/helpers/SetupStepUpdater.ts +1 -1
  98. package/src/helpers/StripeHelper.ts +18 -7
  99. package/src/helpers/XlsxTransformerColumnHelper.ts +18 -18
  100. package/src/services/DocumentService.ts +1 -1
  101. package/src/services/EventNotificationService.ts +1 -1
  102. package/src/services/MemberNumberService.ts +3 -3
  103. package/src/services/SSOService.ts +5 -5
  104. package/src/sql-filters/members.ts +1 -1
  105. package/tests/e2e/api-rate-limits.test.ts +188 -0
  106. package/tests/e2e/private-files.test.ts +3 -3
  107. package/tests/helpers/StripeMocker.ts +7 -1
  108. /package/src/endpoints/global/platform/{GetPlatformEnpoint.ts → GetPlatformEndpoint.ts} +0 -0
@@ -0,0 +1,139 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import { Email, Organization, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from "@stamhoofd/models";
3
+ import { EmailStatus, Email as EmailStruct, PermissionLevel, Permissions, Version } from "@stamhoofd/structures";
4
+ import { TestUtils } from "@stamhoofd/test-utils";
5
+ import { testServer } from "../../../../tests/helpers/TestServer";
6
+ import { PatchEmailEndpoint } from "./PatchEmailEndpoint";
7
+
8
+ const baseUrl = `/v${Version}/email`;
9
+
10
+ describe('Endpoint.PatchEmailEndpoint', () => {
11
+ const endpoint = new PatchEmailEndpoint();
12
+ let period: RegistrationPeriod;
13
+ let organization: Organization;
14
+ let token: Token;
15
+ let user: User;
16
+
17
+ const patchEmail = async (email: EmailStruct, token: Token, organization?: Organization) => {
18
+ const id = email.id;
19
+ const request = Request.buildJson('PATCH', `${baseUrl}/${id}`, organization?.getApiHost(), email);
20
+ request.headers.authorization = 'Bearer ' + token.accessToken;
21
+ return await testServer.test(endpoint, request);
22
+ };
23
+
24
+ beforeEach(async () => {
25
+ TestUtils.setEnvironment('userMode', 'platform');
26
+ });
27
+
28
+ beforeAll(async () => {
29
+ period = await new RegistrationPeriodFactory({
30
+ startDate: new Date(2023, 0, 1),
31
+ endDate: new Date(2023, 11, 31),
32
+ }).create();
33
+
34
+ organization = await new OrganizationFactory({ period })
35
+ .create();
36
+
37
+ user = await new UserFactory({
38
+ organization,
39
+ permissions: Permissions.create({
40
+ level: PermissionLevel.Read,
41
+ }),
42
+ })
43
+ .create();
44
+
45
+ token = await Token.createToken(user);
46
+ });
47
+
48
+ test('Should throw error if no unsubscribe button in email html', async () => {
49
+ const email = new Email();
50
+ email.subject = 'test subject';
51
+ email.status = EmailStatus.Draft;
52
+ email.text = 'test email {{unsubscribeUrl}}';
53
+ email.html = `<!DOCTYPE html>
54
+ <html>
55
+
56
+ <head>
57
+ <meta charset="utf-8" />
58
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
59
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
60
+ <title>test</title>
61
+ </head>
62
+
63
+ <body>
64
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
65
+ </body>
66
+
67
+ </html>`;
68
+ email.json = {
69
+ "content": [
70
+ {
71
+ "content": [
72
+ {
73
+ "text": "test email",
74
+ "type": "text"
75
+ }
76
+ ],
77
+ "type": "paragraph"
78
+ }
79
+ ],
80
+ "type": "doc"
81
+ };
82
+ email.userId = user.id;
83
+ email.organizationId = organization.id;
84
+
85
+ await email.save();
86
+
87
+ const body = EmailStruct.create({...email, fromAddress:'test@test.be', status: EmailStatus.Sending})
88
+
89
+ await expect(async () => await patchEmail(body, token, organization))
90
+ .rejects
91
+ .toThrow('Missing unsubscribe button');
92
+ })
93
+
94
+ test('Should throw error if no unsubscribe button in email text', async () => {
95
+ const email = new Email();
96
+ email.subject = 'test subject';
97
+ email.status = EmailStatus.Draft;
98
+ email.text = 'test email';
99
+ email.html = `<!DOCTYPE html>
100
+ <html>
101
+
102
+ <head>
103
+ <meta charset="utf-8" />
104
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
105
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
106
+ <title>test</title>
107
+ </head>
108
+
109
+ <body>
110
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email {{unsubscribeUrl}}</p>
111
+ </body>
112
+
113
+ </html>`;
114
+ email.json = {
115
+ "content": [
116
+ {
117
+ "content": [
118
+ {
119
+ "text": "test email",
120
+ "type": "text"
121
+ }
122
+ ],
123
+ "type": "paragraph"
124
+ }
125
+ ],
126
+ "type": "doc"
127
+ };
128
+ email.userId = user.id;
129
+ email.organizationId = organization.id;
130
+
131
+ await email.save();
132
+
133
+ const body = EmailStruct.create({...email, fromAddress:'test@test.be', status: EmailStatus.Sending})
134
+
135
+ await expect(async () => await patchEmail(body, token, organization))
136
+ .rejects
137
+ .toThrow('Missing unsubscribe button');
138
+ })
139
+ })
@@ -2,19 +2,15 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
2
2
  import { Email } from '@stamhoofd/models';
3
3
  import { EmailPreview, EmailStatus, Email as EmailStruct } from '@stamhoofd/structures';
4
4
 
5
+ import { AutoEncoderPatchType, Decoder, patchObject } from '@simonbackx/simple-encoding';
5
6
  import { SimpleError } from '@simonbackx/simple-errors';
6
7
  import { Context } from '../../../helpers/Context';
7
- import { AutoEncoderPatchType, Decoder, patchObject } from '@simonbackx/simple-encoding';
8
8
 
9
9
  type Params = { id: string };
10
10
  type Query = undefined;
11
11
  type Body = AutoEncoderPatchType<EmailStruct>;
12
12
  type ResponseBody = EmailPreview;
13
13
 
14
- /**
15
- * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
16
- */
17
-
18
14
  export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
15
  bodyDecoder = EmailStruct.patchType() as Decoder<AutoEncoderPatchType<EmailStruct>>;
20
16
 
@@ -44,7 +40,7 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
44
40
  throw new SimpleError({
45
41
  code: 'not_found',
46
42
  human: 'Email not found',
47
- message: 'Deze e-mail bestaat niet of is verwijderd',
43
+ message: $t(`9ddb6616-f62d-4c91-82a9-e5cf398e4c4a`),
48
44
  statusCode: 404,
49
45
  });
50
46
  }
@@ -53,7 +49,7 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
53
49
  throw new SimpleError({
54
50
  code: 'not_draft',
55
51
  human: 'Email is not a draft',
56
- message: 'Deze e-mail is al verzonden en kan niet meer aangepast worden',
52
+ message: $t(`298b5a46-2899-4aa1-89df-9b634c20806b`),
57
53
  statusCode: 400,
58
54
  });
59
55
  }
@@ -107,6 +103,33 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
107
103
 
108
104
  if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
109
105
  model.throwIfNotReadyToSend();
106
+
107
+ const replacement = '{{unsubscribeUrl}}';
108
+
109
+ if (model.html) {
110
+ // Check email contains an unsubscribe button
111
+ if (!model.html.includes(replacement)) {
112
+ throw new SimpleError({
113
+ code: "missing_unsubscribe_button",
114
+ message: "Missing unsubscribe button",
115
+ human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
116
+ field: "html"
117
+ })
118
+ }
119
+ }
120
+
121
+ if (model.text) {
122
+ // Check email contains an unsubscribe button
123
+ if (!model.text.includes(replacement)) {
124
+ throw new SimpleError({
125
+ code: "missing_unsubscribe_button",
126
+ message: "Missing unsubscribe button",
127
+ human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
128
+ field: "text"
129
+ })
130
+ }
131
+ }
132
+
110
133
  model.send().catch(console.error);
111
134
  }
112
135
 
@@ -142,7 +142,7 @@ export class GetEventNotificationsEndpoint extends Endpoint<Params, Query, Body,
142
142
 
143
143
  for (const notification of notifications) {
144
144
  if (!await Context.auth.canAccessEventNotification(notification)) {
145
- throw Context.auth.error('Je hebt geen toegang om deze melding te bekijken');
145
+ throw Context.auth.error($t(`f18f19ca-c56b-49ad-b131-244cbebb6b1f`));
146
146
  }
147
147
  }
148
148
 
@@ -1,36 +1,17 @@
1
1
  import { PatchableArray, PatchMap, patchObject } from '@simonbackx/simple-encoding';
2
2
  import { Endpoint, Request } from '@simonbackx/simple-endpoints';
3
- import { EventNotificationFactory, EventFactory, EventNotificationTypeFactory, Organization, OrganizationFactory, Token, User, UserFactory, EmailTemplateFactory, RecordCategoryFactory, RegistrationPeriodFactory, RecordAnswerFactory, EventNotification } from '@stamhoofd/models';
4
- import { AccessRight, BaseOrganization, EmailTemplateType, Event, EventNotificationStatus, EventNotification as EventNotificationStruct, PermissionLevel, Permissions, PermissionsResourceType, RecordAnswer, RecordType, ResourcePermissions } from '@stamhoofd/structures';
5
- import { TestUtils } from '@stamhoofd/test-utils';
6
- import { PatchEventNotificationsEndpoint } from './PatchEventNotificationsEndpoint';
7
- import { testServer } from '../../../../tests/helpers/TestServer';
8
3
  import { EmailMocker } from '@stamhoofd/email';
4
+ import { EmailTemplateFactory, EventFactory, EventNotification, EventNotificationFactory, EventNotificationTypeFactory, Organization, OrganizationFactory, RecordAnswerFactory, RecordCategoryFactory, RegistrationPeriodFactory, Token, User, UserFactory } from '@stamhoofd/models';
5
+ import { AccessRight, BaseOrganization, EmailTemplateType, Event, EventNotificationStatus, EventNotification as EventNotificationStruct, Permissions, PermissionsResourceType, RecordType, ResourcePermissions } from '@stamhoofd/structures';
6
+ import { SHExpect, TestUtils } from '@stamhoofd/test-utils';
7
+ import { testServer } from '../../../../tests/helpers/TestServer';
8
+ import { PatchEventNotificationsEndpoint } from './PatchEventNotificationsEndpoint';
9
9
 
10
10
  const baseUrl = `/event-notifications`;
11
11
  const endpoint = new PatchEventNotificationsEndpoint();
12
12
  type EndpointType = typeof endpoint;
13
13
  type Body = EndpointType extends Endpoint<any, any, infer B, any> ? B : never;
14
14
 
15
- const errorWithCode = (code: string) => expect.objectContaining({ code }) as jest.Constructable;
16
- const errorWithMessage = (message: string) => expect.objectContaining({ message }) as jest.Constructable;
17
- const simpleError = (data: {
18
- code?: string;
19
- message?: string;
20
- field?: string;
21
- }) => {
22
- const d = {
23
- code: data.code ?? expect.any(String),
24
- message: data.message ?? expect.any(String),
25
- field: data.field ?? expect.anything(),
26
- };
27
-
28
- if (!data.field) {
29
- delete d.field;
30
- }
31
- return expect.objectContaining(d) as jest.Constructable;
32
- };
33
-
34
15
  const minimumUserPermissions = Permissions.create({
35
16
  resources: new Map([
36
17
  [PermissionsResourceType.Groups, new Map([
@@ -169,7 +150,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
169
150
  );
170
151
 
171
152
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
172
- simpleError({ code: 'invalid_field', field: 'typeId' }),
153
+ SHExpect.simpleError({ code: 'invalid_field', field: 'typeId' }),
173
154
  );
174
155
  });
175
156
 
@@ -194,7 +175,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
194
175
  );
195
176
 
196
177
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
197
- simpleError({ code: 'invalid_field', field: 'events' }),
178
+ SHExpect.simpleError({ code: 'invalid_field', field: 'events' }),
198
179
  );
199
180
  });
200
181
 
@@ -228,7 +209,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
228
209
  }),
229
210
  );
230
211
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
231
- simpleError({ code: 'invalid_period', field: 'startDate' }),
212
+ SHExpect.simpleError({ code: 'invalid_period', field: 'startDate' }),
232
213
  );
233
214
  });
234
215
 
@@ -256,7 +237,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
256
237
  );
257
238
 
258
239
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
259
- simpleError({ code: 'invalid_field', field: 'events' }),
240
+ SHExpect.simpleError({ code: 'invalid_field', field: 'events' }),
260
241
  );
261
242
  });
262
243
 
@@ -416,7 +397,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
416
397
  );
417
398
 
418
399
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
419
- simpleError({ code: 'permission_denied' }),
400
+ SHExpect.simpleError({ code: 'permission_denied' }),
420
401
  );
421
402
  });
422
403
 
@@ -442,7 +423,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
442
423
  );
443
424
 
444
425
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
445
- simpleError({ code: 'permission_denied' }),
426
+ SHExpect.simpleError({ code: 'permission_denied' }),
446
427
  );
447
428
  });
448
429
 
@@ -468,7 +449,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
468
449
  );
469
450
 
470
451
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
471
- simpleError({ code: 'permission_denied' }),
452
+ SHExpect.simpleError({ code: 'permission_denied' }),
472
453
  );
473
454
  });
474
455
 
@@ -494,7 +475,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
494
475
  );
495
476
 
496
477
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
497
- simpleError({ code: 'permission_denied' }),
478
+ SHExpect.simpleError({ code: 'permission_denied' }),
498
479
  );
499
480
  });
500
481
 
@@ -520,7 +501,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
520
501
  );
521
502
 
522
503
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
523
- simpleError({ code: 'permission_denied' }),
504
+ SHExpect.simpleError({ code: 'permission_denied' }),
524
505
  );
525
506
  });
526
507
 
@@ -603,7 +584,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
603
584
  );
604
585
 
605
586
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
606
- simpleError({ code: 'permission_denied' }),
587
+ SHExpect.simpleError({ code: 'permission_denied' }),
607
588
  );
608
589
  });
609
590
 
@@ -991,7 +972,7 @@ describe('Endpoint.PatchEventNotificationsEndpoint', () => {
991
972
  );
992
973
 
993
974
  await expect(TestRequest.patch({ body, user, organization })).rejects.toThrow(
994
- simpleError({ code: 'permission_denied' }),
975
+ SHExpect.simpleError({ code: 'permission_denied' }),
995
976
  );
996
977
  });
997
978
  });
@@ -69,7 +69,7 @@ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Bod
69
69
  throw new SimpleError({
70
70
  code: 'invalid_field',
71
71
  message: 'Invalid event',
72
- human: 'Dit evenement bestaat niet of is niet van jouw organisatie',
72
+ human: $t(`4275fbbe-9921-454e-852c-9c53a9803f1f`),
73
73
  field: 'events',
74
74
  });
75
75
  }
@@ -63,7 +63,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
63
63
  throw new SimpleError({
64
64
  code: 'invalid_field',
65
65
  message: 'Empty groups',
66
- human: 'Kies minstens één leeftijdsgroep',
66
+ human: $t(`93faf169-b78d-4ad2-b13b-3b974267a632`),
67
67
  });
68
68
  }
69
69
 
@@ -71,7 +71,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
71
71
  throw new SimpleError({
72
72
  code: 'invalid_field',
73
73
  message: 'Empty default age groups',
74
- human: 'Kies minstens één standaard leeftijdsgroep',
74
+ human: $t(`2712befc-5cc5-4013-b8df-ec0861a82c36`),
75
75
  });
76
76
  }
77
77
 
@@ -79,7 +79,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
79
79
  throw new SimpleError({
80
80
  code: 'invalid_field',
81
81
  message: 'Empty organization tag ids',
82
- human: 'Kies minstens één tag',
82
+ human: $t(`30230574-2956-4e40-ba11-5523c24c0af8`),
83
83
  });
84
84
  }
85
85
 
@@ -136,7 +136,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
136
136
  throw new SimpleError({
137
137
  code: 'not_found',
138
138
  message: 'Event not found',
139
- human: 'De activiteit werd niet gevonden',
139
+ human: $t(`c5f3d2c3-9d7a-473d-ba91-63ce104a2de5`),
140
140
  });
141
141
  }
142
142
 
@@ -146,7 +146,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
146
146
  throw new SimpleError({
147
147
  code: 'invalid_field',
148
148
  message: 'Cannot patch organizationCache',
149
- human: 'Je kan de organizationCache niet aanpassen via een patch',
149
+ human: $t(`74e6eba6-596c-4e55-b178-b2a5fdbca581`),
150
150
  field: 'meta.organizationCache',
151
151
  });
152
152
  }
@@ -166,7 +166,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
166
166
  throw new SimpleError({
167
167
  code: 'invalid_field',
168
168
  message: 'Empty groups',
169
- human: 'Kies minstens één leeftijdsgroep',
169
+ human: $t(`93faf169-b78d-4ad2-b13b-3b974267a632`),
170
170
  });
171
171
  }
172
172
 
@@ -174,7 +174,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
174
174
  throw new SimpleError({
175
175
  code: 'invalid_field',
176
176
  message: 'Empty default age groups',
177
- human: 'Kies minstens één standaard leeftijdsgroep',
177
+ human: $t(`2712befc-5cc5-4013-b8df-ec0861a82c36`),
178
178
  });
179
179
  }
180
180
 
@@ -182,7 +182,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
182
182
  throw new SimpleError({
183
183
  code: 'invalid_field',
184
184
  message: 'Empty organization tag ids',
185
- human: 'Kies minstens één tag',
185
+ human: $t(`30230574-2956-4e40-ba11-5523c24c0af8`),
186
186
  });
187
187
  }
188
188
 
@@ -320,7 +320,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
320
320
  throw new SimpleError({
321
321
  code: 'invalid_field',
322
322
  message: 'Invalid typeId',
323
- human: 'Dit type activiteit wordt niet ondersteund',
323
+ human: $t(`6b36fc82-d88c-49cc-ae94-4653ad37b3e3`),
324
324
  field: 'typeId',
325
325
  });
326
326
  }
@@ -334,7 +334,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
334
334
  throw new SimpleError({
335
335
  code: 'invalid_field',
336
336
  message: 'Name is too short',
337
- human: 'Vul een naam voor je activiteit in',
337
+ human: $t(`53a66432-0bf5-4193-9eb8-dbd52d86a1f8`),
338
338
  field: 'name',
339
339
  });
340
340
  }
@@ -343,7 +343,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
343
343
  throw new SimpleError({
344
344
  code: 'invalid_dates',
345
345
  message: 'End date is before start date',
346
- human: 'De einddatum moet na de startdatum liggen',
346
+ human: $t(`318924c0-7a79-4cfa-b206-ffc27c4d32b7`),
347
347
  field: 'endDate',
348
348
  });
349
349
  }
@@ -360,7 +360,10 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
360
360
  throw new SimpleError({
361
361
  code: 'minimum_days',
362
362
  message: 'An event with this type has a minimum of ' + type.minimumDays + ' days',
363
- human: 'Een ' + type.name + ' moet minimum ' + Formatter.pluralText(type.minimumDays, 'dag', 'dagen') + ' duren',
363
+ human: $t(`04ff85c0-eb98-46b8-975b-8fd136ddc49a`, {
364
+ name: type.name,
365
+ days: Formatter.pluralText(type.minimumDays, $t(`a6279389-a070-49c9-a085-bb312555e419`), $t(`fca0ce20-d696-4966-a50c-441f54f046c4`)),
366
+ }),
364
367
  field: 'startDate',
365
368
  });
366
369
  }
@@ -369,7 +372,10 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
369
372
  throw new SimpleError({
370
373
  code: 'maximum_days',
371
374
  message: 'An event with this type has a maximum of ' + type.maximumDays + ' days',
372
- human: 'Een ' + type.name + ' mag maximaal ' + Formatter.pluralText(type.maximumDays, 'dag', 'dagen') + ' duren',
375
+ human: $t(`a7d005aa-ceaa-4323-8fac-a02fce174023`, {
376
+ name: type.name,
377
+ days: Formatter.pluralText(type.maximumDays, $t(`a6279389-a070-49c9-a085-bb312555e419`), $t(`fca0ce20-d696-4966-a50c-441f54f046c4`)),
378
+ }),
373
379
  field: 'startDate',
374
380
  });
375
381
  }
@@ -393,7 +399,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
393
399
  throw new SimpleError({
394
400
  code: 'type_maximum_reached',
395
401
  message: 'Maximum number of events with this type reached',
396
- human: 'Het maximum aantal voor ' + type.name + ' is bereikt (' + type.maximum + ')',
402
+ human: $t(`fb7df531-0f89-4841-b685-7b2cfb5b507d`) + ' ' + type.name + ' ' + $t(`073d20dc-88f3-4145-89f5-13cc8ad90207`) + type.maximum + ')',
397
403
  field: 'typeId',
398
404
  });
399
405
  }
@@ -402,7 +408,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
402
408
  throw new SimpleError({
403
409
  code: 'invalid_period',
404
410
  message: 'No period found for this start date',
405
- human: 'Oeps, je kan nog geen evenementen van dit type aanmaken in deze periode',
411
+ human: $t(`7a38bf9d-4df7-4827-85dc-327ffe6cd50a`),
406
412
  field: 'startDate',
407
413
  });
408
414
  }
@@ -420,7 +426,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
420
426
  throw new SimpleError({
421
427
  code: 'invalid_field',
422
428
  message: 'Empty number',
423
- human: 'De locatie is verplicht voor deze soort activiteit.',
429
+ human: $t(`6b72f8bd-cd5b-423f-a556-be102d3c22e9`),
424
430
  field: 'event_required',
425
431
  });
426
432
  }
@@ -65,7 +65,7 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
65
65
  throw new SimpleError({
66
66
  code: 'not_allowed',
67
67
  message: 'Export is pending',
68
- human: 'Je hebt momenteel al een Excel export lopen. Wacht tot die klaar is voor je een nieuwe export start.',
68
+ human: $t(`a77e6624-bd1f-4374-af5a-d25cc60ee4da`),
69
69
  statusCode: 403,
70
70
  });
71
71
  }
@@ -58,7 +58,7 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
58
58
  await Context.setOptionalOrganizationScope();
59
59
  const { user } = await Context.authenticate();
60
60
 
61
- if (!Context.auth.canUpload()) {
61
+ if (!Context.auth.canUpload({ private: request.query.isPrivate })) {
62
62
  throw Context.auth.error();
63
63
  }
64
64
 
@@ -126,6 +126,18 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
126
126
  prefix += '/';
127
127
  }
128
128
 
129
+ prefix += (STAMHOOFD.environment ?? 'development') === 'development' ? ('development/') : ('');
130
+
131
+ // Prepend user id to the file path
132
+ if (request.query.isPrivate && user) {
133
+ // Private files
134
+ prefix += 'users/' + user.id + '/';
135
+ }
136
+ else {
137
+ // Public files
138
+ prefix += 'p/';
139
+ }
140
+
129
141
  // Also include the source, in private mode
130
142
  const fileId = uuidv4();
131
143
  let uploadExt = '';
@@ -162,7 +174,7 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
162
174
  }
163
175
 
164
176
  const filenameWithoutExt = file.originalFilename?.split('.').slice(0, -1).join('.') ?? fileId;
165
- const key = prefix + (STAMHOOFD.environment ?? 'development') + '/' + fileId + '/' + (Formatter.slug(filenameWithoutExt) + (uploadExt ? ('.' + uploadExt) : ''));
177
+ const key = prefix + fileId + '/' + (Formatter.slug(filenameWithoutExt) + (uploadExt ? ('.' + uploadExt) : ''));
166
178
  const params = {
167
179
  Bucket: STAMHOOFD.SPACES_BUCKET,
168
180
  Key: key,
@@ -64,7 +64,7 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
64
64
  await Context.setOptionalOrganizationScope();
65
65
  const { user } = await Context.authenticate();
66
66
 
67
- if (!Context.auth.canUpload()) {
67
+ if (!Context.auth.canUpload({ private: request.query.isPrivate })) {
68
68
  throw Context.auth.error();
69
69
  }
70
70
 
@@ -131,7 +131,7 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
131
131
  });
132
132
 
133
133
  const fileContent = await fs.readFile(file.filepath);
134
- const image = await Image.create(fileContent, file.mimetype ?? undefined, resolutions, request.query.isPrivate);
134
+ const image = await Image.create(fileContent, file.mimetype ?? undefined, resolutions, request.query.isPrivate, user);
135
135
  return new Response(ImageStruct.create(image));
136
136
  }
137
137
  }
@@ -55,7 +55,7 @@ export class GetMemberFamilyEndpoint extends Endpoint<Params, Query, Body, Respo
55
55
 
56
56
  // Check access to this member (this will automatically give access to the family)
57
57
  if (!await Context.auth.canAccessMember(member)) {
58
- throw Context.auth.error('Je hebt geen toegang tot dit lid');
58
+ throw Context.auth.error($t(`c9797487-3e24-4686-9bdc-2a7ec9cee8ed`));
59
59
  }
60
60
  validatedMembers.push(member);
61
61
  continue;
@@ -67,7 +67,7 @@ export class GetMemberFamilyEndpoint extends Endpoint<Params, Query, Body, Respo
67
67
  }
68
68
 
69
69
  if (!foundMember) {
70
- throw Context.auth.error('Je hebt geen toegang tot dit lid');
70
+ throw Context.auth.error($t(`c9797487-3e24-4686-9bdc-2a7ec9cee8ed`));
71
71
  }
72
72
 
73
73
  return new Response(
@@ -245,7 +245,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
245
245
  throw new SimpleError({
246
246
  code: 'timeout',
247
247
  message: 'Query took too long',
248
- human: 'Deze opzoeking is te complex en duurt te lang. Probeer een eenvoudigere zoekopdracht of probeer terug op een rustiger tijdstip.',
248
+ human: $t(`dce51638-6129-448b-8a15-e6d778f3a76a`),
249
249
  });
250
250
  }
251
251
  throw error;