@stamhoofd/backend 2.91.0 → 2.92.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 +4 -4
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +29 -5
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +207 -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 +67 -22
- 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/seeds/1752848560-groups-registration-periods.ts +768 -0
- package/src/seeds/1755181288-remove-duplicate-members.ts +145 -0
- package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
- package/src/services/uitpas/UitpasService.ts +71 -2
- package/src/services/uitpas/checkUitpasNumbers.ts +1 -0
- package/src/sql-filters/emails.ts +65 -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, 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,14 +118,6 @@ 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) {
|
|
84
122
|
model.recipientFilter = patchObject(model.recipientFilter, request.body.recipientFilter);
|
|
85
123
|
rebuild = true;
|
|
@@ -102,19 +140,26 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
102
140
|
}
|
|
103
141
|
|
|
104
142
|
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
|
|
143
|
+
if (!await Context.auth.canSendEmail(model)) {
|
|
144
|
+
throw Context.auth.error({
|
|
145
|
+
message: 'Cannot send emails from this sender',
|
|
146
|
+
human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
105
150
|
model.throwIfNotReadyToSend();
|
|
106
151
|
|
|
107
152
|
const replacement = '{{unsubscribeUrl}}';
|
|
108
|
-
|
|
153
|
+
|
|
109
154
|
if (model.html) {
|
|
110
155
|
// Check email contains an unsubscribe button
|
|
111
156
|
if (!model.html.includes(replacement)) {
|
|
112
157
|
throw new SimpleError({
|
|
113
|
-
code:
|
|
114
|
-
message:
|
|
158
|
+
code: 'missing_unsubscribe_button',
|
|
159
|
+
message: 'Missing unsubscribe button',
|
|
115
160
|
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
116
|
-
field:
|
|
117
|
-
})
|
|
161
|
+
field: 'html',
|
|
162
|
+
});
|
|
118
163
|
}
|
|
119
164
|
}
|
|
120
165
|
|
|
@@ -122,11 +167,11 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
122
167
|
// Check email contains an unsubscribe button
|
|
123
168
|
if (!model.text.includes(replacement)) {
|
|
124
169
|
throw new SimpleError({
|
|
125
|
-
code:
|
|
126
|
-
message:
|
|
170
|
+
code: 'missing_unsubscribe_button',
|
|
171
|
+
message: 'Missing unsubscribe button',
|
|
127
172
|
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
128
|
-
field:
|
|
129
|
-
})
|
|
173
|
+
field: 'text',
|
|
174
|
+
});
|
|
130
175
|
}
|
|
131
176
|
}
|
|
132
177
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
2
|
import { BalanceItem, Order, Webshop } from '@stamhoofd/models';
|
|
4
3
|
import { PermissionLevel } from '@stamhoofd/structures';
|
|
5
4
|
|
|
6
5
|
import { Context } from '../../../../helpers/Context';
|
|
6
|
+
import { UitpasService } from '../../../../services/uitpas/UitpasService';
|
|
7
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
8
|
|
|
8
9
|
type Params = { id: string };
|
|
9
10
|
type Query = undefined;
|
|
@@ -42,6 +43,14 @@ export class DeleteWebshopEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
42
43
|
throw Context.auth.notFoundOrNoAccess();
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
if (await UitpasService.areThereRegisteredTicketSales(webshop.id)) {
|
|
47
|
+
throw new SimpleError({
|
|
48
|
+
code: 'webshop_has_registered_ticket_sales',
|
|
49
|
+
message: `Webshop ${webshop.id} has registered ticket sales`,
|
|
50
|
+
human: $t(`0b3d6ea1-a70b-428c-9ba4-cc0c327ed415`),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
const orders = await Order.where({ webshopId: webshop.id });
|
|
46
55
|
await BalanceItem.deleteForDeletedOrders(orders.map(o => o.id));
|
|
47
56
|
await webshop.delete();
|
|
@@ -7,6 +7,7 @@ import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceIt
|
|
|
7
7
|
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
9
|
import { AuditLogService } from '../../../../services/AuditLogService';
|
|
10
|
+
import { shouldReserveUitpasNumbers, UitpasService } from '../../../../services/uitpas/UitpasService';
|
|
10
11
|
|
|
11
12
|
type Params = { id: string };
|
|
12
13
|
type Query = undefined;
|
|
@@ -132,6 +133,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
132
133
|
|
|
133
134
|
// TODO: validate before updating stock
|
|
134
135
|
order.data.validate(webshopGetter.struct, organization.meta, request.i18n, true);
|
|
136
|
+
order.data.cart = await UitpasService.validateCart(organization.id, webshop.id, order.data.cart);
|
|
135
137
|
|
|
136
138
|
try {
|
|
137
139
|
await order.updateStock(null, true);
|
|
@@ -230,6 +232,8 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
230
232
|
const previousToPay = model.totalToPay;
|
|
231
233
|
const previousStatus = model.status;
|
|
232
234
|
|
|
235
|
+
const shouldReserveBefore = shouldReserveUitpasNumbers(model.status);
|
|
236
|
+
|
|
233
237
|
model.status = patch.status ?? model.status;
|
|
234
238
|
|
|
235
239
|
// For now, we don't invalidate tickets, because they will get invalidated at scan time (the order status is checked)
|
|
@@ -240,13 +244,16 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
240
244
|
const previousData = model.data.clone();
|
|
241
245
|
if (patch.data) {
|
|
242
246
|
model.data.patchOrPut(patch.data);
|
|
243
|
-
|
|
244
247
|
if (model.status !== OrderStatus.Deleted) {
|
|
245
248
|
// Make sure all data is up to date and validated (= possible corrections happen here too)
|
|
246
249
|
model.data.validate(webshopGetter.struct, organization.meta, request.i18n, true);
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
if ((patch.data || !shouldReserveBefore) && shouldReserveUitpasNumbers(model.status)) {
|
|
254
|
+
model.data.cart = await UitpasService.validateCart(organization.id, webshop.id, model.data.cart, model.id);
|
|
255
|
+
}
|
|
256
|
+
|
|
250
257
|
if (model.status === OrderStatus.Deleted) {
|
|
251
258
|
model.data.removePersonalData();
|
|
252
259
|
|