@stamhoofd/backend 2.92.0 → 2.94.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 +2 -2
- package/src/crons/amazon-ses.ts +100 -4
- package/src/crons/balance-emails.ts +1 -1
- package/src/email-recipient-loaders/receivable-balances.ts +3 -1
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +26 -2
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -3
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +27 -14
- 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/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +4 -15
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +2 -0
- package/src/helpers/AdminPermissionChecker.ts +4 -0
- package/src/helpers/AuthenticatedStructures.ts +1 -1
- package/src/helpers/EmailResumer.ts +2 -2
- package/src/seeds/1755790070-fill-email-recipient-errors.ts +96 -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/seeds/1756293699-fill-previous-next-period-id.ts +34 -0
- package/src/seeds/1756303697-update-email-counts.ts +76 -0
- package/src/sql-filters/email-recipients.ts +59 -0
- package/src/sql-filters/emails.ts +37 -2
- package/src/sql-filters/events.ts +10 -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/seeds/1734596144-fill-previous-period-id.ts +0 -55
- /package/src/seeds/{1755181288-remove-duplicate-members.ts → 1755876819-remove-duplicate-members.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.94.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"@simonbackx/simple-encoding": "2.22.0",
|
|
46
46
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
47
47
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
48
|
-
"@stamhoofd/backend-i18n": "2.
|
|
49
|
-
"@stamhoofd/backend-middleware": "2.
|
|
50
|
-
"@stamhoofd/email": "2.
|
|
51
|
-
"@stamhoofd/models": "2.
|
|
52
|
-
"@stamhoofd/queues": "2.
|
|
53
|
-
"@stamhoofd/sql": "2.
|
|
54
|
-
"@stamhoofd/structures": "2.
|
|
55
|
-
"@stamhoofd/utility": "2.
|
|
48
|
+
"@stamhoofd/backend-i18n": "2.94.0",
|
|
49
|
+
"@stamhoofd/backend-middleware": "2.94.0",
|
|
50
|
+
"@stamhoofd/email": "2.94.0",
|
|
51
|
+
"@stamhoofd/models": "2.94.0",
|
|
52
|
+
"@stamhoofd/queues": "2.94.0",
|
|
53
|
+
"@stamhoofd/sql": "2.94.0",
|
|
54
|
+
"@stamhoofd/structures": "2.94.0",
|
|
55
|
+
"@stamhoofd/utility": "2.94.0",
|
|
56
56
|
"archiver": "^7.0.1",
|
|
57
57
|
"axios": "^1.8.2",
|
|
58
58
|
"cookie": "^0.7.0",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "7a1a0be03249d69ad0664ee85d446c7b596768f0"
|
|
74
74
|
}
|
|
@@ -58,8 +58,8 @@ export const EmailLogger = new ModelLogger(Email, {
|
|
|
58
58
|
type: AuditLogReplacementType.Email,
|
|
59
59
|
})],
|
|
60
60
|
['c', AuditLogReplacement.create({
|
|
61
|
-
value: Formatter.integer(model.
|
|
62
|
-
count: model.
|
|
61
|
+
value: Formatter.integer(model.emailRecipientsCount ?? 0),
|
|
62
|
+
count: model.emailRecipientsCount ?? 0,
|
|
63
63
|
})],
|
|
64
64
|
]);
|
|
65
65
|
if (options.data.recipient) {
|
package/src/crons/amazon-ses.ts
CHANGED
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
} from '@aws-sdk/client-sqs';
|
|
7
7
|
import { registerCron } from '@stamhoofd/crons';
|
|
8
8
|
import { Email, EmailAddress } from '@stamhoofd/email';
|
|
9
|
-
import { AuditLog, Organization } from '@stamhoofd/models';
|
|
9
|
+
import { AuditLog, EmailRecipient, Organization } from '@stamhoofd/models';
|
|
10
10
|
import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
|
|
11
11
|
import { ForwardHandler } from '../helpers/ForwardHandler';
|
|
12
|
+
import { Email as EmailModel } from '@stamhoofd/models';
|
|
12
13
|
|
|
13
14
|
registerCron('checkComplaints', checkComplaints);
|
|
14
15
|
registerCron('checkReplies', checkReplies);
|
|
@@ -52,13 +53,88 @@ async function saveLog({ email, organization, type, subType, subject, response,
|
|
|
52
53
|
await log.save();
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
async function storeEmailStatus({ headers, type, message }: { headers: Record<string, string>; type: 'hard-bounce' | 'soft-bounce' | 'complaint'; message: string }) {
|
|
57
|
+
const emailId = headers['x-email-id'];
|
|
58
|
+
const recipientId = headers['x-email-recipient-id'];
|
|
59
|
+
|
|
60
|
+
if (emailId && recipientId) {
|
|
61
|
+
// check
|
|
62
|
+
const emailRecipient = await EmailRecipient.select()
|
|
63
|
+
.where('id', recipientId)
|
|
64
|
+
.where('emailId', emailId)
|
|
65
|
+
.first(false);
|
|
66
|
+
|
|
67
|
+
if (!emailRecipient) {
|
|
68
|
+
console.log('[AWS FORWARDING] Invalid email or recipient id in headers', headers);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let isNew = true;
|
|
73
|
+
|
|
74
|
+
switch (type) {
|
|
75
|
+
case 'hard-bounce': {
|
|
76
|
+
if (emailRecipient.hardBounceError) {
|
|
77
|
+
isNew = false;
|
|
78
|
+
}
|
|
79
|
+
emailRecipient.hardBounceError = message;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'soft-bounce': {
|
|
83
|
+
if (emailRecipient.softBounceError) {
|
|
84
|
+
isNew = false;
|
|
85
|
+
}
|
|
86
|
+
emailRecipient.softBounceError = message;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case 'complaint': {
|
|
90
|
+
if (emailRecipient.spamComplaintError) {
|
|
91
|
+
isNew = false;
|
|
92
|
+
}
|
|
93
|
+
emailRecipient.spamComplaintError = message;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log('[AWS FORWARDING] Marking email recipient ' + recipientId + ' for email ' + emailId + ' as ' + type);
|
|
99
|
+
if (await emailRecipient.save()) {
|
|
100
|
+
if (isNew) {
|
|
101
|
+
await EmailModel.bumpNotificationCount(emailId, type);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readHeaders(message: any) {
|
|
108
|
+
try {
|
|
109
|
+
const mail = message.mail;
|
|
110
|
+
const headers: Record<string, string> = {};
|
|
111
|
+
if (mail && typeof mail === 'object' && mail !== null && Array.isArray(mail.headers)) {
|
|
112
|
+
for (const header of mail.headers) {
|
|
113
|
+
if (header.name && header.value) {
|
|
114
|
+
headers[(header.name as string).toLowerCase()] = header.value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log('[AWS] Missing mail headers', message);
|
|
120
|
+
}
|
|
121
|
+
return headers;
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
console.log('[AWS] Failed to read headers', e, message);
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
55
129
|
async function handleBounce(message: any) {
|
|
56
130
|
if (message.bounce) {
|
|
131
|
+
console.log('[AWS BOUNCES] Handling bounce message', message);
|
|
132
|
+
const headers = readHeaders(message);
|
|
133
|
+
|
|
57
134
|
const b = message.bounce;
|
|
58
135
|
// Block all receivers that generate a permanent bounce
|
|
59
136
|
const type = b.bounceType;
|
|
60
137
|
const subtype = b.bounceSubType;
|
|
61
|
-
|
|
62
138
|
const source = message.mail.source;
|
|
63
139
|
|
|
64
140
|
// try to find organization that is responsible for this e-mail address
|
|
@@ -80,6 +156,12 @@ async function handleBounce(message: any) {
|
|
|
80
156
|
emailAddress.hardBounce = true;
|
|
81
157
|
await emailAddress.save();
|
|
82
158
|
|
|
159
|
+
await storeEmailStatus({
|
|
160
|
+
headers,
|
|
161
|
+
type: 'hard-bounce',
|
|
162
|
+
message: recipient.diagnosticCode || 'Permanent bounce',
|
|
163
|
+
});
|
|
164
|
+
|
|
83
165
|
await saveLog({
|
|
84
166
|
id: b.feedbackId,
|
|
85
167
|
email,
|
|
@@ -95,6 +177,13 @@ async function handleBounce(message: any) {
|
|
|
95
177
|
type === 'Transient'
|
|
96
178
|
) {
|
|
97
179
|
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
180
|
+
|
|
181
|
+
await storeEmailStatus({
|
|
182
|
+
headers,
|
|
183
|
+
type: 'soft-bounce',
|
|
184
|
+
message: recipient.diagnosticCode || 'Soft bounce',
|
|
185
|
+
});
|
|
186
|
+
|
|
98
187
|
await saveLog({
|
|
99
188
|
id: b.feedbackId,
|
|
100
189
|
email,
|
|
@@ -116,6 +205,7 @@ async function handleBounce(message: any) {
|
|
|
116
205
|
|
|
117
206
|
async function handleComplaint(message: any) {
|
|
118
207
|
if (message.complaint) {
|
|
208
|
+
const headers = readHeaders(message);
|
|
119
209
|
const b = message.complaint;
|
|
120
210
|
const source = message.mail.source;
|
|
121
211
|
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
@@ -129,10 +219,16 @@ async function handleComplaint(message: any) {
|
|
|
129
219
|
await emailAddress.save();
|
|
130
220
|
|
|
131
221
|
if (type !== 'not-spam') {
|
|
222
|
+
await storeEmailStatus({
|
|
223
|
+
headers,
|
|
224
|
+
type: 'complaint',
|
|
225
|
+
message: recipient.diagnosticCode || type || 'Complaint',
|
|
226
|
+
});
|
|
227
|
+
|
|
132
228
|
if (type === 'virus' || type === 'fraud') {
|
|
133
229
|
await saveLog({
|
|
134
230
|
id: b.feedbackId,
|
|
135
|
-
email
|
|
231
|
+
email,
|
|
136
232
|
organization,
|
|
137
233
|
type: AuditLogType.EmailAddressFraudComplaint,
|
|
138
234
|
subType: type || 'unknown',
|
|
@@ -144,7 +240,7 @@ async function handleComplaint(message: any) {
|
|
|
144
240
|
else {
|
|
145
241
|
await saveLog({
|
|
146
242
|
id: b.feedbackId,
|
|
147
|
-
email
|
|
243
|
+
email,
|
|
148
244
|
organization,
|
|
149
245
|
type: AuditLogType.EmailAddressMarkedAsSpam,
|
|
150
246
|
subType: type || 'unknown',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CachedBalance, Email } from '@stamhoofd/models';
|
|
2
|
-
import { BalanceItem as BalanceItemStruct, compileToInMemoryFilter, EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, receivableBalanceObjectContactInMemoryFilterCompilers, Replacement, StamhoofdFilter } from '@stamhoofd/structures';
|
|
2
|
+
import { BalanceItem as BalanceItemStruct, compileToInMemoryFilter, EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, receivableBalanceObjectContactInMemoryFilterCompilers, ReceivableBalanceType, Replacement, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
3
|
import { Formatter } from '@stamhoofd/utility';
|
|
4
4
|
import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint';
|
|
5
5
|
|
|
@@ -22,6 +22,8 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
|
|
|
22
22
|
for (const email of contact.emails) {
|
|
23
23
|
const recipient = EmailRecipient.create({
|
|
24
24
|
objectId: balance.id, // Note: not set member, user or organization id here - should be the queryable balance id
|
|
25
|
+
userId: balance.objectType === ReceivableBalanceType.user ? balance.object.id : null,
|
|
26
|
+
memberId: balance.objectType === ReceivableBalanceType.member ? balance.object.id : null,
|
|
25
27
|
firstName: contact.firstName,
|
|
26
28
|
lastName: contact.lastName,
|
|
27
29
|
email,
|
|
@@ -120,8 +120,32 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
120
120
|
await model.buildExampleRecipient();
|
|
121
121
|
model.updateCount();
|
|
122
122
|
|
|
123
|
-
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
|
|
124
|
-
|
|
123
|
+
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
|
|
124
|
+
if (!await Context.auth.canSendEmail(model)) {
|
|
125
|
+
throw Context.auth.error({
|
|
126
|
+
message: 'Cannot send emails from this sender',
|
|
127
|
+
human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
await model.queueForSending();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Delete open drafts with the same content, from the same user
|
|
134
|
+
const duplicates = await Email.select()
|
|
135
|
+
.where('userId', user.id)
|
|
136
|
+
.where('organizationId', model.organizationId)
|
|
137
|
+
.where('status', EmailStatus.Draft)
|
|
138
|
+
.where('subject', model.subject)
|
|
139
|
+
.where('html', model.html)
|
|
140
|
+
.where('text', model.text)
|
|
141
|
+
.where('deletedAt', null)
|
|
142
|
+
.whereNot('id', model.id)
|
|
143
|
+
.limit(100)
|
|
144
|
+
.fetch();
|
|
145
|
+
|
|
146
|
+
for (const duplicate of duplicates) {
|
|
147
|
+
duplicate.deletedAt = new Date();
|
|
148
|
+
await duplicate.save();
|
|
125
149
|
}
|
|
126
150
|
|
|
127
151
|
return new Response(await model.getPreviewStructure());
|
|
@@ -104,7 +104,8 @@ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, Respon
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
const query = Email.select()
|
|
107
|
+
const query = Email.select()
|
|
108
|
+
.where('deletedAt', null);
|
|
108
109
|
|
|
109
110
|
if (scopeFilter) {
|
|
110
111
|
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
@@ -138,8 +139,6 @@ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, Respon
|
|
|
138
139
|
query.limit(q.limit);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
console.log('Building query for GetAdminEmailsEndpoint', query.getSQL());
|
|
142
|
-
|
|
143
142
|
return query;
|
|
144
143
|
}
|
|
145
144
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import { Email, Platform } from '@stamhoofd/models';
|
|
3
|
-
import { EmailPreview, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
|
|
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';
|
|
@@ -50,15 +50,6 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
50
50
|
throw Context.auth.error();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
if (model.status !== EmailStatus.Draft) {
|
|
54
|
-
throw new SimpleError({
|
|
55
|
-
code: 'not_draft',
|
|
56
|
-
human: 'Email is not a draft',
|
|
57
|
-
message: $t(`298b5a46-2899-4aa1-89df-9b634c20806b`),
|
|
58
|
-
statusCode: 400,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
53
|
let rebuild = false;
|
|
63
54
|
|
|
64
55
|
if (request.body.subject !== undefined) {
|
|
@@ -78,6 +69,11 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
78
69
|
model.senderId = sender.id;
|
|
79
70
|
model.fromAddress = sender.email;
|
|
80
71
|
model.fromName = sender.name;
|
|
72
|
+
|
|
73
|
+
// Check if we still have write access to the email
|
|
74
|
+
if (!await Context.auth.canAccessEmail(model, PermissionLevel.Write)) {
|
|
75
|
+
throw Context.auth.error();
|
|
76
|
+
}
|
|
81
77
|
}
|
|
82
78
|
else {
|
|
83
79
|
throw new SimpleError({
|
|
@@ -119,6 +115,15 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
119
115
|
}
|
|
120
116
|
|
|
121
117
|
if (request.body.recipientFilter) {
|
|
118
|
+
if (model.status !== EmailStatus.Draft) {
|
|
119
|
+
throw new SimpleError({
|
|
120
|
+
code: 'not_draft',
|
|
121
|
+
human: 'Email is not a draft',
|
|
122
|
+
message: $t(`Je kan de ontvangerslijst alleen aanpassen als de e-mail nog een concept is`),
|
|
123
|
+
statusCode: 400,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
122
127
|
model.recipientFilter = patchObject(model.recipientFilter, request.body.recipientFilter);
|
|
123
128
|
rebuild = true;
|
|
124
129
|
}
|
|
@@ -129,17 +134,24 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
129
134
|
model.validateAttachments();
|
|
130
135
|
}
|
|
131
136
|
|
|
137
|
+
if (request.body.deletedAt !== undefined) {
|
|
138
|
+
if (!await Context.auth.canAccessEmail(model, PermissionLevel.Full)) {
|
|
139
|
+
throw Context.auth.error();
|
|
140
|
+
}
|
|
141
|
+
model.deletedAt = request.body.deletedAt;
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
await model.save();
|
|
133
145
|
|
|
134
146
|
if (rebuild) {
|
|
135
147
|
await model.buildExampleRecipient();
|
|
136
|
-
model.updateCount();
|
|
137
148
|
|
|
138
149
|
// Force null - because we have stale data
|
|
139
|
-
model.
|
|
150
|
+
model.emailRecipientsCount = null;
|
|
151
|
+
model.updateCount();
|
|
140
152
|
}
|
|
141
153
|
|
|
142
|
-
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
|
|
154
|
+
if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
|
|
143
155
|
if (!await Context.auth.canSendEmail(model)) {
|
|
144
156
|
throw Context.auth.error({
|
|
145
157
|
message: 'Cannot send emails from this sender',
|
|
@@ -175,7 +187,8 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
175
187
|
}
|
|
176
188
|
}
|
|
177
189
|
|
|
178
|
-
|
|
190
|
+
// Preview the sending status
|
|
191
|
+
await model.queueForSending();
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
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
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
2
|
+
import { GetEmailRecipientsEndpoint } from './GetEmailRecipientsEndpoint';
|
|
3
|
+
import { AccessRight, EmailStatus, LimitedFilteredRequest, OrganizationEmail, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
|
|
4
|
+
import { Email, EmailRecipient, Organization, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from '@stamhoofd/models';
|
|
5
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
6
|
+
import { testServer } from '../../../../tests/helpers/TestServer';
|
|
7
|
+
|
|
8
|
+
const baseUrl = `/email-recipients`;
|
|
9
|
+
|
|
10
|
+
describe('Endpoint.GetEmailRecipients', () => {
|
|
11
|
+
const endpoint = new GetEmailRecipientsEndpoint();
|
|
12
|
+
let period: RegistrationPeriod;
|
|
13
|
+
let organization: Organization;
|
|
14
|
+
let token: Token;
|
|
15
|
+
let user: User;
|
|
16
|
+
let sender: OrganizationEmail;
|
|
17
|
+
let sender2: OrganizationEmail;
|
|
18
|
+
|
|
19
|
+
let token2: Token;
|
|
20
|
+
let user2: User;
|
|
21
|
+
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
TestUtils.setPermanentEnvironment('userMode', 'platform');
|
|
24
|
+
period = await new RegistrationPeriodFactory({
|
|
25
|
+
startDate: new Date(2023, 0, 1),
|
|
26
|
+
endDate: new Date(2023, 11, 31),
|
|
27
|
+
}).create();
|
|
28
|
+
|
|
29
|
+
organization = await new OrganizationFactory({ period })
|
|
30
|
+
.create();
|
|
31
|
+
|
|
32
|
+
sender = OrganizationEmail.create({
|
|
33
|
+
email: 'groepsleiding@voorbeeld.com',
|
|
34
|
+
name: 'Groepsleiding',
|
|
35
|
+
});
|
|
36
|
+
sender2 = OrganizationEmail.create({
|
|
37
|
+
email: 'kapoenen@voorbeeld.com',
|
|
38
|
+
name: 'Kapoenen',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
organization.privateMeta.emails.push(sender);
|
|
42
|
+
organization.privateMeta.emails.push(sender2);
|
|
43
|
+
await organization.save();
|
|
44
|
+
|
|
45
|
+
user = await new UserFactory({
|
|
46
|
+
organization,
|
|
47
|
+
permissions: Permissions.create({
|
|
48
|
+
level: PermissionLevel.None,
|
|
49
|
+
resources: new Map([
|
|
50
|
+
[PermissionsResourceType.Senders, new Map([['', ResourcePermissions.create({
|
|
51
|
+
resourceName: sender.name!,
|
|
52
|
+
level: PermissionLevel.Read,
|
|
53
|
+
})]])],
|
|
54
|
+
]),
|
|
55
|
+
}),
|
|
56
|
+
})
|
|
57
|
+
.create();
|
|
58
|
+
|
|
59
|
+
token = await Token.createToken(user);
|
|
60
|
+
|
|
61
|
+
user2 = await new UserFactory({
|
|
62
|
+
organization,
|
|
63
|
+
permissions: Permissions.create({
|
|
64
|
+
level: PermissionLevel.None,
|
|
65
|
+
resources: new Map([
|
|
66
|
+
[PermissionsResourceType.Senders, new Map([[sender2.id, ResourcePermissions.create({
|
|
67
|
+
resourceName: sender.name!,
|
|
68
|
+
level: PermissionLevel.Read,
|
|
69
|
+
})]])],
|
|
70
|
+
]),
|
|
71
|
+
}),
|
|
72
|
+
})
|
|
73
|
+
.create();
|
|
74
|
+
|
|
75
|
+
token2 = await Token.createToken(user2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('It can request all email recipients if read permission for all senders', async () => {
|
|
79
|
+
const email = new Email();
|
|
80
|
+
email.subject = 'test subject';
|
|
81
|
+
email.status = EmailStatus.Draft;
|
|
82
|
+
email.text = 'test email';
|
|
83
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
84
|
+
email.json = {};
|
|
85
|
+
email.organizationId = organization.id;
|
|
86
|
+
email.senderId = sender.id;
|
|
87
|
+
await email.save();
|
|
88
|
+
|
|
89
|
+
const emailRecipient = new EmailRecipient();
|
|
90
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
91
|
+
emailRecipient.firstName = 'Jan';
|
|
92
|
+
emailRecipient.lastName = 'Janssens';
|
|
93
|
+
emailRecipient.emailId = email.id;
|
|
94
|
+
emailRecipient.organizationId = organization.id;
|
|
95
|
+
await emailRecipient.save();
|
|
96
|
+
|
|
97
|
+
const request = Request.get({
|
|
98
|
+
path: baseUrl,
|
|
99
|
+
host: organization.getApiHost(),
|
|
100
|
+
query: new LimitedFilteredRequest({
|
|
101
|
+
filter: {},
|
|
102
|
+
limit: 10,
|
|
103
|
+
}),
|
|
104
|
+
headers: {
|
|
105
|
+
authorization: 'Bearer ' + token.accessToken,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const result = await testServer.test(endpoint, request);
|
|
109
|
+
expect(result.body.results).toHaveLength(1);
|
|
110
|
+
expect(result.body.results[0]).toMatchObject({
|
|
111
|
+
id: emailRecipient.id,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('It can not request all email recipients if not read permission for all senders', async () => {
|
|
116
|
+
const email = new Email();
|
|
117
|
+
email.subject = 'test subject';
|
|
118
|
+
email.status = EmailStatus.Draft;
|
|
119
|
+
email.text = 'test email';
|
|
120
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
121
|
+
email.json = {};
|
|
122
|
+
email.organizationId = organization.id;
|
|
123
|
+
email.senderId = sender2.id;
|
|
124
|
+
await email.save();
|
|
125
|
+
|
|
126
|
+
const emailRecipient = new EmailRecipient();
|
|
127
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
128
|
+
emailRecipient.firstName = 'Jan';
|
|
129
|
+
emailRecipient.lastName = 'Janssens';
|
|
130
|
+
emailRecipient.emailId = email.id;
|
|
131
|
+
emailRecipient.organizationId = organization.id;
|
|
132
|
+
await emailRecipient.save();
|
|
133
|
+
|
|
134
|
+
const request = Request.get({
|
|
135
|
+
path: baseUrl,
|
|
136
|
+
host: organization.getApiHost(),
|
|
137
|
+
query: new LimitedFilteredRequest({
|
|
138
|
+
filter: {},
|
|
139
|
+
limit: 10,
|
|
140
|
+
}),
|
|
141
|
+
headers: {
|
|
142
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
await expect(testServer.test(endpoint, request))
|
|
146
|
+
.rejects
|
|
147
|
+
.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('It request all email recipients of a single email if read permission for that sender', async () => {
|
|
151
|
+
const email = new Email();
|
|
152
|
+
email.subject = 'test subject';
|
|
153
|
+
email.status = EmailStatus.Draft;
|
|
154
|
+
email.text = 'test email';
|
|
155
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
156
|
+
email.json = {};
|
|
157
|
+
email.organizationId = organization.id;
|
|
158
|
+
email.senderId = sender2.id;
|
|
159
|
+
await email.save();
|
|
160
|
+
|
|
161
|
+
const emailRecipient = new EmailRecipient();
|
|
162
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
163
|
+
emailRecipient.firstName = 'Jan';
|
|
164
|
+
emailRecipient.lastName = 'Janssens';
|
|
165
|
+
emailRecipient.emailId = email.id;
|
|
166
|
+
emailRecipient.organizationId = organization.id;
|
|
167
|
+
await emailRecipient.save();
|
|
168
|
+
|
|
169
|
+
const request = Request.get({
|
|
170
|
+
path: baseUrl,
|
|
171
|
+
host: organization.getApiHost(),
|
|
172
|
+
query: new LimitedFilteredRequest({
|
|
173
|
+
filter: {
|
|
174
|
+
emailId: email.id,
|
|
175
|
+
},
|
|
176
|
+
limit: 10,
|
|
177
|
+
}),
|
|
178
|
+
headers: {
|
|
179
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const result = await testServer.test(endpoint, request);
|
|
183
|
+
expect(result.body.results).toHaveLength(1);
|
|
184
|
+
expect(result.body.results[0]).toMatchObject({
|
|
185
|
+
id: emailRecipient.id,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('It cannot request all email recipients of a single email if read permission for another sender', async () => {
|
|
190
|
+
const email = new Email();
|
|
191
|
+
email.subject = 'test subject';
|
|
192
|
+
email.status = EmailStatus.Draft;
|
|
193
|
+
email.text = 'test email';
|
|
194
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
195
|
+
email.json = {};
|
|
196
|
+
email.organizationId = organization.id;
|
|
197
|
+
email.senderId = sender.id;
|
|
198
|
+
await email.save();
|
|
199
|
+
|
|
200
|
+
const emailRecipient = new EmailRecipient();
|
|
201
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
202
|
+
emailRecipient.firstName = 'Jan';
|
|
203
|
+
emailRecipient.lastName = 'Janssens';
|
|
204
|
+
emailRecipient.emailId = email.id;
|
|
205
|
+
emailRecipient.organizationId = organization.id;
|
|
206
|
+
await emailRecipient.save();
|
|
207
|
+
|
|
208
|
+
const request = Request.get({
|
|
209
|
+
path: baseUrl,
|
|
210
|
+
host: organization.getApiHost(),
|
|
211
|
+
query: new LimitedFilteredRequest({
|
|
212
|
+
filter: {
|
|
213
|
+
emailId: email.id,
|
|
214
|
+
},
|
|
215
|
+
limit: 10,
|
|
216
|
+
}),
|
|
217
|
+
headers: {
|
|
218
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
await expect(testServer.test(endpoint, request))
|
|
222
|
+
.rejects
|
|
223
|
+
.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
224
|
+
});
|
|
225
|
+
});
|