@stamhoofd/backend 2.91.0 → 2.93.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.
- package/package.json +10 -10
- package/src/audit-logs/EmailLogger.ts +6 -6
- package/src/crons/amazon-ses.ts +100 -4
- package/src/crons/balance-emails.ts +1 -1
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -0
- package/src/email-recipient-loaders/receivable-balances.ts +3 -1
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +37 -7
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +205 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +81 -26
- package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +47 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +225 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +164 -0
- package/src/endpoints/global/email-recipients/helpers/validateEmailRecipientFilter.ts +64 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +5 -1
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +19 -1
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
- package/src/helpers/AdminPermissionChecker.ts +81 -5
- package/src/helpers/EmailResumer.ts +2 -2
- package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
- package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
- package/src/seeds/1755790070-fill-email-recipient-errors.ts +96 -0
- package/src/seeds/1755876819-remove-duplicate-members.ts +145 -0
- package/src/seeds/1756115432-remove-old-drafts.ts +16 -0
- package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +30 -0
- package/src/services/uitpas/UitpasService.ts +71 -2
- package/src/services/uitpas/checkUitpasNumbers.ts +1 -0
- package/src/sql-filters/email-recipients.ts +59 -0
- package/src/sql-filters/emails.ts +95 -0
- package/src/sql-filters/members.ts +42 -1
- package/src/sql-filters/registration-periods.ts +5 -0
- package/src/sql-sorters/email-recipients.ts +69 -0
- package/src/sql-sorters/emails.ts +47 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
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';
|
|
3
|
+
import { AccessRight, EmailStatus, Email as EmailStruct, OrganizationEmail, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
|
|
4
|
+
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
5
5
|
import { testServer } from '../../../../tests/helpers/TestServer';
|
|
6
6
|
import { PatchEmailEndpoint } from './PatchEmailEndpoint';
|
|
7
|
+
import { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
|
|
7
8
|
|
|
8
9
|
const baseUrl = `/v${Version}/email`;
|
|
9
10
|
|
|
@@ -13,8 +14,13 @@ describe('Endpoint.PatchEmailEndpoint', () => {
|
|
|
13
14
|
let organization: Organization;
|
|
14
15
|
let token: Token;
|
|
15
16
|
let user: User;
|
|
17
|
+
let sender: OrganizationEmail;
|
|
18
|
+
let sender2: OrganizationEmail;
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
let token2: Token;
|
|
21
|
+
let user2: User;
|
|
22
|
+
|
|
23
|
+
const patchEmail = async (email: AutoEncoderPatchType<EmailStruct>, token: Token, organization?: Organization) => {
|
|
18
24
|
const id = email.id;
|
|
19
25
|
const request = Request.buildJson('PATCH', `${baseUrl}/${id}`, organization?.getApiHost(), email);
|
|
20
26
|
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
@@ -34,15 +40,360 @@ describe('Endpoint.PatchEmailEndpoint', () => {
|
|
|
34
40
|
organization = await new OrganizationFactory({ period })
|
|
35
41
|
.create();
|
|
36
42
|
|
|
43
|
+
sender = OrganizationEmail.create({
|
|
44
|
+
email: 'groepsleiding@voorbeeld.com',
|
|
45
|
+
name: 'Groepsleiding',
|
|
46
|
+
});
|
|
47
|
+
sender2 = OrganizationEmail.create({
|
|
48
|
+
email: 'kapoenen@voorbeeld.com',
|
|
49
|
+
name: 'Kapoenen',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
organization.privateMeta.emails.push(sender);
|
|
53
|
+
organization.privateMeta.emails.push(sender2);
|
|
54
|
+
await organization.save();
|
|
55
|
+
|
|
37
56
|
user = await new UserFactory({
|
|
38
57
|
organization,
|
|
39
58
|
permissions: Permissions.create({
|
|
40
|
-
level: PermissionLevel.
|
|
59
|
+
level: PermissionLevel.None,
|
|
60
|
+
resources: new Map([
|
|
61
|
+
[PermissionsResourceType.Senders, new Map([[sender.id, ResourcePermissions.create({
|
|
62
|
+
resourceName: sender.name!,
|
|
63
|
+
level: PermissionLevel.None,
|
|
64
|
+
accessRights: [AccessRight.SendMessages],
|
|
65
|
+
})]])],
|
|
66
|
+
]),
|
|
41
67
|
}),
|
|
42
68
|
})
|
|
43
69
|
.create();
|
|
44
70
|
|
|
45
71
|
token = await Token.createToken(user);
|
|
72
|
+
|
|
73
|
+
user2 = await new UserFactory({
|
|
74
|
+
organization,
|
|
75
|
+
permissions: Permissions.create({
|
|
76
|
+
level: PermissionLevel.None,
|
|
77
|
+
resources: new Map([
|
|
78
|
+
[PermissionsResourceType.Senders, new Map([[sender2.id, ResourcePermissions.create({
|
|
79
|
+
resourceName: sender.name!,
|
|
80
|
+
level: PermissionLevel.Write,
|
|
81
|
+
accessRights: [AccessRight.SendMessages],
|
|
82
|
+
})]])],
|
|
83
|
+
]),
|
|
84
|
+
}),
|
|
85
|
+
})
|
|
86
|
+
.create();
|
|
87
|
+
|
|
88
|
+
token2 = await Token.createToken(user2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('Should throw for invalid senderId', async () => {
|
|
92
|
+
const email = new Email();
|
|
93
|
+
email.subject = 'test subject';
|
|
94
|
+
email.status = EmailStatus.Draft;
|
|
95
|
+
email.text = 'test email {{unsubscribeUrl}}';
|
|
96
|
+
email.html = `<!DOCTYPE html>
|
|
97
|
+
<html>
|
|
98
|
+
|
|
99
|
+
<head>
|
|
100
|
+
<meta charset="utf-8" />
|
|
101
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
102
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
103
|
+
<title>test</title>
|
|
104
|
+
</head>
|
|
105
|
+
|
|
106
|
+
<body>
|
|
107
|
+
<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
|
|
108
|
+
|
|
109
|
+
{{unsubscribeUrl}}
|
|
110
|
+
</body>
|
|
111
|
+
|
|
112
|
+
</html>`;
|
|
113
|
+
email.json = {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
text: 'test email',
|
|
119
|
+
type: 'text',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
type: 'paragraph',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
type: 'doc',
|
|
126
|
+
};
|
|
127
|
+
email.userId = user.id;
|
|
128
|
+
email.organizationId = organization.id;
|
|
129
|
+
|
|
130
|
+
await email.save();
|
|
131
|
+
|
|
132
|
+
const body = EmailStruct.patch({
|
|
133
|
+
id: email.id,
|
|
134
|
+
status: EmailStatus.Sending,
|
|
135
|
+
senderId: 'invalid-sender-id',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await expect(async () => await patchEmail(body, token, organization))
|
|
139
|
+
.rejects
|
|
140
|
+
.toThrow(STExpect.errorWithCode('invalid_sender'));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('Should throw when patching other users email without sender id', async () => {
|
|
144
|
+
const email = new Email();
|
|
145
|
+
email.subject = 'test subject';
|
|
146
|
+
email.status = EmailStatus.Draft;
|
|
147
|
+
email.text = 'test email {{unsubscribeUrl}}';
|
|
148
|
+
email.html = `<!DOCTYPE html>
|
|
149
|
+
<html>
|
|
150
|
+
|
|
151
|
+
<head>
|
|
152
|
+
<meta charset="utf-8" />
|
|
153
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
154
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
155
|
+
<title>test</title>
|
|
156
|
+
</head>
|
|
157
|
+
|
|
158
|
+
<body>
|
|
159
|
+
<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
|
|
160
|
+
|
|
161
|
+
{{unsubscribeUrl}}
|
|
162
|
+
</body>
|
|
163
|
+
|
|
164
|
+
</html>`;
|
|
165
|
+
email.json = {
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
text: 'test email',
|
|
171
|
+
type: 'text',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
type: 'paragraph',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
type: 'doc',
|
|
178
|
+
};
|
|
179
|
+
email.userId = user2.id;
|
|
180
|
+
email.organizationId = organization.id;
|
|
181
|
+
|
|
182
|
+
await email.save();
|
|
183
|
+
|
|
184
|
+
const body = EmailStruct.patch({
|
|
185
|
+
id: email.id,
|
|
186
|
+
subject: 'new subject',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await expect(async () => await patchEmail(body, token, organization))
|
|
190
|
+
.rejects
|
|
191
|
+
.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('Should throw when patching other users email even when matching sender', async () => {
|
|
195
|
+
const email = new Email();
|
|
196
|
+
email.subject = 'test subject';
|
|
197
|
+
email.status = EmailStatus.Draft;
|
|
198
|
+
email.text = 'test email {{unsubscribeUrl}}';
|
|
199
|
+
email.html = `<!DOCTYPE html>
|
|
200
|
+
<html>
|
|
201
|
+
|
|
202
|
+
<head>
|
|
203
|
+
<meta charset="utf-8" />
|
|
204
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
205
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
206
|
+
<title>test</title>
|
|
207
|
+
</head>
|
|
208
|
+
|
|
209
|
+
<body>
|
|
210
|
+
<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
|
|
211
|
+
|
|
212
|
+
{{unsubscribeUrl}}
|
|
213
|
+
</body>
|
|
214
|
+
|
|
215
|
+
</html>`;
|
|
216
|
+
email.json = {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
text: 'test email',
|
|
222
|
+
type: 'text',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
type: 'paragraph',
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
type: 'doc',
|
|
229
|
+
};
|
|
230
|
+
email.userId = user2.id;
|
|
231
|
+
email.organizationId = organization.id;
|
|
232
|
+
email.senderId = sender.id;
|
|
233
|
+
|
|
234
|
+
await email.save();
|
|
235
|
+
|
|
236
|
+
const body = EmailStruct.patch({
|
|
237
|
+
id: email.id,
|
|
238
|
+
subject: 'new subject',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await expect(async () => await patchEmail(body, token, organization))
|
|
242
|
+
.rejects
|
|
243
|
+
.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('Should not throw when patching other users email when having write access to sender', async () => {
|
|
247
|
+
const email = new Email();
|
|
248
|
+
email.subject = 'test subject';
|
|
249
|
+
email.status = EmailStatus.Draft;
|
|
250
|
+
email.text = 'test email {{unsubscribeUrl}}';
|
|
251
|
+
email.html = `<!DOCTYPE html>
|
|
252
|
+
<html>
|
|
253
|
+
|
|
254
|
+
<head>
|
|
255
|
+
<meta charset="utf-8" />
|
|
256
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
257
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
258
|
+
<title>test</title>
|
|
259
|
+
</head>
|
|
260
|
+
|
|
261
|
+
<body>
|
|
262
|
+
<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
|
|
263
|
+
|
|
264
|
+
{{unsubscribeUrl}}
|
|
265
|
+
</body>
|
|
266
|
+
|
|
267
|
+
</html>`;
|
|
268
|
+
email.json = {
|
|
269
|
+
content: [
|
|
270
|
+
{
|
|
271
|
+
content: [
|
|
272
|
+
{
|
|
273
|
+
text: 'test email',
|
|
274
|
+
type: 'text',
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
type: 'paragraph',
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
type: 'doc',
|
|
281
|
+
};
|
|
282
|
+
email.userId = user.id; // other user
|
|
283
|
+
email.organizationId = organization.id;
|
|
284
|
+
email.senderId = sender2.id; // write access to this sender
|
|
285
|
+
|
|
286
|
+
await email.save();
|
|
287
|
+
|
|
288
|
+
const body = EmailStruct.patch({
|
|
289
|
+
id: email.id,
|
|
290
|
+
subject: 'new subject',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await expect(patchEmail(body, token2, organization)).toResolve();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('Should throw when patching if no permission for sender', async () => {
|
|
297
|
+
const email = new Email();
|
|
298
|
+
email.subject = 'test subject';
|
|
299
|
+
email.status = EmailStatus.Draft;
|
|
300
|
+
email.text = 'test email {{unsubscribeUrl}}';
|
|
301
|
+
email.html = `<!DOCTYPE html>
|
|
302
|
+
<html>
|
|
303
|
+
|
|
304
|
+
<head>
|
|
305
|
+
<meta charset="utf-8" />
|
|
306
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
307
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
308
|
+
<title>test</title>
|
|
309
|
+
</head>
|
|
310
|
+
|
|
311
|
+
<body>
|
|
312
|
+
<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
|
|
313
|
+
|
|
314
|
+
{{unsubscribeUrl}}
|
|
315
|
+
</body>
|
|
316
|
+
|
|
317
|
+
</html>`;
|
|
318
|
+
email.json = {
|
|
319
|
+
content: [
|
|
320
|
+
{
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
323
|
+
text: 'test email',
|
|
324
|
+
type: 'text',
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
type: 'paragraph',
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
type: 'doc',
|
|
331
|
+
};
|
|
332
|
+
email.userId = user.id;
|
|
333
|
+
email.organizationId = organization.id;
|
|
334
|
+
|
|
335
|
+
await email.save();
|
|
336
|
+
|
|
337
|
+
const body = EmailStruct.patch({
|
|
338
|
+
id: email.id,
|
|
339
|
+
senderId: sender2.id,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await expect(async () => await patchEmail(body, token, organization))
|
|
343
|
+
.rejects
|
|
344
|
+
.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('Should throw when sending if no permission for sender', async () => {
|
|
348
|
+
const email = new Email();
|
|
349
|
+
email.subject = 'test subject';
|
|
350
|
+
email.status = EmailStatus.Draft;
|
|
351
|
+
email.text = 'test email {{unsubscribeUrl}}';
|
|
352
|
+
email.html = `<!DOCTYPE html>
|
|
353
|
+
<html>
|
|
354
|
+
|
|
355
|
+
<head>
|
|
356
|
+
<meta charset="utf-8" />
|
|
357
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
358
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
359
|
+
<title>test</title>
|
|
360
|
+
</head>
|
|
361
|
+
|
|
362
|
+
<body>
|
|
363
|
+
<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
|
|
364
|
+
|
|
365
|
+
{{unsubscribeUrl}}
|
|
366
|
+
</body>
|
|
367
|
+
|
|
368
|
+
</html>`;
|
|
369
|
+
email.json = {
|
|
370
|
+
content: [
|
|
371
|
+
{
|
|
372
|
+
content: [
|
|
373
|
+
{
|
|
374
|
+
text: 'test email',
|
|
375
|
+
type: 'text',
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
type: 'paragraph',
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
type: 'doc',
|
|
382
|
+
};
|
|
383
|
+
email.userId = user.id;
|
|
384
|
+
email.organizationId = organization.id;
|
|
385
|
+
email.senderId = sender2.id;
|
|
386
|
+
|
|
387
|
+
await email.save();
|
|
388
|
+
|
|
389
|
+
const body = EmailStruct.patch({
|
|
390
|
+
id: email.id,
|
|
391
|
+
status: EmailStatus.Sending,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await expect(async () => await patchEmail(body, token, organization))
|
|
395
|
+
.rejects
|
|
396
|
+
.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
46
397
|
});
|
|
47
398
|
|
|
48
399
|
test('Should throw error if no unsubscribe button in email html', async () => {
|
|
@@ -84,11 +435,11 @@ describe('Endpoint.PatchEmailEndpoint', () => {
|
|
|
84
435
|
|
|
85
436
|
await email.save();
|
|
86
437
|
|
|
87
|
-
const body = EmailStruct.
|
|
438
|
+
const body = EmailStruct.patch({ id: email.id, senderId: sender.id, status: EmailStatus.Sending });
|
|
88
439
|
|
|
89
440
|
await expect(async () => await patchEmail(body, token, organization))
|
|
90
441
|
.rejects
|
|
91
|
-
.toThrow('
|
|
442
|
+
.toThrow(STExpect.errorWithCode('missing_unsubscribe_button'));
|
|
92
443
|
});
|
|
93
444
|
|
|
94
445
|
test('Should throw error if no unsubscribe button in email text', async () => {
|
|
@@ -130,10 +481,55 @@ describe('Endpoint.PatchEmailEndpoint', () => {
|
|
|
130
481
|
|
|
131
482
|
await email.save();
|
|
132
483
|
|
|
133
|
-
const body = EmailStruct.
|
|
484
|
+
const body = EmailStruct.patch({ id: email.id, senderId: sender.id, status: EmailStatus.Sending });
|
|
134
485
|
|
|
135
486
|
await expect(async () => await patchEmail(body, token, organization))
|
|
136
487
|
.rejects
|
|
137
|
-
.toThrow('
|
|
488
|
+
.toThrow(STExpect.errorWithCode('missing_unsubscribe_button'));
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test('Can send an email', async () => {
|
|
492
|
+
const email = new Email();
|
|
493
|
+
email.subject = 'test subject';
|
|
494
|
+
email.status = EmailStatus.Draft;
|
|
495
|
+
email.text = 'test email {{unsubscribeUrl}}';
|
|
496
|
+
email.html = `<!DOCTYPE html>
|
|
497
|
+
<html>
|
|
498
|
+
|
|
499
|
+
<head>
|
|
500
|
+
<meta charset="utf-8" />
|
|
501
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
502
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
503
|
+
<title>test</title>
|
|
504
|
+
</head>
|
|
505
|
+
|
|
506
|
+
<body>
|
|
507
|
+
<p style="margin: 0; padding: 0; line-height: 1.4;">test email {{unsubscribeUrl}}</p>
|
|
508
|
+
</body>
|
|
509
|
+
|
|
510
|
+
</html>`;
|
|
511
|
+
email.json = {
|
|
512
|
+
content: [
|
|
513
|
+
{
|
|
514
|
+
content: [
|
|
515
|
+
{
|
|
516
|
+
text: 'test email',
|
|
517
|
+
type: 'text',
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
type: 'paragraph',
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
type: 'doc',
|
|
524
|
+
};
|
|
525
|
+
email.userId = user.id;
|
|
526
|
+
email.organizationId = organization.id;
|
|
527
|
+
email.senderId = sender.id;
|
|
528
|
+
|
|
529
|
+
await email.save();
|
|
530
|
+
|
|
531
|
+
const body = EmailStruct.patch({ id: email.id, status: EmailStatus.Sending });
|
|
532
|
+
|
|
533
|
+
await expect(patchEmail(body, token, organization)).toResolve();
|
|
138
534
|
});
|
|
139
535
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { Email } from '@stamhoofd/models';
|
|
3
|
-
import { EmailPreview, EmailStatus, Email as EmailStruct } from '@stamhoofd/structures';
|
|
2
|
+
import { Email, Platform } from '@stamhoofd/models';
|
|
3
|
+
import { EmailPreview, EmailRecipientsStatus, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { AutoEncoderPatchType, Decoder, patchObject } from '@simonbackx/simple-encoding';
|
|
6
6
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
@@ -29,14 +29,15 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
29
29
|
|
|
30
30
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
31
31
|
const organization = await Context.setOptionalOrganizationScope();
|
|
32
|
-
|
|
32
|
+
await Context.authenticate();
|
|
33
33
|
|
|
34
|
-
if (!Context.auth.
|
|
34
|
+
if (!await Context.auth.canReadEmails(organization)) {
|
|
35
|
+
// Fast fail before query
|
|
35
36
|
throw Context.auth.error();
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const model = await Email.getByID(request.params.id);
|
|
39
|
-
if (!model ||
|
|
40
|
+
if (!model || (model.organizationId !== (organization?.id ?? null))) {
|
|
40
41
|
throw new SimpleError({
|
|
41
42
|
code: 'not_found',
|
|
42
43
|
human: 'Email not found',
|
|
@@ -45,6 +46,10 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
if (!await Context.auth.canAccessEmail(model, PermissionLevel.Write)) {
|
|
50
|
+
throw Context.auth.error();
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
if (model.status !== EmailStatus.Draft) {
|
|
49
54
|
throw new SimpleError({
|
|
50
55
|
code: 'not_draft',
|
|
@@ -60,6 +65,47 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
60
65
|
model.subject = request.body.subject;
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
if (request.body.senderId !== undefined) {
|
|
69
|
+
const list = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
|
|
70
|
+
const sender = list.find(e => e.id === request.body.senderId);
|
|
71
|
+
if (sender) {
|
|
72
|
+
if (!await Context.auth.canSendEmailsFrom(organization, sender.id)) {
|
|
73
|
+
throw Context.auth.error({
|
|
74
|
+
message: 'Cannot send emails from this sender',
|
|
75
|
+
human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
model.senderId = sender.id;
|
|
79
|
+
model.fromAddress = sender.email;
|
|
80
|
+
model.fromName = sender.name;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
throw new SimpleError({
|
|
84
|
+
code: 'invalid_sender',
|
|
85
|
+
human: 'Sender not found',
|
|
86
|
+
message: $t(`94adb4e0-2ef1-4ee8-9f02-5a76efa51c1d`),
|
|
87
|
+
statusCode: 400,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (model.senderId) {
|
|
92
|
+
// Update data, to avoid sending from an old address
|
|
93
|
+
const list = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
|
|
94
|
+
const sender = list.find(e => e.id === model.senderId);
|
|
95
|
+
if (sender) {
|
|
96
|
+
model.fromAddress = sender.email;
|
|
97
|
+
model.fromName = sender.name;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
throw new SimpleError({
|
|
101
|
+
code: 'invalid_sender',
|
|
102
|
+
human: 'Sender not found',
|
|
103
|
+
message: $t(`f08cccb3-faf9-473f-b729-16120fadec9c`),
|
|
104
|
+
statusCode: 400,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
63
109
|
if (request.body.html !== undefined) {
|
|
64
110
|
model.html = request.body.html;
|
|
65
111
|
}
|
|
@@ -72,15 +118,16 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
72
118
|
model.json = request.body.json;
|
|
73
119
|
}
|
|
74
120
|
|
|
75
|
-
if (request.body.fromAddress !== undefined) {
|
|
76
|
-
model.fromAddress = request.body.fromAddress;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (request.body.fromName !== undefined) {
|
|
80
|
-
model.fromName = request.body.fromName;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
121
|
if (request.body.recipientFilter) {
|
|
122
|
+
if (model.status !== EmailStatus.Draft) {
|
|
123
|
+
throw new SimpleError({
|
|
124
|
+
code: 'not_draft',
|
|
125
|
+
human: 'Email is not a draft',
|
|
126
|
+
message: $t(`Je kan de ontvangerslijst alleen aanpassen als de e-mail nog een concept is`),
|
|
127
|
+
statusCode: 400,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
84
131
|
model.recipientFilter = patchObject(model.recipientFilter, request.body.recipientFilter);
|
|
85
132
|
rebuild = true;
|
|
86
133
|
}
|
|
@@ -95,26 +142,33 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
95
142
|
|
|
96
143
|
if (rebuild) {
|
|
97
144
|
await model.buildExampleRecipient();
|
|
98
|
-
model.updateCount();
|
|
99
145
|
|
|
100
146
|
// Force null - because we have stale data
|
|
101
|
-
model.
|
|
147
|
+
model.emailRecipientsCount = null;
|
|
148
|
+
model.updateCount();
|
|
102
149
|
}
|
|
103
150
|
|
|
104
|
-
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
|
|
151
|
+
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
|
|
152
|
+
if (!await Context.auth.canSendEmail(model)) {
|
|
153
|
+
throw Context.auth.error({
|
|
154
|
+
message: 'Cannot send emails from this sender',
|
|
155
|
+
human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
105
159
|
model.throwIfNotReadyToSend();
|
|
106
160
|
|
|
107
161
|
const replacement = '{{unsubscribeUrl}}';
|
|
108
|
-
|
|
162
|
+
|
|
109
163
|
if (model.html) {
|
|
110
164
|
// Check email contains an unsubscribe button
|
|
111
165
|
if (!model.html.includes(replacement)) {
|
|
112
166
|
throw new SimpleError({
|
|
113
|
-
code:
|
|
114
|
-
message:
|
|
167
|
+
code: 'missing_unsubscribe_button',
|
|
168
|
+
message: 'Missing unsubscribe button',
|
|
115
169
|
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
116
|
-
field:
|
|
117
|
-
})
|
|
170
|
+
field: 'html',
|
|
171
|
+
});
|
|
118
172
|
}
|
|
119
173
|
}
|
|
120
174
|
|
|
@@ -122,15 +176,16 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
122
176
|
// Check email contains an unsubscribe button
|
|
123
177
|
if (!model.text.includes(replacement)) {
|
|
124
178
|
throw new SimpleError({
|
|
125
|
-
code:
|
|
126
|
-
message:
|
|
179
|
+
code: 'missing_unsubscribe_button',
|
|
180
|
+
message: 'Missing unsubscribe button',
|
|
127
181
|
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
128
|
-
field:
|
|
129
|
-
})
|
|
182
|
+
field: 'text',
|
|
183
|
+
});
|
|
130
184
|
}
|
|
131
185
|
}
|
|
132
186
|
|
|
133
|
-
|
|
187
|
+
// Preview the sending status
|
|
188
|
+
await model.queueForSending();
|
|
134
189
|
}
|
|
135
190
|
|
|
136
191
|
return new Response(await model.getPreviewStructure());
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
import { Context } from '../../../helpers/Context';
|
|
6
|
+
import { GetEmailRecipientsEndpoint } from './GetEmailRecipientsEndpoint';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = CountFilteredRequest;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = CountResponse;
|
|
12
|
+
|
|
13
|
+
export class GetEmailRecipientsCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
+
queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
|
|
15
|
+
|
|
16
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
+
if (request.method !== 'GET') {
|
|
18
|
+
return [false];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const params = Endpoint.parseParameters(request.url, '/email-recipients/count', {});
|
|
22
|
+
|
|
23
|
+
if (params) {
|
|
24
|
+
return [true, params as Params];
|
|
25
|
+
}
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
+
const organization = await Context.setOptionalOrganizationScope();
|
|
31
|
+
await Context.authenticate();
|
|
32
|
+
|
|
33
|
+
if (!await Context.auth.canReadEmails(organization)) {
|
|
34
|
+
throw Context.auth.error();
|
|
35
|
+
}
|
|
36
|
+
const query = await GetEmailRecipientsEndpoint.buildQuery(request.query);
|
|
37
|
+
|
|
38
|
+
const count = await query
|
|
39
|
+
.count();
|
|
40
|
+
|
|
41
|
+
return new Response(
|
|
42
|
+
CountResponse.create({
|
|
43
|
+
count,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|