@stamhoofd/backend 1.0.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 (150) hide show
  1. package/.env.template.json +63 -0
  2. package/.eslintrc.js +61 -0
  3. package/README.md +40 -0
  4. package/index.ts +172 -0
  5. package/jest.config.js +11 -0
  6. package/migrations.ts +33 -0
  7. package/package.json +48 -0
  8. package/src/crons.ts +845 -0
  9. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +42 -0
  10. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +320 -0
  11. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +171 -0
  12. package/src/endpoints/auth/CreateAdminEndpoint.ts +137 -0
  13. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +68 -0
  14. package/src/endpoints/auth/CreateTokenEndpoint.ts +200 -0
  15. package/src/endpoints/auth/DeleteTokenEndpoint.ts +31 -0
  16. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +70 -0
  17. package/src/endpoints/auth/GetUserEndpoint.test.ts +64 -0
  18. package/src/endpoints/auth/GetUserEndpoint.ts +57 -0
  19. package/src/endpoints/auth/PatchApiUserEndpoint.ts +90 -0
  20. package/src/endpoints/auth/PatchUserEndpoint.ts +122 -0
  21. package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +37 -0
  22. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +41 -0
  23. package/src/endpoints/auth/SignupEndpoint.ts +107 -0
  24. package/src/endpoints/auth/VerifyEmailEndpoint.ts +89 -0
  25. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +95 -0
  26. package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +31 -0
  27. package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +101 -0
  28. package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +53 -0
  29. package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +57 -0
  30. package/src/endpoints/global/files/UploadFile.ts +147 -0
  31. package/src/endpoints/global/files/UploadImage.ts +119 -0
  32. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +76 -0
  33. package/src/endpoints/global/members/GetMembersCountEndpoint.ts +43 -0
  34. package/src/endpoints/global/members/GetMembersEndpoint.ts +429 -0
  35. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +734 -0
  36. package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +45 -0
  37. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +105 -0
  38. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +146 -0
  39. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +52 -0
  40. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +80 -0
  41. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +49 -0
  42. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +58 -0
  43. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +62 -0
  44. package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +153 -0
  45. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +134 -0
  46. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +44 -0
  47. package/src/endpoints/global/platform/GetPlatformEnpoint.ts +39 -0
  48. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +63 -0
  49. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +68 -0
  50. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +39 -0
  51. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +80 -0
  52. package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +41 -0
  53. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +134 -0
  54. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +521 -0
  55. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +37 -0
  56. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +115 -0
  57. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +187 -0
  58. package/src/endpoints/organization/dashboard/billing/ActivatePackagesEndpoint.ts +424 -0
  59. package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +67 -0
  60. package/src/endpoints/organization/dashboard/billing/GetBillingStatusEndpoint.ts +39 -0
  61. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +57 -0
  62. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +50 -0
  63. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +50 -0
  64. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +129 -0
  65. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +114 -0
  66. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +50 -0
  67. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +234 -0
  68. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +62 -0
  69. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +85 -0
  70. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +80 -0
  71. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +54 -0
  72. package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +49 -0
  73. package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +63 -0
  74. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +61 -0
  75. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +64 -0
  76. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +84 -0
  77. package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +43 -0
  78. package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +42 -0
  79. package/src/endpoints/organization/dashboard/organization/GetOrganizationSSOEndpoint.ts +43 -0
  80. package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +65 -0
  81. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +281 -0
  82. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +338 -0
  83. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +196 -0
  84. package/src/endpoints/organization/dashboard/organization/SetOrganizationSSOEndpoint.ts +50 -0
  85. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +48 -0
  86. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +207 -0
  87. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +202 -0
  88. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +233 -0
  89. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +66 -0
  90. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -0
  91. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +93 -0
  92. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +59 -0
  93. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +78 -0
  94. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +40 -0
  95. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +69 -0
  96. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +52 -0
  97. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +73 -0
  98. package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +60 -0
  99. package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +47 -0
  100. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +41 -0
  101. package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +217 -0
  102. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +51 -0
  103. package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +47 -0
  104. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +83 -0
  105. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +68 -0
  106. package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +69 -0
  107. package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +125 -0
  108. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +204 -0
  109. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +278 -0
  110. package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +80 -0
  111. package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +60 -0
  112. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +379 -0
  113. package/src/endpoints/organization/shared/GetDocumentHtml.ts +54 -0
  114. package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +45 -0
  115. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +78 -0
  116. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +34 -0
  117. package/src/endpoints/organization/shared/auth/OpenIDConnectCallbackEndpoint.ts +44 -0
  118. package/src/endpoints/organization/shared/auth/OpenIDConnectStartEndpoint.ts +82 -0
  119. package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +59 -0
  120. package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +51 -0
  121. package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +40 -0
  122. package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +124 -0
  123. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +130 -0
  124. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +50 -0
  125. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +450 -0
  126. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +335 -0
  127. package/src/helpers/AddressValidator.test.ts +40 -0
  128. package/src/helpers/AddressValidator.ts +256 -0
  129. package/src/helpers/AdminPermissionChecker.ts +1031 -0
  130. package/src/helpers/AuthenticatedStructures.ts +158 -0
  131. package/src/helpers/BuckarooHelper.ts +279 -0
  132. package/src/helpers/CheckSettlements.ts +215 -0
  133. package/src/helpers/Context.ts +202 -0
  134. package/src/helpers/CookieHelper.ts +45 -0
  135. package/src/helpers/ForwardHandler.test.ts +216 -0
  136. package/src/helpers/ForwardHandler.ts +140 -0
  137. package/src/helpers/OpenIDConnectHelper.ts +284 -0
  138. package/src/helpers/StripeHelper.ts +293 -0
  139. package/src/helpers/StripePayoutChecker.ts +188 -0
  140. package/src/middleware/ContextMiddleware.ts +16 -0
  141. package/src/migrations/1646578856-validate-addresses.ts +60 -0
  142. package/src/seeds/0000000000-example.ts +13 -0
  143. package/src/seeds/1715028563-user-permissions.ts +52 -0
  144. package/tests/e2e/stock.test.ts +2120 -0
  145. package/tests/e2e/tickets.test.ts +926 -0
  146. package/tests/helpers/StripeMocker.ts +362 -0
  147. package/tests/helpers/TestServer.ts +21 -0
  148. package/tests/jest.global.setup.ts +29 -0
  149. package/tests/jest.setup.ts +59 -0
  150. package/tsconfig.json +42 -0
