@stamhoofd/backend 2.73.3 → 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.
- package/index.ts +7 -2
- package/package.json +13 -13
- package/src/audit-logs/MemberPlatformMembershipLogger.ts +1 -1
- package/src/crons/update-cached-balances.ts +1 -2
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -2
- package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -15
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +2 -2
- package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +43 -0
- package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +181 -0
- package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +288 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +2 -2
- package/src/endpoints/global/files/UploadFile.ts +56 -4
- package/src/endpoints/global/files/UploadImage.ts +9 -3
- package/src/endpoints/global/members/GetMembersEndpoint.ts +2 -2
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +14 -5
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -5
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +7 -0
- package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +1 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +1756 -164
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -2
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +48 -2
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +8 -0
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +2 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +1 -2
- package/src/helpers/AdminPermissionChecker.ts +80 -2
- package/src/helpers/AuthenticatedStructures.ts +88 -2
- package/src/helpers/FlagMomentCleanup.ts +1 -8
- package/src/helpers/GlobalHelper.ts +15 -0
- package/src/helpers/MembershipCharger.ts +2 -1
- package/src/seeds-temporary/README.md +1 -0
- package/src/services/EventNotificationService.ts +201 -0
- package/src/services/FileSignService.ts +227 -0
- package/src/services/PlatformMembershipService.ts +38 -14
- package/src/sql-filters/event-notifications.ts +39 -0
- package/src/sql-filters/organizations.ts +1 -1
- package/src/sql-sorters/event-notifications.ts +96 -0
- package/src/sql-sorters/events.ts +2 -2
- package/src/sql-sorters/organizations.ts +2 -2
- package/tests/e2e/private-files.test.ts +497 -0
- package/tests/e2e/register.test.ts +762 -0
- package/tests/helpers/TestServer.ts +3 -0
- package/tests/jest.setup.ts +15 -2
- package/tsconfig.json +1 -0
- /package/src/{seeds → seeds-temporary}/1732117645-move-rrn.ts +0 -0
- /package/src/{seeds → seeds-temporary}/1736266448-recall-balance-item-price-paid.ts +0 -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 =
|
|
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', {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
234
|
+
applySQLSorter(query, q.sort, sorters);
|
|
235
235
|
query.limit(q.limit);
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -428,7 +428,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
428
428
|
throw new SimpleError({
|
|
429
429
|
code: 'invalid_field',
|
|
430
430
|
message: 'Invalid period',
|
|
431
|
-
human: Context.i18n.$t(`
|
|
431
|
+
human: Context.i18n.$t(`62103514-05f5-4dc0-a5cc-c9321f21c63d`),
|
|
432
432
|
field: 'periodId',
|
|
433
433
|
});
|
|
434
434
|
}
|
|
@@ -437,7 +437,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
437
437
|
throw new SimpleError({
|
|
438
438
|
code: 'invalid_field',
|
|
439
439
|
message: 'Invalid period',
|
|
440
|
-
human: Context.i18n.$t(`
|
|
440
|
+
human: Context.i18n.$t(`745f5355-3398-406d-842e-5c9f7a700e91`, { period: period?.getBaseStructure().name }),
|
|
441
441
|
field: 'periodId',
|
|
442
442
|
});
|
|
443
443
|
}
|
|
@@ -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();
|
|
@@ -563,7 +564,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
563
564
|
throw new SimpleError({
|
|
564
565
|
code: 'invalid_field',
|
|
565
566
|
message: 'Invalid period',
|
|
566
|
-
human: Context.i18n.$t(`
|
|
567
|
+
human: Context.i18n.$t(`1f1d657d-bc73-4cae-9025-b3ec67a705e7`),
|
|
567
568
|
field: 'periodId',
|
|
568
569
|
});
|
|
569
570
|
}
|
|
@@ -572,13 +573,21 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
572
573
|
throw new SimpleError({
|
|
573
574
|
code: 'invalid_field',
|
|
574
575
|
message: 'Invalid period',
|
|
575
|
-
human: Context.i18n.$t(`
|
|
576
|
+
human: Context.i18n.$t(`2e615670-813a-414f-b06c-f76136891bf8`, { period: period?.getBaseStructure().name }),
|
|
576
577
|
field: 'periodId',
|
|
577
578
|
});
|
|
578
579
|
}
|
|
579
580
|
}
|
|
580
581
|
|
|
581
|
-
if (!membership.canDelete(
|
|
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
|
-
|
|
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
|
|
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];
|