@stamhoofd/backend 2.74.0 → 2.75.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.
Files changed (49) hide show
  1. package/index.ts +7 -2
  2. package/package.json +13 -13
  3. package/src/crons/update-cached-balances.ts +1 -2
  4. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -2
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -15
  6. package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +0 -5
  7. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +2 -2
  8. package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +43 -0
  9. package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +181 -0
  10. package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
  11. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +288 -0
  12. package/src/endpoints/global/events/PatchEventsEndpoint.ts +2 -2
  13. package/src/endpoints/global/files/UploadFile.ts +56 -4
  14. package/src/endpoints/global/files/UploadImage.ts +9 -3
  15. package/src/endpoints/global/members/GetMembersEndpoint.ts +2 -2
  16. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +10 -1
  17. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -5
  18. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +7 -0
  19. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +1 -1
  20. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +2084 -164
  21. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -2
  22. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +48 -2
  23. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +2 -2
  24. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
  25. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +2 -2
  26. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +2 -2
  27. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +8 -0
  28. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +3 -3
  29. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +2 -2
  30. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +2 -2
  31. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +1 -2
  32. package/src/helpers/AdminPermissionChecker.ts +80 -2
  33. package/src/helpers/AuthenticatedStructures.ts +88 -2
  34. package/src/helpers/FlagMomentCleanup.ts +1 -8
  35. package/src/helpers/GlobalHelper.ts +15 -0
  36. package/src/helpers/MembershipCharger.ts +2 -1
  37. package/src/services/EventNotificationService.ts +201 -0
  38. package/src/services/FileSignService.ts +217 -0
  39. package/src/services/SSOService.ts +7 -2
  40. package/src/sql-filters/event-notifications.ts +39 -0
  41. package/src/sql-filters/organizations.ts +1 -1
  42. package/src/sql-sorters/event-notifications.ts +96 -0
  43. package/src/sql-sorters/events.ts +2 -2
  44. package/src/sql-sorters/organizations.ts +2 -2
  45. package/tests/e2e/private-files.test.ts +497 -0
  46. package/tests/e2e/register.test.ts +1197 -0
  47. package/tests/helpers/TestServer.ts +3 -0
  48. package/tests/jest.setup.ts +15 -2
  49. package/tsconfig.json +1 -0
package/index.ts CHANGED
@@ -3,7 +3,6 @@ backendEnv.load();
3
3
 
4
4
  import { Column, Database, Migration } from '@simonbackx/simple-database';
5
5
  import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
6
- import { I18n } from '@stamhoofd/backend-i18n';
7
6
  import { CORSMiddleware, LogMiddleware, VersionMiddleware } from '@stamhoofd/backend-middleware';
8
7
  import { Email } from '@stamhoofd/email';
9
8
  import { loadLogger } from '@stamhoofd/logging';
@@ -13,10 +12,12 @@ import { sleep } from '@stamhoofd/utility';
13
12
  import { startCrons, stopCrons, waitForCrons } from '@stamhoofd/crons';
14
13
  import { Platform } from '@stamhoofd/models';
15
14
  import { resumeEmails } from './src/helpers/EmailResumer';
15
+ import { GlobalHelper } from './src/helpers/GlobalHelper';
16
16
  import { SetupStepUpdater } from './src/helpers/SetupStepUpdater';
17
17
  import { ContextMiddleware } from './src/middleware/ContextMiddleware';
18
18
  import { AuditLogService } from './src/services/AuditLogService';
19
19
  import { DocumentService } from './src/services/DocumentService';
20
+ import { FileSignService } from './src/services/FileSignService';
20
21
  import { PlatformMembershipService } from './src/services/PlatformMembershipService';
21
22
 
