@stamhoofd/backend 2.86.0 → 2.87.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.
- package/package.json +12 -12
- package/src/crons/delete-old-email-drafts.ts +37 -0
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -2
- package/src/crons/index.ts +1 -0
- package/src/endpoints/global/files/UploadFile.ts +4 -34
- package/src/endpoints/global/members/GetMembersEndpoint.ts +1 -0
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +14 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +2 -2
- package/src/helpers/AdminPermissionChecker.ts +18 -8
- package/src/helpers/AuthenticatedStructures.ts +36 -21
- package/src/helpers/FlagMomentCleanup.ts +14 -2
- package/src/seeds/1751445358-upload-email-attachments.ts +106 -0
- package/src/services/EventNotificationService.ts +1 -1
- package/src/sql-filters/members.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.87.1",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"lint": "eslint"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@types/cookie": "^0.
|
|
26
|
+
"@types/cookie": "^0.6.0",
|
|
27
27
|
"@types/luxon": "3.4.2",
|
|
28
28
|
"@types/mailparser": "3.4.4",
|
|
29
29
|
"@types/mysql": "^2.15.20",
|
|
@@ -44,17 +44,17 @@
|
|
|
44
44
|
"@simonbackx/simple-encoding": "2.22.0",
|
|
45
45
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
46
46
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
47
|
-
"@stamhoofd/backend-i18n": "2.
|
|
48
|
-
"@stamhoofd/backend-middleware": "2.
|
|
49
|
-
"@stamhoofd/email": "2.
|
|
50
|
-
"@stamhoofd/models": "2.
|
|
51
|
-
"@stamhoofd/queues": "2.
|
|
52
|
-
"@stamhoofd/sql": "2.
|
|
53
|
-
"@stamhoofd/structures": "2.
|
|
54
|
-
"@stamhoofd/utility": "2.
|
|
47
|
+
"@stamhoofd/backend-i18n": "2.87.1",
|
|
48
|
+
"@stamhoofd/backend-middleware": "2.87.1",
|
|
49
|
+
"@stamhoofd/email": "2.87.1",
|
|
50
|
+
"@stamhoofd/models": "2.87.1",
|
|
51
|
+
"@stamhoofd/queues": "2.87.1",
|
|
52
|
+
"@stamhoofd/sql": "2.87.1",
|
|
53
|
+
"@stamhoofd/structures": "2.87.1",
|
|
54
|
+
"@stamhoofd/utility": "2.87.1",
|
|
55
55
|
"archiver": "^7.0.1",
|
|
56
56
|
"axios": "^1.8.2",
|
|
57
|
-
"cookie": "^0.
|
|
57
|
+
"cookie": "^0.7.0",
|
|
58
58
|
"formidable": "3.5.4",
|
|
59
59
|
"handlebars": "^4.7.7",
|
|
60
60
|
"jsonwebtoken": "9.0.0",
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"publishConfig": {
|
|
70
70
|
"access": "public"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "a9a8e07e38224fe34e805e400b1a46d5c286a10a"
|
|
73
73
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { registerCron } from '@stamhoofd/crons';
|
|
2
|
+
import { Email } from '@stamhoofd/models';
|
|
3
|
+
import { EmailStatus } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
let lastRunDate: number | null = null;
|
|
6
|
+
|
|
7
|
+
registerCron('deleteOldEmailDrafts', deleteOldEmailDrafts);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run every night at 5 AM.
|
|
11
|
+
*/
|
|
12
|
+
export async function deleteOldEmailDrafts() {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
|
|
15
|
+
if (now.getDate() === lastRunDate) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const hour = now.getHours();
|
|
20
|
+
|
|
21
|
+
// between 5 and 6 AM
|
|
22
|
+
if (hour !== 5 && STAMHOOFD.environment !== 'development') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Clear old email drafts older than 7 days
|
|
27
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
28
|
+
const result = await Email.delete()
|
|
29
|
+
.where('status', EmailStatus.Draft)
|
|
30
|
+
.where('createdAt', '<', sevenDaysAgo)
|
|
31
|
+
.where('updatedAt', '<', sevenDaysAgo)
|
|
32
|
+
.where('sentAt', null);
|
|
33
|
+
|
|
34
|
+
console.log(`Deleted ${result.affectedRows} old email drafts.`);
|
|
35
|
+
|
|
36
|
+
lastRunDate = now.getDate();
|
|
37
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { registerCron } from '@stamhoofd/crons';
|
|
2
2
|
import { FlagMomentCleanup } from '../helpers/FlagMomentCleanup';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// Only delete responsibilities when the server is running during a month change.
|
|
5
|
+
// Chances are almost zero that we reboot during a month change
|
|
6
|
+
// Running on every reboot also would have unintended consequences
|
|
7
|
+
const now = new Date();
|
|
8
|
+
let lastCleanupYear: number = now.getFullYear();
|
|
9
|
+
let lastCleanupMonth: number = now.getMonth();
|
|
6
10
|
|
|
7
11
|
registerCron('endFunctionsOfUsersWithoutRegistration', endFunctionsOfUsersWithoutRegistration);
|
|
8
12
|
|
package/src/crons/index.ts
CHANGED
|
@@ -135,40 +135,9 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
135
135
|
|
|
136
136
|
// Also include the source, in private mode
|
|
137
137
|
const fileId = uuidv4();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
switch (file.mimetype?.toLocaleLowerCase()) {
|
|
141
|
-
case 'image/jpeg':
|
|
142
|
-
case 'image/jpg':
|
|
143
|
-
uploadExt = 'jpg';
|
|
144
|
-
break;
|
|
145
|
-
case 'image/png':
|
|
146
|
-
uploadExt = 'png';
|
|
147
|
-
break;
|
|
148
|
-
case 'image/gif':
|
|
149
|
-
uploadExt = 'gif';
|
|
150
|
-
break;
|
|
151
|
-
case 'image/webp':
|
|
152
|
-
uploadExt = 'webp';
|
|
153
|
-
break;
|
|
154
|
-
case 'image/svg+xml':
|
|
155
|
-
uploadExt = 'svg';
|
|
156
|
-
break;
|
|
157
|
-
case 'application/pdf':
|
|
158
|
-
uploadExt = 'pdf';
|
|
159
|
-
break;
|
|
160
|
-
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
|
|
161
|
-
case 'application/vnd.ms-excel':
|
|
162
|
-
uploadExt = 'xlsx';
|
|
163
|
-
break;
|
|
164
|
-
|
|
165
|
-
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
|
|
166
|
-
case 'application/msword':
|
|
167
|
-
uploadExt = 'docx';
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
138
|
+
const uploadExt = File.contentTypeToExtension(file.mimetype ?? '') ?? '';
|
|
170
139
|
|
|
171
|
-
const filenameWithoutExt = file.originalFilename
|
|
140
|
+
const filenameWithoutExt = file.originalFilename ? File.removeExtension(file.originalFilename) : fileId;
|
|
172
141
|
const key = prefix + fileId + '/' + (Formatter.slug(filenameWithoutExt) + (uploadExt ? ('.' + uploadExt) : ''));
|
|
173
142
|
|
|
174
143
|
const fileStruct = new File({
|
|
@@ -178,6 +147,7 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
178
147
|
size: fileContent.length,
|
|
179
148
|
name: file.originalFilename,
|
|
180
149
|
isPrivate: request.query.isPrivate,
|
|
150
|
+
contentType: file.mimetype ?? null,
|
|
181
151
|
});
|
|
182
152
|
|
|
183
153
|
// Generate an upload signature for this file if it is private
|
|
@@ -196,7 +166,7 @@ export class UploadFile extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
196
166
|
Bucket: STAMHOOFD.SPACES_BUCKET,
|
|
197
167
|
Key: key,
|
|
198
168
|
Body: fileContent,
|
|
199
|
-
ContentType: file.mimetype ?? 'application/
|
|
169
|
+
ContentType: file.mimetype ?? 'application/octet-stream',
|
|
200
170
|
ACL: request.query.isPrivate ? 'private' : 'public-read',
|
|
201
171
|
});
|
|
202
172
|
await Image.getS3Client().send(cmd);
|
|
@@ -286,6 +286,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
286
286
|
|
|
287
287
|
for (const member of members) {
|
|
288
288
|
if (!await Context.auth.canAccessMember(member, permissionLevel)) {
|
|
289
|
+
console.error('Unexpected member returned', member.id, requestQuery, query.getSQL());
|
|
289
290
|
throw Context.auth.error();
|
|
290
291
|
}
|
|
291
292
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
-
import { EmailVerificationCode, Organization, User } from '@stamhoofd/models';
|
|
5
|
-
import { CreateOrganization, PermissionLevel, Permissions, SignupResponse, UserPermissions } from '@stamhoofd/structures';
|
|
4
|
+
import { EmailVerificationCode, Organization, RegistrationPeriod, User } from '@stamhoofd/models';
|
|
5
|
+
import { CreateOrganization, PermissionLevel, Permissions, RegistrationPeriodSettings, SignupResponse, UserPermissions } from '@stamhoofd/structures';
|
|
6
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
7
|
|
|
8
8
|
type Params = Record<string, never>;
|
|
@@ -85,6 +85,16 @@ export class CreateOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
85
85
|
organization.address = request.body.organization.address;
|
|
86
86
|
organization.privateMeta.acquisitionTypes = request.body.organization.privateMeta?.acquisitionTypes ?? [];
|
|
87
87
|
|
|
88
|
+
const period = new RegistrationPeriod();
|
|
89
|
+
|
|
90
|
+
// WIP
|
|
91
|
+
period.settings = RegistrationPeriodSettings.create({});
|
|
92
|
+
period.startDate = new Date();
|
|
93
|
+
period.endDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * 31); // 1 month
|
|
94
|
+
|
|
95
|
+
await period.save();
|
|
96
|
+
organization.periodId = period.id;
|
|
97
|
+
|
|
88
98
|
try {
|
|
89
99
|
await organization.save();
|
|
90
100
|
}
|
|
@@ -96,6 +106,8 @@ export class CreateOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
96
106
|
statusCode: 500,
|
|
97
107
|
});
|
|
98
108
|
}
|
|
109
|
+
period.organizationId = organization.id;
|
|
110
|
+
await period.save();
|
|
99
111
|
|
|
100
112
|
const user = await User.register(
|
|
101
113
|
organization,
|
|
@@ -294,8 +294,8 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
294
294
|
});
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
const maximumStart = 1000 * 60 * 60 * 24 * 31 *
|
|
298
|
-
if (
|
|
297
|
+
const maximumStart = 1000 * 60 * 60 * 24 * 31 * 8; // 8 months in advance
|
|
298
|
+
if (period.startDate > new Date(Date.now() + maximumStart)) {
|
|
299
299
|
throw new SimpleError({
|
|
300
300
|
code: 'invalid_field',
|
|
301
301
|
message: 'Period start date is too far in the future',
|
|
@@ -181,8 +181,10 @@ export class AdminPermissionChecker {
|
|
|
181
181
|
const organization = await this.getOrganization(group.organizationId);
|
|
182
182
|
|
|
183
183
|
if (group.periodId !== organization.periodId) {
|
|
184
|
-
if (
|
|
185
|
-
|
|
184
|
+
if (STAMHOOFD.userMode === 'organization' || group.periodId !== this.platform.period.id) {
|
|
185
|
+
if (!await this.hasFullAccess(group.organizationId)) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
|
|
@@ -940,7 +942,7 @@ export class AdminPermissionChecker {
|
|
|
940
942
|
/**
|
|
941
943
|
* Return a list of RecordSettings the current user can view or edit
|
|
942
944
|
*/
|
|
943
|
-
async hasFinancialMemberAccess(member: MemberWithRegistrations, level: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
|
|
945
|
+
async hasFinancialMemberAccess(member: MemberWithRegistrations, level: PermissionLevel = PermissionLevel.Read, organizationId?: string): Promise<boolean> {
|
|
944
946
|
const isUserManager = this.isUserManager(member);
|
|
945
947
|
|
|
946
948
|
if (isUserManager && level === PermissionLevel.Read) {
|
|
@@ -959,11 +961,19 @@ export class AdminPermissionChecker {
|
|
|
959
961
|
organizations.push(await this.getOrganization(member.organizationId));
|
|
960
962
|
}
|
|
961
963
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
964
|
+
else {
|
|
965
|
+
if (organizationId) {
|
|
966
|
+
if (this.checkScope(organizationId)) {
|
|
967
|
+
organizations.push(await this.getOrganization(organizationId));
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
for (const registration of member.registrations) {
|
|
972
|
+
if (this.checkScope(registration.organizationId)) {
|
|
973
|
+
if (!organizations.find(o => o.id === registration.organizationId)) {
|
|
974
|
+
organizations.push(await this.getOrganization(registration.organizationId));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
967
977
|
}
|
|
968
978
|
}
|
|
969
979
|
}
|
|
@@ -400,6 +400,10 @@ export class AuthenticatedStructures {
|
|
|
400
400
|
await BalanceItemService.flushCaches(Context.organization.id);
|
|
401
401
|
}
|
|
402
402
|
const balances = await CachedBalance.getForObjects(registrationIds, null);
|
|
403
|
+
const memberIds = members.map(m => m.id);
|
|
404
|
+
const allMemberBalances = Context.organization
|
|
405
|
+
? (await CachedBalance.getForObjects(memberIds, Context.organization.id))
|
|
406
|
+
: [];
|
|
403
407
|
|
|
404
408
|
if (includeUser) {
|
|
405
409
|
for (const organizationId of includeUser.permissions?.organizationPermissions.keys() ?? []) {
|
|
@@ -451,15 +455,27 @@ export class AuthenticatedStructures {
|
|
|
451
455
|
}
|
|
452
456
|
}
|
|
453
457
|
member.registrations = member.registrations.filter(r => (Context.auth.organization && Context.auth.organization.active && r.organizationId === Context.auth.organization.id) || (organizations.get(r.organizationId)?.active ?? false));
|
|
454
|
-
const balancesPermission = await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read);
|
|
458
|
+
const balancesPermission = await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, Context.organization?.id);
|
|
459
|
+
|
|
460
|
+
let memberBalances: GenericBalance[] = [];
|
|
461
|
+
|
|
462
|
+
if (balancesPermission && Context.organization) {
|
|
463
|
+
// Only return balances if in an organization scope and you have permission for the finances of that specific organization AND member
|
|
464
|
+
memberBalances = allMemberBalances
|
|
465
|
+
.filter(b => member.id === b.objectId && b.objectType === ReceivableBalanceType.member)
|
|
466
|
+
.map((b) => {
|
|
467
|
+
return GenericBalance.create(b);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
455
470
|
|
|
456
471
|
const blob = MemberWithRegistrationsBlob.create({
|
|
457
472
|
...member,
|
|
473
|
+
balances: memberBalances,
|
|
458
474
|
registrations: member.registrations.map((r) => {
|
|
459
475
|
const base = r.getStructure();
|
|
460
476
|
|
|
461
477
|
base.balances = balancesPermission
|
|
462
|
-
? (balances.filter(b => r.id === b.objectId).map((b) => {
|
|
478
|
+
? (balances.filter(b => r.id === b.objectId && b.objectType === ReceivableBalanceType.registration).map((b) => {
|
|
463
479
|
return GenericBalance.create(b);
|
|
464
480
|
}))
|
|
465
481
|
: [];
|
|
@@ -579,7 +595,6 @@ export class AuthenticatedStructures {
|
|
|
579
595
|
|
|
580
596
|
return RegistrationWithMemberBlob.create({
|
|
581
597
|
...registration,
|
|
582
|
-
balances: memberBlob.registrations.find(r => r.id === registration.id)?.balances ?? [],
|
|
583
598
|
member: memberBlob,
|
|
584
599
|
});
|
|
585
600
|
});
|
|
@@ -874,15 +889,15 @@ export class AuthenticatedStructures {
|
|
|
874
889
|
|
|
875
890
|
...((member.details.defaultAge <= 18 || member.details.getMemberEmails().length === 0)
|
|
876
891
|
? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
892
|
+
firstName: p.firstName ?? '',
|
|
893
|
+
lastName: p.lastName ?? '',
|
|
894
|
+
emails: p.getEmails(),
|
|
895
|
+
meta: {
|
|
896
|
+
type: 'parent',
|
|
897
|
+
responsibilityIds: [],
|
|
898
|
+
url,
|
|
899
|
+
},
|
|
900
|
+
}))
|
|
886
901
|
: []),
|
|
887
902
|
],
|
|
888
903
|
});
|
|
@@ -917,15 +932,15 @@ export class AuthenticatedStructures {
|
|
|
917
932
|
|
|
918
933
|
...((member.details.defaultAge <= 18 || member.details.getMemberEmails().length === 0)
|
|
919
934
|
? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
935
|
+
firstName: p.firstName ?? '',
|
|
936
|
+
lastName: p.lastName ?? '',
|
|
937
|
+
emails: p.getEmails(),
|
|
938
|
+
meta: {
|
|
939
|
+
type: 'parent',
|
|
940
|
+
responsibilityIds: [],
|
|
941
|
+
url,
|
|
942
|
+
},
|
|
943
|
+
}))
|
|
929
944
|
: []),
|
|
930
945
|
],
|
|
931
946
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Group, MemberResponsibilityRecord, Platform, Registration } from '@stamhoofd/models';
|
|
2
|
-
import { SQL, SQLWhereExists
|
|
1
|
+
import { Group, Member, MemberResponsibilityRecord, Platform, Registration } from '@stamhoofd/models';
|
|
2
|
+
import { SQL, SQLWhereExists } from '@stamhoofd/sql';
|
|
3
3
|
import { GroupType } from '@stamhoofd/structures';
|
|
4
|
+
import { MemberUserSyncer } from './MemberUserSyncer';
|
|
4
5
|
|
|
5
6
|
export class FlagMomentCleanup {
|
|
6
7
|
/**
|
|
@@ -16,6 +17,11 @@ export class FlagMomentCleanup {
|
|
|
16
17
|
responsibility.endDate = now;
|
|
17
18
|
await responsibility.save();
|
|
18
19
|
console.log(`Ended responsibility with id ${responsibility.id}`);
|
|
20
|
+
|
|
21
|
+
const member = await Member.getByID(responsibility.memberId);
|
|
22
|
+
if (member) {
|
|
23
|
+
await MemberUserSyncer.onChangeMember(member);
|
|
24
|
+
}
|
|
19
25
|
}));
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -50,9 +56,15 @@ export class FlagMomentCleanup {
|
|
|
50
56
|
).where(
|
|
51
57
|
SQL.column(Registration.table, 'deactivatedAt'),
|
|
52
58
|
null,
|
|
59
|
+
).whereNot(
|
|
60
|
+
SQL.column(Registration.table, 'registeredAt'),
|
|
61
|
+
null,
|
|
53
62
|
).where(
|
|
54
63
|
SQL.column(Group.table, 'type'),
|
|
55
64
|
GroupType.Membership,
|
|
65
|
+
).where(
|
|
66
|
+
SQL.column(Group.table, 'deletedAt'),
|
|
67
|
+
null,
|
|
56
68
|
),
|
|
57
69
|
),
|
|
58
70
|
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { PutObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
3
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
5
|
+
import { Email, Image } from '@stamhoofd/models';
|
|
6
|
+
import { File } from '@stamhoofd/structures';
|
|
7
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
8
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Replace all base64 email attachments with file attachments.
|
|
12
|
+
*/
|
|
13
|
+
export default new Migration(async () => {
|
|
14
|
+
if (STAMHOOFD.environment == 'test') {
|
|
15
|
+
console.log('skipped in tests');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
process.stdout.write('\n');
|
|
20
|
+
let c = 0;
|
|
21
|
+
|
|
22
|
+
await logger.setContext({ tags: ['seed'] }, async () => {
|
|
23
|
+
for await (const email of Email.select().limit(10).all()) {
|
|
24
|
+
let save = false;
|
|
25
|
+
for (const attachment of email.attachments) {
|
|
26
|
+
if (attachment.content) {
|
|
27
|
+
try {
|
|
28
|
+
console.log('Uploading base64 attachment for email ' + email.id);
|
|
29
|
+
save = true;
|
|
30
|
+
|
|
31
|
+
let prefix = (STAMHOOFD.SPACES_PREFIX ?? '');
|
|
32
|
+
if (prefix.length > 0) {
|
|
33
|
+
prefix += '/';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
prefix += (STAMHOOFD.environment ?? 'development') === 'development' ? ('development/') : ('');
|
|
37
|
+
// Prepend user id to the file path
|
|
38
|
+
// Private files
|
|
39
|
+
if (email.userId) {
|
|
40
|
+
prefix += 'users/' + email.userId + '/';
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Public files
|
|
44
|
+
prefix += 'p/';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fileContent = Buffer.from(attachment.content, 'base64');
|
|
48
|
+
|
|
49
|
+
const fileId = uuidv4();
|
|
50
|
+
const uploadExt = File.contentTypeToExtension(attachment.contentType ?? '') ?? '';
|
|
51
|
+
const filenameWithoutExt = File.removeExtension(attachment.filename);
|
|
52
|
+
const key = prefix + fileId + '/' + (Formatter.slug(filenameWithoutExt) + (uploadExt ? ('.' + uploadExt) : ''));
|
|
53
|
+
|
|
54
|
+
const fileStruct = new File({
|
|
55
|
+
id: fileId,
|
|
56
|
+
server: 'https://' + STAMHOOFD.SPACES_BUCKET + '.' + STAMHOOFD.SPACES_ENDPOINT,
|
|
57
|
+
path: key,
|
|
58
|
+
size: fileContent.length,
|
|
59
|
+
name: attachment.filename,
|
|
60
|
+
isPrivate: true,
|
|
61
|
+
contentType: attachment.contentType ?? null,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Generate an upload signature for this file if it is private
|
|
65
|
+
if (!await fileStruct.sign()) {
|
|
66
|
+
throw new SimpleError({
|
|
67
|
+
code: 'failed_to_sign',
|
|
68
|
+
message: 'Failed to sign file',
|
|
69
|
+
statusCode: 500,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const cmd = new PutObjectCommand({
|
|
74
|
+
Bucket: STAMHOOFD.SPACES_BUCKET,
|
|
75
|
+
Key: key,
|
|
76
|
+
Body: fileContent,
|
|
77
|
+
ContentType: attachment.contentType ?? 'application/octet-stream',
|
|
78
|
+
ACL: 'private',
|
|
79
|
+
});
|
|
80
|
+
await Image.getS3Client().send(cmd);
|
|
81
|
+
|
|
82
|
+
console.log('Successfully uploaded base64 attachment for email ' + email.id + ' as file ' + fileStruct.id);
|
|
83
|
+
|
|
84
|
+
attachment.file = fileStruct;
|
|
85
|
+
attachment.content = null; // Clear the base64 content
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
console.error('Failed to upload base64 attachment for email ' + email.id, e);
|
|
89
|
+
continue; // Skip this email if it fails
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (save) {
|
|
95
|
+
await email.save();
|
|
96
|
+
c++;
|
|
97
|
+
process.stdout.write('.');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log('\nUpdated attachments for ' + c + ' emails');
|
|
103
|
+
|
|
104
|
+
// Do something here
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
});
|
|
@@ -152,7 +152,7 @@ export class EventNotificationService {
|
|
|
152
152
|
}),
|
|
153
153
|
Replacement.create({
|
|
154
154
|
token: 'reviewUrl',
|
|
155
|
-
value: forReviewers ? Context.i18n.localizedDomains.adminUrl + '/kampmeldingen/' + encodeURIComponent(notification.id) : (events.length === 0 ? organization.getBaseStructure().dashboardUrl : (organization.getBaseStructure().dashboardUrl + '/activiteiten/' + events[0].id + '/' + Formatter.slug(type.title))),
|
|
155
|
+
value: forReviewers ? Context.i18n.localizedDomains.adminUrl() + '/kampmeldingen/' + encodeURIComponent(notification.id) : (events.length === 0 ? organization.getBaseStructure().dashboardUrl : (organization.getBaseStructure().dashboardUrl + '/activiteiten/' + events[0].id + '/' + Formatter.slug(type.title))),
|
|
156
156
|
}),
|
|
157
157
|
Replacement.create({
|
|
158
158
|
token: 'dateRange',
|
|
@@ -170,7 +170,11 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
|
|
|
170
170
|
SQL.column('groups', 'deletedAt'),
|
|
171
171
|
null,
|
|
172
172
|
),
|
|
173
|
-
|
|
173
|
+
{
|
|
174
|
+
...baseRegistrationFilterCompilers,
|
|
175
|
+
// Override the registration periodId - can be outdated - and always use the group periodId
|
|
176
|
+
periodId: createSQLColumnFilterCompiler(SQL.column('groups', 'periodId')),
|
|
177
|
+
},
|
|
174
178
|
),
|
|
175
179
|
'responsibilities': createSQLRelationFilterCompiler(
|
|
176
180
|
SQL.select()
|