@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.
@@ -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, EmailRecipientsStatus, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
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
- human: 'Email not found',
44
- message: $t(`9ddb6616-f62d-4c91-82a9-e5cf398e4c4a`),
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
- human: 'Email is not a draft',
122
- message: $t(`Je kan de ontvangerslijst alleen aanpassen als de e-mail nog een concept is`),
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(`Je hebt niet voldoende toegangsrechten om alle email ontvangers te bekijken. Filter op één specifieke e-mail.`),
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
- const unwrapped = unwrapFilter(filter, requiredFilter);
12
+ let unwrapped = unwrapFilter(filter, requiredFilter);
13
13
  if (!unwrapped.match) {
14
- return false;
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(`Je hebt niet voldoende toegangsrechten om alle email ontvangers te bekijken.`),
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(`Je hebt geen toegangsrechten tot de ontvangers van deze email`),
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 = SQL
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: SQLResultNamespacedRow[];
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 registrationData = data.map((r) => {
181
- if (typeof r.registrations.memberId === 'string' && typeof r.registrations.id === 'string') {
182
- return { memberId: r.registrations.memberId, id: r.registrations.id };
198
+ for (const registration of data) {
199
+ if (!await Context.auth.canAccessRegistration(registration, permissionLevel)) {
200
+ throw Context.auth.error();
183
201
  }
184
- throw new Error('Expected string');
185
- });
202
+ }
186
203
 
187
- const members = await Member.getBlobByIds(...registrationData.map(r => r.memberId));
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(registrationData, members);
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
- // User is limited to a scope
165
- if (this.user.organizationId) {
166
- return false;
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 = member.registrations.filter(r => (Context.auth.organization && Context.auth.organization.active && r.organizationId === Context.auth.organization.id) || (organizations.get(r.organizationId)?.active ?? false));
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 = [...organizations.values()].filter(o => o.active);
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(registrationData: {
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 = registrationData.map(({ id, memberId }) => {
600
- const memberBlob = memberBlobs.find(m => m.id === memberId);
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
- const registration = memberBlob.registrations.find(r => r.id === id);
606
- if (!registration) {
607
- throw new Error('Registration not found: ' + id);
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
- return RegistrationWithMemberBlob.create({
611
- ...registration,
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,
@@ -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: 'Full access is required to view inactive organizations',
316
- human: $t('31bc55e4-1cf3-495a-8b35-686a4cc25f69'),
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('De ontvanger heeft zich afgemeld voor e-mails'),
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('De ontvanger heeft zich afgemeld voor e-mails'),
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('Deze ontvanger komt voor op de gedeelde bounce of spamlijst. De ontvanger was eerder permanent onbereikbaar of heeft eerder een e-mail als spam gemarkeerd.'),
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('Deze e-mail bestaat niet (meer)'),
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('Je hebt niet voldoende toegangsrechten om te filteren op deze e-mail'),
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,