@stamhoofd/backend 2.74.0 → 2.75.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 (47) hide show
  1. package/index.ts +7 -2
  2. package/package.json +13 -13
  3. package/src/crons/update-cached-balances.ts +1 -2
  4. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -2
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -15
  6. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +2 -2
  7. package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +43 -0
  8. package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +181 -0
  9. package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
  10. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +288 -0
  11. package/src/endpoints/global/events/PatchEventsEndpoint.ts +2 -2
  12. package/src/endpoints/global/files/UploadFile.ts +56 -4
  13. package/src/endpoints/global/files/UploadImage.ts +9 -3
  14. package/src/endpoints/global/members/GetMembersEndpoint.ts +2 -2
  15. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +10 -1
  16. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -5
  17. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +7 -0
  18. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +1 -1
  19. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +1756 -164
  20. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -2
  21. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +48 -2
  22. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +2 -2
  23. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
  24. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +2 -2
  25. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +2 -2
  26. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +8 -0
  27. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +3 -3
  28. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +2 -2
  29. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +2 -2
  30. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +1 -2
  31. package/src/helpers/AdminPermissionChecker.ts +80 -2
  32. package/src/helpers/AuthenticatedStructures.ts +88 -2
  33. package/src/helpers/FlagMomentCleanup.ts +1 -8
  34. package/src/helpers/GlobalHelper.ts +15 -0
  35. package/src/helpers/MembershipCharger.ts +2 -1
  36. package/src/services/EventNotificationService.ts +201 -0
  37. package/src/services/FileSignService.ts +227 -0
  38. package/src/sql-filters/event-notifications.ts +39 -0
  39. package/src/sql-filters/organizations.ts +1 -1
  40. package/src/sql-sorters/event-notifications.ts +96 -0
  41. package/src/sql-sorters/events.ts +2 -2
  42. package/src/sql-sorters/organizations.ts +2 -2
  43. package/tests/e2e/private-files.test.ts +497 -0
  44. package/tests/e2e/register.test.ts +762 -0
  45. package/tests/helpers/TestServer.ts +3 -0
  46. package/tests/jest.setup.ts +15 -2
  47. package/tsconfig.json +1 -0
