@stamhoofd/backend 2.91.0 → 2.92.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/package.json +10 -10
- package/src/audit-logs/EmailLogger.ts +4 -4
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +29 -5
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +207 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +67 -22
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
- package/src/helpers/AdminPermissionChecker.ts +81 -5
- package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
- package/src/seeds/1755181288-remove-duplicate-members.ts +145 -0
- package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
- package/src/services/uitpas/UitpasService.ts +71 -2
- package/src/services/uitpas/checkUitpasNumbers.ts +1 -0
- package/src/sql-filters/emails.ts +65 -0
- package/src/sql-sorters/emails.ts +47 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.92.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"@simonbackx/simple-encoding": "2.22.0",
|
|
46
46
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
47
47
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
48
|
-
"@stamhoofd/backend-i18n": "2.
|
|
49
|
-
"@stamhoofd/backend-middleware": "2.
|
|
50
|
-
"@stamhoofd/email": "2.
|
|
51
|
-
"@stamhoofd/models": "2.
|
|
52
|
-
"@stamhoofd/queues": "2.
|
|
53
|
-
"@stamhoofd/sql": "2.
|
|
54
|
-
"@stamhoofd/structures": "2.
|
|
55
|
-
"@stamhoofd/utility": "2.
|
|
48
|
+
"@stamhoofd/backend-i18n": "2.92.0",
|
|
49
|
+
"@stamhoofd/backend-middleware": "2.92.0",
|
|
50
|
+
"@stamhoofd/email": "2.92.0",
|
|
51
|
+
"@stamhoofd/models": "2.92.0",
|
|
52
|
+
"@stamhoofd/queues": "2.92.0",
|
|
53
|
+
"@stamhoofd/sql": "2.92.0",
|
|
54
|
+
"@stamhoofd/structures": "2.92.0",
|
|
55
|
+
"@stamhoofd/utility": "2.92.0",
|
|
56
56
|
"archiver": "^7.0.1",
|
|
57
57
|
"axios": "^1.8.2",
|
|
58
58
|
"cookie": "^0.7.0",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "43d1edfd8061dada1d418a02691fe5fb158aca6a"
|
|
74
74
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Email, EmailRecipient
|
|
2
|
-
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, EmailStatus } from '@stamhoofd/structures';
|
|
1
|
+
import { Email, EmailRecipient } from '@stamhoofd/models';
|
|
2
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, EmailStatus, replaceEmailHtml } from '@stamhoofd/structures';
|
|
3
3
|
import { Formatter } from '@stamhoofd/utility';
|
|
4
4
|
import { ModelLogger } from './ModelLogger';
|
|
5
5
|
|
|
@@ -54,7 +54,7 @@ export const EmailLogger = new ModelLogger(Email, {
|
|
|
54
54
|
const map = new Map([
|
|
55
55
|
['e', AuditLogReplacement.create({
|
|
56
56
|
id: model.id,
|
|
57
|
-
value:
|
|
57
|
+
value: replaceEmailHtml(model.subject ?? '', options.data.recipient?.replacements ?? []),
|
|
58
58
|
type: AuditLogReplacementType.Email,
|
|
59
59
|
})],
|
|
60
60
|
['c', AuditLogReplacement.create({
|
|
@@ -64,7 +64,7 @@ export const EmailLogger = new ModelLogger(Email, {
|
|
|
64
64
|
]);
|
|
65
65
|
if (options.data.recipient) {
|
|
66
66
|
map.set('html', AuditLogReplacement.html(
|
|
67
|
-
|
|
67
|
+
replaceEmailHtml(model.html ?? '', options.data.recipient?.replacements ?? []),
|
|
68
68
|
));
|
|
69
69
|
}
|
|
70
70
|
return map;
|
|
@@ -33,6 +33,12 @@ export async function endFunctionsOfUsersWithoutRegistration() {
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// If period is ending within 15 days, also skip cleanup
|
|
37
|
+
if (period.endDate && period.endDate < new Date(Date.now() + 1000 * 60 * 60 * 24 * 15)) {
|
|
38
|
+
console.warn('Current registration period is ending within 15 days or has ended, skipping cleanup.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
await FlagMomentCleanup.endFunctionsOfUsersWithoutRegistration();
|
|
37
43
|
lastCleanupYear = currentYear;
|
|
38
44
|
lastCleanupMonth = currentMonth;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
-
import { Email, RateLimiter } from '@stamhoofd/models';
|
|
3
|
+
import { Email, Platform, RateLimiter } from '@stamhoofd/models';
|
|
4
4
|
import { EmailPreview, EmailStatus, Email as EmailStruct, EmailTemplate as EmailTemplateStruct } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
6
|
import { Context } from '../../../helpers/Context';
|
|
7
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
8
|
|
|
8
9
|
type Params = Record<string, never>;
|
|
9
10
|
type Query = undefined;
|
|
@@ -64,8 +65,11 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
64
65
|
const organization = await Context.setOptionalOrganizationScope();
|
|
65
66
|
const { user } = await Context.authenticate();
|
|
66
67
|
|
|
67
|
-
if (!Context.auth.canSendEmails()) {
|
|
68
|
-
throw Context.auth.error(
|
|
68
|
+
if (!await Context.auth.canSendEmails(organization)) {
|
|
69
|
+
throw Context.auth.error({
|
|
70
|
+
message: 'Cannot send emails',
|
|
71
|
+
human: $t('f7b7ac75-f7df-49cc-8961-b2478d9683e3'),
|
|
72
|
+
});
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
const model = new Email();
|
|
@@ -79,8 +83,28 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
79
83
|
model.json = request.body.json;
|
|
80
84
|
model.status = request.body.status;
|
|
81
85
|
model.attachments = request.body.attachments;
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
|
|
87
|
+
const list = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
|
|
88
|
+
const sender = list.find(e => e.id === request.body.senderId);
|
|
89
|
+
if (sender) {
|
|
90
|
+
if (!await Context.auth.canSendEmailsFrom(organization, sender.id)) {
|
|
91
|
+
throw Context.auth.error({
|
|
92
|
+
message: 'Cannot send emails from this sender',
|
|
93
|
+
human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
model.senderId = sender.id;
|
|
97
|
+
model.fromAddress = sender.email;
|
|
98
|
+
model.fromName = sender.name;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
throw new SimpleError({
|
|
102
|
+
code: 'invalid_sender',
|
|
103
|
+
human: 'Sender not found',
|
|
104
|
+
message: $t(`94adb4e0-2ef1-4ee8-9f02-5a76efa51c1d`),
|
|
105
|
+
statusCode: 400,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
84
108
|
|
|
85
109
|
model.validateAttachments();
|
|
86
110
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { assertSort, CountFilteredRequest, EmailPreview, EmailStatus, getSortFilter, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
5
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
+
import { Email, Platform } from '@stamhoofd/models';
|
|
7
|
+
import { applySQLSorter, compileToSQLFilter, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
8
|
+
import { Context } from '../../../helpers/Context';
|
|
9
|
+
import { emailFilterCompilers } from '../../../sql-filters/emails';
|
|
10
|
+
import { emailSorters } from '../../../sql-sorters/emails';
|
|
11
|
+
|
|
12
|
+
type Params = Record<string, never>;
|
|
13
|
+
type Query = LimitedFilteredRequest;
|
|
14
|
+
type Body = undefined;
|
|
15
|
+
type ResponseBody = PaginatedResponse<EmailPreview[], LimitedFilteredRequest>;
|
|
16
|
+
|
|
17
|
+
const filterCompilers: SQLFilterDefinitions = emailFilterCompilers;
|
|
18
|
+
const sorters: SQLSortDefinitions<Email> = emailSorters;
|
|
19
|
+
|
|
20
|
+
export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
21
|
+
queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
|
|
22
|
+
|
|
23
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
24
|
+
if (request.method !== 'GET') {
|
|
25
|
+
return [false];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const params = Endpoint.parseParameters(request.url, '/email', {});
|
|
29
|
+
|
|
30
|
+
if (params) {
|
|
31
|
+
return [true, params as Params];
|
|
32
|
+
}
|
|
33
|
+
return [false];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
37
|
+
const organization = Context.organization;
|
|
38
|
+
const user = Context.user;
|
|
39
|
+
if (!user) {
|
|
40
|
+
throw new Error('Not authenticated');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
44
|
+
|
|
45
|
+
const canReadAllEmails = await Context.auth.canReadAllEmails(organization ?? null);
|
|
46
|
+
scopeFilter = {
|
|
47
|
+
organizationId: organization?.id ?? null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (!canReadAllEmails) {
|
|
51
|
+
const senders = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
|
|
52
|
+
const ids: string[] = [];
|
|
53
|
+
for (const sender of senders) {
|
|
54
|
+
if (await Context.auth.canReadAllEmails(organization ?? null, sender.id)) {
|
|
55
|
+
ids.push(sender.id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (ids.length === 0) {
|
|
59
|
+
throw Context.auth.error();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
scopeFilter = {
|
|
63
|
+
$and: [
|
|
64
|
+
{
|
|
65
|
+
organizationId: organization?.id ?? null,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
$or: [
|
|
69
|
+
{
|
|
70
|
+
senderId: {
|
|
71
|
+
$in: ids,
|
|
72
|
+
},
|
|
73
|
+
status: {
|
|
74
|
+
$neq: EmailStatus.Draft,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
userId: user.id,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
scopeFilter = {
|
|
87
|
+
$and: [
|
|
88
|
+
{
|
|
89
|
+
organizationId: organization?.id ?? null,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
$or: [
|
|
93
|
+
{
|
|
94
|
+
status: {
|
|
95
|
+
$neq: EmailStatus.Draft,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
userId: user.id,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const query = Email.select();
|
|
108
|
+
|
|
109
|
+
if (scopeFilter) {
|
|
110
|
+
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (q.filter) {
|
|
114
|
+
query.where(await compileToSQLFilter(q.filter, filterCompilers));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (q.search) {
|
|
118
|
+
let searchFilter: StamhoofdFilter | null = null;
|
|
119
|
+
|
|
120
|
+
searchFilter = {
|
|
121
|
+
subject: {
|
|
122
|
+
$contains: q.search,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (searchFilter) {
|
|
127
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (q instanceof LimitedFilteredRequest) {
|
|
132
|
+
if (q.pageFilter) {
|
|
133
|
+
query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
137
|
+
applySQLSorter(query, q.sort, sorters);
|
|
138
|
+
query.limit(q.limit);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log('Building query for GetAdminEmailsEndpoint', query.getSQL());
|
|
142
|
+
|
|
143
|
+
return query;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
147
|
+
const query = await GetAdminEmailsEndpoint.buildQuery(requestQuery);
|
|
148
|
+
const emails = await query.fetch();
|
|
149
|
+
|
|
150
|
+
let next: LimitedFilteredRequest | undefined;
|
|
151
|
+
|
|
152
|
+
if (emails.length >= requestQuery.limit) {
|
|
153
|
+
const lastObject = emails[emails.length - 1];
|
|
154
|
+
const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
|
|
155
|
+
|
|
156
|
+
next = new LimitedFilteredRequest({
|
|
157
|
+
filter: requestQuery.filter,
|
|
158
|
+
pageFilter: nextFilter,
|
|
159
|
+
sort: requestQuery.sort,
|
|
160
|
+
limit: requestQuery.limit,
|
|
161
|
+
search: requestQuery.search,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
|
|
165
|
+
console.error('Found infinite loading loop for', requestQuery);
|
|
166
|
+
next = undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return new PaginatedResponse<EmailPreview[], LimitedFilteredRequest>({
|
|
171
|
+
results: await Promise.all(emails.map(email => email.getPreviewStructure())),
|
|
172
|
+
next,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
177
|
+
const organization = await Context.setOptionalOrganizationScope();
|
|
178
|
+
await Context.authenticate();
|
|
179
|
+
|
|
180
|
+
if (!await Context.auth.canReadEmails(organization)) {
|
|
181
|
+
// This is a first fast check, we'll limit it later in the scope query
|
|
182
|
+
throw Context.auth.error();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
186
|
+
|
|
187
|
+
if (request.query.limit > maxLimit) {
|
|
188
|
+
throw new SimpleError({
|
|
189
|
+
code: 'invalid_field',
|
|
190
|
+
field: 'limit',
|
|
191
|
+
message: 'Limit can not be more than ' + maxLimit,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (request.query.limit < 1) {
|
|
196
|
+
throw new SimpleError({
|
|
197
|
+
code: 'invalid_field',
|
|
198
|
+
field: 'limit',
|
|
199
|
+
message: 'Limit can not be less than 1',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new Response(
|
|
204
|
+
await GetAdminEmailsEndpoint.buildData(request.query),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -32,7 +32,7 @@ export class GetEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody
|
|
|
32
32
|
const organization = await Context.setOptionalOrganizationScope();
|
|
33
33
|
const { user } = await Context.authenticate();
|
|
34
34
|
|
|
35
|
-
if (!Context.auth.
|
|
35
|
+
if (!await Context.auth.canReadEmails(organization)) {
|
|
36
36
|
throw Context.auth.error();
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -46,6 +46,10 @@ export class GetEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
if (!await Context.auth.canAccessEmail(model)) {
|
|
50
|
+
throw Context.auth.error();
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
return new Response(await model.getPreviewStructure());
|
|
50
54
|
}
|
|
51
55
|
}
|