@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.91.0",
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.91.0",
49
- "@stamhoofd/backend-middleware": "2.91.0",
50
- "@stamhoofd/email": "2.91.0",
51
- "@stamhoofd/models": "2.91.0",
52
- "@stamhoofd/queues": "2.91.0",
53
- "@stamhoofd/sql": "2.91.0",
54
- "@stamhoofd/structures": "2.91.0",
55
- "@stamhoofd/utility": "2.91.0",
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": "37ff6e89f07320c0736d32c9b911c8c1c35dc421"
73
+ "gitHead": "43d1edfd8061dada1d418a02691fe5fb158aca6a"
74
74
  }
@@ -1,5 +1,5 @@
1
- import { Email, EmailRecipient, replaceHtml } from '@stamhoofd/models';
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: replaceHtml(model.subject ?? '', options.data.recipient?.replacements ?? []),
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
- replaceHtml(model.html ?? '', options.data.recipient?.replacements ?? []),
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
- model.fromAddress = request.body.fromAddress;
83
- model.fromName = request.body.fromName;
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.canSendEmails()) {
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
  }