@@ -0,0 +1,288 @@
1
+ import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, patchObject, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { Event, EventNotification, RegistrationPeriod, Platform } from '@stamhoofd/models';
4
+ import { EmailTemplateType, EventNotificationStatus, EventNotification as EventNotificationStruct, PermissionLevel, RecordCategory } from '@stamhoofd/structures';
5
+
6
+ import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
7
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
8
+ import { Context } from '../../../helpers/Context';
9
+ import { EventNotificationService } from '../../../services/EventNotificationService';
10
+
11
+ type Params = Record<string, never>;
12
+ type Query = undefined;
13
+ type Body = PatchableArrayAutoEncoder<EventNotificationStruct>;
14
+ type ResponseBody = EventNotificationStruct[];
15
+
16
+ export class PatchEventNotificationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
17
+ bodyDecoder = new PatchableArrayDecoder(EventNotificationStruct as Decoder<EventNotificationStruct>, EventNotificationStruct.patchType() as Decoder<AutoEncoderPatchType<EventNotificationStruct>>, StringDecoder);
18
+
19
+ protected doesMatch(request: Request): [true, Params] | [false] {
20
+ if (request.method !== 'PATCH') {
21
+ return [false];
22
+ }
23
+
24
+ const params = Endpoint.parseParameters(request.url, '/event-notifications', {});
25
+
26
+ if (params) {
27
+ return [true, params as Params];
28
+ }
29
+ return [false];
30
+ }
31
+
32
+ async handle(request: DecodedRequest<Params, Query, Body>) {
33
+ const organization = await Context.setOptionalOrganizationScope();
34
+ const { user } = await Context.authenticate();
35
+
36
+ if (organization) {
37
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
38
+ throw Context.auth.error();
39
+ }
40
+ }
41
+ else {
42
+ if (!Context.auth.hasSomePlatformAccess()) {
43
+ throw Context.auth.error();
44
+ }
45
+ }
46
+
47
+ const notifications: EventNotification[] = [];
48
+
49
+ for (const { put } of request.body.getPuts()) {
50
+ if (put.events.length === 0) {
51
+ // Required for authentication
52
+ throw new SimpleError({
53
+ code: 'invalid_field',
54
+ message: 'At least one event is required',
55
+ field: 'events',
56
+ });
57
+ }
58
+
59
+ const notification = new EventNotification();
60
+ notification.organizationId = put.organization.id;
61
+ notification.typeId = put.typeId;
62
+ const type = await this.validateType(notification);
63
+
64
+ const validatedEvents: Event[] = [];
65
+
66
+ for (const [index, event] of put.events.entries()) {
67
+ const model = await Event.getByID(event.id);
68
+ if (!model || model.organizationId !== notification.organizationId) {
69
+ throw new SimpleError({
70
+ code: 'invalid_field',
71
+ message: 'Invalid event',
72
+ human: 'Dit evenement bestaat niet of is niet van jouw organisatie',
73
+ field: 'events',
74
+ });
75
+ }
76
+ if (!await Context.auth.canAccessEvent(model)) {
77
+ throw Context.auth.error('Cannot access event');
78
+ }
79
+ validatedEvents.push(model);
80
+
81
+ if (index === 0) {
82
+ notification.startDate = model.startDate;
83
+ notification.endDate = model.endDate;
84
+ const period = await RegistrationPeriod.getByDate(event.startDate);
85
+
86
+ if (!period) {
87
+ throw new SimpleError({
88
+ code: 'invalid_period',
89
+ message: 'No period found for this start date',
90
+ human: Context.i18n.$t('5959a6a9-064a-413c-871f-c74a145ed569'),
91
+ field: 'startDate',
92
+ });
93
+ }
94
+
95
+ if (period.locked) {
96
+ throw new SimpleError({
97
+ code: 'invalid_period',
98
+ message: 'Period is locked',
99
+ human: Context.i18n.$t('97616151-90c3-4644-8854-e228c4f355f5'),
100
+ field: 'startDate',
101
+ });
102
+ }
103
+
104
+ notification.periodId = period.id;
105
+ }
106
+ else {
107
+ await this.validateEventDate(notification, model);
108
+ }
109
+ }
110
+
111
+ notification.recordAnswers = put.recordAnswers;
112
+ await EventNotificationService.cleanAnswers(notification);
113
+ notification.createdBy = user.id;
114
+ notification.status = EventNotificationStatus.Draft;
115
+
116
+ // More + validation
117
+
118
+ await notification.save();
119
+
120
+ // Link events
121
+ await EventNotification.events.link(notification, validatedEvents);
122
+
123
+ notifications.push(notification);
124
+ }
125
+
126
+ const patchingNotifications = await EventNotification.getByIDs(...request.body.getPatches().map(p => p.id));
127
+
128
+ for (const patch of request.body.getPatches()) {
129
+ const notification = patchingNotifications.find(e => e.id === patch.id);
130
+
131
+ if (!notification) {
132
+ throw new SimpleError({
133
+ code: 'not_found',
134
+ message: 'EventNotification not found',
135
+ human: Context.i18n.$t('a0c39573-d44e-4ac0-aaeb-f9062fa1b3ce'),
136
+ });
137
+ }
138
+
139
+ let requiredPermissionLevel = PermissionLevel.Write;
140
+
141
+ if (
142
+ notification.status === EventNotificationStatus.Pending
143
+ || notification.status === EventNotificationStatus.Accepted
144
+ || (patch.status && patch.status !== EventNotificationStatus.Pending)
145
+ || patch.feedbackText !== undefined
146
+ ) {
147
+ requiredPermissionLevel = PermissionLevel.Full;
148
+ }
149
+
150
+ if (!await Context.auth.canAccessEventNotification(notification, requiredPermissionLevel)) {
151
+ // Requires `OrganizationEventNotificationReviewer` access right for the organization
152
+ if (notification.status === EventNotificationStatus.Pending) {
153
+ throw Context.auth.error(Context.i18n.$t('c5dcd14a-868e-4eba-a5dd-409932e44ce9'));
154
+ }
155
+ if (notification.status === EventNotificationStatus.Accepted) {
156
+ throw Context.auth.error(Context.i18n.$t('289e2f29-cdc9-4f44-92de-d7188d6563d7'));
157
+ }
158
+ throw Context.auth.error(Context.i18n.$t('b47ce42b-ac72-451e-b871-deb07d93b5fa'));
159
+ }
160
+
161
+ const period = await RegistrationPeriod.getByID(notification.periodId);
162
+ if (!period) {
163
+ throw new SimpleError({
164
+ code: 'not_found',
165
+ message: 'Period not found',
166
+ human: Context.i18n.$t('16a3b696-f8da-4b2b-94b7-49c85ee1c38c'),
167
+ });
168
+ }
169
+
170
+ if (period.locked) {
171
+ throw new SimpleError({
172
+ code: 'invalid_period',
173
+ message: 'Period is locked',
174
+ human: Context.i18n.$t('43cc054d-cf2f-432b-9259-e7764dc929e3'),
175
+ });
176
+ }
177
+
178
+ // Save answers
179
+ notification.recordAnswers = patchObject(notification.recordAnswers, patch.recordAnswers);
180
+ if (patch.recordAnswers?.size) {
181
+ await EventNotificationService.cleanAnswers(notification);
182
+ }
183
+ notification.feedbackText = patchObject(notification.feedbackText, patch.feedbackText);
184
+
185
+ if (patch.status && patch.status !== notification.status) {
186
+ if (patch.status !== EventNotificationStatus.Rejected && patch.status !== EventNotificationStatus.Draft) {
187
+ // Only allowed if complete
188
+ await this.validateAnswers(notification);
189
+ }
190
+ notification.status = patch.status; // checks already happened
191
+ if (patch.status === EventNotificationStatus.Pending) {
192
+ notification.submittedBy = user.id;
193
+ notification.submittedAt = new Date();
194
+ }
195
+
196
+ if (patch.status === EventNotificationStatus.Pending) {
197
+ await EventNotificationService.sendSubmitterEmail(EmailTemplateType.EventNotificationSubmittedCopy, notification);
198
+ await EventNotificationService.sendReviewerEmail(EmailTemplateType.EventNotificationSubmittedReviewer, notification);
199
+ }
200
+
201
+ if (patch.status === EventNotificationStatus.Accepted) {
202
+ await EventNotificationService.sendSubmitterEmail(EmailTemplateType.EventNotificationAccepted, notification);
203
+ }
204
+
205
+ if (patch.status === EventNotificationStatus.Rejected) {
206
+ await EventNotificationService.sendSubmitterEmail(EmailTemplateType.EventNotificationRejected, notification);
207
+ }
208
+ }
209
+
210
+ await notification.save();
211
+ notifications.push(notification);
212
+ }
213
+
214
+ for (const id of request.body.getDeletes()) {
215
+ const notification = await EventNotification.getByID(id);
216
+
217
+ if (!notification) {
218
+ throw new SimpleError({
219
+ code: 'not_found',
220
+ message: 'EventNotification not found',
221
+ human: Context.i18n.$t('a0c39573-d44e-4ac0-aaeb-f9062fa1b3ce'),
222
+ });
223
+ }
224
+
225
+ if (!await Context.auth.canAccessEventNotification(notification, PermissionLevel.Full)) {
226
+ throw Context.auth.error();
227
+ }
228
+
229
+ await notification.delete();
230
+ }
231
+
232
+ const structures = await AuthenticatedStructures.eventNotifications(notifications);
233
+ return new Response(
234
+ structures,
235
+ );
236
+ }
237
+
238
+ async validateType(notification: EventNotification) {
239
+ const platform = await Platform.getSharedPrivateStruct();
240
+ const type = platform.config.eventNotificationTypes.find(t => t.id === notification.typeId);
241
+
242
+ if (!type) {
243
+ throw new SimpleError({
244
+ code: 'invalid_field',
245
+ message: 'Invalid type',
246
+ human: Context.i18n.$t('4d8be2b1-559a-4c16-a76f-67a8ba85de7f'),
247
+ field: 'typeId',
248
+ });
249
+ }
250
+
251
+ return type;
252
+ }
253
+
254
+ async validateAnswers(notification: EventNotification) {
255
+ const type = await this.validateType(notification);
256
+ const struct = await AuthenticatedStructures.eventNotification(notification);
257
+
258
+ try {
259
+ RecordCategory.validate(type.recordCategories, struct);
260
+ }
261
+ catch (e) {
262
+ if (isSimpleError(e) || isSimpleErrors(e)) {
263
+ e.addNamespace('recordAnswers');
264
+ }
265
+ throw e;
266
+ }
267
+ }
268
+
269
+ async validateEventDate(notification: EventNotification, event: Event) {
270
+ if (notification.startDate !== event.startDate) {
271
+ throw new SimpleError({
272
+ code: 'invalid_field',
273
+ message: 'Invalid start date',
274
+ human: Context.i18n.$t('daa3726b-9b63-4f36-b41b-8f25d1b89cf4'),
275
+ field: 'startDate',
276
+ });
277
+ }
278
+
279
+ if (notification.endDate !== event.endDate) {
280
+ throw new SimpleError({
281
+ code: 'invalid_field',
282
+ message: 'Invalid end date',
283
+ human: Context.i18n.$t('d326210d-ecc0-421c-b38b-d12ae0209420'),
284
+ field: 'endDate',
285
+ });
286
+ }
287
+ }
288
+ }
@@ -11,7 +11,7 @@ import { Context } from '../../../helpers/Context';
11
11
  import { AuditLogService } from '../../../services/AuditLogService';
