@stamhoofd/backend 2.78.3 → 2.79.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.
Files changed (33) hide show
  1. package/.env.ci.json +19 -8
  2. package/index.ts +8 -1
  3. package/package.json +13 -12
  4. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +1 -1
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +1 -1
  6. package/src/endpoints/auth/CreateTokenEndpoint.ts +1 -1
  7. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
  8. package/src/endpoints/auth/PatchUserEndpoint.ts +1 -1
  9. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +2686 -0
  10. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +206 -20
  11. package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +9 -21
  12. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +1 -1
  13. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +2 -2
  14. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +15 -14
  15. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +3 -23
  16. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -0
  17. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -3
  18. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +2 -2
  19. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +164 -0
  20. package/src/helpers/AdminPermissionChecker.ts +41 -2
  21. package/src/helpers/AuthenticatedStructures.ts +77 -20
  22. package/src/helpers/ForwardHandler.test.ts +16 -5
  23. package/src/helpers/ForwardHandler.ts +21 -9
  24. package/src/helpers/MemberUserSyncer.test.ts +822 -0
  25. package/src/helpers/MemberUserSyncer.ts +137 -108
  26. package/src/helpers/TagHelper.ts +3 -3
  27. package/src/seeds/1734596144-fill-previous-period-id.ts +1 -1
  28. package/src/seeds/1741008870-fix-auditlog-description.ts +50 -0
  29. package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +88 -0
  30. package/src/services/FileSignService.ts +1 -1
  31. package/src/services/PlatformMembershipService.ts +7 -2
  32. package/src/services/SSOService.ts +1 -1
  33. package/tests/e2e/register.test.ts +2 -2
