@stamhoofd/backend 2.94.0 → 2.95.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/endpoints/global/email/GetAdminEmailsEndpoint.ts +25 -36
- package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -2
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +163 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +13 -2
- package/src/endpoints/global/registration/GetUserDetailedPayableBalanceEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +25 -11
- package/src/seeds/1752848561-groups-registration-periods.ts +715 -0
- package/src/sql-filters/email-recipients.ts +25 -0
- package/src/sql-filters/emails.ts +35 -2
- package/src/seeds/1752848560-groups-registration-periods.ts +0 -768
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.95.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.95.0",
|
|
49
|
+
"@stamhoofd/backend-middleware": "2.95.0",
|
|
50
|
+
"@stamhoofd/email": "2.95.0",
|
|
51
|
+
"@stamhoofd/models": "2.95.0",
|
|
52
|
+
"@stamhoofd/queues": "2.95.0",
|
|
53
|
+
"@stamhoofd/sql": "2.95.0",
|
|
54
|
+
"@stamhoofd/structures": "2.95.0",
|
|
55
|
+
"@stamhoofd/utility": "2.95.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": "baed3abb7d0814204ae52004bfec7287e397d28e"
|
|
74
74
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { assertSort, CountFilteredRequest, EmailPreview, EmailStatus, getSortFilter, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter } from '@stamhoofd/structures';
|
|
2
|
+
import { assertSort, CountFilteredRequest, EmailPreview, EmailStatus, getSortFilter, LimitedFilteredRequest, mergeFilters, PaginatedResponse, PermissionLevel, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
4
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
@@ -40,12 +40,15 @@ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, Respon
|
|
|
40
40
|
throw new Error('Not authenticated');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
let scopeFilter: StamhoofdFilter
|
|
43
|
+
let scopeFilter: StamhoofdFilter = null;
|
|
44
44
|
|
|
45
45
|
const canReadAllEmails = await Context.auth.canReadAllEmails(organization ?? null);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
|
|
47
|
+
if (organization || Context.auth.getPlatformAccessibleOrganizationTags(PermissionLevel.Full) !== 'all') {
|
|
48
|
+
scopeFilter = {
|
|
49
|
+
organizationId: organization?.id ?? null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
49
52
|
|
|
50
53
|
if (!canReadAllEmails) {
|
|
51
54
|
const senders = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
|
|
@@ -59,49 +62,35 @@ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, Respon
|
|
|
59
62
|
throw Context.auth.error();
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
scopeFilter = {
|
|
63
|
-
$
|
|
65
|
+
scopeFilter = mergeFilters([scopeFilter, {
|
|
66
|
+
$or: [
|
|
64
67
|
{
|
|
65
|
-
|
|
68
|
+
senderId: {
|
|
69
|
+
$in: ids,
|
|
70
|
+
},
|
|
71
|
+
status: {
|
|
72
|
+
$neq: EmailStatus.Draft,
|
|
73
|
+
},
|
|
66
74
|
},
|
|
67
75
|
{
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
senderId: {
|
|
71
|
-
$in: ids,
|
|
72
|
-
},
|
|
73
|
-
status: {
|
|
74
|
-
$neq: EmailStatus.Draft,
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
userId: user.id,
|
|
79
|
-
},
|
|
80
|
-
],
|
|
76
|
+
userId: user.id,
|
|
81
77
|
},
|
|
82
78
|
],
|
|
83
|
-
};
|
|
79
|
+
}]);
|
|
84
80
|
}
|
|
85
81
|
else {
|
|
86
|
-
scopeFilter = {
|
|
87
|
-
$
|
|
82
|
+
scopeFilter = mergeFilters([scopeFilter, {
|
|
83
|
+
$or: [
|
|
88
84
|
{
|
|
89
|
-
|
|
85
|
+
status: {
|
|
86
|
+
$neq: EmailStatus.Draft,
|
|
87
|
+
},
|
|
90
88
|
},
|
|
91
89
|
{
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
status: {
|
|
95
|
-
$neq: EmailStatus.Draft,
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
userId: user.id,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
90
|
+
userId: user.id,
|
|
102
91
|
},
|
|
103
92
|
],
|
|
104
|
-
};
|
|
93
|
+
}]);
|
|
105
94
|
}
|
|
106
95
|
|
|
107
96
|
const query = Email.select()
|
|
@@ -30,14 +30,14 @@ export class GetEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody
|
|
|
30
30
|
|
|
31
31
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
32
32
|
const organization = await Context.setOptionalOrganizationScope();
|
|
33
|
-
|
|
33
|
+
await Context.authenticate();
|
|
34
34
|
|
|
35
35
|
if (!await Context.auth.canReadEmails(organization)) {
|
|
36
36
|
throw Context.auth.error();
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const model = await Email.getByID(request.params.id);
|
|
40
|
-
if (!model ||
|
|
40
|
+
if (!model || (model.organizationId !== (organization?.id ?? null))) {
|
|
41
41
|
throw new SimpleError({
|
|
42
42
|
code: 'not_found',
|
|
43
43
|
human: 'Email not found',
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { assertSort, CountFilteredRequest, EmailWithRecipients, EmailStatus, getSortFilter, LimitedFilteredRequest, mergeFilters, PaginatedResponse, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
5
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
+
import { Email, Member } from '@stamhoofd/models';
|
|
7
|
+
import { applySQLSorter, compileToSQLFilter, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
8
|
+
import { Context } from '../../../helpers/Context';
|
|
9
|
+
import { emailFilterCompilers, userEmailFilterCompilers } 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<EmailWithRecipients[], LimitedFilteredRequest>;
|
|
16
|
+
|
|
17
|
+
const filterCompilers: SQLFilterDefinitions = emailFilterCompilers;
|
|
18
|
+
const sorters: SQLSortDefinitions<Email> = emailSorters;
|
|
19
|
+
|
|
20
|
+
export class GetUserEmailsEndpoint 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, '/user/email', {});
|
|
29
|
+
|
|
30
|
+
if (params) {
|
|
31
|
+
return [true, params as Params];
|
|
32
|
+
}
|
|
33
|
+
return [false];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static async buildQuery(memberIds: string[], 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 | null = null;
|
|
44
|
+
|
|
45
|
+
if (organization) {
|
|
46
|
+
scopeFilter = mergeFilters([scopeFilter, {
|
|
47
|
+
organizationId: organization.id,
|
|
48
|
+
}]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
scopeFilter = mergeFilters([scopeFilter, {
|
|
52
|
+
recipients: {
|
|
53
|
+
$elemMatch: {
|
|
54
|
+
memberId: {
|
|
55
|
+
$in: memberIds,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}]);
|
|
60
|
+
|
|
61
|
+
const query = Email.select()
|
|
62
|
+
.where('deletedAt', null)
|
|
63
|
+
.where('status', EmailStatus.Sent);
|
|
64
|
+
|
|
65
|
+
if (scopeFilter) {
|
|
66
|
+
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (q.filter) {
|
|
70
|
+
// This one is more strict
|
|
71
|
+
query.where(await compileToSQLFilter(q.filter, userEmailFilterCompilers));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (q.search) {
|
|
75
|
+
let searchFilter: StamhoofdFilter | null = null;
|
|
76
|
+
|
|
77
|
+
searchFilter = {
|
|
78
|
+
subject: {
|
|
79
|
+
$contains: q.search,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (searchFilter) {
|
|
84
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (q instanceof LimitedFilteredRequest) {
|
|
89
|
+
if (q.pageFilter) {
|
|
90
|
+
// More strict as well, since this comes from the frontend
|
|
91
|
+
query.where(await compileToSQLFilter(q.pageFilter, userEmailFilterCompilers));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
95
|
+
applySQLSorter(query, q.sort, sorters);
|
|
96
|
+
query.limit(q.limit);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return query;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
103
|
+
const user = Context.user;
|
|
104
|
+
if (!user) {
|
|
105
|
+
throw new Error('Not authenticated');
|
|
106
|
+
}
|
|
107
|
+
const memberIds = await Member.getMemberIdsForUser(user);
|
|
108
|
+
const query = await GetUserEmailsEndpoint.buildQuery(memberIds, requestQuery);
|
|
109
|
+
const emails = await query.fetch();
|
|
110
|
+
|
|
111
|
+
let next: LimitedFilteredRequest | undefined;
|
|
112
|
+
|
|
113
|
+
if (emails.length >= requestQuery.limit) {
|
|
114
|
+
const lastObject = emails[emails.length - 1];
|
|
115
|
+
const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
|
|
116
|
+
|
|
117
|
+
next = new LimitedFilteredRequest({
|
|
118
|
+
filter: requestQuery.filter,
|
|
119
|
+
pageFilter: nextFilter,
|
|
120
|
+
sort: requestQuery.sort,
|
|
121
|
+
limit: requestQuery.limit,
|
|
122
|
+
search: requestQuery.search,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
|
|
126
|
+
console.error('Found infinite loading loop for', requestQuery);
|
|
127
|
+
next = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return new PaginatedResponse<EmailWithRecipients[], LimitedFilteredRequest>({
|
|
132
|
+
results: await Promise.all(emails.map(email => email.getStructureForUser(user, memberIds))),
|
|
133
|
+
next,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
138
|
+
await Context.setOptionalOrganizationScope();
|
|
139
|
+
await Context.authenticate();
|
|
140
|
+
|
|
141
|
+
const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
|
|
142
|
+
|
|
143
|
+
if (request.query.limit > maxLimit) {
|
|
144
|
+
throw new SimpleError({
|
|
145
|
+
code: 'invalid_field',
|
|
146
|
+
field: 'limit',
|
|
147
|
+
message: 'Limit can not be more than ' + maxLimit,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (request.query.limit < 1) {
|
|
152
|
+
throw new SimpleError({
|
|
153
|
+
code: 'invalid_field',
|
|
154
|
+
field: 'limit',
|
|
155
|
+
message: 'Limit can not be less than 1',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return new Response(
|
|
160
|
+
await GetUserEmailsEndpoint.buildData(request.query),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -3,7 +3,7 @@ import { assertSort, CountFilteredRequest, EmailRecipient as EmailRecipientStruc
|
|
|
3
3
|
|
|
4
4
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
-
import { EmailRecipient } from '@stamhoofd/models';
|
|
6
|
+
import { EmailRecipient, fillRecipientReplacements } from '@stamhoofd/models';
|
|
7
7
|
import { applySQLSorter, compileToSQLFilter, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
8
8
|
import { Context } from '../../../helpers/Context';
|
|
9
9
|
import { emailRecipientsFilterCompilers } from '../../../sql-filters/email-recipients';
|
|
@@ -126,7 +126,18 @@ export class GetEmailRecipientsEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
return new PaginatedResponse<EmailRecipientStruct[], LimitedFilteredRequest>({
|
|
129
|
-
results: recipients.map(r =>
|
|
129
|
+
results: await Promise.all((await EmailRecipient.getStructures(recipients)).map(async (r) => {
|
|
130
|
+
const rr = r.getRecipient();
|
|
131
|
+
await fillRecipientReplacements(rr, {
|
|
132
|
+
organization: Context.organization ?? null,
|
|
133
|
+
from: null,
|
|
134
|
+
replyTo: null,
|
|
135
|
+
forPreview: true,
|
|
136
|
+
forceRefresh: false,
|
|
137
|
+
});
|
|
138
|
+
r.replacements = rr.replacements;
|
|
139
|
+
return r;
|
|
140
|
+
})),
|
|
130
141
|
next,
|
|
131
142
|
});
|
|
132
143
|
}
|
|
@@ -31,7 +31,7 @@ export class GetUserDetailedPayableBalanceEndpoint extends Endpoint<Params, Quer
|
|
|
31
31
|
const organization = await Context.setUserOrganizationScope();
|
|
32
32
|
const { user } = await Context.authenticate();
|
|
33
33
|
|
|
34
|
-
const memberIds = await Member.
|
|
34
|
+
const memberIds = await Member.getMemberIdsForUser(user);
|
|
35
35
|
|
|
36
36
|
const balanceItemModels = await BalanceItem.balanceItemsForUsersAndMembers(organization?.id ?? null, [user.id], memberIds);
|
|
37
37
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions,
|
|
2
|
+
import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
4
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
@@ -407,12 +407,18 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
407
407
|
model.settings.period = period.getBaseStructure();
|
|
408
408
|
|
|
409
409
|
if (model.type !== GroupType.EventRegistration) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
if (!model.settings.hasCustomDates) {
|
|
411
|
+
model.settings.endDate = period.endDate;
|
|
412
|
+
|
|
413
|
+
// Note: start date is customizable, as long as it stays between period start and end
|
|
414
|
+
if (model.settings.startDate < period.startDate || model.settings.startDate > period.endDate) {
|
|
415
|
+
model.settings.startDate = period.startDate;
|
|
416
|
+
}
|
|
413
417
|
}
|
|
414
418
|
|
|
415
|
-
model.settings.
|
|
419
|
+
if (model.settings.startDate > model.settings.endDate) {
|
|
420
|
+
model.settings.startDate = model.settings.endDate;
|
|
421
|
+
}
|
|
416
422
|
}
|
|
417
423
|
}
|
|
418
424
|
|
|
@@ -530,15 +536,23 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
530
536
|
model.status = struct.status;
|
|
531
537
|
model.type = struct.type;
|
|
532
538
|
model.settings.period = period.getBaseStructure();
|
|
533
|
-
model.settings.endDate = period.endDate;
|
|
534
|
-
model.settings.registeredMembers = 0;
|
|
535
|
-
model.settings.reservedMembers = 0;
|
|
536
539
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
+
if (!model.settings.hasCustomDates) {
|
|
541
|
+
model.settings.endDate = period.endDate;
|
|
542
|
+
|
|
543
|
+
// Note: start date is customizable, as long as it stays between period start and end
|
|
544
|
+
if (model.settings.startDate < period.startDate || model.settings.startDate > period.endDate) {
|
|
545
|
+
model.settings.startDate = period.startDate;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (model.settings.startDate > model.settings.endDate) {
|
|
550
|
+
model.settings.startDate = model.settings.endDate;
|
|
540
551
|
}
|
|
541
552
|
|
|
553
|
+
model.settings.registeredMembers = 0;
|
|
554
|
+
model.settings.reservedMembers = 0;
|
|
555
|
+
|
|
542
556
|
if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
543
557
|
// Create a temporary permission role for this user
|
|
544
558
|
const organizationPermissions = user.permissions?.organizationPermissions?.get(organizationId);
|