12
12
  import { PatchOrganizationRegistrationPeriodsEndpoint } from '../../organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint';
13
13
 
14
- type Params = { id: string };
14
+ type Params = Record<string, never>;
15
15
  type Query = undefined;
16
16
  type Body = PatchableArrayAutoEncoder<EventStruct>;
17
17
  type ResponseBody = EventStruct[];
@@ -24,7 +24,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
24
24
  return [false];
25
25
  }
26
26
 
27
- const params = Endpoint.parseParameters(request.url, '/events', { id: String });
27
+ const params = Endpoint.parseParameters(request.url, '/events', {});
28
28
 
29
29
  if (params) {
30
30
  return [true, params as Params];
@@ -9,9 +9,14 @@ import { v4 as uuidv4 } from 'uuid';
9
9
 
10
10
  import { Context } from '../../../helpers/Context';
11
11
  import { limiter } from './UploadImage';
12
+ import { AutoEncoder, BooleanDecoder, Decoder, field } from '@simonbackx/simple-encoding';
12
13
 
13
14
  type Params = Record<string, never>;
14
- type Query = Record<string, never>;
15
+ class Query extends AutoEncoder {
16
+ @field({ decoder: BooleanDecoder, optional: true, field: 'private' })
17
+ isPrivate: boolean = false;
18
+ }
19
+
15
20
  type Body = undefined;
16
21
  type ResponseBody = File;
17
22
 
@@ -33,6 +38,8 @@ interface FormidableFile {
33
38
  }
34
39
 
35
40
  export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
41
+ queryDecoder = Query as Decoder<Query>;
42
+
36
43
  protected doesMatch(request: Request): [true, Params] | [false] {
37
44
  if (request.method !== 'POST') {
38
45
  return [false];
@@ -121,15 +128,47 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
121
128
 
122
129
  // Also include the source, in private mode
123
130
  const fileId = uuidv4();
124
- const uploadExt = file.mimetype == 'application/pdf' ? 'pdf' : 'pdf';
131
+ let uploadExt = '';
132
+
133
+ switch (file.mimetype?.toLocaleLowerCase()) {
134
+ case 'image/jpeg':
135
+ case 'image/jpg':
136
+ uploadExt = 'jpg';
137
+ break;
138
+ case 'image/png':
139
+ uploadExt = 'png';
140
+ break;
141
+ case 'image/gif':
142
+ uploadExt = 'gif';
143
+ break;
144
+ case 'image/webp':
145
+ uploadExt = 'webp';
146
+ break;
147
+ case 'image/svg+xml':
148
+ uploadExt = 'svg';
149
+ break;
150
+ case 'application/pdf':
151
+ uploadExt = 'pdf';
152
+ break;
153
+ case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
154
+ case 'application/vnd.ms-excel':
155
+ uploadExt = 'xlsx';
156
+ break;
157
+
158
+ case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
159
+ case 'application/msword':
160
+ uploadExt = 'docx';
161
+ break;
162
+ }
163
+
125
164
  const filenameWithoutExt = file.originalFilename?.split('.').slice(0, -1).join('.') ?? fileId;
126
- const key = prefix + (STAMHOOFD.environment ?? 'development') + '/' + fileId + '/' + (Formatter.slug(filenameWithoutExt) + '.' + uploadExt);
165
+ const key = prefix + (STAMHOOFD.environment ?? 'development') + '/' + fileId + '/' + (Formatter.slug(filenameWithoutExt) + (uploadExt ? ('.' + uploadExt) : ''));
127
166
  const params = {
128
167
  Bucket: STAMHOOFD.SPACES_BUCKET,
129
168
  Key: key,
130
169
  Body: fileContent, // TODO
131
170
  ContentType: file.mimetype ?? 'application/pdf',
132
- ACL: 'public-read',
171
+ ACL: request.query.isPrivate ? 'private' : 'public-read',
133
172
  };
134
173
 
135
174
  const fileStruct = new File({
@@ -138,8 +177,21 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
138
177
  path: key,
139
178
  size: fileContent.length,
140
179
  name: file.originalFilename,
180
+ isPrivate: request.query.isPrivate,
141
181
  });
142
182
 
183
+ // Generate an upload signature for this file if it is private
184
+ if (request.query.isPrivate) {
185
+ if (!await fileStruct.sign()) {
186
+ throw new SimpleError({
187
+ code: 'failed_to_sign',
188
+ message: 'Failed to sign file',
189
+ human: $t('Er ging iet mis bij het uploaden van jouw bestand. Probeer het later opnieuw (foutcode: SIGN).'),
190
+ statusCode: 500,
191
+ });
192
+ }
193
+ }
194
+
143
195
  await s3.putObject(params).promise();
144
196
 
145
197
  return new Response(fileStruct);
@@ -1,4 +1,4 @@
1
- import { Decoder, ObjectData } from '@simonbackx/simple-encoding';
1
+ import { AutoEncoder, BooleanDecoder, Decoder, field, ObjectData } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Image, RateLimiter } from '@stamhoofd/models';
@@ -9,7 +9,10 @@ import { promises as fs } from 'fs';
9
9
  import { Context } from '../../../helpers/Context';
10
10
 
11
11
  type Params = Record<string, never>;
12
- type Query = Record<string, never>;
12
+ class Query extends AutoEncoder {
13
+ @field({ decoder: BooleanDecoder, optional: true, field: 'private' })
14
+ isPrivate: boolean = false;
15
+ }
13
16
  type Body = undefined;
14
17
  type ResponseBody = ImageStruct;
15
18
 
@@ -41,6 +44,8 @@ export const limiter = new RateLimiter({
41
44
  });
42
45
 
43
46
  export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
47
+ queryDecoder = Query as Decoder<Query>;
48
+
44
49
  protected doesMatch(request: Request): [true, Params] | [false] {
45
50
  if (request.method !== 'POST') {
46
51
  return [false];
@@ -114,6 +119,7 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
114
119
  }));
115
120
  return;
116
121
  }