@@ -0,0 +1,164 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import { GroupFactory, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
3
+ import { GroupType, PermissionLevel, Permissions, Version } from '@stamhoofd/structures';
4
+ import { testServer } from '../../../../../tests/helpers/TestServer';
5
+ import { PatchRegistrationPeriodsEndpoint } from './GetOrganizationRegistrationPeriodsEndpoint';
6
+
7
+ const baseUrl = `/v${Version}/organization/registration-periods`;
8
+
9
+ describe('Endpoint.GetOrganizationRegistrationPeriods', () => {
10
+ const endpoint = new PatchRegistrationPeriodsEndpoint();
11
+
12
+ let registrationPeriod1: RegistrationPeriod;
13
+ let registrationPeriod2: RegistrationPeriod;
14
+
15
+ const get = async (organization: Organization, token: Token) => {
16
+ const request = Request.buildJson('GET', baseUrl, organization.getApiHost());
17
+ request.headers.authorization = 'Bearer ' + token.accessToken;
18
+ return await testServer.test(endpoint, request);
19
+ };
20
+
21
+ beforeAll(async () => {
22
+ registrationPeriod2 = await new RegistrationPeriodFactory({
23
+ startDate: new Date(2022, 0, 1),
24
+ endDate: new Date(2022, 11, 31),
25
+ }).create();
26
+
27
+ registrationPeriod1 = await new RegistrationPeriodFactory({
28
+ startDate: new Date(2023, 0, 1),
29
+ endDate: new Date(2023, 11, 31),
30
+ previousPeriodId: registrationPeriod2.id,
31
+ }).create();
32
+ });
33
+
34
+ const initOrganization = async (registrationPeriod: RegistrationPeriod) => {
35
+ return await new OrganizationFactory({ period: registrationPeriod })
36
+ .create();
37
+ };
38
+
39
+ describe('groups', () => {
40
+ test('Should contain waiting lists', async () => {
41
+ // arrange
42
+ const organization = await initOrganization(registrationPeriod1);
43
+ await new OrganizationRegistrationPeriodFactory({
44
+ organization,
45
+ period: registrationPeriod1,
46
+ }).create();
47
+
48
+ const user = await new UserFactory({
49
+ organization,
50
+ permissions: Permissions.create({
51
+ level: PermissionLevel.Full,
52
+ }),
53
+ })
54
+ .create();
55
+ const token = await Token.createToken(user);
56
+
57
+ const otherOrganization = await initOrganization(registrationPeriod1);
58
+
59
+ const waitingList1 = await new GroupFactory({
60
+ organization,
61
+ type: GroupType.WaitingList,
62
+ }).create();
63
+
64
+ const waitingList2 = await new GroupFactory({
65
+ organization,
66
+ type: GroupType.WaitingList,
67
+ }).create();
68
+
69
+ // waiting list of other organization
70
+ await new GroupFactory({
71
+ organization: otherOrganization,
72
+ type: GroupType.WaitingList,
73
+ }).create();
74
+
75
+ const groupWithWaitingList = await new GroupFactory({
76
+ organization,
77
+ }).create();
78
+
79
+ groupWithWaitingList.waitingListId = waitingList1.id;
80
+ await groupWithWaitingList.save();
81
+
82
+ // act
83
+ const result = await get(organization, token);
84
+
85
+ // assert
86
+ expect(result.body).toBeDefined();
87
+ expect(result.body.organizationPeriods.length).toBe(1);
88
+ const organizationPeriod = result.body.organizationPeriods[0];
89
+ const groups = organizationPeriod.groups;
90
+ expect(groups.length).toBe(3);
91
+ expect(groups).toEqual(expect.arrayContaining([
92
+ expect.objectContaining({
93
+ id: groupWithWaitingList.id,
94
+ }),
95
+ expect.objectContaining({
96
+ id: waitingList1.id,
97
+ }),
98
+ expect.objectContaining({
99
+ id: waitingList2.id,
100
+ }),
101
+ ]));
102
+ expect(organizationPeriod.waitingLists.length).toBe(2);
103
+ });
104
+
105
+ test('Should contain only groups of the organization registration period', async () => {
106
+ // arrange
107
+ const organization = await initOrganization(registrationPeriod1);
108
+ await new OrganizationRegistrationPeriodFactory({
109
+ organization,
110
+ period: registrationPeriod1,
111
+ }).create();
112
+
113
+ const user = await new UserFactory({
114
+ organization,
115
+ permissions: Permissions.create({
116
+ level: PermissionLevel.Full,
117
+ }),
118
+ })
119
+ .create();
120
+
121
+ const token = await Token.createToken(user);
122
+
123
+ const group1 = await new GroupFactory({
124
+ organization,
125
+ period: registrationPeriod1,
126
+ }).create();
127
+
128
+ const group2 = await new GroupFactory({
129
+ organization,
130
+ period: registrationPeriod1,
131
+ }).create();
132
+
133
+ // group list of other organization
134
+ const otherOrganization = await initOrganization(registrationPeriod1);
135
+
136
+ await new GroupFactory({
137
+ organization: otherOrganization,
138
+ }).create();
139
+
140
+ // group of other period
141
+ await new GroupFactory({
142
+ organization,
143
+ period: registrationPeriod2,
144
+ }).create();
145
+
146
+ // act
147
+ const result = await get(organization, token);
148
+
149
+ // assert
150
+ expect(result.body).toBeDefined();
151
+ expect(result.body.organizationPeriods.length).toBe(1);
152
+ const groups = result.body.organizationPeriods[0].groups;
153
+ expect(groups.length).toBe(2);
154
+ expect(groups).toEqual(expect.arrayContaining([
155
+ expect.objectContaining({
156
+ id: group1.id,
157
+ }),
158
+ expect.objectContaining({
159
+ id: group2.id,
160
+ }),
161
+ ]));
162
+ });
163
+ });
164
+ });
@@ -1,7 +1,7 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
2
2
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
3
  import { BalanceItem, CachedBalance, Document, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
4
- import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory, RecordSettings } from '@stamhoofd/structures';
4
+ import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory, RecordSettings } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
7
7
  import { MemberRecordStore } from '../services/MemberRecordStore';