22
23
  process.on('unhandledRejection', (error: Error) => {
@@ -52,7 +53,8 @@ const seeds = async () => {
52
53
  const start = async () => {
53
54
  console.log('Running server at v' + Version);
54
55
  loadLogger();
55
- await I18n.load();
56
+ await GlobalHelper.load();
57
+
56
58
  const router = new Router();
57
59
  await router.loadAllEndpoints(__dirname + '/src/endpoints/global/*');
58
60
  await router.loadAllEndpoints(__dirname + '/src/endpoints/admin/*');
@@ -72,6 +74,9 @@ const start = async () => {
72
74
  routerServer.addRequestMiddleware(LogMiddleware);
73
75
  routerServer.addResponseMiddleware(LogMiddleware);
74
76
 
77
+ routerServer.addResponseMiddleware(FileSignService);
78
+ routerServer.addRequestMiddleware(FileSignService);
79
+
75
80
  // Contexts
76
81
  routerServer.addRequestMiddleware(ContextMiddleware);
77
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.74.0",
3
+ "version": "2.75.1",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -33,18 +33,18 @@
33
33
  "dependencies": {
34
34
  "@bwip-js/node": "^4.5.1",
35
35
  "@mollie/api-client": "3.7.0",
36
- "@simonbackx/simple-database": "1.28.0",
37
- "@simonbackx/simple-encoding": "2.19.0",
38
- "@simonbackx/simple-endpoints": "1.15.0",
36
+ "@simonbackx/simple-database": "1.29.0",
37
+ "@simonbackx/simple-encoding": "2.20.0",
38
+ "@simonbackx/simple-endpoints": "1.19.0",
39
39
  "@simonbackx/simple-logging": "^1.0.1",
40
- "@stamhoofd/backend-i18n": "2.74.0",
41
- "@stamhoofd/backend-middleware": "2.74.0",
42
- "@stamhoofd/email": "2.74.0",
43
- "@stamhoofd/models": "2.74.0",
44
- "@stamhoofd/queues": "2.74.0",
45
- "@stamhoofd/sql": "2.74.0",
46
- "@stamhoofd/structures": "2.74.0",
47
- "@stamhoofd/utility": "2.74.0",
40
+ "@stamhoofd/backend-i18n": "2.75.1",
41
+ "@stamhoofd/backend-middleware": "2.75.1",
42
+ "@stamhoofd/email": "2.75.1",
43
+ "@stamhoofd/models": "2.75.1",
44
+ "@stamhoofd/queues": "2.75.1",
45
+ "@stamhoofd/sql": "2.75.1",
46
+ "@stamhoofd/structures": "2.75.1",
47
+ "@stamhoofd/utility": "2.75.1",
48
48
  "archiver": "^7.0.1",
49
49
  "aws-sdk": "^2.885.0",
50
50
  "axios": "1.6.8",
@@ -64,5 +64,5 @@
64
64
  "publishConfig": {
65
65
  "access": "public"
66
66
  },
67
- "gitHead": "3a77692b8163f10c231785de2f720c423a909762"
67
+ "gitHead": "4c6a7f14c62e4832885325d02c5aaeeccf27568b"
68
68
  }
@@ -5,8 +5,7 @@ registerCron('updateCachedBalances', updateCachedBalances);
5
5
 
6
6
  async function updateCachedBalances() {
7
7
  // Check if between 3 - 6 AM
8
- if ((new Date().getHours() > 6 || new Date().getHours() < 3) && STAMHOOFD.environment !== 'development') {
9
- console.log('Not between 3 and 6 AM, skipping.');
8
+ if ((new Date().getHours() > 6 || new Date().getHours() < 3)) {
10
9
  return;
11
10
  }
12
11
 
@@ -2,7 +2,7 @@ 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
4
  import { Organization } from '@stamhoofd/models';
5
- import { SQL, compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
5
+ import { SQL, compileToSQLFilter, applySQLSorter } from '@stamhoofd/sql';
6
6
  import { CountFilteredRequest, LimitedFilteredRequest, Organization as OrganizationStruct, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
7
7
 
8
8
  import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
@@ -90,7 +90,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
90
90
  }
91
91
 
92
92
  q.sort = assertSort(q.sort, [{ key: 'id' }]);
93
- query.orderBy(compileToSQLSorter(q.sort, sorters));
93
+ applySQLSorter(query, q.sort, sorters);
94
94
  query.limit(q.limit);
95
95
  }
96
96
 
@@ -74,15 +74,10 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
74
74
  }
75
75
 
76
76
  if (organization) {
77
- admin.permissions = UserPermissions.limitedPatch(admin.permissions, request.body.permissions, organization.id);
77
+ admin.permissions = UserPermissions.limitedAdd(admin.permissions, request.body.permissions, organization.id);
78
78
  }
79
79
  else {
80
- if (admin.permissions) {
81
- admin.permissions.patchOrPut(request.body.permissions);
82
- }
83
- else {
84
- admin.permissions = request.body.permissions.isPut() ? request.body.permissions : null;
85
- }
80
+ admin.permissions = UserPermissions.add(admin.permissions, request.body.permissions);
86
81
  }
87
82
 
88
83
  if (!admin.firstName && request.body.firstName) {
@@ -114,12 +109,10 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
114
109
  await sendEmailTemplate(organization, {
115
110
  recipients: [
116
111
  Recipient.create({
112
+ firstName: admin.firstName,
113
+ lastName: admin.lastName,
117
114
  email,
118
115
  replacements: [
119
- Replacement.create({
120
- token: 'greeting',
121
- value: admin.firstName ? `Dag ${admin.firstName},` : 'Hallo!',
122
- }),
123
116
  Replacement.create({
124
117
  token: 'resetUrl',
125
118
  value: recoveryUrl,
@@ -136,10 +129,6 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
136
129
  token: 'validUntil',
137
130
  value: dateTime,
138
131
  }),
139
- Replacement.create({
140
- token: 'email',
141
- value: admin.email,
142
- }),
143
132
  ],
144
133
  }),
145
134
  ],
@@ -30,11 +30,6 @@ export class OpenIDConnectStartEndpoint extends Endpoint<Params, Query, Body, Re
30
30
  // Check webshop and/or organization
31
31
  await Context.setUserOrganizationScope();
32
32
  await Context.optionalAuthenticate({ allowWithoutAccount: false });
33
- console.log('Full start connect body;', await request.request.body);
34
-
35
- if (Context.user) {
36
- console.log('User:', Context.user);
37
- }
38
33
  const service = await SSOService.fromContext(request.body.provider);
39
34
  return await service.validateAndStartAuthCodeFlow(request.body);
40
35
  }
@@ -2,7 +2,7 @@ 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
4
  import { AuditLog } from '@stamhoofd/models';
5
- import { SQL, SQLFilterDefinitions, SQLSortDefinitions, compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
5
+ import { SQL, SQLFilterDefinitions, SQLSortDefinitions, compileToSQLFilter, applySQLSorter } from '@stamhoofd/sql';
6
6
  import { AuditLog as AuditLogStruct, CountFilteredRequest, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -82,7 +82,7 @@ export class GetAuditLogsEndpoint extends Endpoint<Params, Query, Body, Response
82
82
  }
83
83
 
84
84
  q.sort = assertSort(q.sort, [{ key: 'id' }]);
85
- query.orderBy(compileToSQLSorter(q.sort, sorters));
85
+ applySQLSorter(query, q.sort, sorters);
86
86
  query.limit(q.limit);
87
87
  }
88
88
 
@@ -0,0 +1,43 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
4
+
5
+ import { Context } from '../../../helpers/Context';
6
+ import { GetEventNotificationsEndpoint } from './GetEventNotificationsEndpoint';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = CountFilteredRequest;
10
+ type Body = undefined;
11
+ type ResponseBody = CountResponse;
12
+
13
+ export class GetEventNotificationsCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
15
+
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method !== 'GET') {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, '/event-notifications/count', {});
22
+
23
+ if (params) {
24
+ return [true, params as Params];
25
+ }
26
+ return [false];
27
+ }
28
+
29
+ async handle(request: DecodedRequest<Params, Query, Body>) {
30
+ await Context.setOptionalOrganizationScope();
31
+ await Context.authenticate();
32
+ const query = await GetEventNotificationsEndpoint.buildQuery(request.query);
33
+
34
+ const count = await query
35
+ .count();
36
+
37
+ return new Response(
38
+ CountResponse.create({
39
+ count,
40
+ }),
41
+ );
42
+ }
43
+ }
@@ -0,0 +1,181 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { EventNotification } from '@stamhoofd/models';
5
+ import { SQL, SQLFilterDefinitions, SQLSortDefinitions, applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
6
+ import { AccessRight, CountFilteredRequest, EventNotification as EventNotificationStruct, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
7
+
8
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
9
+ import { Context } from '../../../helpers/Context';
10
+ import { eventNotificationsFilterCompilers } from '../../../sql-filters/event-notifications';
11
+ import { eventNotificationsSorters } from '../../../sql-sorters/event-notifications';
12
+ import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
13
+
14
+ type Params = Record<string, never>;
15
+ type Query = LimitedFilteredRequest;
16
+ type Body = undefined;
17
+ type ResponseBody = PaginatedResponse<EventNotificationStruct[], LimitedFilteredRequest>;
18
+
19
+ const filterCompilers: SQLFilterDefinitions = eventNotificationsFilterCompilers;
20
+ const sorters: SQLSortDefinitions<SQLResultNamespacedRow> = eventNotificationsSorters;
21
+
22
+ export class GetEventNotificationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
23
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
24
+
25
+ protected doesMatch(request: Request): [true, Params] | [false] {
26
+ if (request.method !== 'GET') {
27
+ return [false];
28
+ }
29
+
30
+ const params = Endpoint.parseParameters(request.url, '/event-notifications', {});
31
+
32
+ if (params) {
33
+ return [true, params as Params];
34
+ }
35
+ return [false];
36
+ }
37
+
38
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
39
+ const organization = Context.organization;
40
+ let scopeFilter: StamhoofdFilter | undefined = undefined;
41
+
42
+ // todo: add proper scoping and permission checking
43
+ if (organization) {
44
+ scopeFilter = {
45
+ organizationId: organization.id,
46
+ };
47
+ }
48
+
49
+ if (!organization) {
50
+ // Get all tags
51
+ const tags = Context.auth.getOrganizationTagsWithAccessRight(AccessRight.OrganizationEventNotificationReviewer);
52
+
53
+ if (tags !== 'all') {
54
+ if (tags.length === 0) {
55
+ throw Context.auth.error();
56
+ }
57
+
58
+ scopeFilter = {
59
+ organization: {
60
+ $elemMatch: {
61
+ tags: {
62
+ $in: tags,
63
+ },
64
+ },
65
+ },
66
+ };
67
+ }
68
+ }
69
+
70
+ const query = SQL
71
+ .select(
72
+ SQL.wildcard(EventNotification.table),
73
+ )
74
+ .from(
75
+ SQL.table(EventNotification.table),
76
+ );
77
+
78
+ if (scopeFilter) {
79
+ query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
80
+ }
81
+
82
+ if (q.filter) {
83
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
84
+ }
85
+
86
+ if (q.search) {
87
+ let searchFilter: StamhoofdFilter | null = null;
88
+
89
+ searchFilter = {
90
+ events: {
91
+ $elemMatch: {
92
+ name: {
93
+ $contains: q.search,
94
+ },
95
+ },
96
+ },
97
+ };
98
+
99
+ if (searchFilter) {
100
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
101
+ }
102
+ }
103
+
104
+ if (q instanceof LimitedFilteredRequest) {
105
+ if (q.pageFilter) {
106
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
107
+ }
108
+
109
+ q.sort = assertSort(q.sort, [{ key: 'id' }]);
110
+ applySQLSorter(query, q.sort, sorters);
111
+ query.limit(q.limit);
112
+ }
113
+
114
+ return query;
115
+ }
116
+
117
+ static async buildData(requestQuery: LimitedFilteredRequest) {
118
+ const query = await GetEventNotificationsEndpoint.buildQuery(requestQuery);
119
+ const data = await query.fetch();
120
+
121
+ const notifications = EventNotification.fromRows(data, EventNotification.table);
122
+
123
+ let next: LimitedFilteredRequest | undefined;
124
+
125
+ if (notifications.length >= requestQuery.limit) {
126
+ const lastObject = data[data.length - 1];
127
+ const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
128
+
129
+ next = new LimitedFilteredRequest({
130
+ filter: requestQuery.filter,
131
+ pageFilter: nextFilter,
132
+ sort: requestQuery.sort,
133
+ limit: requestQuery.limit,
134
+ search: requestQuery.search,
135
+ });
136
+
137
+ if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
138
+ console.error('Found infinite loading loop for', requestQuery);
139
+ next = undefined;
140
+ }
141
+ }
142
+
143
+ for (const notification of notifications) {
144
+ if (!await Context.auth.canAccessEventNotification(notification)) {
145
+ throw Context.auth.error('Je hebt geen toegang om deze melding te bekijken');
146
+ }
147
+ }
148
+
149
+ return new PaginatedResponse<EventNotificationStruct[], LimitedFilteredRequest>({
150
+ results: await AuthenticatedStructures.eventNotifications(notifications),
151
+ next,
152
+ });
153
+ }
154
+
155
+ async handle(request: DecodedRequest<Params, Query, Body>) {
156
+ await Context.setOptionalOrganizationScope();
157
+ await Context.authenticate();
158
+
159
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
160
+
161
+ if (request.query.limit > maxLimit) {
162
+ throw new SimpleError({
163
+ code: 'invalid_field',
164
+ field: 'limit',
165
+ message: 'Limit can not be more than ' + maxLimit,
166
+ });
167
+ }
168
+
169
+ if (request.query.limit < 1) {
170
+ throw new SimpleError({
171
+ code: 'invalid_field',
172
+ field: 'limit',
173
+ message: 'Limit can not be less than 1',
174
+ });
175
+ }
176
+
177
+ return new Response(
178
+ await GetEventNotificationsEndpoint.buildData(request.query),
179
+ );
180
+ }
181
+ }
@@ -2,7 +2,7 @@ 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
4
  import { Event } from '@stamhoofd/models';
5
- import { SQL, SQLFilterDefinitions, SQLSortDefinitions, compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
5
+ import { SQL, SQLFilterDefinitions, SQLSortDefinitions, compileToSQLFilter, applySQLSorter } from '@stamhoofd/sql';
6
6
  import { CountFilteredRequest, Event as EventStruct, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -88,7 +88,7 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
88
88
  }
89
89
 
90
90
  q.sort = assertSort(q.sort, [{ key: 'id' }]);
91
- query.orderBy(compileToSQLSorter(q.sort, sorters));
91
+ applySQLSorter(query, q.sort, sorters);
92
92
  query.limit(q.limit);
93
93
  }
94
94