@stamhoofd/backend 2.79.8 → 2.80.1

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.
@@ -81,7 +81,7 @@ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Bod
81
81
  if (index === 0) {
82
82
  notification.startDate = model.startDate;
83
83
  notification.endDate = model.endDate;
84
- const period = await RegistrationPeriod.getByDate(event.startDate);
84
+ const period = await RegistrationPeriod.getByDate(model.startDate);
85
85
 
86
86
  if (!period) {
87
87
  throw new SimpleError({
@@ -141,7 +141,7 @@ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Bod
141
141
  if (
142
142
  notification.status === EventNotificationStatus.Pending
143
143
  || notification.status === EventNotificationStatus.Accepted
144
- || (patch.status && patch.status !== EventNotificationStatus.Pending)
144
+ || (patch.status && (patch.status !== EventNotificationStatus.Pending || (notification.status !== EventNotificationStatus.Draft && notification.status !== EventNotificationStatus.Rejected && notification.status !== EventNotificationStatus.PartiallyAccepted)))
145
145
  || patch.feedbackText !== undefined
146
146
  ) {
147
147
  requiredPermissionLevel = PermissionLevel.Full;
@@ -187,6 +187,7 @@ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Bod
187
187
  // Only allowed if complete
188
188
  await this.validateAnswers(notification);
189
189
  }
190
+ const previousStatus = notification.status;
190
191
  notification.status = patch.status; // checks already happened
191
192
  if (patch.status === EventNotificationStatus.Pending) {
192
193
  notification.submittedBy = user.id;
@@ -198,15 +199,30 @@ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Bod
198
199
  await EventNotificationService.sendReviewerEmail(EmailTemplateType.EventNotificationSubmittedReviewer, notification);
199
200
  }
200
201
 
201
- if (patch.status === EventNotificationStatus.Accepted) {
202
+ if ((patch.status === EventNotificationStatus.Accepted) && previousStatus !== EventNotificationStatus.Accepted) {
203
+ // Make sure the accepted record answers stay in sync
204
+ notification.acceptedRecordAnswers = notification.recordAnswers;
205
+
202
206
  await EventNotificationService.sendSubmitterEmail(EmailTemplateType.EventNotificationAccepted, notification);
203
207
  }
204
208
 
209
+ if ((patch.status === EventNotificationStatus.PartiallyAccepted) && previousStatus !== EventNotificationStatus.Accepted && previousStatus !== EventNotificationStatus.PartiallyAccepted) {
210
+ // Make sure the accepted record answers stay in sync
211
+ notification.acceptedRecordAnswers = notification.recordAnswers;
212
+
213
+ await EventNotificationService.sendSubmitterEmail(EmailTemplateType.EventNotificationPartiallyAccepted, notification);
214
+ }
215
+
205
216
  if (patch.status === EventNotificationStatus.Rejected) {
206
217
  await EventNotificationService.sendSubmitterEmail(EmailTemplateType.EventNotificationRejected, notification);
207
218
  }
208
219
  }
209
220
 
221
+ if (notification.status === EventNotificationStatus.Accepted) {
222
+ // Make sure the accepted record answers stay in sync (only for full accepted, since these cannot be changed)
223
+ notification.acceptedRecordAnswers = notification.recordAnswers;
224
+ }
225
+
210
226
  await notification.save();
211
227
  notifications.push(notification);
212
228
  }
@@ -37,12 +37,12 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
37
37
  return [false];
38
38
  }
39
39
 
40
- static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
40
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest, permissionLevel: PermissionLevel = PermissionLevel.Read) {
41
41
  const organization = Context.organization;
42
42
  let scopeFilter: StamhoofdFilter | undefined = undefined;
43
43
 
44
44
  if (!organization && !Context.auth.canAccessAllPlatformMembers()) {
45
- const tags = Context.auth.getPlatformAccessibleOrganizationTags(PermissionLevel.Read);
45
+ const tags = Context.auth.getPlatformAccessibleOrganizationTags(permissionLevel);
46
46
  if (tags !== 'all' && tags.length === 0) {
47
47
  throw Context.auth.error();
48
48
  }
@@ -68,14 +68,14 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
68
68
 
69
69
  if (organization && !Context.auth.canAccessAllPlatformMembers()) {
70
70
  // Add organization scope filter
71
- const groups = await Context.auth.getAccessibleGroups(organization.id);
71
+ const groups = await Context.auth.getAccessibleGroups(organization.id, permissionLevel);
72
72
 
73
73
  if (groups.length === 0) {
74
74
  throw Context.auth.error();
75
75
  }
76
76
 
77
77
  if (groups === 'all') {
78
- if (await Context.auth.hasFullAccess(organization.id)) {
78
+ if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
79
79
  // Can access full history for now
80
80
  scopeFilter = {
81
81
  registrations: {
@@ -233,8 +233,8 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
233
233
  return query;
234
234
  }
235
235
 
236
- static async buildData(requestQuery: LimitedFilteredRequest) {
237
- const query = await GetMembersEndpoint.buildQuery(requestQuery);
236
+ static async buildData(requestQuery: LimitedFilteredRequest, permissionLevel = PermissionLevel.Read) {
237
+ const query = await GetMembersEndpoint.buildQuery(requestQuery, permissionLevel);
238
238
  let data: SQLResultNamespacedRow[];
239
239
 
240
240
  try {
@@ -263,7 +263,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
263
263
  const members = memberIds.map(id => _members.find(m => m.id === id)!);
264
264
 
265
265
  for (const member of members) {
266
- if (!await Context.auth.canAccessMember(member, PermissionLevel.Read)) {
266
+ if (!await Context.auth.canAccessMember(member, permissionLevel)) {
267
267
  throw Context.auth.error();
268
268
  }
269
269
  }
@@ -133,9 +133,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
133
133
  value: STAMHOOFD.domains.registrationCname + '.',
134
134
  }));
135
135
  }
136
- }
137
136
 
138
- if (request.body.mailDomain !== null) {
139
137
  let priv: string;
140
138
  let pub: string;
141
139
 
@@ -158,9 +156,18 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
158
156
  // DKIM records
159
157
  organization.privateMeta.dnsRecords.push(DNSRecord.create({
160
158
  type: DNSRecordType.TXT,
161
- name: 'stamhoofd._domainkey.' + request.body.mailDomain + '.',
159
+ name: Formatter.slug(STAMHOOFD.platformName) + '._domainkey.' + organization.privateMeta.pendingMailDomain + '.',
162
160
  value: 'v=DKIM1; k=rsa; p=' + pub + '',
163
161
  }));
162
+
163
+ // DMARC records
164
+ organization.privateMeta.dnsRecords.push(DNSRecord.create({
165
+ type: DNSRecordType.TXT,
166
+ name: '_dmarc.' + organization.privateMeta.pendingMailDomain + '.',
167
+ value: 'v=DMARC1; p=quarantine; pct=100; sp=quarantine; aspf=r; adkim=r;',
168
+ description: 'Opgelet met het instellen van deze DMARC-record voor je domeinnaam. Mogelijks bestaat er al een record met deze naam, voeg deze dan zeker niet dubbel toe en behoud best de huidige waarde (wel zou aspf en adkim op r moeten staan). De waarde die we voorstellen zorgt voor een sterke beveiliging, maar kan mogelijks problemen veroorzaken als je andere diensten gebruikt die op een onveilige manier emails versturen (zonder DKIM of SPF).',
169
+ optional: true,
170
+ }));
164
171
  }
165
172
  else {
166
173
  if (oldMailDomain) {
@@ -1,10 +1,10 @@
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, GroupType, 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, RecordSettings } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
- import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
7
6
  import { MemberRecordStore } from '../services/MemberRecordStore';
7
+ import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
8
8
 
9
9
  /**
10
10
  * One class with all the responsabilities of checking permissions to each resource in the system by a given user, possibly in an organization context.
@@ -1091,7 +1091,7 @@ export class AdminPermissionChecker {
1091
1091
  }
1092
1092
 
1093
1093
  async getAccessibleGroups(organizationId: string, level: PermissionLevel = PermissionLevel.Read): Promise<string[] | 'all'> {
1094
- if (await this.hasFullAccess(organizationId)) {
1094
+ if (await this.hasFullAccess(organizationId, level)) {
1095
1095
  return 'all';
1096
1096
  }
1097
1097
 
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
2
 
3
3
  import { Email, EmailAddress, EmailInterface, EmailInterfaceRecipient } from '@stamhoofd/email';
4
- import { Organization } from '@stamhoofd/models';
4
+ import { Organization, Platform } from '@stamhoofd/models';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { simpleParser } from 'mailparser';
7
7
 
@@ -66,7 +66,8 @@ export class ForwardHandler {
66
66
  // Send a new e-mail
67
67
  let defaultEmail: EmailInterfaceRecipient[] = [Email.getWebmasterToEmail()];
68
68
  let organizationEmails: EmailInterfaceRecipient[] = [];
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.';
69
+ const platform = await Platform.getShared();
70
+ const extraDescription = 'Dit bericht werd verstuurd naar ' + email + ', en werd automatisch doorgestuurd naar alle beheerders. Stel in ' + platform.config.name + ' de e-mailadressen in om ervoor te zorgen dat antwoorden naar een specifiek e-mailadres worden verstuurd.';
70
71
 
71
72
  function doBounce() {
72
73
  if (!from) {
@@ -0,0 +1,39 @@
1
+ import { BalanceItem } from '@stamhoofd/models';
2
+ import { BalanceItemType, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
3
+ import { BalanceItemService } from '../services/BalanceItemService';
4
+
5
+ export class MemberCharger {
6
+ static async chargeMany({ chargingOrganizationId, membersToCharge, price, amount, description, dueAt, createdAt }: { chargingOrganizationId: string; membersToCharge: MemberWithRegistrationsBlob[]; price: number; amount?: number; description: string; dueAt: Date | null; createdAt: Date | null }) {
7
+ const balanceItems = membersToCharge.map(memberBeingCharged => MemberCharger.createBalanceItem({
8
+ price,
9
+ amount,
10
+ description,
11
+ chargingOrganizationId,
12
+ memberBeingCharged,
13
+ dueAt,
14
+ createdAt,
15
+ }));
16
+
17
+ await Promise.all(balanceItems.map(balanceItem => balanceItem.save()));
18
+ await BalanceItem.updateOutstanding(balanceItems);
19
+
20
+ // Reallocate
21
+ await BalanceItemService.reallocate(balanceItems, chargingOrganizationId);
22
+ }
23
+
24
+ private static createBalanceItem({ price, amount, description, chargingOrganizationId, memberBeingCharged, dueAt, createdAt }: { price: number; amount?: number; description: string; chargingOrganizationId: string; memberBeingCharged: MemberWithRegistrationsBlob; dueAt: Date | null; createdAt: Date | null }): BalanceItem {
25
+ const balanceItem = new BalanceItem();
26
+ balanceItem.unitPrice = price;
27
+ balanceItem.amount = amount ?? 1;
28
+ balanceItem.description = description;
29
+ balanceItem.type = BalanceItemType.Other;
30
+ balanceItem.memberId = memberBeingCharged.id;
31
+ balanceItem.organizationId = chargingOrganizationId;
32
+ balanceItem.dueAt = dueAt;
33
+ if (createdAt !== null) {
34
+ balanceItem.createdAt = createdAt;
35
+ }
36
+
37
+ return balanceItem;
38
+ }
39
+ }
@@ -1,32 +1,17 @@
1
- import { SimpleError } from '@simonbackx/simple-errors';
2
- import { BalanceItem, Platform } from '@stamhoofd/models';
1
+ import { BalanceItem } from '@stamhoofd/models';
3
2
  import { BalanceItemType, Organization as OrganizationStruct } from '@stamhoofd/structures';
4
3
  import { BalanceItemService } from '../services/BalanceItemService';
5
4
 
6
5
  export class OrganizationCharger {
7
- static async chargeFromPlatform(args: { organizationsToCharge: OrganizationStruct[]; price: number; amount?: number; description: string }) {
8
- const platform = await Platform.getShared();
9
-
10
- const chargeVia = platform.membershipOrganizationId;
11
-
12
- if (!chargeVia) {
13
- throw new SimpleError({
14
- code: 'missing_membership_organization',
15
- message: 'Missing membershipOrganizationId',
16
- human: 'Er is geen lokale groep verantwoordelijk voor de aanrekening van aansluitingen geconfigureerd',
17
- });
18
- }
19
-
20
- await OrganizationCharger.chargeMany({ chargingOrganizationId: chargeVia, ...args });
21
- }
22
-
23
- static async chargeMany({ chargingOrganizationId, organizationsToCharge, price, amount, description }: { chargingOrganizationId: string; organizationsToCharge: OrganizationStruct[]; price: number; amount?: number; description: string }) {
6
+ static async chargeMany({ chargingOrganizationId, organizationsToCharge, price, amount, description, dueAt, createdAt }: { chargingOrganizationId: string; organizationsToCharge: OrganizationStruct[]; price: number; amount?: number; description: string; dueAt: Date | null; createdAt: Date | null }) {
24
7
  const balanceItems = organizationsToCharge.map(organizationBeingCharged => OrganizationCharger.createBalanceItem({
25
8
  price,
26
9
  amount,
27
10
  description,
28
11
  chargingOrganizationId,
29
12
  organizationBeingCharged,
13
+ dueAt,
14
+ createdAt,
30
15
  }));
31
16
 
32
17
  await Promise.all(balanceItems.map(balanceItem => balanceItem.save()));
@@ -36,7 +21,7 @@ export class OrganizationCharger {
36
21
  await BalanceItemService.reallocate(balanceItems, chargingOrganizationId);
37
22
  }
38
23
 
39
- private static createBalanceItem({ price, amount, description, chargingOrganizationId, organizationBeingCharged }: { price: number; amount?: number; description: string; chargingOrganizationId: string; organizationBeingCharged: OrganizationStruct }): BalanceItem {
24
+ private static createBalanceItem({ price, amount, description, chargingOrganizationId, organizationBeingCharged, dueAt, createdAt }: { price: number; amount?: number; description: string; chargingOrganizationId: string; organizationBeingCharged: OrganizationStruct; dueAt: Date | null; createdAt: Date | null }): BalanceItem {
40
25
  const balanceItem = new BalanceItem();
41
26
  balanceItem.unitPrice = price;
42
27
  balanceItem.amount = amount ?? 1;
@@ -44,6 +29,10 @@ export class OrganizationCharger {
44
29
  balanceItem.type = BalanceItemType.Other;
45
30
  balanceItem.payingOrganizationId = organizationBeingCharged.id;
46
31
  balanceItem.organizationId = chargingOrganizationId;
32
+ balanceItem.dueAt = dueAt;
33
+ if (createdAt !== null) {
34
+ balanceItem.createdAt = createdAt;
35
+ }
47
36
 
48
37
  return balanceItem;
49
38
  }
@@ -171,6 +171,7 @@ export class EventNotificationService {
171
171
 
172
172
  static async sendSubmitterEmail(type: EmailTemplateType, notification: EventNotification) {
173
173
  if (notification.endDate < new Date()) {
174
+ console.log('Skipped submitter email because it is in the past');
174
175
  // Ignore
175
176
  return;
176
177
  }
@@ -186,6 +187,8 @@ export class EventNotificationService {
186
187
 
187
188
  static async sendReviewerEmail(type: EmailTemplateType, notification: EventNotification) {
188
189
  if (notification.endDate < new Date()) {
190
+ console.log('Skipped reviewer email because it is in the past');
191
+
189
192
  // Ignore
190
193
  return;
191
194
  }