@@ -91,6 +91,10 @@ export class AdminPermissionChecker {
91
91
  });
92
92
  }
93
93
 
94
+ memberNotFoundOrNoAccess(): SimpleError {
95
+ return this.notFoundOrNoAccess($t('Je hebt geen toegang tot dit lid of het bestaat niet'));
96
+ }
97
+
94
98
  notFoundOrNoAccess(message?: string): SimpleError {
95
99
  return new SimpleError({
96
100
  code: 'not_found',
@@ -181,6 +185,15 @@ export class AdminPermissionChecker {
181
185
  return true;
182
186
  }
183
187
 
188
+ if (group.type === GroupType.EventRegistration) {
189
+ // Check if we can access the event
190
+ const event = await Event.select().where('groupId', group.id).first(false);
191
+
192
+ if (event && event.organizationId === group.organizationId && await this.canAccessEvent(event)) {
193
+ return true;
194
+ }
195
+ }
196
+
184
197
  // Check parent categories
185
198
  const organizationPeriod = await this.getOrganizationCurrentPeriod(organization);
186
199
  const parentCategories = group.getParentCategories(organizationPeriod.settings.categories);
@@ -193,6 +206,18 @@ export class AdminPermissionChecker {
193
206
  return false;
194
207
  }
195
208
 
209
+ async canRegisterMembersInGroup(group: Group, asOrganizationId: string | null) {
210
+ if (await this.canAccessGroup(group, PermissionLevel.Write)) {
211
+ return true;
212
+ }
213
+ if (asOrganizationId) {
214
+ if (group.settings.allowRegistrationsByOrganization) {
215
+ return await this.hasFullAccess(asOrganizationId);
216
+ }
217
+ }
218
+ return false;
219
+ }
220
+
196
221
  /**
197
222
  * Will throw error if not allowed to edit/add/delete this event
198
223
  * @param event
@@ -1043,6 +1068,20 @@ export class AdminPermissionChecker {
1043
1068
  };
1044
1069
  }
1045
1070
 
1071
+ // It is possible that this is a platform admin, and inherits automatic permissions for tags. So'll need to loop all the organizations where this member has an active registration for
1072
+ if (!record.organizationId && this.platformPermissions) {
1073
+ const organizations = Formatter.uniqueArray(member.registrations.map(r => r.organizationId));
1074
+ for (const organizationId of organizations) {
1075
+ const organizationPermissions = await this.getOrganizationPermissions(organizationId);
1076
+ if (organizationPermissions && organizationPermissions.hasResourceAccess(PermissionsResourceType.RecordCategories, record.rootCategoryId, level)) {
1077
+ return {
1078
+ canAccess: true,
1079
+ record: record.record,
1080
+ };
1081
+ }
1082
+ }
1083
+ }
1084
+
1046
1085
  return {
1047
1086
  canAccess: false,
1048
1087
  record: record.record,
@@ -1088,8 +1127,8 @@ export class AdminPermissionChecker {
1088
1127
  if (isUserManager) {
1089
1128
  // For a user manager without an organization, we don't delete data, because when registering a new member, it doesn't have any organizations yet...
1090
1129
  if (!(await this.canAccessMember(member, PermissionLevel.Full))) {
1091
- cloned.details.securityCode = null;
1092
1130
  cloned.details.notes = null;
1131
+ // a user manager can see the security codes
1093
1132
  }
1094
1133
 
1095
1134
  return cloned;
@@ -1,6 +1,6 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import { AuditLog, BalanceItem, CachedBalance, Document, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
3
- import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
3
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
  import { Sorter } from '@stamhoofd/utility';
5
5
 
6
6
  import { SQL } from '@stamhoofd/sql';
@@ -58,11 +58,29 @@ export class AuthenticatedStructures {
58
58
 
59
59
  static async groups(groups: Group[]) {
60
60
  const waitingListIds = Formatter.uniqueArray(groups.map(g => g.waitingListId).filter(id => id !== null));
61
- const waitingLists = waitingListIds.length > 0 ? await Group.getByIDs(...waitingListIds) : [];
61
+
62
+ const waitingLists: Group[] = [];
63
+ const waitingListsToFetch: string[] = [];
64
+
65
+ for (const waitingListId of waitingListIds) {
66
+ const existingGroup = groups.find(g => g.id === waitingListId);
67
+
68
+ if (existingGroup) {
69
+ waitingLists.push(existingGroup);
70
+ }
71
+ else {
72
+ waitingListsToFetch.push(waitingListId);
73
+ }
74
+ }
75
+
76
+ if (waitingListsToFetch.length) {
77
+ const fetchedWaitingLists = await Group.getByIDs(...waitingListsToFetch);
78
+ waitingLists.push(...fetchedWaitingLists);
79
+ }
62
80
 
63
81
  const structs: GroupStruct[] = [];
64
82
  for (const group of groups) {
65
- const waitingList = waitingLists.find(g => g.id == group.waitingListId) ?? null;
83
+ const waitingList = waitingLists.find(g => g.id === group.waitingListId) ?? null;
66
84
  const waitingListStruct = waitingList ? GroupStruct.create(waitingList) : null;
67
85
  if (waitingList && waitingListStruct && !await Context.optionalAuth?.canAccessGroup(waitingList)) {
68
86
  waitingListStruct.privateSettings = null;
@@ -88,31 +106,70 @@ export class AuthenticatedStructures {
88
106
  return [];
89
107
  }
90
108
 
109
+ const periodIds = periods ? Formatter.uniqueArray(periods.map(p => p.id)) : Formatter.uniqueArray(organizationRegistrationPeriods.map(p => p.periodId));
110
+
91
111
  if (!periods) {
92
- const periodIds = Formatter.uniqueArray(organizationRegistrationPeriods.map(p => p.periodId));
93
112
  periods = await RegistrationPeriod.getByIDs(...periodIds);
94
113
  }
95
114
 
115
+ const organizationIds = Formatter.uniqueArray(organizationRegistrationPeriods.map(r => r.organizationId));
116
+
96
117
  const groupIds = Formatter.uniqueArray(organizationRegistrationPeriods.flatMap(p => p.settings.categories.flatMap(c => c.groupIds)));
97
- const groups = groupIds.length ? await Group.getByIDs(...groupIds) : [];
118
+
119
+ let groups: Group[] = [];
120
+
121
+ const whereWaitingList = SQL.where('organizationId', organizationIds)
122
+ .and('periodId', periodIds)
123
+ .and('type', GroupType.WaitingList)
124
+ .and('deletedAt', null);
125
+
126
+ if (groupIds.length) {
127
+ const whereGroupIds = SQL.where('id', groupIds);
128
+
129
+ if (organizationIds.length && periodIds.length) {
130
+ groups = await Group.select()
131
+ .where(whereGroupIds)
132
+ .orWhere(whereWaitingList)
133
+ .fetch();
134
+ }
135
+ else {
136
+ groups = await Group.select()
137
+ .where(whereGroupIds).fetch();
138
+ }
139
+ }
140
+ else if (organizationIds.length && periodIds.length) {
141
+ groups = await Group.select()
142
+ .where(whereWaitingList)
143
+ .fetch();
144
+ }
98
145
 
99
146
  const groupStructs = await this.groups(groups);
100
147
 
101
148
  const structs: OrganizationRegistrationPeriodStruct[] = [];
102
149
  for (const organizationPeriod of organizationRegistrationPeriods) {
103
- const period = periods.find(p => p.id == organizationPeriod.periodId) ?? null;
150
+ const period = periods.find(p => p.id === organizationPeriod.periodId) ?? null;
104
151
  if (!period) {
105
152
  continue;
106
153
  }
107
- const groupIds = Formatter.uniqueArray(organizationPeriod.settings.categories.flatMap(c => c.groupIds));
108
154
 
109
- structs.push(
110
- OrganizationRegistrationPeriodStruct.create({
111
- ...organizationPeriod,
112
- period: period.getStructure(),
113
- groups: groupStructs.filter(gg => groupIds.includes(gg.id)).sort(GroupStruct.defaultSort),
114
- }),
115
- );
155
+ const struct = OrganizationRegistrationPeriodStruct.create({
156
+ ...organizationPeriod,
157
+ period: period.getStructure(),
158
+ groups: groupStructs.filter((gg) => {
159
+ if (gg.organizationId !== organizationPeriod.organizationId) {
160
+ return false;
161
+ }
162
+
163
+ if (gg.periodId !== organizationPeriod.periodId) {
164
+ return false;
165
+ }
166
+
167
+ return true;
168
+ })
169
+ .sort(GroupStruct.defaultSort),
170
+ });
171
+
172
+ structs.push(struct);
116
173
  }
117
174
 
118
175
  return structs;
@@ -767,11 +824,11 @@ export class AuthenticatedStructures {
767
824
  ]
768
825
  : []),
769
826
 
770
- ...(member.details.parentsHaveAccess
771
- ? member.details.parents.filter(p => !!p.email).map(p => ReceivableBalanceObjectContact.create({
827
+ ...((member.details.defaultAge <= 18 || member.details.getMemberEmails().length === 0)
828
+ ? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
772
829
  firstName: p.firstName ?? '',
773
830
  lastName: p.lastName ?? '',
774
- emails: [p.email!],
831
+ emails: p.getEmails(),
775
832
  meta: {
776
833
  type: 'parent',
777
834
  responsibilityIds: [],
@@ -810,11 +867,11 @@ export class AuthenticatedStructures {
810
867
  ]
811
868
  : []),
812
869
 
813
- ...(member.details.parentsHaveAccess
814
- ? member.details.parents.filter(p => !!p.email).map(p => ReceivableBalanceObjectContact.create({
870
+ ...((member.details.defaultAge <= 18 || member.details.getMemberEmails().length === 0)
871
+ ? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
815
872
  firstName: p.firstName ?? '',
816
873
  lastName: p.lastName ?? '',
817
- emails: [p.email!],
874
+ emails: p.getEmails(),
818
875
  meta: {
819
876
  type: 'parent',
820
877
  responsibilityIds: [],
@@ -34,7 +34,9 @@ describe('ForwardHandler', () => {
34
34
  },
35
35
  ],
36
36
  subject: 'Hello',
37
- replyTo: 'someone@example.com',
37
+ replyTo: {
38
+ email: 'someone@example.com',
39
+ },
38
40
  });
39
41
  expect(options!.text).toContain('Content hier');
40
42
  });
@@ -67,7 +69,9 @@ describe('ForwardHandler', () => {
67
69
  },
68
70
  ],
69
71
  subject: 'Hello',
70
- replyTo: 'someone@example.com',
72
+ replyTo: {
73
+ email: 'someone@example.com',
74
+ },
71
75
  });
72
76
  expect(options!.text).toContain('Content hier');
73
77
  });
@@ -92,7 +96,9 @@ describe('ForwardHandler', () => {
92
96
  },
93
97
  ],
94
98
  subject: 'Hello',
95
- replyTo: 'someone@example.com',
99
+ replyTo: {
100
+ email: 'someone@example.com',
101
+ },
96
102
  });
97
103
  expect(options!.text).toContain('Content hier');
98
104
 
@@ -119,7 +125,9 @@ describe('ForwardHandler', () => {
119
125
 
120
126
  expect(options).toMatchObject({
121
127
  subject: 'Hello',
122
- replyTo: 'someone@example.com',
128
+ replyTo: {
129
+ email: 'someone@example.com',
130
+ },
123
131
  });
124
132
  expect(options!.to).toIncludeAllMembers([
125
133
  {
@@ -207,7 +215,10 @@ describe('ForwardHandler', () => {
207
215
  dmarcVerdict: { status: 'PASS' },
208
216
  });
209
217
  expect(options).toMatchObject({
210
- to: 'hallo@stamhoofd.be',
218
+ to: [{
219
+ email: 'hallo@stamhoofd.be',
220
+ name: 'Stamhoofd',
221
+ }],
211
222
  subject: 'E-mail unsubscribe mislukt',
212
223
  });
213
224
  });
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
- /* eslint-disable @typescript-eslint/no-redundant-type-constituents */
3
- import { Email, EmailAddress, EmailInterfaceRecipient } from '@stamhoofd/email';
2
+
3
+ import { Email, EmailAddress, EmailInterface, EmailInterfaceRecipient } from '@stamhoofd/email';
4
4
  import { Organization } from '@stamhoofd/models';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { simpleParser } from 'mailparser';
@@ -49,7 +49,7 @@ export class ForwardHandler {
49
49
  // Forward
50
50
  return {
51
51
  from: Email.getWebmasterFromEmail(),
52
- to: Email.getWebmasterToEmail(),
52
+ to: [Email.getWebmasterToEmail()],
53
53
  subject: 'E-mail unsubscribe mislukt',
54
54
  text: 'Beste,\n\nEr werd een unsubscribe gemeld op ' + email + ' die niet kon worden verwerkt. Gelieve dit na te kijken.\n\nStamhoofd',
55
55
  };
@@ -64,7 +64,7 @@ export class ForwardHandler {
64
64
  }
65
65
 
66
66
  // Send a new e-mail
67
- let defaultEmail: EmailInterfaceRecipient[] | string = Email.getWebmasterToEmail();
67
+ let defaultEmail: EmailInterfaceRecipient[] = [Email.getWebmasterToEmail()];
68
68
  let organizationEmails: EmailInterfaceRecipient[] = [];
69
69
  const extraDescription = 'Dit bericht werd verstuurd naar ' + email + ', en werd automatisch doorgestuurd naar alle beheerders. Stel in Stamhoofd de e-mailadressen in om ervoor te zorgen dat antwoorden naar een specifiek e-mailadres worden verstuurd.';
70
70
 
@@ -80,8 +80,12 @@ export class ForwardHandler {
80
80
 
81
81
  // Send back to receiver without including the original message to avoid spam
82
82
  return {
83
- from: email ?? Email.getWebmasterToEmail(),
84
- to: from,
83
+ from: email
84
+ ? {
85
+ email,
86
+ }
87
+ : Email.getWebmasterToEmail(),
88
+ to: [{ email: from }],
85
89
  subject: 'Ongeldig e-mailadres',
86
90
  text: 'Beste,\n\nDe vereniging die je probeert te bereiken via ' + email + ' is helaas niet bereikbaar via dit e-mailadres. Dit e-mailadres wordt enkel gebruikt voor het versturen van automatische e-mails in naam van een vereniging. Probeer de vereniging te contacteren via een ander e-mailadres.\n\nBedankt.',
87
91
  };
@@ -123,10 +127,18 @@ export class ForwardHandler {
123
127
  }
124
128
  }
125
129
 
126
- const options = {
127
- from: email ?? Email.getWebmasterToEmail(),
130
+ const options: EmailInterface = {
131
+ from: email
132
+ ? {
133
+ email,
134
+ }
135
+ : Email.getWebmasterToEmail(),
128
136
  to: defaultEmail,
129
- replyTo: parsed.from?.text,
137
+ replyTo: parsed.from?.text
138
+ ? {
139
+ email: parsed.from?.text,
140
+ }
141
+ : undefined,
130
142
  subject: parsed.subject ?? 'Doorgestuurd bericht',
131
143
  text: parsed.text ? extraDescription + '\n\n' + parsed.text : undefined,
132
144
  html: html,