122
+
117
123
  try {
118
124
  const resolutions = new ObjectData(JSON.parse(fields.resolutions[0]), { version: request.request.getVersion() }).array(ResolutionRequest as Decoder<ResolutionRequest>);
119
125
  resolve([files.file[0], resolutions]);
@@ -125,7 +131,7 @@ export class UploadImage extends Endpoint<Params, Query, Body, ResponseBody> {
125
131
  });
126
132
 
127
133
  const fileContent = await fs.readFile(file.filepath);
128
- const image = await Image.create(fileContent, file.mimetype ?? undefined, resolutions);
134
+ const image = await Image.create(fileContent, file.mimetype ?? undefined, resolutions, request.query.isPrivate);
129
135
  return new Response(ImageStruct.create(image));
130
136
  }
131
137
  }
@@ -2,7 +2,7 @@ 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
4
  import { Member, Platform } from '@stamhoofd/models';
5
- import { SQL, compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
5
+ import { SQL, compileToSQLFilter, applySQLSorter } from '@stamhoofd/sql';
6
6
  import { CountFilteredRequest, Country, CountryCode, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
7
7
  import { DataValidator } from '@stamhoofd/utility';
8
8
 
@@ -231,7 +231,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
231
231
  }
232
232
 
233
233
  q.sort = assertSort(q.sort, [{ key: 'id' }]);
234
- query.orderBy(compileToSQLSorter(q.sort, sorters));
234
+ applySQLSorter(query, q.sort, sorters);
235
235
  query.limit(q.limit);
236
236
  }
