@stamhoofd/backend 2.90.3 → 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/audit-logs/ModelLogger.ts +0 -1
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +20 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +30 -7
- 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/global/members/PatchOrganizationMembersEndpoint.ts +6 -4
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -7
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +112 -105
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -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/dashboard/webshops/SearchUitpasEventsEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
- package/src/helpers/AdminPermissionChecker.ts +81 -10
- package/src/helpers/FlagMomentCleanup.ts +13 -1
- package/src/helpers/GroupedThrottledQueue.ts +5 -3
- package/src/helpers/PeriodHelper.ts +10 -137
- package/src/helpers/SetupStepUpdater.ts +54 -7
- package/src/helpers/UitpasTokenRepository.ts +3 -3
- package/src/seeds/1750090030-records-configuration.ts +5 -1
- 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/BalanceItemService.ts +12 -7
- package/src/services/DocumentService.ts +0 -1
- package/src/services/RegistrationService.ts +30 -1
- package/src/services/uitpas/UitpasService.ts +72 -3
- package/src/services/uitpas/cancelTicketSales.ts +1 -1
- package/src/services/uitpas/checkPermissionsFor.ts +9 -9
- package/src/services/uitpas/checkUitpasNumbers.ts +3 -2
- package/src/services/uitpas/getSocialTariffForEvent.ts +4 -4
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +5 -5
- package/src/services/uitpas/registerTicketSales.ts +4 -4
- package/src/services/uitpas/searchUitpasEvents.ts +3 -3
- package/src/services/uitpas/searchUitpasOrganizers.ts +3 -3
- package/src/sql-filters/emails.ts +65 -0
- package/src/sql-filters/members.ts +1 -1
- package/src/sql-filters/organizations.ts +52 -0
- package/src/sql-sorters/emails.ts +47 -0
- package/tests/e2e/register.test.ts +1 -1
|
@@ -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
|
|
|
@@ -3,7 +3,7 @@ import { AutoEncoderPatchType, ConvertArrayToPatchableArray, Decoder, isEmptyPat
|
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { AuditLog, BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
|
|
6
|
-
import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType, EmergencyContact, GroupType, MemberDetails, MemberResponsibility, MembersBlob, MemberWithRegistrationsBlob, Parent, PermissionLevel } from '@stamhoofd/structures';
|
|
6
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType, EmergencyContact, GroupType, MemberDetails, MemberResponsibility, MembersBlob, MemberWithRegistrationsBlob, Parent, PermissionLevel, SetupStepType } from '@stamhoofd/structures';
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { Email } from '@stamhoofd/email';
|
|
@@ -317,7 +317,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
317
317
|
throw new SimpleError({
|
|
318
318
|
code: 'invalid_field',
|
|
319
319
|
message: 'Invalid organization',
|
|
320
|
-
human:
|
|
320
|
+
human: platformResponsibility ? $t('ec6a555e-6bb1-4b5f-b17e-38eaa8a478b5') : $t('d41cdbe3-57e3-4a2e-83bc-cb9e65c9c840'),
|
|
321
321
|
});
|
|
322
322
|
}
|
|
323
323
|
|
|
@@ -688,7 +688,9 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
688
688
|
}
|
|
689
689
|
|
|
690
690
|
if (shouldUpdateSetupSteps && organization) {
|
|
691
|
-
SetupStepUpdater.updateForOrganization(organization
|
|
691
|
+
SetupStepUpdater.updateForOrganization(organization, { types: [
|
|
692
|
+
SetupStepType.Responsibilities,
|
|
693
|
+
] }).catch(console.error);
|
|
692
694
|
}
|
|
693
695
|
|
|
694
696
|
return new Response(
|
|
@@ -957,7 +959,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
957
959
|
throw new SimpleError({
|
|
958
960
|
code: 'known_member_missing_rights',
|
|
959
961
|
message: 'Creating known member without sufficient access rights',
|
|
960
|
-
human: $t(`
|
|
962
|
+
human: $t(`510807a1-d4c7-45fa-9e3b-ddc8764d3f6e`, { member: member.details.firstName }),
|
|
961
963
|
statusCode: 400,
|
|
962
964
|
});
|
|
963
965
|
}
|
|
@@ -90,6 +90,10 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
90
90
|
throw Context.auth.error();
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
if (request.body.config.organizationLevelRecordsConfiguration) {
|
|
94
|
+
shouldUpdateSetupSteps = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
93
97
|
const newConfig = request.body.config;
|
|
94
98
|
|
|
95
99
|
// Update config
|
|
@@ -100,13 +104,11 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
100
104
|
platform.config = patchObject(platform.config, newConfig);
|
|
101
105
|
const currentConfig: PlatformConfig = platform.config;
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
109
|
-
}
|
|
107
|
+
shouldUpdateSetupSteps = shouldUpdateSetupSteps || this.shouldUpdateSetupSteps(
|
|
108
|
+
currentConfig,
|
|
109
|
+
newConfig,
|
|
110
|
+
oldConfig,
|
|
111
|
+
);
|
|
110
112
|
}
|
|
111
113
|
else {
|
|
112
114
|
platform.config = patchObject(platform.config, newConfig);
|