@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.
- package/package.json +23 -17
- package/src/crons/drip-emails.ts +2 -1
- package/src/crons.ts +4 -2
- package/src/email-recipient-loaders/payments.ts +3 -2
- package/src/endpoints/auth/CreateAdminEndpoint.ts +3 -2
- package/src/endpoints/auth/CreateTokenEndpoint.ts +2 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +3 -2
- package/src/endpoints/auth/PatchUserEndpoint.ts +6 -3
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +2 -1
- package/src/endpoints/auth/SignupEndpoint.ts +4 -2
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +2 -1
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -5
- package/src/endpoints/global/files/UploadFile.test.ts +206 -0
- package/src/endpoints/global/files/UploadFile.ts +31 -6
- package/src/endpoints/global/files/UploadImage.test.ts +177 -0
- package/src/endpoints/global/files/UploadImage.ts +23 -4
- package/src/endpoints/global/files/upload-security.test.ts +837 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +2 -1
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +5 -3
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
- package/src/excel-loaders/balance-items.ts +1 -1
- package/src/excel-loaders/event-notifications.ts +6 -7
- package/src/excel-loaders/members.test.ts +2 -2
- package/src/excel-loaders/members.ts +9 -10
- package/src/excel-loaders/organizations.ts +14 -20
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/platform-memberships.ts +7 -7
- package/src/excel-loaders/platform-sheets.test.ts +113 -0
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +9 -9
- package/src/helpers/AdminPermissionChecker.ts +1 -1
- package/src/helpers/AuthenticatedStructures.ts +12 -9
- package/src/helpers/MembershipCharger.ts +3 -2
- package/src/services/BalanceItemService.ts +2 -1
- package/src/services/DocumentRenderService.test.ts +229 -0
- package/src/services/DocumentRenderService.ts +180 -0
- package/src/services/EmailPreviewService.test.ts +300 -0
- package/src/services/EmailPreviewService.ts +239 -0
- package/src/services/FileSignService.test.ts +85 -0
- package/src/services/FileSignService.ts +23 -1
- package/src/services/OrderService.test.ts +308 -0
- package/src/services/OrderService.ts +214 -0
- package/src/services/OrganizationDNSService.test.ts +177 -0
- package/src/services/OrganizationDNSService.ts +282 -0
- package/src/services/OrganizationEmailService.test.ts +57 -0
- package/src/services/OrganizationEmailService.ts +211 -0
- package/src/services/PasswordForgotService.test.ts +99 -0
- package/src/services/PasswordForgotService.ts +38 -0
- package/src/services/PlatformMembershipService.test.ts +180 -0
- package/src/services/PlatformMembershipService.ts +281 -4
- package/src/services/RegistrationService.ts +3 -2
- package/src/services/STPackageService.test.ts +191 -0
- package/src/services/STPackageService.ts +114 -2
- package/src/services/VerificationCodeService.test.ts +71 -0
- package/src/services/VerificationCodeService.ts +100 -0
- package/tests/e2e/documents.test.ts +2 -1
- 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.
|
|
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.
|
|
62
|
-
"@stamhoofd/backend-i18n": "2.137.
|
|
63
|
-
"@stamhoofd/backend-middleware": "2.137.
|
|
64
|
-
"@stamhoofd/crons": "2.137.
|
|
65
|
-
"@stamhoofd/email": "2.137.
|
|
66
|
-
"@stamhoofd/excel-writer": "2.137.
|
|
67
|
-
"@stamhoofd/logging": "2.137.
|
|
68
|
-
"@stamhoofd/models": "2.137.
|
|
69
|
-
"@stamhoofd/object-differ": "2.137.
|
|
70
|
-
"@stamhoofd/queues": "2.137.
|
|
71
|
-
"@stamhoofd/sql": "2.137.
|
|
72
|
-
"@stamhoofd/structures": "2.137.
|
|
73
|
-
"@stamhoofd/types": "2.137.
|
|
74
|
-
"@stamhoofd/utility": "2.137.
|
|
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.
|
|
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": "
|
|
117
|
+
"gitHead": "52c8072d0294d8005c9d36a79219708ea148a357"
|
|
112
118
|
}
|
package/src/crons/drip-emails.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =>
|
|
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
|
|
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
|
|
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 =>
|
|
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
|
|
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
|
-
|
|
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.
|
|
162
|
+
definitions: loader.getSheets(platform),
|
|
159
163
|
writer,
|
|
160
164
|
dataGenerator: fetchToAsyncIterator(request.filter, loader, { signFiles: true }),
|
|
161
165
|
filter: request.workbookFilter,
|