@@ -0,0 +1,429 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
+ import { Decoder } from '@simonbackx/simple-encoding';
3
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
4
+ import { SimpleError } from '@simonbackx/simple-errors';
5
+ import { Member, MemberWithRegistrations, Platform } from '@stamhoofd/models';
6
+ import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions, SQLOrderBy, SQLOrderByDirection, SQLScalar, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler, joinSQLQuery } from "@stamhoofd/sql";
7
+ import { CountFilteredRequest, GroupStatus, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
8
+ import { DataValidator, Formatter } from '@stamhoofd/utility';
9
+
10
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
11
+ import { Context } from '../../../helpers/Context';
12
+ import { filterCompilers as organizationFilterCompilers } from '../../admin/organizations/GetOrganizationsEndpoint';
13
+
14
+ type Params = Record<string, never>;
15
+ type Query = LimitedFilteredRequest;
16
+ type Body = undefined;
17
+ type ResponseBody = PaginatedResponse<MembersBlob, LimitedFilteredRequest>
18
+
19
+ const registrationFilterCompilers: SQLFilterDefinitions = {
20
+ ...baseSQLFilterCompilers,
21
+ "price": createSQLColumnFilterCompiler('price'),
22
+ "pricePaid": createSQLColumnFilterCompiler('pricePaid'),
23
+ "waitingList": createSQLColumnFilterCompiler('waitingList'),
24
+ "canRegister": createSQLColumnFilterCompiler('canRegister'),
25
+ "cycle": createSQLColumnFilterCompiler('cycle'),
26
+
27
+ "cycleOffset": createSQLExpressionFilterCompiler({
28
+ getSQL(options) {
29
+ return joinSQLQuery([
30
+ SQL.column('groups', 'cycle').getSQL(options),
31
+ ' - ',
32
+ SQL.column('registrations', 'cycle').getSQL(options)
33
+ ])
34
+ },
35
+ }),
36
+
37
+ "organizationId": createSQLColumnFilterCompiler('organizationId'),
38
+ "groupId": createSQLColumnFilterCompiler('groupId'),
39
+ "registeredAt": createSQLColumnFilterCompiler('registeredAt'),
40
+ "periodId": createSQLColumnFilterCompiler(SQL.column('registrations', 'periodId')),
41
+
42
+ "group": createSQLFilterNamespace({
43
+ ...baseSQLFilterCompilers,
44
+ id: createSQLColumnFilterCompiler('groupId'),
45
+ name: createSQLExpressionFilterCompiler(
46
+ SQL.jsonValue(SQL.column('groups', 'settings'), '$.value.name')
47
+ ),
48
+ status: createSQLExpressionFilterCompiler(
49
+ SQL.column('groups', 'status')
50
+ ),
51
+ })
52
+ }
53
+
54
+ const filterCompilers: SQLFilterDefinitions = {
55
+ ...baseSQLFilterCompilers,
56
+ id: createSQLColumnFilterCompiler('id'),
57
+ name: createSQLExpressionFilterCompiler(
58
+ new SQLConcat(
59
+ SQL.column('firstName'),
60
+ new SQLScalar(' '),
61
+ SQL.column('lastName'),
62
+ )
63
+ ),
64
+ age: createSQLExpressionFilterCompiler(
65
+ new SQLAge(SQL.column('birthDay'))
66
+ ),
67
+ gender: createSQLExpressionFilterCompiler(
68
+ SQL.jsonValue(SQL.column('details'), '$.value.gender'),
69
+ undefined,
70
+ true,
71
+ false
72
+ ),
73
+ birthDay: createSQLColumnFilterCompiler('birthDay', (d) => {
74
+ if (typeof d === 'number') {
75
+ const date = new Date(d)
76
+ return Formatter.dateIso(date);
77
+ }
78
+ return d;
79
+ }),
80
+ organizationName: createSQLExpressionFilterCompiler(
81
+ SQL.column('organizations', 'name')
82
+ ),
83
+
84
+ email: createSQLExpressionFilterCompiler(
85
+ SQL.jsonValue(SQL.column('details'), '$.value.email'),
86
+ undefined,
87
+ true,
88
+ false
89
+ ),
90
+
91
+ parentEmail: createSQLExpressionFilterCompiler(
92
+ SQL.jsonValue(SQL.column('details'), '$.value.parents[*].email'),
93
+ undefined,
94
+ true,
95
+ true
96
+ ),
97
+
98
+ registrations: createSQLRelationFilterCompiler(
99
+ SQL.select()
100
+ .from(
101
+ SQL.table('registrations')
102
+ ).join(
103
+ SQL.join(
104
+ SQL.table('groups')
105
+ ).where(
106
+ SQL.column('groups', 'id'),
107
+ SQL.column('registrations', 'groupId')
108
+ )
109
+ )
110
+ .join(
111
+ SQL.join(
112
+ SQL.table('organizations')
113
+ ).where(
114
+ SQL.column('organizations', 'id'),
115
+ SQL.column('registrations', 'organizationId')
116
+ )
117
+ )
118
+ .where(
119
+ SQL.column('memberId'),
120
+ SQL.column('members', 'id'),
121
+ ),
122
+ {
123
+ ...registrationFilterCompilers,
124
+ "organization": createSQLFilterNamespace(organizationFilterCompilers)
125
+ }
126
+ ),
127
+
128
+ /**
129
+ * @deprecated?
130
+ */
131
+ activeRegistrations: createSQLRelationFilterCompiler(
132
+ SQL.select()
133
+ .from(
134
+ SQL.table('registrations')
135
+ ).join(
136
+ SQL.join(
137
+ SQL.table('groups')
138
+ ).where(
139
+ SQL.column('groups', 'id'),
140
+ SQL.column('registrations', 'groupId')
141
+ )
142
+ )
143
+ .where(
144
+ SQL.column('memberId'),
145
+ SQL.column('members', 'id'),
146
+ ).whereNot(
147
+ SQL.column('registeredAt'),
148
+ null,
149
+ ).whereNot(
150
+ SQL.column('groups', 'status'),
151
+ GroupStatus.Archived
152
+ ),
153
+ registrationFilterCompilers
154
+ ),
155
+
156
+ organizations: createSQLRelationFilterCompiler(
157
+ SQL.select()
158
+ .from(
159
+ SQL.table('registrations')
160
+ ).join(
161
+ SQL.join(
162
+ SQL.table('groups')
163
+ ).where(
164
+ SQL.column('groups', 'id'),
165
+ SQL.column('registrations', 'groupId')
166
+ )
167
+ ).join(
168
+ SQL.join(
169
+ SQL.table('organizations')
170
+ ).where(
171
+ SQL.column('organizations', 'id'),
172
+ SQL.column('registrations', 'organizationId')
173
+ )
174
+ ).where(
175
+ SQL.column('memberId'),
176
+ SQL.column('members', 'id'),
177
+ ).whereNot(
178
+ SQL.column('registeredAt'),
179
+ null,
180
+ ).whereNot(
181
+ SQL.column('groups', 'status'),
182
+ GroupStatus.Archived
183
+ ),
184
+ organizationFilterCompilers
185
+ ),
186
+ }
187
+
188
+ const sorters: SQLSortDefinitions<MemberWithRegistrations> = {
189
+ 'id': {
190
+ getValue(a) {
191
+ return a.id
192
+ },
193
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
194
+ return new SQLOrderBy({
195
+ column: SQL.column('id'),
196
+ direction
197
+ })
198
+ }
199
+ },
200
+ 'name': {
201
+ getValue(a) {
202
+ return a.details.name
203
+ },
204
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
205
+ return SQLOrderBy.combine([
206
+ new SQLOrderBy({
207
+ column: SQL.column('firstName'),
208
+ direction
209
+ }),
210
+ new SQLOrderBy({
211
+ column: SQL.column('lastName'),
212
+ direction
213
+ })
214
+ ])
215
+ }
216
+ },
217
+ 'birthDay': {
218
+ getValue(a) {
219
+ return a.details.birthDay ? Formatter.dateIso(a.details.birthDay) : null
220
+ },
221
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
222
+ return new SQLOrderBy({
223
+ column: SQL.column('birthDay'),
224
+ direction
225
+ })
226
+ }
227
+ }
228
+ // Note: never add mapped sortings, that should happen in the frontend -> e.g. map age to birthDay
229
+ }
230
+
231
+ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
232
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>
233
+
234
+ protected doesMatch(request: Request): [true, Params] | [false] {
235
+ if (request.method != "GET") {
236
+ return [false];
237
+ }
238
+
239
+ const params = Endpoint.parseParameters(request.url, "/members", {});
240
+
241
+ if (params) {
242
+ return [true, params as Params];
243
+ }
244
+ return [false];
245
+ }
246
+
247
+ static async buildQuery(q: CountFilteredRequest|LimitedFilteredRequest) {
248
+ const organization = Context.organization
249
+ let scopeFilter: StamhoofdFilter|undefined = undefined;
250
+
251
+ if (!organization && !Context.auth.canAccessAllPlatformMembers()) {
252
+ const tags = Context.auth.getPlatformAccessibleOrganizationTags(PermissionLevel.Read)
253
+ if (tags != 'all' && tags.length === 0) {
254
+ throw Context.auth.error()
255
+ }
256
+
257
+
258
+ if (tags !== 'all') {
259
+ const platform = await Platform.getShared()
260
+
261
+ // Add organization scope filter
262
+ scopeFilter = {
263
+ registrations: {
264
+ $elemMatch: {
265
+ organization: {
266
+ tags: {
267
+ $in: tags
268
+ }
269
+ },
270
+ periodId: platform.periodId,
271
+ registeredAt: {
272
+ $neq: null
273
+ }
274
+ }
275
+ }
276
+ };
277
+ }
278
+ }
279
+
280
+ if (organization) {
281
+ // Add organization scope filter
282
+ scopeFilter = {
283
+ registrations: {
284
+ $elemMatch: {
285
+ organizationId: organization.id,
286
+ periodId: organization.periodId,
287
+ registeredAt: {
288
+ $neq: null
289
+ }
290
+ }
291
+ }
292
+ };
293
+ }
294
+
295
+ const query = SQL
296
+ .select(
297
+ SQL.column('members', 'id')
298
+ )
299
+ .from(
300
+ SQL.table('members')
301
+ );
302
+
303
+ if (scopeFilter) {
304
+ query.where(compileToSQLFilter(scopeFilter, filterCompilers))
305
+ }
306
+
307
+ if (q.filter) {
308
+ query.where(compileToSQLFilter(q.filter, filterCompilers))
309
+ }
310
+
311
+ if (q.search) {
312
+ let searchFilter: StamhoofdFilter|null = null
313
+
314
+ // Two search modes:
315
+ // e-mail or name based searching
316
+ if (q.search.includes('@')) {
317
+ const isCompleteAddress = DataValidator.isEmailValid(q.search);
318
+
319
+ // Member email address contains, or member parent contains
320
+ searchFilter = {
321
+ '$or': [
322
+ {
323
+ email: {
324
+ [(isCompleteAddress ? '$eq' : '$contains')]: q.search
325
+ }
326
+ },
327
+ {
328
+ parentEmail: {
329
+ [(isCompleteAddress ? '$eq' : '$contains')]: q.search
330
+ }
331
+ }
332
+ ]
333
+ } as any as StamhoofdFilter
334
+ } else {
335
+ searchFilter = {
336
+ name: {
337
+ $contains: q.search
338
+ }
339
+ }
340
+ }
341
+
342
+ // todo: Address search detection
343
+ // todo: Phone number search detection
344
+
345
+ if (searchFilter) {
346
+ query.where(compileToSQLFilter(searchFilter, filterCompilers))
347
+ }
348
+ }
349
+
350
+ if (q instanceof LimitedFilteredRequest) {
351
+ if (q.pageFilter) {
352
+ query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
353
+ }
354
+
355
+ query.orderBy(compileToSQLSorter(q.sort, sorters))
356
+ query.limit(q.limit)
357
+ }
358
+
359
+ return query
360
+ }
361
+
362
+ async handle(request: DecodedRequest<Params, Query, Body>) {
363
+ await Context.setOptionalOrganizationScope();
364
+ await Context.authenticate()
365
+
366
+ if (request.query.limit > 100) {
367
+ throw new SimpleError({
368
+ code: 'invalid_field',
369
+ field: 'limit',
370
+ message: 'Limit can not be more than 100'
371
+ })
372
+ }
373
+
374
+ if (request.query.limit < 1) {
375
+ throw new SimpleError({
376
+ code: 'invalid_field',
377
+ field: 'limit',
378
+ message: 'Limit can not be less than 1'
379
+ })
380
+ }
381
+
382
+ const query = await GetMembersEndpoint.buildQuery(request.query)
383
+ const data = await query.fetch()
384
+
385
+ const memberIds = data.map((r) => {
386
+ if (typeof r.members.id === 'string') {
387
+ return r.members.id
388
+ }
389
+ throw new Error('Expected string')
390
+ });
391
+
392
+ const _members = await Member.getBlobByIds(...memberIds)
393
+ // Make sure members is in same order as memberIds
394
+ const members = memberIds.map(id => _members.find(m => m.id === id)!)
395
+
396
+ for (const member of members) {
397
+ if (!await Context.auth.canAccessMember(member, PermissionLevel.Read)) {
398
+ throw Context.auth.error()
399
+ }
400
+ }
401
+
402
+ let next: LimitedFilteredRequest|undefined;
403
+
404
+ if (memberIds.length >= request.query.limit) {
405
+ const lastObject = members[members.length - 1];
406
+ const nextFilter = getSortFilter(lastObject, sorters, request.query.sort);
407
+
408
+ next = new LimitedFilteredRequest({
409
+ filter: request.query.filter,
410
+ pageFilter: nextFilter,
411
+ sort: request.query.sort,
412
+ limit: request.query.limit,
413
+ search: request.query.search
414
+ })
415
+
416
+ if (JSON.stringify(nextFilter) === JSON.stringify(request.query.pageFilter)) {
417
+ console.error('Found infinite loading loop for', request.query);
418
+ next = undefined;
419
+ }
420
+ }
421
+
422
+ return new Response(
423
+ new PaginatedResponse<MembersBlob, LimitedFilteredRequest>({
424
+ results: await AuthenticatedStructures.membersBlob(members),
425
+ next
426
+ })
427
+ );
428
+ }
429
+ }