@stamhoofd/backend 2.137.4 → 2.137.5

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 (68) hide show
  1. package/package.json +23 -17
  2. package/src/crons/drip-emails.ts +2 -1
  3. package/src/crons.ts +4 -2
  4. package/src/email-recipient-loaders/payments.ts +3 -2
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +3 -2
  6. package/src/endpoints/auth/CreateTokenEndpoint.ts +2 -1
  7. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +3 -2
  8. package/src/endpoints/auth/PatchUserEndpoint.ts +6 -3
  9. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +2 -1
  10. package/src/endpoints/auth/SignupEndpoint.ts +4 -2
  11. package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -1
  12. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
  13. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
  14. package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
  15. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
  16. package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
  17. package/src/endpoints/global/email/PatchEmailEndpoint.ts +2 -1
  18. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -5
  19. package/src/endpoints/global/files/UploadFile.test.ts +206 -0
  20. package/src/endpoints/global/files/UploadFile.ts +31 -6
  21. package/src/endpoints/global/files/UploadImage.test.ts +177 -0
  22. package/src/endpoints/global/files/UploadImage.ts +23 -4
  23. package/src/endpoints/global/files/upload-security.test.ts +837 -0
  24. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
  25. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +2 -1
  26. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +2 -1
  27. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
  28. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +5 -3
  29. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
  30. package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
  31. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
  32. package/src/excel-loaders/balance-items.ts +1 -1
  33. package/src/excel-loaders/event-notifications.ts +6 -7
  34. package/src/excel-loaders/members.test.ts +2 -2
  35. package/src/excel-loaders/members.ts +9 -10
  36. package/src/excel-loaders/organizations.ts +14 -20
  37. package/src/excel-loaders/payments.ts +1 -1
  38. package/src/excel-loaders/platform-memberships.ts +7 -7
  39. package/src/excel-loaders/platform-sheets.test.ts +113 -0
  40. package/src/excel-loaders/receivable-balances.ts +1 -1
  41. package/src/excel-loaders/registrations.ts +9 -9
  42. package/src/helpers/AdminPermissionChecker.ts +1 -1
  43. package/src/helpers/AuthenticatedStructures.ts +12 -9
  44. package/src/helpers/MembershipCharger.ts +3 -2
  45. package/src/services/BalanceItemService.ts +2 -1
  46. package/src/services/DocumentRenderService.test.ts +229 -0
  47. package/src/services/DocumentRenderService.ts +180 -0
  48. package/src/services/EmailPreviewService.test.ts +300 -0
  49. package/src/services/EmailPreviewService.ts +239 -0
  50. package/src/services/FileSignService.test.ts +85 -0
  51. package/src/services/FileSignService.ts +23 -1
  52. package/src/services/OrderService.test.ts +308 -0
  53. package/src/services/OrderService.ts +214 -0
  54. package/src/services/OrganizationDNSService.test.ts +177 -0
  55. package/src/services/OrganizationDNSService.ts +282 -0
  56. package/src/services/OrganizationEmailService.test.ts +57 -0
  57. package/src/services/OrganizationEmailService.ts +211 -0
  58. package/src/services/PasswordForgotService.test.ts +99 -0
  59. package/src/services/PasswordForgotService.ts +38 -0
  60. package/src/services/PlatformMembershipService.test.ts +180 -0
  61. package/src/services/PlatformMembershipService.ts +281 -4
  62. package/src/services/RegistrationService.ts +3 -2
  63. package/src/services/STPackageService.test.ts +191 -0
  64. package/src/services/STPackageService.ts +114 -2
  65. package/src/services/VerificationCodeService.test.ts +71 -0
  66. package/src/services/VerificationCodeService.ts +100 -0
  67. package/tests/e2e/documents.test.ts +2 -1
  68. package/tests/e2e/private-files.test.ts +77 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.137.4",
3
+ "version": "2.137.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -19,6 +19,12 @@
19
19
  "import": "./dist/src/boot.js",
20
20
  "require": "./dist/src/boot.js"
21
21
  },
