@stamhoofd/backend 2.90.3 → 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.
Files changed (47) hide show
  1. package/package.json +10 -10
  2. package/src/audit-logs/EmailLogger.ts +4 -4
  3. package/src/audit-logs/ModelLogger.ts +0 -1
  4. package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +20 -0
  5. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -0
  6. package/src/endpoints/global/email/CreateEmailEndpoint.ts +30 -7
  7. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +207 -0
  8. package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
  9. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
  10. package/src/endpoints/global/email/PatchEmailEndpoint.ts +67 -22
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +6 -4
  12. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -7
  13. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +112 -105
  14. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
  15. package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +5 -5
  16. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -1
  17. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
  18. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
  19. package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +1 -1
  20. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
  21. package/src/helpers/AdminPermissionChecker.ts +81 -10
  22. package/src/helpers/FlagMomentCleanup.ts +13 -1
  23. package/src/helpers/GroupedThrottledQueue.ts +5 -3
  24. package/src/helpers/PeriodHelper.ts +10 -137
  25. package/src/helpers/SetupStepUpdater.ts +54 -7
  26. package/src/helpers/UitpasTokenRepository.ts +3 -3
  27. package/src/seeds/1750090030-records-configuration.ts +5 -1
  28. package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
  29. package/src/seeds/1755181288-remove-duplicate-members.ts +145 -0
  30. package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
  31. package/src/services/BalanceItemService.ts +12 -7
  32. package/src/services/DocumentService.ts +0 -1
  33. package/src/services/RegistrationService.ts +30 -1
  34. package/src/services/uitpas/UitpasService.ts +72 -3
  35. package/src/services/uitpas/cancelTicketSales.ts +1 -1
  36. package/src/services/uitpas/checkPermissionsFor.ts +9 -9
  37. package/src/services/uitpas/checkUitpasNumbers.ts +3 -2
  38. package/src/services/uitpas/getSocialTariffForEvent.ts +4 -4
  39. package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +5 -5
  40. package/src/services/uitpas/registerTicketSales.ts +4 -4
  41. package/src/services/uitpas/searchUitpasEvents.ts +3 -3
  42. package/src/services/uitpas/searchUitpasOrganizers.ts +3 -3
  43. package/src/sql-filters/emails.ts +65 -0
  44. package/src/sql-filters/members.ts +1 -1
  45. package/src/sql-filters/organizations.ts +52 -0
  46. package/src/sql-sorters/emails.ts +47 -0
  47. package/tests/e2e/register.test.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.90.3",
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.90.3",
49
- "@stamhoofd/backend-middleware": "2.90.3",
50
- "@stamhoofd/email": "2.90.3",
51
- "@stamhoofd/models": "2.90.3",
52
- "@stamhoofd/queues": "2.90.3",
53
- "@stamhoofd/sql": "2.90.3",
54
- "@stamhoofd/structures": "2.90.3",
55
- "@stamhoofd/utility": "2.90.3",
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": "81248f4f4d578fb67e29c48c55ddb1a1beb12313"
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;
@@ -193,7 +193,6 @@ export class ModelLogger<ModelType extends typeof Model, M extends InstanceType<
193
193
 
194
194
  if (log.patchList.length === 0 && !log.description) {
195
195
  // No changes or all skipped
196
- console.log('No changes after secundary filtering');
197
196
  return false;
198
197
  }
199
198
  }
@@ -1,5 +1,6 @@
1
1
  import { registerCron } from '@stamhoofd/crons';
2
2
  import { FlagMomentCleanup } from '../helpers/FlagMomentCleanup';
3
+ import { Platform, RegistrationPeriod } from '@stamhoofd/models';
3
4
 
4
5
  // Only delete responsibilities when the server is running during a month change.
5
6
  // Chances are almost zero that we reboot during a month change
@@ -19,6 +20,25 @@ export async function endFunctionsOfUsersWithoutRegistration() {
19
20
  return;
20
21
  }
21
22
 
23
+ // Check if the current period is active for more than 2 months
24
+ const platform = await Platform.getShared();
25
+ const period = await RegistrationPeriod.getByID(platform.periodId);
26
+ if (!period) {
27
+ console.warn('No active registration period found, skipping cleanup.');
28
+ return;
29
+ }
30
+
31
+ if (period.startDate > new Date(Date.now() - 1000 * 60 * 60 * 24 * 55)) {
32
+ console.warn('Current registration period is less than 2 months old, skipping cleanup.');
33
+ return;
34
+ }
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
+
22
42
  await FlagMomentCleanup.endFunctionsOfUsersWithoutRegistration();
23
43
  lastCleanupYear = currentYear;
24
44
  lastCleanupMonth = currentMonth;
@@ -94,6 +94,8 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
94
94
  query.limit(q.limit);
95
95
  }
96
96
 
97
+ console.log('GetOrganizationsEndpoint query', query.getSQL());
98
+
97
99
  return query;
98
100
  }
99
101
 
@@ -1,10 +1,10 @@
1
1
  import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { Email, EmailTemplate, RateLimiter } from '@stamhoofd/models';
4
- import { EmailPreview, EmailStatus, Email as EmailStruct, Version, EmailTemplate as EmailTemplateStruct } from '@stamhoofd/structures';
3
+ import { Email, Platform, RateLimiter } from '@stamhoofd/models';
4
+ import { EmailPreview, EmailStatus, Email as EmailStruct, EmailTemplate as EmailTemplateStruct } from '@stamhoofd/structures';
5
5
 
6
6
  import { Context } from '../../../helpers/Context';
7
- import { SQL } from '@stamhoofd/sql';
7
+ import { SimpleError } from '@simonbackx/simple-errors';
8
8
 
9
9
  type Params = Record<string, never>;
10
10
  type Query = undefined;
@@ -65,8 +65,11 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
65
65
  const organization = await Context.setOptionalOrganizationScope();
66
66
  const { user } = await Context.authenticate();
67
67
 
68
- if (!Context.auth.canSendEmails()) {
69
- 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
+ });
70
73
  }
71
74
 
72
75
  const model = new Email();
@@ -80,8 +83,28 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
80
83
  model.json = request.body.json;
81
84
  model.status = request.body.status;
82
85
  model.attachments = request.body.attachments;
83
- model.fromAddress = request.body.fromAddress;
84
- 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
+ }
85
108
 
86
109
  model.validateAttachments();
87
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
  }