237
237
 
@@ -532,6 +532,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
532
532
  membership.startDate = new Date(Math.max(Date.now(), put.startDate.getTime()));
533
533
  membership.endDate = put.endDate;
534
534
  membership.expireDate = put.expireDate;
535
+ membership.locked = put.locked;
535
536
 
536
537
  await membership.calculatePrice(member);
537
538
  await membership.save();
@@ -578,7 +579,15 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
578
579
  }
579
580
  }
580
581
 
581
- if (!membership.canDelete() && !Context.auth.hasPlatformFullAccess()) {
582
+ if (!membership.canDelete(Context.auth.hasPlatformFullAccess())) {
583
+ if (membership.locked) {
584
+ throw new SimpleError({
585
+ code: 'invalid_field',
586
+ message: 'Invalid invoice',
587
+ human: 'Je kan geen aansluiting verwijderen die vergrendeld is',
588
+ });
589
+ }
590
+
582
591
  throw new SimpleError({
583
592
  code: 'invalid_field',
584
593
  message: 'Invalid invoice',
@@ -32,11 +32,7 @@ export class GetPlatformAdminsEndpoint extends Endpoint<Params, Query, Body, Res
32
32
  }
33
33
 
34
34
  // Get all admins
35
- let admins = await User.where({ organizationId: null, permissions: { sign: '!=', value: null } });
36
-
37
- // Hide api accounts
38
- admins = admins.filter(a => !a.isApiUser);
39
- admins = admins.filter(a => !!a.permissions?.globalPermissions);
35
+ const admins = await User.getPlatformAdmins();
40
36
 
41
37
  return new Response(
42
38
  await AuthenticatedStructures.usersWithMembers(admins),
@@ -154,6 +154,13 @@ export class PatchPlatformEndpoint extends Endpoint<
154
154
  message: 'Invalid period',
155
155
  });
156
156
  }
157
+ if (period.locked) {
158
+ throw new SimpleError({
159
+ code: 'cannot_set_locked_period',
160
+ message: 'Platform period cannot be set to a locked period',
161
+ human: 'Er kan niet overgeschakeld worden naar een vergrendeld werkjaar',
162
+ });
163
+ }
157
164
  platform.periodId = period.id;
158
165
  shouldUpdateSetupSteps = true;
159
166
  shouldMoveToPeriod = period;
@@ -12,7 +12,7 @@ type ResponseBody = DocumentStruct[];
12
12
  /**
13
13
  * Get the members of the user
14
14
  */
15
- export class GetUserMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ export class GetUserDocumentsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
16
  protected doesMatch(request: Request): [true, Params] | [false] {
17
17
  if (request.method !== 'GET') {
18
18
  return [false];