22
+ "./services/*": {
23
+ "types": "./dist/src/services/*.d.ts",
24
+ "import": "./dist/src/services/*.js",
25
+ "require": "./dist/src/services/*.js",
26
+ "@stamhoofd/source": "./src/services/*.ts"
27
+ },
22
28
  "./tests/helpers": {
23
29
  "types": "./dist/tests/helpers/index.d.ts",
24
30
  "import": "./dist/tests/helpers/index.js",
@@ -58,20 +64,20 @@
58
64
  "@simonbackx/simple-endpoints": "1.22.0",
59
65
  "@simonbackx/simple-errors": "1.5.0",
60
66
  "@simonbackx/simple-logging": "1.0.1",
61
- "@stamhoofd/backend-env": "2.137.4",
62
- "@stamhoofd/backend-i18n": "2.137.4",
63
- "@stamhoofd/backend-middleware": "2.137.4",
64
- "@stamhoofd/crons": "2.137.4",
65
- "@stamhoofd/email": "2.137.4",
66
- "@stamhoofd/excel-writer": "2.137.4",
67
- "@stamhoofd/logging": "2.137.4",
68
- "@stamhoofd/models": "2.137.4",
69
- "@stamhoofd/object-differ": "2.137.4",
70
- "@stamhoofd/queues": "2.137.4",
71
- "@stamhoofd/sql": "2.137.4",
72
- "@stamhoofd/structures": "2.137.4",
73
- "@stamhoofd/types": "2.137.4",
74
- "@stamhoofd/utility": "2.137.4",
67
+ "@stamhoofd/backend-env": "2.137.5",
68
+ "@stamhoofd/backend-i18n": "2.137.5",
69
+ "@stamhoofd/backend-middleware": "2.137.5",
70
+ "@stamhoofd/crons": "2.137.5",
71
+ "@stamhoofd/email": "2.137.5",
72
+ "@stamhoofd/excel-writer": "2.137.5",
73
+ "@stamhoofd/logging": "2.137.5",
74
+ "@stamhoofd/models": "2.137.5",
75
+ "@stamhoofd/object-differ": "2.137.5",
76
+ "@stamhoofd/queues": "2.137.5",
77
+ "@stamhoofd/sql": "2.137.5",
78
+ "@stamhoofd/structures": "2.137.5",
79
+ "@stamhoofd/types": "2.137.5",
80
+ "@stamhoofd/utility": "2.137.5",
75
81
  "archiver": "7.0.1",
76
82
  "axios": "1.18.1",
77
83
  "base-x": "3.0.11",
@@ -93,7 +99,7 @@
93
99
  "uuid": "14.0.1"
94
100
  },
95
101
  "devDependencies": {
96
- "@stamhoofd/test-utils": "2.137.4",
102
+ "@stamhoofd/test-utils": "2.137.5",
97
103
  "@types/cookie": "0.6.0",
98
104
  "@types/luxon": "3.7.2",
99
105
  "@types/mailparser": "3.4.6",
@@ -108,5 +114,5 @@
108
114
  "publishConfig": {
109
115
  "access": "public"
110
116
  },
111
- "gitHead": "22cb2b32de8f785d8e0d94f324940d6419fbe7ea"
117
+ "gitHead": "52c8072d0294d8005c9d36a79219708ea148a357"
112
118
  }
@@ -1,5 +1,6 @@
1
1
  import { registerCron } from '@stamhoofd/crons';
2
2
  import { Organization } from '@stamhoofd/models';
3
+ import { OrganizationEmailService } from '../services/OrganizationEmailService.js';
3
4
  import { isOutside } from './helpers/isOutside.js';
4
5
  import { useSavedIterator } from './helpers/useSavedIterator.js';
5
6
 
