@stamhoofd/backend 2.96.2 → 2.97.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 +27 -22
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +8 -0
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +174 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -2
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +623 -0
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +672 -3
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +28 -33
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +167 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +1 -1
- package/src/endpoints/global/email-recipients/helpers/validateEmailRecipientFilter.ts +21 -4
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +36 -19
- package/src/helpers/AdminPermissionChecker.ts +8 -3
- package/src/helpers/AuthenticatedStructures.ts +42 -14
- package/src/helpers/Context.ts +3 -3
- package/src/seeds/1755790070-fill-email-recipient-errors.ts +3 -3
- package/src/sql-filters/members.ts +2 -2
- package/src/sql-filters/registrations.ts +1 -1
|
@@ -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,
|
|
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';
|
|
@@ -40,8 +40,8 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
40
40
|
if (!model || (model.organizationId !== (organization?.id ?? null))) {
|
|
41
41
|
throw new SimpleError({
|
|
42
42
|
code: 'not_found',
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
message: 'Email not found',
|
|
44
|
+
human: $t(`9ddb6616-f62d-4c91-82a9-e5cf398e4c4a`),
|
|
45
45
|
statusCode: 404,
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -118,8 +118,8 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
118
118
|
if (model.status !== EmailStatus.Draft) {
|
|
119
119
|
throw new SimpleError({
|
|
120
120
|
code: 'not_draft',
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
message: 'Email is not a draft',
|
|
122
|
+
human: $t(`ace4d2e8-88d6-479f-bd8b-d576cc0ed1f2`),
|
|
123
123
|
statusCode: 400,
|
|
124
124
|
});
|
|
125
125
|
}
|
|
@@ -128,6 +128,29 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
128
128
|
rebuild = true;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
if (request.body.sendAsEmail !== undefined) {
|
|
132
|
+
if (model.status !== EmailStatus.Draft) {
|
|
133
|
+
throw new SimpleError({
|
|
134
|
+
code: 'not_draft',
|
|
135
|
+
message: 'Email is not a draft',
|
|
136
|
+
human: $t(`02b05c0d-908b-4200-8fb8-5fc01f539514`),
|
|
137
|
+
statusCode: 400,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
model.sendAsEmail = request.body.sendAsEmail ?? true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (request.body.showInMemberPortal !== undefined) {
|
|
145
|
+
model.showInMemberPortal = request.body.showInMemberPortal ?? false;
|
|
146
|
+
|
|
147
|
+
if (model.showInMemberPortal) {
|
|
148
|
+
if (!model.recipientFilter.canShowInMemberPortal) {
|
|
149
|
+
model.showInMemberPortal = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
131
154
|
// Attachments
|
|
132
155
|
if (request.body.attachments !== undefined) {
|
|
133
156
|
model.attachments = patchObject(model.attachments, request.body.attachments);
|
|
@@ -159,34 +182,6 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
159
182
|
});
|
|
160
183
|
}
|
|
161
184
|
|
|
162
|
-
model.throwIfNotReadyToSend();
|
|
163
|
-
|
|
164
|
-
const replacement = '{{unsubscribeUrl}}';
|
|
165
|
-
|
|
166
|
-
if (model.html) {
|
|
167
|
-
// Check email contains an unsubscribe button
|
|
168
|
-
if (!model.html.includes(replacement)) {
|
|
169
|
-
throw new SimpleError({
|
|
170
|
-
code: 'missing_unsubscribe_button',
|
|
171
|
-
message: 'Missing unsubscribe button',
|
|
172
|
-
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
173
|
-
field: 'html',
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (model.text) {
|
|
179
|
-
// Check email contains an unsubscribe button
|
|
180
|
-
if (!model.text.includes(replacement)) {
|
|
181
|
-
throw new SimpleError({
|
|
182
|
-
code: 'missing_unsubscribe_button',
|
|
183
|
-
message: 'Missing unsubscribe button',
|
|
184
|
-
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
185
|
-
field: 'text',
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
185
|
// Preview the sending status
|
|
191
186
|
await model.queueForSending();
|
|
192
187
|
}
|
|
@@ -186,6 +186,173 @@ describe('Endpoint.GetEmailRecipients', () => {
|
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
test('It can request all email recipients of a single email in combination with other filters', 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 = sender2.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
|
+
email: {
|
|
215
|
+
$contains: 'jan',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
limit: 10,
|
|
219
|
+
}),
|
|
220
|
+
headers: {
|
|
221
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const result = await testServer.test(endpoint, request);
|
|
225
|
+
expect(result.body.results).toHaveLength(1);
|
|
226
|
+
expect(result.body.results[0]).toMatchObject({
|
|
227
|
+
id: emailRecipient.id,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('[Regression] It can request all email recipients of a single email in combination with $and filters', async () => {
|
|
232
|
+
const email = new Email();
|
|
233
|
+
email.subject = 'test subject';
|
|
234
|
+
email.status = EmailStatus.Draft;
|
|
235
|
+
email.text = 'test email';
|
|
236
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
237
|
+
email.json = {};
|
|
238
|
+
email.organizationId = organization.id;
|
|
239
|
+
email.senderId = sender2.id;
|
|
240
|
+
await email.save();
|
|
241
|
+
|
|
242
|
+
const emailRecipient = new EmailRecipient();
|
|
243
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
244
|
+
emailRecipient.firstName = 'Jan';
|
|
245
|
+
emailRecipient.lastName = 'Janssens';
|
|
246
|
+
emailRecipient.emailId = email.id;
|
|
247
|
+
emailRecipient.organizationId = organization.id;
|
|
248
|
+
await emailRecipient.save();
|
|
249
|
+
|
|
250
|
+
const request = Request.get({
|
|
251
|
+
path: baseUrl,
|
|
252
|
+
host: organization.getApiHost(),
|
|
253
|
+
query: new LimitedFilteredRequest({
|
|
254
|
+
filter: {
|
|
255
|
+
$and: [
|
|
256
|
+
{ emailId: email.id },
|
|
257
|
+
{ email: { $contains: 'jan' } },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
limit: 10,
|
|
261
|
+
}),
|
|
262
|
+
headers: {
|
|
263
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
const result = await testServer.test(endpoint, request);
|
|
267
|
+
expect(result.body.results).toHaveLength(1);
|
|
268
|
+
expect(result.body.results[0]).toMatchObject({
|
|
269
|
+
id: emailRecipient.id,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('[Regression] It can request all email recipients of a single email in combination with multiple $and filters', async () => {
|
|
274
|
+
const email = new Email();
|
|
275
|
+
email.subject = 'test subject';
|
|
276
|
+
email.status = EmailStatus.Draft;
|
|
277
|
+
email.text = 'test email';
|
|
278
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
279
|
+
email.json = {};
|
|
280
|
+
email.organizationId = organization.id;
|
|
281
|
+
email.senderId = sender2.id;
|
|
282
|
+
await email.save();
|
|
283
|
+
|
|
284
|
+
const emailRecipient = new EmailRecipient();
|
|
285
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
286
|
+
emailRecipient.firstName = 'Jan';
|
|
287
|
+
emailRecipient.lastName = 'Janssens';
|
|
288
|
+
emailRecipient.emailId = email.id;
|
|
289
|
+
emailRecipient.organizationId = organization.id;
|
|
290
|
+
await emailRecipient.save();
|
|
291
|
+
|
|
292
|
+
const request = Request.get({
|
|
293
|
+
path: baseUrl,
|
|
294
|
+
host: organization.getApiHost(),
|
|
295
|
+
query: new LimitedFilteredRequest({
|
|
296
|
+
filter: {
|
|
297
|
+
$and: [
|
|
298
|
+
{ emailId: email.id },
|
|
299
|
+
{ email: { $contains: 'jan' } },
|
|
300
|
+
],
|
|
301
|
+
email: { $contains: 'ssens' },
|
|
302
|
+
},
|
|
303
|
+
limit: 10,
|
|
304
|
+
}),
|
|
305
|
+
headers: {
|
|
306
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
const result = await testServer.test(endpoint, request);
|
|
310
|
+
expect(result.body.results).toHaveLength(1);
|
|
311
|
+
expect(result.body.results[0]).toMatchObject({
|
|
312
|
+
id: emailRecipient.id,
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('It cannot request all email recipients of a single email in combination with $or filters', async () => {
|
|
317
|
+
const email = new Email();
|
|
318
|
+
email.subject = 'test subject';
|
|
319
|
+
email.status = EmailStatus.Draft;
|
|
320
|
+
email.text = 'test email';
|
|
321
|
+
email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
|
|
322
|
+
email.json = {};
|
|
323
|
+
email.organizationId = organization.id;
|
|
324
|
+
email.senderId = sender2.id;
|
|
325
|
+
await email.save();
|
|
326
|
+
|
|
327
|
+
const emailRecipient = new EmailRecipient();
|
|
328
|
+
emailRecipient.email = 'jan.janssens@geenemail.com';
|
|
329
|
+
emailRecipient.firstName = 'Jan';
|
|
330
|
+
emailRecipient.lastName = 'Janssens';
|
|
331
|
+
emailRecipient.emailId = email.id;
|
|
332
|
+
emailRecipient.organizationId = organization.id;
|
|
333
|
+
await emailRecipient.save();
|
|
334
|
+
|
|
335
|
+
const request = Request.get({
|
|
336
|
+
path: baseUrl,
|
|
337
|
+
host: organization.getApiHost(),
|
|
338
|
+
query: new LimitedFilteredRequest({
|
|
339
|
+
filter: {
|
|
340
|
+
$or: [
|
|
341
|
+
{ emailId: email.id },
|
|
342
|
+
{ email: { $contains: 'jan' } },
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
limit: 10,
|
|
346
|
+
}),
|
|
347
|
+
headers: {
|
|
348
|
+
authorization: 'Bearer ' + token2.accessToken,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
await expect(testServer.test(endpoint, request))
|
|
352
|
+
.rejects
|
|
353
|
+
.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
354
|
+
});
|
|
355
|
+
|
|
189
356
|
test('It cannot request all email recipients of a single email if read permission for another sender', async () => {
|
|
190
357
|
const email = new Email();
|
|
191
358
|
email.subject = 'test subject';
|
|
@@ -50,7 +50,7 @@ export class GetEmailRecipientsEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
50
50
|
if (!await validateEmailRecipientFilter({ filter: q.filter, permissionLevel: PermissionLevel.Read })) {
|
|
51
51
|
throw Context.auth.error({
|
|
52
52
|
message: 'You do not have sufficient permissions to view all email recipients',
|
|
53
|
-
human: $t(`
|
|
53
|
+
human: $t(`d499972f-270c-44f6-ad7e-9d5359aef609`),
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -9,9 +9,26 @@ export async function validateEmailRecipientFilter({ filter, permissionLevel }:
|
|
|
9
9
|
emailId: FilterWrapperMarker,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
let unwrapped = unwrapFilter(filter, requiredFilter);
|
|
13
13
|
if (!unwrapped.match) {
|
|
14
|
-
|
|
14
|
+
if (typeof filter === 'object'
|
|
15
|
+
&& filter !== null
|
|
16
|
+
&& filter['$and']
|
|
17
|
+
&& Array.isArray(filter['$and'])
|
|
18
|
+
&& Object.keys(filter).length >= 1 // does not matter if more than 1, because root is always $and together
|
|
19
|
+
&& filter['$and'].length > 0
|
|
20
|
+
) {
|
|
21
|
+
for (const subFilter of filter['$and']) {
|
|
22
|
+
unwrapped = unwrapFilter(subFilter as StamhoofdFilter, requiredFilter);
|
|
23
|
+
if (unwrapped.match) {
|
|
24
|
+
// Found!
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!unwrapped.match) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
const emailIds = typeof unwrapped.markerValue === 'string'
|
|
@@ -25,7 +42,7 @@ export async function validateEmailRecipientFilter({ filter, permissionLevel }:
|
|
|
25
42
|
code: 'invalid_field',
|
|
26
43
|
field: 'filter',
|
|
27
44
|
message: 'You must filter on an email id of the email recipients you are trying to access',
|
|
28
|
-
human: $t(`
|
|
45
|
+
human: $t(`9f352fd4-5e4d-4899-81ce-b69889ebfe9d`),
|
|
29
46
|
});
|
|
30
47
|
}
|
|
31
48
|
|
|
@@ -55,7 +72,7 @@ export async function validateEmailRecipientFilter({ filter, permissionLevel }:
|
|
|
55
72
|
if (!await Context.auth.canAccessEmail(email, permissionLevel)) {
|
|
56
73
|
throw Context.auth.error({
|
|
57
74
|
message: 'You do not have access to this email',
|
|
58
|
-
human: $t(`
|
|
75
|
+
human: $t(`590a37ed-0f3a-4c66-8e20-08ac00ae761d`),
|
|
59
76
|
});
|
|
60
77
|
}
|
|
61
78
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
-
import { Group, Member, Platform } from '@stamhoofd/models';
|
|
5
|
-
import { SQL, SQLSortDefinitions, applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
4
|
+
import { Group, Member, Platform, Registration } from '@stamhoofd/models';
|
|
5
|
+
import { SQL, SQLExpression, SQLSelect, SQLSortDefinitions, applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
|
|
6
6
|
import { CountFilteredRequest, GroupType, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
8
|
import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
@@ -11,7 +11,7 @@ import { RegistrationWithMemberBlob } from '@stamhoofd/structures/dist/src/membe
|
|
|
11
11
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
12
12
|
import { Context } from '../../../helpers/Context';
|
|
13
13
|
import { LimitedFilteredRequestHelper } from '../../../helpers/LimitedFilteredRequestHelper';
|
|
14
|
-
import { registrationFilterCompilers } from '../../../sql-filters/registrations';
|
|
14
|
+
import { groupJoin, registrationFilterCompilers } from '../../../sql-filters/registrations';
|
|
15
15
|
import { registrationSorters } from '../../../sql-sorters/registrations';
|
|
16
16
|
import { GetMembersEndpoint } from '../members/GetMembersEndpoint';
|
|
17
17
|
import { validateGroupFilter } from '../members/helpers/validateGroupFilter';
|
|
@@ -40,6 +40,29 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
40
40
|
return [false];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
static selectRegistrationWithGroup(...columns: (SQLExpression | string)[]): SQLSelect<Registration & { group: Group }> {
|
|
44
|
+
const transformer = (row: SQLResultNamespacedRow): Registration & { group: Group } => {
|
|
45
|
+
const d = Registration.fromRow(row[Registration.table]);
|
|
46
|
+
|
|
47
|
+
if (!d) {
|
|
48
|
+
console.error('Could not transform row', row, 'into model', Registration.table, 'check if the primary key is returned in the query');
|
|
49
|
+
throw new Error('Missing data for model ' + Registration.table);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const g = Group.fromRow(row[Group.table]);
|
|
53
|
+
if (!g) {
|
|
54
|
+
console.error('Could not transform row', row, 'into model', Group.table, 'check if the primary key is returned in the query');
|
|
55
|
+
throw new Error('Missing data for model ' + Group.table);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return d.setRelation(Registration.group, g);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const select = new SQLSelect(transformer, ...(columns.length === 0 ? [SQL.wildcard(), SQL.wildcard(Group.table)] : columns));
|
|
62
|
+
select.join(groupJoin);
|
|
63
|
+
return select.from(SQL.table(Registration.table));
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest, permissionLevel: PermissionLevel = PermissionLevel.Read) {
|
|
44
67
|
const organization = Context.organization;
|
|
45
68
|
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
@@ -115,15 +138,8 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
115
138
|
}
|
|
116
139
|
}
|
|
117
140
|
|
|
118
|
-
const query =
|
|
119
|
-
.select(
|
|
120
|
-
SQL.column('registrations', 'id'),
|
|
121
|
-
SQL.column('registrations', 'memberId'),
|
|
122
|
-
)
|
|
141
|
+
const query = this.selectRegistrationWithGroup()
|
|
123
142
|
.setMaxExecutionTime(15 * 1000)
|
|
124
|
-
.from(
|
|
125
|
-
SQL.table('registrations'),
|
|
126
|
-
)
|
|
127
143
|
.where('registeredAt', '!=', null);
|
|
128
144
|
|
|
129
145
|
if (scopeFilter) {
|
|
@@ -161,7 +177,9 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
161
177
|
|
|
162
178
|
static async buildData(requestQuery: LimitedFilteredRequest, permissionLevel = PermissionLevel.Read) {
|
|
163
179
|
const query = await GetRegistrationsEndpoint.buildQuery(requestQuery, permissionLevel);
|
|
164
|
-
let data:
|
|
180
|
+
let data: (Registration & {
|
|
181
|
+
group: Group;
|
|
182
|
+
})[];
|
|
165
183
|
|
|
166
184
|
try {
|
|
167
185
|
data = await query.fetch();
|
|
@@ -177,14 +195,13 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
177
195
|
throw error;
|
|
178
196
|
}
|
|
179
197
|
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
|
|
198
|
+
for (const registration of data) {
|
|
199
|
+
if (!await Context.auth.canAccessRegistration(registration, permissionLevel)) {
|
|
200
|
+
throw Context.auth.error();
|
|
183
201
|
}
|
|
184
|
-
|
|
185
|
-
});
|
|
202
|
+
}
|
|
186
203
|
|
|
187
|
-
const members = await Member.getBlobByIds(...
|
|
204
|
+
const members = await Member.getBlobByIds(...data.map(r => r.memberId));
|
|
188
205
|
|
|
189
206
|
for (const member of members) {
|
|
190
207
|
if (!await Context.auth.canAccessMember(member, permissionLevel)) {
|
|
@@ -192,7 +209,7 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
192
209
|
}
|
|
193
210
|
}
|
|
194
211
|
|
|
195
|
-
const registrationsBlob = await AuthenticatedStructures.registrationsBlob(
|
|
212
|
+
const registrationsBlob = await AuthenticatedStructures.registrationsBlob(data, members);
|
|
196
213
|
|
|
197
214
|
const next = LimitedFilteredRequestHelper.fixInfiniteLoadingLoop({
|
|
198
215
|
request: requestQuery,
|
|
@@ -161,9 +161,14 @@ export class AdminPermissionChecker {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
else {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
if (STAMHOOFD.userMode === 'organization') {
|
|
165
|
+
// User is limited to a scope: can't access platform resources
|
|
166
|
+
if (this.user.organizationId) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// User can access platform resources (e.g. API keys)
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
|
|
@@ -456,6 +456,9 @@ export class AuthenticatedStructures {
|
|
|
456
456
|
|
|
457
457
|
const memberBlobs: MemberWithRegistrationsBlob[] = [];
|
|
458
458
|
for (const member of members) {
|
|
459
|
+
const filtered: (Registration & {
|
|
460
|
+
group: Group;
|
|
461
|
+
})[] = [];
|
|
459
462
|
for (const registration of member.registrations) {
|
|
460
463
|
if (includeContextOrganization || registration.organizationId !== Context.auth.organization?.id) {
|
|
461
464
|
const found = organizations.get(registration.id);
|
|
@@ -464,8 +467,11 @@ export class AuthenticatedStructures {
|
|
|
464
467
|
organizations.set(organization.id, organization);
|
|
465
468
|
}
|
|
466
469
|
}
|
|
470
|
+
if (organizations.get(registration.organizationId)?.active || (Context.auth.organization && Context.auth.organization.active && registration.organizationId === Context.auth.organization.id) || await Context.auth.hasFullAccess(registration.organizationId)) {
|
|
471
|
+
filtered.push(registration);
|
|
472
|
+
}
|
|
467
473
|
}
|
|
468
|
-
member.registrations =
|
|
474
|
+
member.registrations = filtered;
|
|
469
475
|
const balancesPermission = await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, Context.organization?.id);
|
|
470
476
|
|
|
471
477
|
let memberBalances: GenericBalance[] = [];
|
|
@@ -521,7 +527,13 @@ export class AuthenticatedStructures {
|
|
|
521
527
|
}
|
|
522
528
|
}
|
|
523
529
|
|
|
524
|
-
const activeOrganizations = [
|
|
530
|
+
const activeOrganizations: Organization[] = [];
|
|
531
|
+
|
|
532
|
+
for (const organization of organizations.values()) {
|
|
533
|
+
if (organization.active || await Context.auth.hasFullAccess(organization.id)) {
|
|
534
|
+
activeOrganizations.push(organization);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
525
537
|
const organizationStructs = await this.organizations(activeOrganizations);
|
|
526
538
|
|
|
527
539
|
// Load missing groups
|
|
@@ -588,30 +600,46 @@ export class AuthenticatedStructures {
|
|
|
588
600
|
return (await this.eventNotifications([eventNotification]))[0];
|
|
589
601
|
}
|
|
590
602
|
|
|
591
|
-
static async registrationsBlob(
|
|
592
|
-
memberId: string;
|
|
593
|
-
id: string;
|
|
594
|
-
}[], members: MemberWithRegistrations[], includeContextOrganization = false, includeUser?: User): Promise<RegistrationsBlob> {
|
|
603
|
+
static async registrationsBlob(registrations: (Registration & { group: Group })[], members: MemberWithRegistrations[], includeContextOrganization = false, includeUser?: User): Promise<RegistrationsBlob> {
|
|
595
604
|
const membersBlob = await this.membersBlob(members, includeContextOrganization, includeUser);
|
|
596
605
|
|
|
597
606
|
const memberBlobs = membersBlob.members;
|
|
598
607
|
|
|
599
|
-
const registrationWithMemberBlobs =
|
|
600
|
-
|
|
608
|
+
const registrationWithMemberBlobs: RegistrationWithMemberBlob[] = [];
|
|
609
|
+
for (const registration of registrations) {
|
|
610
|
+
const memberBlob = memberBlobs.find(m => m.id === registration.memberId);
|
|
601
611
|
if (!memberBlob) {
|
|
602
612
|
throw new Error('Member not found');
|
|
603
613
|
}
|
|
604
614
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
615
|
+
let r = memberBlob.registrations.find(r => r.id === registration.id);
|
|
616
|
+
|
|
617
|
+
if (!r) {
|
|
618
|
+
const member = members.find(m => m.id === registration.memberId);
|
|
619
|
+
const balancesPermission = member ? (await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, Context.organization?.id)) : false;
|
|
620
|
+
r = registration.getStructure();
|
|
621
|
+
r.balances = balancesPermission
|
|
622
|
+
? ((await CachedBalance.getForObjects([registration.id], null)).map((b) => {
|
|
623
|
+
return GenericBalance.create(b);
|
|
624
|
+
}))
|
|
625
|
+
: [];
|
|
626
|
+
r.group = await this.group(registration.group);
|
|
627
|
+
|
|
628
|
+
memberBlob.registrations.push(r);
|
|
629
|
+
|
|
630
|
+
// Add organization if missing
|
|
631
|
+
if (!membersBlob.organizations.find(o => o.id === r!.organizationId)) {
|
|
632
|
+
const organization = await Context.auth.getOrganization(r!.organizationId);
|
|
633
|
+
membersBlob.organizations.push(organization.getBaseStructure());
|
|
634
|
+
}
|
|
608
635
|
}
|
|
609
636
|
|
|
610
|
-
|
|
611
|
-
...
|
|
637
|
+
const struct = RegistrationWithMemberBlob.create({
|
|
638
|
+
...r,
|
|
612
639
|
member: memberBlob,
|
|
613
640
|
});
|
|
614
|
-
|
|
641
|
+
registrationWithMemberBlobs.push(struct);
|
|
642
|
+
}
|
|
615
643
|
|
|
616
644
|
return RegistrationsBlob.create({
|
|
617
645
|
registrations: registrationWithMemberBlobs,
|
package/src/helpers/Context.ts
CHANGED
|
@@ -309,11 +309,11 @@ export class ContextInstance {
|
|
|
309
309
|
|
|
310
310
|
if (this.organization && !this.organization.active) {
|
|
311
311
|
// For inactive organizations, you always need permissions to view them
|
|
312
|
-
if (!await Context.auth.hasFullAccess(this.organization.id)) {
|
|
312
|
+
if (!Context.auth.hasSomePlatformAccess() || !await Context.auth.hasFullAccess(this.organization.id)) {
|
|
313
313
|
throw new SimpleError({
|
|
314
314
|
code: 'archived',
|
|
315
|
-
message: '
|
|
316
|
-
human: $t('
|
|
315
|
+
message: 'Platform access is required to view inactive organizations',
|
|
316
|
+
human: $t('3e8dba08-a505-41ec-96c1-b2b5c1c17852'),
|
|
317
317
|
statusCode: 401,
|
|
318
318
|
});
|
|
319
319
|
}
|
|
@@ -28,7 +28,7 @@ function stringToError(message: string) {
|
|
|
28
28
|
new SimpleError({
|
|
29
29
|
code: 'email_skipped_unsubscribed',
|
|
30
30
|
message: 'Recipient has unsubscribed',
|
|
31
|
-
human: $t('
|
|
31
|
+
human: $t('ffbebae7-eac3-44fe-863b-25942c5be7d0'),
|
|
32
32
|
}),
|
|
33
33
|
);
|
|
34
34
|
}
|
|
@@ -38,7 +38,7 @@ function stringToError(message: string) {
|
|
|
38
38
|
new SimpleError({
|
|
39
39
|
code: 'email_skipped_unsubscribed',
|
|
40
40
|
message: 'Recipient has unsubscribed from marketing',
|
|
41
|
-
human: $t('
|
|
41
|
+
human: $t('ffbebae7-eac3-44fe-863b-25942c5be7d0'),
|
|
42
42
|
}),
|
|
43
43
|
);
|
|
44
44
|
}
|
|
@@ -48,7 +48,7 @@ function stringToError(message: string) {
|
|
|
48
48
|
new SimpleError({
|
|
49
49
|
code: 'all_filtered',
|
|
50
50
|
message: 'All recipients are filtered due to hard bounce or spam',
|
|
51
|
-
human: $t('
|
|
51
|
+
human: $t('f6ca0939-f191-4aba-9c53-cd370453c0bc'),
|
|
52
52
|
}),
|
|
53
53
|
);
|
|
54
54
|
}
|
|
@@ -440,14 +440,14 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
|
|
|
440
440
|
throw new SimpleError({
|
|
441
441
|
code: 'not_found',
|
|
442
442
|
message: 'This email does not exist.',
|
|
443
|
-
human: $t('
|
|
443
|
+
human: $t('491156e4-a75d-4487-a97e-b208cd3a1d11'),
|
|
444
444
|
statusCode: 404,
|
|
445
445
|
});
|
|
446
446
|
}
|
|
447
447
|
if (!await Context.auth.canAccessEmail(email)) {
|
|
448
448
|
throw Context.auth.error({
|
|
449
449
|
message: 'No permissions to access this email.',
|
|
450
|
-
human: $t('
|
|
450
|
+
human: $t('be46002c-d77e-42ca-b163-23bde0bc628c'),
|
|
451
451
|
});
|
|
452
452
|
}
|
|
453
453
|
},
|
|
@@ -5,7 +5,7 @@ import { memberFilterCompilers } from './members';
|
|
|
5
5
|
|
|
6
6
|
export const memberJoin = SQL.join(Member.table).where(SQL.column(Member.table, 'id'), SQL.column(Registration.table, 'memberId'));
|
|
7
7
|
|
|
8
|
-
const groupJoin = SQL.join(Group.table).where(SQL.column(Group.table, 'id'), SQL.column(Registration.table, 'groupId'));
|
|
8
|
+
export const groupJoin = SQL.join(Group.table).where(SQL.column(Group.table, 'id'), SQL.column(Registration.table, 'groupId'));
|
|
9
9
|
|
|
10
10
|
export const registrationFilterCompilers: SQLFilterDefinitions = {
|
|
11
11
|
...baseSQLFilterCompilers,
|