@@ -30,7 +31,7 @@ async function checkDrips() {
30
31
  // Get the next x organization to send e-mails for
31
32
  for await (const organization of iterate()) {
32
33
  try {
33
- await organization.checkDrips();
34
+ await OrganizationEmailService.checkDrips(organization);
34
35
  } catch (e) {
35
36
  console.error(e);
36
37
  }
package/src/crons.ts CHANGED
@@ -4,8 +4,10 @@ import { Group, Organization, Payment, Registration, STPackage, Webshop } from '
4
4
  import { SQL } from '@stamhoofd/sql';
5
5
  import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
6
6
  import { checkSettlements } from './helpers/CheckSettlements.js';
7
+ import { OrganizationDNSService } from './services/OrganizationDNSService.js';
7
8
  import { PaymentService } from './services/PaymentService.js';
8
9
  import { RegistrationService } from './services/RegistrationService.js';
10
+ import { STPackageService } from './services/STPackageService.js';
9
11
 
10
12
  let lastDNSCheck: Date | null = null;
11
13
  let lastDNSId = '';
@@ -39,7 +41,7 @@ async function checkDNS() {
39
41
  console.log('[DNS] ' + organization.name);
40
42
  }
41
43
  try {
42
- await organization.updateDNSRecords();
44
+ await OrganizationDNSService.updateDNSRecords(organization);
43
45
  } catch (e) {
44
46
  console.error(e);
45
47
  }
@@ -76,7 +78,7 @@ async function checkExpirationEmails() {
76
78
  console.log('[EXPIRATION EMAILS] Sending expiration emails...');
77
79
 
78
80
  for (const pack of packages) {
79
- await pack.sendExpiryEmail();
81
+ await STPackageService.sendExpiryEmail(pack);
80
82
  }
81
83
  lastExpirationCheck = new Date();
82
84
  }
@@ -1,5 +1,5 @@
1
1
  import type { RecipientLoader } from '@stamhoofd/models';
2
- import { BalanceItem, BalanceItemPayment, Email, Member, MemberResponsibilityRecord, Organization, Payment, User } from '@stamhoofd/models';
2
+ import { BalanceItem, BalanceItemPayment, Email, Member, MemberResponsibilityRecord, Organization, Payment, Platform, User } from '@stamhoofd/models';
3
3
  import { compileToSQLFilter, SQL } from '@stamhoofd/sql';
4
4
  import type { LimitedFilteredRequest, PaymentGeneral, StamhoofdFilter } from '@stamhoofd/structures';
5
5
  import { CountFilteredRequest, EmailRecipient, PaginatedResponse, PaymentMethod } from '@stamhoofd/structures';
@@ -301,12 +301,13 @@ async function getOrganizationRecipients(ids: { organizationId: string; payment:
301
301
  if (subFilter === null) {
302
302
  // Use full admins instead
303
303
  const admins = await User.getAdmins(allOrganizationIds, { verified: true });
304
+ const platform = await Platform.getSharedStruct();
304
305
  for (const { organizationId, payment } of ids) {
305
306
  const organization = organizationMap.get(organizationId);
306
307
  if (!organization) {
307
308
  continue;
308
309
  }
309
- const users = admins.filter(a => a.permissions?.forOrganization(organization)?.hasFullAccess());
310
+ const users = admins.filter(a => a.permissions?.forOrganization(organization, platform, { inheritFromPlatform: false })?.hasFullAccess());
310
311
 
311
312
  if (users.length === 0) {
312
313
  console.warn('No admins found for organization with id ', organizationId, ' while fetching email recipients for payment with id ', payment.id);
@@ -2,13 +2,14 @@ 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 { PasswordToken, Platform, sendEmailTemplate, User } from '@stamhoofd/models';
5
+ import { Platform, sendEmailTemplate, User } from '@stamhoofd/models';
6
6
  import type { UserWithMembers } from '@stamhoofd/structures';
7
7
  import { EmailTemplateType, Recipient, Replacement, UserPermissions, User as UserStruct } from '@stamhoofd/structures';
8
8
  import { Formatter } from '@stamhoofd/utility';
9
9
 
10
10
  import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures.js';
11
11
  import { Context } from '../../helpers/Context.js';
12
+ import { PasswordForgotService } from '../../services/PasswordForgotService.js';
12
13
  type Params = Record<string, never>;
13
14
  type Query = undefined;
14
15
  type Body = UserStruct;
@@ -117,7 +118,7 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
117
118
  validUntil.setTime(validUntil.getTime() + 7 * 24 * 3600 * 1000);
118
119
 
119
120
  const dateTime = Formatter.dateTime(validUntil);
120
- const recoveryUrl = await PasswordToken.getPasswordRecoveryUrl(admin, organization, request.i18n, validUntil);
121
+ const recoveryUrl = await PasswordForgotService.getPasswordRecoveryUrl(admin, organization, request.i18n, validUntil);
121
122
  const platformName = ((await Platform.getSharedStruct()).config.name);
122
123
 
123
124
  const name = organization?.name ?? platformName;
@@ -6,6 +6,7 @@ import type { ChallengeGrantStruct, PasswordGrantStruct, PasswordTokenGrantStruc
6
6
  import { CreateTokenStruct, LoginMethod, SignupResponse, Token as TokenStruct } from '@stamhoofd/structures';
7
7
 
8
8
  import { Context } from '../../helpers/Context.js';
9
+ import { VerificationCodeService } from '../../services/VerificationCodeService.js';
9
10
 
10
11
  type Params = Record<string, never>;
11
12
  type Query = undefined;
@@ -127,7 +128,7 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
127
128
  // if not: throw a validation error (e-mail validation is required)
128
129
  if (!user.verified) {
129
130
  const code = await EmailVerificationCode.createFor(user, user.email);
130
- code.send(user, organization, request.i18n).catch(console.error);
131
+ VerificationCodeService.send(code, user, organization, request.i18n).catch(console.error);
131
132
 
132
133
  throw new SimpleError({
133
134
  code: 'verify_email',
@@ -1,11 +1,12 @@
1
1
  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
- import { PasswordToken, Platform, sendEmailTemplate, User } from '@stamhoofd/models';
4
+ import { Platform, sendEmailTemplate, User } from '@stamhoofd/models';
5
5
  import { EmailTemplateType, ForgotPasswordRequest, LoginMethod, Recipient, Replacement } from '@stamhoofd/structures';
6
6
 
7
7
  import { SimpleError } from '@simonbackx/simple-errors';
8
8
  import { Context } from '../../helpers/Context.js';
9
+ import { PasswordForgotService } from '../../services/PasswordForgotService.js';
9
10
 
10
11
  type Params = Record<string, never>;
11
12
  type Query = undefined;
@@ -72,7 +73,7 @@ export class ForgotPasswordEndpoint extends Endpoint<Params, Query, Body, Respon
72
73
  return new Response(undefined);
73
74
  }
74
75
 
75
- const recoveryUrl = await PasswordToken.getPasswordRecoveryUrl(user, organization, request.i18n);
76
+ const recoveryUrl = await PasswordForgotService.getPasswordRecoveryUrl(user, organization, request.i18n);
76
77
 
77
78
  // Create e-mail builder
78
79
  await sendEmailTemplate(organization, {
@@ -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 { VerificationCodeService } from '../../services/VerificationCodeService.js';
13
14
 
14
15
  type Params = { id: string };
15
16
  type Query = undefined;
@@ -79,10 +80,12 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
79
80
  }
80
81
 
81
82
  if (request.body.permissions) {
83
+ const platform = await Platform.getSharedStruct();
84
+
82
85
  if (organization) {
83
86
  editUser.permissions = UserPermissions.limitedPatch(editUser.permissions, request.body.permissions, organization.id);
84
87
 
85
- if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization)?.hasFullAccess()) && STAMHOOFD.environment !== 'development') {
88
+ if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization, platform, { inheritFromPlatform: false })?.hasFullAccess()) && STAMHOOFD.environment !== 'development') {
86
89
  throw new SimpleError({
87
90
  code: 'permission_denied',
88
91
  message: $t(`%DG`),
@@ -99,7 +102,7 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
99
102
  editUser.permissions = null;
100
103
  }
101
104
 
102
- if (editUser.id === user.id && !editUser.permissions?.platform?.hasFullAccess() && STAMHOOFD.environment !== 'development') {
105
+ if (editUser.id === user.id && !editUser.permissions?.forPlatform(platform)?.hasFullAccess() && STAMHOOFD.environment !== 'development') {
103
106
  throw new SimpleError({
104
107
  code: 'permission_denied',
105
108
  message: $t(`%DG`),
@@ -187,7 +190,7 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
187
190
  // Create an validation code
188
191
  // We always need the code, to return it. Also on password recovery -> may not be visible to the client whether the user exists or not
189
192
  const code = await EmailVerificationCode.createFor(editUser, request.body.email);
190
- code.send(editUser, organization, request.i18n, editUser.id === user.id).catch(console.error);
193
+ VerificationCodeService.send(code, editUser, organization, request.i18n, editUser.id === user.id).catch(console.error);
191
194
 
192
195
  throw new SimpleError({
193
196
  code: 'verify_email',
@@ -5,6 +5,7 @@ import { EmailVerificationCode } from '@stamhoofd/models';
5
5
  import { PollEmailVerificationRequest, PollEmailVerificationResponse } from '@stamhoofd/structures';
6
6
 
7
7
  import { Context } from '../../helpers/Context.js';
8
+ import { VerificationCodeService } from '../../services/VerificationCodeService.js';
8
9
 
9
10
  type Params = Record<string, never>;
10
11
  type Query = undefined;
@@ -32,7 +33,7 @@ export class PollEmailVerificationEndpoint extends Endpoint<Params, Query, Body,
32
33
  const valid = await EmailVerificationCode.poll(organization?.id ?? null, request.body.token);
33
34
 
34
35
  if (valid) {
35
- EmailVerificationCode.resend(organization, request.body.token, request.i18n).catch(console.error);
36
+ VerificationCodeService.resend(organization, request.body.token, request.i18n).catch(console.error);
36
37
  }
37
38
 
38
39
  return new Response(PollEmailVerificationResponse.create({
@@ -6,6 +6,8 @@ import { EmailVerificationCode, PasswordToken, Platform, sendEmailTemplate, User
6
6
  import { EmailTemplateType, LoginMethod, NewUser, Recipient, Replacement, SignupResponse } from '@stamhoofd/structures';
7
7
 
8
8
  import { Context } from '../../helpers/Context.js';
9
+ import { PasswordForgotService } from '../../services/PasswordForgotService.js';
10
+ import { VerificationCodeService } from '../../services/VerificationCodeService.js';
9
11
 
10
12
  type Params = Record<string, never>;
11
13
  type Query = undefined;
@@ -77,7 +79,7 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
77
79
  // We don't await this block to avoid user enumeration attack using request response time
78
80
  (async () => {
79
81
  // Send an e-mail to say you already have an account + follow password forgot flow
80
- const recoveryUrl = await PasswordToken.getPasswordRecoveryUrl(user, organization, request.i18n);
82
+ const recoveryUrl = await PasswordForgotService.getPasswordRecoveryUrl(user, organization, request.i18n);
81
83
 
82
84
  // Create e-mail builder
83
85
  await sendEmailTemplate(organization, {
@@ -118,7 +120,7 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
118
120
  const code = await EmailVerificationCode.createFor(user, user.email);
119
121
 
120
122
  if (sendCode) {
121
- code.send(user, organization, request.i18n).catch(console.error);
123
+ VerificationCodeService.send(code, user, organization, request.i18n).catch(console.error);
122
124
  }
123
125
 
124
126
  return new Response(SignupResponse.create({
@@ -7,6 +7,7 @@ import { EmailStatus, Email as EmailStruct, EmailTemplate as EmailTemplateStruct
7
7
 
8
8
  import { Context } from '../../../helpers/Context.js';
9
9
  import { SimpleError } from '@simonbackx/simple-errors';
10
+ import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
10
11
 
11
12
  type Params = Record<string, never>;
12
13
  type Query = undefined;
@@ -162,6 +163,6 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
162
163
  await duplicate.save();
163
164
  }
164
165
 
165
- return new Response(await model.getPreviewStructure({ allLanguages: true }));
166
+ return new Response(await EmailPreviewService.getPreviewStructure(model, { allLanguages: true }));
166
167
  }
167
168
  }
@@ -157,7 +157,7 @@ describe('Endpoint.getAdminEmails', () => {
157
157
  expect(unsubscribeUrlReplacement!.value).toMatch(/^https:\/\//); // Should still be a valid URL
158
158
  expect(unsubscribeUrlReplacement!.value).not.toContain('secret-token-12345'); // Original sensitive token should be gone
159
159
 
160
- // This tests that Email.getStructureForUser properly handles sensitive data by:
160
+ // This tests that EmailPreviewService.getStructureForUser properly handles sensitive data by:
161
161
  // 1. Removing original sensitive replacements from other users' data
162
162
  // 2. Creating fresh, appropriate replacements for the current viewer
163
163
  // 3. Ensuring web safety of all replacement values
@@ -9,6 +9,7 @@ import { Email, Platform } from '@stamhoofd/models';
9
9
  import type { SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
10
10
  import { applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
11
11
  import { Context } from '../../../helpers/Context.js';
12
+ import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
12
13
  import { emailFilterCompilers } from '../../../sql-filters/emails.js';
13
14
  import { emailSorters } from '../../../sql-sorters/emails.js';
14
15
 
@@ -158,7 +159,7 @@ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, Respon
158
159
  }
159
160
 
160
161
  return new PaginatedResponse<EmailPreview[], LimitedFilteredRequest>({
161
- results: await Promise.all(emails.map(email => email.getPreviewStructure({ allLanguages: true }))),
162
+ results: await Promise.all(emails.map(email => EmailPreviewService.getPreviewStructure(email, { allLanguages: true }))),
162
163
  next,
163
164
  });
164
165
  }
@@ -5,6 +5,7 @@ import type { EmailPreview } from '@stamhoofd/structures';
5
5
 
6
6
  import { SimpleError } from '@simonbackx/simple-errors';
7
7
  import { Context } from '../../../helpers/Context.js';
8
+ import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
8
9
 
9
10
  type Params = { id: string };
10
11
  type Query = undefined;
@@ -51,6 +52,6 @@ export class GetEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody
51
52
  throw Context.auth.error();
52
53
  }
53
54
 
54
- return new Response(await model.getPreviewStructure({ allLanguages: true }));
55
+ return new Response(await EmailPreviewService.getPreviewStructure(model, { allLanguages: true }));
55
56
  }
56
57
  }
@@ -537,7 +537,7 @@ describe('Endpoint.GetUserEmails', () => {
537
537
  expect(unsubscribeUrlReplacement!.value).toMatch(/^https:\/\//); // Should still be a valid URL
538
538
  expect(unsubscribeUrlReplacement!.value).not.toContain('secret-token-12345'); // Original sensitive token should be gone
539
539
 
540
- // This tests that Email.getStructureForUser properly handles sensitive data by:
540
+ // This tests that EmailPreviewService.getStructureForUser properly handles sensitive data by:
541
541
  // 1. Removing original sensitive replacements from other users' data
542
542
  // 2. Creating fresh, appropriate replacements for the current viewer
543
543
  // 3. Ensuring web safety of all replacement values
@@ -11,6 +11,7 @@ import type { Language } from '@stamhoofd/types/Language';
11
11
  import type { SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
12
12
  import { applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
13
13
  import { Context } from '../../../helpers/Context.js';
14
+ import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
14
15
  import { emailFilterCompilers, userEmailFilterCompilers } from '../../../sql-filters/emails.js';
15
16
  import { emailSorters } from '../../../sql-sorters/emails.js';
16
17
 
@@ -135,7 +136,7 @@ export class GetUserEmailsEndpoint extends Endpoint<Params, Query, Body, Respons
135
136
  }
136
137
 
137
138
  return new PaginatedResponse<EmailWithRecipients[], LimitedFilteredRequest>({
138
- results: await Promise.all(emails.map(email => email.getStructureForUser(user, memberIds, options))),
139
+ results: await Promise.all(emails.map(email => EmailPreviewService.getStructureForUser(email, user, memberIds, options))),
139
140
  next,
140
141
  });
141
142
  }
@@ -8,6 +8,7 @@ import type { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding'
8
8
  import { patchObject } from '@simonbackx/simple-encoding';
9
9
  import { SimpleError } from '@simonbackx/simple-errors';
10
10
  import { Context } from '../../../helpers/Context.js';
11
+ import { EmailPreviewService } from '../../../services/EmailPreviewService.js';
11
12
 
12
13
  type Params = { id: string };
13
14
  type Query = undefined;
@@ -211,6 +212,6 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
211
212
  await model.queueForSending();
212
213
  }
213
214
 
214
- return new Response(await model.getPreviewStructure({ allLanguages: true }));
215
+ return new Response(await EmailPreviewService.getPreviewStructure(model, { allLanguages: true }));
215
216
  }
216
217
  }
@@ -6,7 +6,7 @@ import type { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
6
6
  import { ArchiverWriterAdapter, exportToExcel, XlsxWriter } from '@stamhoofd/excel-writer';
7
7
  import { Platform, RateLimiter, sendEmailTemplate } from '@stamhoofd/models';
8
8
  import { QueueHandler } from '@stamhoofd/queues';
9
- import type { ExcelExportType, IPaginatedResponse, LimitedFilteredRequest } from '@stamhoofd/structures';
9
+ import type { ExcelExportType, IPaginatedResponse, LimitedFilteredRequest, Platform as PlatformStruct } from '@stamhoofd/structures';
10
10
  import { EmailTemplateType, ExcelExportRequest, ExcelExportResponse, Replacement, Version } from '@stamhoofd/structures';
11
11
  import { sleep } from '@stamhoofd/utility';
12
12
  import { Context } from '../../../helpers/Context.js';
@@ -20,7 +20,12 @@ type ResponseBody = ExcelExportResponse;
20
20
 
21
21
  type ExcelExporter<T> = {
22
22
  fetch(request: LimitedFilteredRequest): Promise<IPaginatedResponse<T[], LimitedFilteredRequest>>;
23
- sheets: XlsxTransformerSheet<T, unknown>[];
23
+
24
+ /**
25
+ * The sheet definitions depend on the platform: they are built per export request instead of
26
+ * once at module load, so the callbacks never have to read a process wide platform singleton.
27
+ */
28
+ getSheets(platform: PlatformStruct): XlsxTransformerSheet<T, unknown>[];
24
29
  };
25
30
 
26
31
  export const limiter = new RateLimiter({
@@ -86,8 +91,6 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
86
91
  limiter.track(user.id, 1);
87
92
  let sendEmail = false;
88
93
 
89
- await Platform.getSharedStruct();
90
-
91
94
  const result = await Promise.race([
92
95
  this.job(loader, request.body, request.params.type, user.id).then(async (url: string) => {
93
96
  if (sendEmail) {
@@ -146,6 +149,7 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
146
149
  // Estimate how long it will take.
147
150
  // If too long, we'll schedule it and write it to Digitalocean Spaces
148
151
  // Otherwise we'll just return the file directly
152
+ const platform = await Platform.getSharedStruct();
149
153
  const { file, stream } = await FileCache.getWriteStream('.xlsx');
150
154
 
151
155
  const zipWriterAdapter = new ArchiverWriterAdapter(stream);
@@ -155,7 +159,7 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
155
159
  request.filter.limit = STAMHOOFD.environment === 'development' ? 1000 : 100; // in development, we need to check if total count matches and pagination is working correctly
156
160
 
157
161
  await exportToExcel({
158
- definitions: loader.sheets,
162
+ definitions: loader.getSheets(platform),
159
163
  writer,
160
164
  dataGenerator: fetchToAsyncIterator(request.filter, loader, { signFiles: true }),
161
165
  filter: request.workbookFilter,