@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,42 @@
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 { GetOrganizationsEndpoint } from './GetOrganizationsEndpoint';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = CountFilteredRequest;
10
+ type Body = undefined;
11
+ type ResponseBody = CountResponse;
12
+
13
+ export class GetOrganizationsCountEndpoint 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, "/admin/organizations/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.authenticate()
31
+ const query = GetOrganizationsEndpoint.buildQuery(request.query)
32
+
33
+ const count = await query
34
+ .count();
35
+
36
+ return new Response(
37
+ CountResponse.create({
38
+ count
39
+ })
40
+ );
41
+ }
42
+ }
@@ -0,0 +1,320 @@
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 { Organization } from '@stamhoofd/models';
6
+ import { SQL, SQLConcat, SQLFilterDefinitions, SQLNow, SQLNull, SQLOrderBy, SQLOrderByDirection, SQLScalar, SQLSortDefinitions, SQLWhereEqual, SQLWhereOr, SQLWhereSign, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLRelationFilterCompiler } from "@stamhoofd/sql";
7
+ import { CountFilteredRequest, LimitedFilteredRequest, Organization as OrganizationStruct, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter } from '@stamhoofd/structures';
8
+
9
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
+ import { Context } from '../../../helpers/Context';
11
+
12
+ type Params = Record<string, never>;
13
+ type Query = LimitedFilteredRequest;
14
+ type Body = undefined;
15
+ type ResponseBody = PaginatedResponse<OrganizationStruct[], LimitedFilteredRequest>
16
+
17
+ export const filterCompilers: SQLFilterDefinitions = {
18
+ ...baseSQLFilterCompilers,
19
+ id: createSQLExpressionFilterCompiler(
20
+ SQL.column('organizations', 'id')
21
+ ),
22
+ name: createSQLExpressionFilterCompiler(
23
+ SQL.column('organizations', 'name')
24
+ ),
25
+ city: createSQLExpressionFilterCompiler(
26
+ SQL.jsonValue(SQL.column('organizations', 'address'), '$.value.city'),
27
+ undefined,
28
+ true,
29
+ false
30
+ ),
31
+ country: createSQLExpressionFilterCompiler(
32
+ SQL.jsonValue(SQL.column('organizations', 'address'), '$.value.country'),
33
+ undefined,
34
+ true,
35
+ false
36
+ ),
37
+ umbrellaOrganization: createSQLExpressionFilterCompiler(
38
+ SQL.jsonValue(SQL.column('organizations', 'meta'), '$.value.umbrellaOrganization'),
39
+ undefined,
40
+ true,
41
+ false
42
+ ),
43
+ type: createSQLExpressionFilterCompiler(
44
+ SQL.jsonValue(SQL.column('organizations', 'meta'), '$.value.type'),
45
+ undefined,
46
+ true,
47
+ false
48
+ ),
49
+ tags: createSQLExpressionFilterCompiler(
50
+ SQL.jsonValue(SQL.column('organizations', 'meta'), '$.value.tags'),
51
+ undefined,
52
+ true,
53
+ true
54
+ ),
55
+ packages: createSQLRelationFilterCompiler(
56
+ SQL.select().from(
57
+ SQL.table('stamhoofd_packages')
58
+ ).where(
59
+ SQL.column('organizationId'),
60
+ SQL.column('organizations', 'id'),
61
+ )
62
+ .andWhere(
63
+ SQL.column('validAt'),
64
+ SQLWhereSign.NotEqual,
65
+ new SQLNull()
66
+ ).andWhere(
67
+ new SQLWhereOr([
68
+ new SQLWhereEqual(
69
+ SQL.column('validUntil'),
70
+ SQLWhereSign.Equal,
71
+ new SQLNull()
72
+ ),
73
+ new SQLWhereEqual(
74
+ SQL.column('validUntil'),
75
+ SQLWhereSign.Greater,
76
+ new SQLNow()
77
+ )
78
+ ])
79
+ ).andWhere(
80
+ new SQLWhereOr([
81
+ new SQLWhereEqual(
82
+ SQL.column('removeAt'),
83
+ SQLWhereSign.Equal,
84
+ new SQLNull()
85
+ ),
86
+ new SQLWhereEqual(
87
+ SQL.column('removeAt'),
88
+ SQLWhereSign.Greater,
89
+ new SQLNow()
90
+ )
91
+ ])
92
+ ),
93
+
94
+ // const pack1 = await STPackage.where({ organizationId, validAt: { sign: "!=", value: null }, removeAt: { sign: ">", value: new Date() }})
95
+ // const pack2 = await STPackage.where({ organizationId, validAt: { sign: "!=", value: null }, removeAt: null })
96
+ {
97
+ ...baseSQLFilterCompilers,
98
+ "type": createSQLExpressionFilterCompiler(
99
+ SQL.jsonValue(SQL.column('meta'), '$.value.type'),
100
+ undefined,
101
+ true,
102
+ false
103
+ )
104
+ }
105
+ ),
106
+ members: createSQLRelationFilterCompiler(
107
+ SQL.select().from(
108
+ SQL.table('members')
109
+ ).join(
110
+ SQL.join(
111
+ SQL.table('registrations')
112
+ ).where(
113
+ SQL.column('members', 'id'),
114
+ SQL.column('registrations', 'memberId')
115
+ )
116
+ ).where(
117
+ SQL.column('registrations', 'organizationId'),
118
+ SQL.column('organizations', 'id'),
119
+ ),
120
+
121
+ {
122
+ ...baseSQLFilterCompilers,
123
+ name: createSQLExpressionFilterCompiler(
124
+ new SQLConcat(
125
+ SQL.column('firstName'),
126
+ new SQLScalar(' '),
127
+ SQL.column('lastName'),
128
+ )
129
+ ),
130
+ "firstName": createSQLColumnFilterCompiler('firstName'),
131
+ "lastName": createSQLColumnFilterCompiler('lastName'),
132
+ "email": createSQLColumnFilterCompiler('email')
133
+ }
134
+ ),
135
+ }
136
+
137
+ const sorters: SQLSortDefinitions<Organization> = {
138
+ 'id': {
139
+ getValue(a) {
140
+ return a.id
141
+ },
142
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
143
+ return new SQLOrderBy({
144
+ column: SQL.column('id'),
145
+ direction
146
+ })
147
+ }
148
+ },
149
+ 'name': {
150
+ getValue(a) {
151
+ return a.name
152
+ },
153
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
154
+ return new SQLOrderBy({
155
+ column: SQL.column('name'),
156
+ direction
157
+ })
158
+ }
159
+ },
160
+ 'type': {
161
+ getValue(a) {
162
+ return a.meta.type
163
+ },
164
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
165
+ return new SQLOrderBy({
166
+ column: SQL.jsonValue(SQL.column('meta'), '$.value.type'),
167
+ direction
168
+ })
169
+ }
170
+ },
171
+ 'city': {
172
+ getValue(a) {
173
+ return a.address.city
174
+ },
175
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
176
+ return new SQLOrderBy({
177
+ column: SQL.jsonValue(SQL.column('address'), '$.value.city'),
178
+ direction
179
+ })
180
+ }
181
+ },
182
+ 'country': {
183
+ getValue(a) {
184
+ return a.address.country
185
+ },
186
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
187
+ return new SQLOrderBy({
188
+ column: SQL.jsonValue(SQL.column('address'), '$.value.country'),
189
+ direction
190
+ })
191
+ }
192
+ }
193
+ }
194
+
195
+ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
196
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>
197
+
198
+ protected doesMatch(request: Request): [true, Params] | [false] {
199
+ if (request.method != "GET") {
200
+ return [false];
201
+ }
202
+
203
+ const params = Endpoint.parseParameters(request.url, "/admin/organizations", {});
204
+
205
+ if (params) {
206
+ return [true, params as Params];
207
+ }
208
+ return [false];
209
+ }
210
+
211
+ static buildQuery(q: CountFilteredRequest|LimitedFilteredRequest) {
212
+ const tags = Context.auth.getPlatformAccessibleOrganizationTags(PermissionLevel.Read)
213
+ if (tags != 'all' && tags.length === 0) {
214
+ throw Context.auth.error()
215
+ }
216
+
217
+ let scopeFilter: StamhoofdFilter|undefined = undefined;
218
+
219
+ if (tags !== 'all') {
220
+ // Add organization scope filter
221
+ scopeFilter = {
222
+ tags: {
223
+ $in: tags
224
+ }
225
+ };
226
+ }
227
+
228
+ const query = SQL
229
+ .select(
230
+ SQL.wildcard('organizations')
231
+ )
232
+ .from(
233
+ SQL.table('organizations')
234
+ );
235
+
236
+ if (scopeFilter) {
237
+ query.where(compileToSQLFilter(scopeFilter, filterCompilers))
238
+ }
239
+
240
+ if (q.filter) {
241
+ query.where(compileToSQLFilter(q.filter, filterCompilers))
242
+ }
243
+
244
+ if (q.search) {
245
+ let searchFilter: StamhoofdFilter|null = null
246
+
247
+ // todo: auto detect e-mailaddresses and search on admins
248
+ searchFilter = {
249
+ name: {
250
+ $contains: q.search
251
+ }
252
+ }
253
+
254
+ if (searchFilter) {
255
+ query.where(compileToSQLFilter(searchFilter, filterCompilers))
256
+ }
257
+ }
258
+
259
+ if (q instanceof LimitedFilteredRequest) {
260
+ if (q.pageFilter) {
261
+ query.where(compileToSQLFilter(q.pageFilter, filterCompilers))
262
+ }
263
+
264
+ query.orderBy(compileToSQLSorter(q.sort, sorters))
265
+ query.limit(q.limit)
266
+ }
267
+
268
+ return query
269
+ }
270
+
271
+ async handle(request: DecodedRequest<Params, Query, Body>) {
272
+ await Context.authenticate()
273
+
274
+ if (request.query.limit > 100) {
275
+ throw new SimpleError({
276
+ code: 'invalid_field',
277
+ field: 'limit',
278
+ message: 'Limit can not be more than 100'
279
+ })
280
+ }
281
+
282
+ if (request.query.limit < 1) {
283
+ throw new SimpleError({
284
+ code: 'invalid_field',
285
+ field: 'limit',
286
+ message: 'Limit can not be less than 1'
287
+ })
288
+ }
289
+
290
+ const data = await GetOrganizationsEndpoint.buildQuery(request.query).fetch()
291
+ const organizations = Organization.fromRows(data, 'organizations');
292
+
293
+ let next: LimitedFilteredRequest|undefined;
294
+
295
+ if (organizations.length >= request.query.limit) {
296
+ const lastObject = organizations[organizations.length - 1];
297
+ const nextFilter = getSortFilter(lastObject, sorters, request.query.sort);
298
+
299
+ next = new LimitedFilteredRequest({
300
+ filter: request.query.filter,
301
+ pageFilter: nextFilter,
302
+ sort: request.query.sort,
303
+ limit: request.query.limit,
304
+ search: request.query.search
305
+ })
306
+
307
+ if (JSON.stringify(nextFilter) === JSON.stringify(request.query.pageFilter)) {
308
+ console.error('Found infinite loading loop for', request.query);
309
+ next = undefined;
310
+ }
311
+ }
312
+
313
+ return new Response(
314
+ new PaginatedResponse<OrganizationStruct[], LimitedFilteredRequest>({
315
+ results: await AuthenticatedStructures.adminOrganizations(organizations),
316
+ next
317
+ })
318
+ );
319
+ }
320
+ }
@@ -0,0 +1,171 @@
1
+ import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError } from "@simonbackx/simple-errors";
4
+ import { Organization, OrganizationRegistrationPeriod, Platform } from '@stamhoofd/models';
5
+ import { OrganizationMetaData, Organization as OrganizationStruct } from "@stamhoofd/structures";
6
+
7
+ import { Context } from '../../../helpers/Context';
8
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
9
+ import { Formatter } from '@stamhoofd/utility';
10
+
11
+ type Params = Record<string, never>;
12
+ type Query = undefined;
13
+ type Body = PatchableArrayAutoEncoder<OrganizationStruct>
14
+ type ResponseBody = OrganizationStruct[]
15
+
16
+ export class PatchOrganizationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
17
+ bodyDecoder = new PatchableArrayDecoder(OrganizationStruct as Decoder<OrganizationStruct>, OrganizationStruct.patchType() as Decoder<AutoEncoderPatchType<OrganizationStruct>>, StringDecoder)
18
+
19
+ protected doesMatch(request: Request): [true, Params] | [false] {
20
+ if (request.method != "PATCH") {
21
+ return [false];
22
+ }
23
+
24
+ const params = Endpoint.parseParameters(request.url, "/admin/organizations", {});
25
+
26
+ if (params) {
27
+ return [true, params as Params];
28
+ }
29
+ return [false];
30
+ }
31
+
32
+ async handle(request: DecodedRequest<Params, Query, Body>) {
33
+ await Context.authenticate()
34
+ if (!Context.auth.hasPlatformFullAccess()) {
35
+ throw Context.auth.error()
36
+ }
37
+
38
+ if (request.body.changes.length == 0) {
39
+ return new Response([]);
40
+ }
41
+
42
+ const result: Organization[] = [];
43
+ const platform = await Platform.getShared()
44
+
45
+ for (const id of request.body.getDeletes()) {
46
+ const organization = await Organization.getByID(id);
47
+ if (!organization) {
48
+ throw new SimpleError({ code: "not_found", message: "Organization not found", statusCode: 404 });
49
+ }
50
+
51
+ await organization.delete();
52
+ }
53
+
54
+ // Bulk tag editing
55
+ for (const patch of request.body.getPatches()) {
56
+ const organization = await Organization.getByID(patch.id);
57
+ if (!organization) {
58
+ throw new SimpleError({ code: "not_found", message: "Organization not found", statusCode: 404 });
59
+ }
60
+
61
+ if (patch.meta?.tags) {
62
+ const cleanedPatch = OrganizationMetaData.patch({
63
+ tags: patch.meta.tags as any
64
+ })
65
+ const patchedMeta = organization.meta.patch(cleanedPatch);
66
+ for (const tag of patchedMeta.tags) {
67
+ if (!platform.config.tags.find(t => t.id === tag)) {
68
+ throw new SimpleError({ code: "invalid_tag", message: "Invalid tag", statusCode: 400 });
69
+ }
70
+ }
71
+
72
+ // Sort tags based on platform config order
73
+ patchedMeta.tags.sort((a, b) => {
74
+ const aIndex = platform.config.tags.findIndex(t => t.id === a);
75
+ const bIndex = platform.config.tags.findIndex(t => t.id === b);
76
+ return aIndex - bIndex;
77
+ })
78
+
79
+ organization.meta.tags = patchedMeta.tags;
80
+ }
81
+
82
+ await organization.save();
83
+ result.push(organization);
84
+ }
85
+
86
+ // Organization creation
87
+ for (const {put} of request.body.getPuts()) {
88
+ if (put.name.length < 4) {
89
+ if (put.name.length == 0) {
90
+ throw new SimpleError({
91
+ code: "invalid_field",
92
+ message: "Should not be empty",
93
+ human: "Je bent de naam van je organisatie vergeten in te vullen",
94
+ field: "organization.name"
95
+ })
96
+ }
97
+
98
+ throw new SimpleError({
99
+ code: "invalid_field",
100
+ message: "Field is too short",
101
+ human: "Kijk de naam van je organisatie na, deze is te kort. Vul eventueel aan met de gemeente.",
102
+ field: "organization.name"
103
+ })
104
+ }
105
+
106
+ const uri = put.uri || Formatter.slug(put.name);
107
+
108
+ if (uri.length > 100) {
109
+ throw new SimpleError({
110
+ code: "invalid_field",
111
+ message: "Field is too long",
112
+ human: "De naam van de vereniging is te lang. Probeer de naam wat te verkorten en probeer opnieuw.",
113
+ field: "organization.name"
114
+ })
115
+ }
116
+ const uriExists = await Organization.getByURI(uri);
117
+
118
+ if (uriExists) {
119
+ throw new SimpleError({
120
+ code: "name_taken",
121
+ message: "An organization with the same name already exists",
122
+ human: "Er bestaat al een vereniging met dezelfde URI. Pas deze aan zodat deze uniek is, en controleer of deze vereniging niet al bestaat.",
123
+ field: "name",
124
+ });
125
+ }
126
+
127
+ const alreadyExists = await Organization.getByURI(Formatter.slug(put.name));
128
+
129
+ if (alreadyExists) {
130
+ throw new SimpleError({
131
+ code: "name_taken",
132
+ message: "An organization with the same name already exists",
133
+ human: "Er bestaat al een vereniging met dezelfde naam. Voeg bijvoorbeeld de naam van je gemeente toe.",
134
+ field: "name",
135
+ });
136
+ }
137
+
138
+ const organization = new Organization();
139
+ organization.id = put.id;
140
+ organization.name = put.name;
141
+
142
+ organization.uri = put.uri;
143
+ organization.meta = put.meta
144
+ organization.address = put.address
145
+
146
+ if (put.privateMeta) {
147
+ organization.privateMeta = put.privateMeta
148
+ }
149
+
150
+ try {
151
+ await organization.save();
152
+ } catch (e) {
153
+ console.error(e);
154
+ throw new SimpleError({
155
+ code: "creating_organization",
156
+ message: "Something went wrong while creating the organization. Please try again later or contact us.",
157
+ statusCode: 500
158
+ });
159
+ }
160
+
161
+ const organizationPeriod = new OrganizationRegistrationPeriod();
162
+ organizationPeriod.organizationId = organization.id;
163
+ organizationPeriod.periodId = (await Platform.getShared()).periodId
164
+ await organizationPeriod.save();
165
+
166
+ result.push(organization);
167
+ }
168
+
169
+ return new Response(await AuthenticatedStructures.adminOrganizations(result));
170
+ }
171
+ }
@@ -0,0 +1,137 @@
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 { Email } from '@stamhoofd/email';
5
+ import { PasswordToken, User } from '@stamhoofd/models';
6
+ import { User as UserStruct,UserPermissions } from "@stamhoofd/structures";
7
+ import { Formatter } from '@stamhoofd/utility';
8
+
9
+ import { Context } from '../../helpers/Context';
10
+ type Params = Record<string, never>;
11
+ type Query = undefined;
12
+ type Body = UserStruct
13
+ type ResponseBody = UserStruct
14
+
15
+ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
+ bodyDecoder = UserStruct as Decoder<UserStruct>
17
+
18
+ protected doesMatch(request: Request): [true, Params] | [false] {
19
+ if (request.method != "POST") {
20
+ return [false];
21
+ }
22
+
23
+ const params = Endpoint.parseParameters(request.url, "/user", {});
24
+
25
+ if (params) {
26
+ return [true, params as Params];
27
+ }
28
+ return [false];
29
+ }
30
+
31
+ async handle(request: DecodedRequest<Params, Query, Body>) {
32
+ const organization = await Context.setOptionalOrganizationScope();
33
+ const {user} = await Context.authenticate()
34
+
35
+ // Fast throw first (more in depth checking for patches later)
36
+ if (organization) {
37
+ if (!await Context.auth.canManageAdmins(organization.id)) {
38
+ throw Context.auth.error()
39
+ }
40
+ } else {
41
+ // Fast throw first (more in depth checking for patches later)
42
+ if (!Context.auth.canManagePlatformAdmins()) {
43
+ throw Context.auth.error()
44
+ }
45
+ }
46
+
47
+ // First check if a user exists with this email?
48
+ const existing = await User.getForRegister(organization?.id ?? null, request.body.email)
49
+
50
+ const admin = existing ?? (await User.createInvited(organization, {
51
+ firstName: request.body.firstName,
52
+ lastName: request.body.lastName,
53
+ email: request.body.email,
54
+ allowPlatform: true
55
+ }))
56
+
57
+ if (!admin) {
58
+ throw new SimpleError({
59
+ code: 'internal_error',
60
+ message: 'Something went wrong while creating the admin',
61
+ human: 'Er ging iets mis bij het aanmaken van dit account',
62
+ statusCode: 500
63
+ })
64
+ }
65
+
66
+ // Merge permissions
67
+ if (!request.body.permissions) {
68
+ throw new SimpleError({
69
+ code: 'missing_field',
70
+ message: 'When creating administrators, you are required to specify permissions in the request',
71
+ field: 'permissions'
72
+ })
73
+ }
74
+
75
+ if (organization) {
76
+ admin.permissions = UserPermissions.limitedPatch(admin.permissions, request.body.permissions, organization.id)
77
+ } else {
78
+ if (admin.permissions) {
79
+ admin.permissions.patchOrPut(request.body.permissions)
80
+ } else {
81
+ admin.permissions = request.body.permissions.isPut() ? request.body.permissions : null
82
+ }
83
+ }
84
+
85
+ if (!admin.firstName && request.body.firstName) {
86
+ // Allow setting the name if the user didn't had a name yet
87
+ admin.firstName = request.body.firstName
88
+ }
89
+
90
+ if (!admin.lastName && request.body.lastName) {
91
+ // Allow setting the name if the user didn't had a name yet
92
+ admin.lastName = request.body.lastName
93
+ }
94
+
95
+ await admin.save();
96
+
97
+ const { from, replyTo } = {
98
+ from: organization ? organization.getStrongEmail(request.i18n) : Email.getInternalEmailFor(request.i18n),
99
+ replyTo: undefined
100
+ }
101
+
102
+ // Create a password token that is valid for 7 days
103
+ const validUntil = new Date();
104
+ validUntil.setTime(validUntil.getTime() + 7 * 24 * 3600 * 1000);
105
+
106
+ const dateTime = Formatter.dateTime(validUntil)
107
+ const recoveryUrl = await PasswordToken.getPasswordRecoveryUrl(admin, organization, request.i18n, validUntil)
108
+
109
+ const name = organization?.name ?? request.i18n.t("shared.platformName")
110
+ const what = organization ? `de vereniging ${name} op ${request.i18n.t("shared.platformName")}` : `${request.i18n.t("shared.platformName")}`
111
+
112
+ if (admin.hasAccount()) {
113
+ const url = "https://"+(STAMHOOFD.domains.dashboard ?? "stamhoofd.app")+"/"+request.i18n.locale;
114
+
115
+ Email.send({
116
+ from,
117
+ replyTo,
118
+ to: admin.getEmailTo(),
119
+ subject: "✉️ Beheerder van "+name,
120
+ type: "transactional",
121
+ text: (admin.firstName ? "Dag "+admin.firstName : "Hallo") + `, \n\n${user.firstName ?? 'Iemand'} heeft je toegevoegd als beheerder van ${what}. Je kan inloggen met je bestaande account (${admin.email}) door te surfen naar:\n${url}\n\nDaar kan je jouw vereniging zoeken en aanklikken.\n\n----\n\nWeet je jouw wachtwoord niet meer? Dan kan je een nieuw wachtwoord instellen via de onderstaande link:\n`+recoveryUrl+"\n\nDeze link is geldig tot "+dateTime+".\n\nKen je deze vereniging niet? Dan kan je deze e-mail veilig negeren.\n\nMet vriendelijke groeten,\n"+request.i18n.t("shared.platformName")+"\n"
122
+ });
123
+ } else {
124
+ // Send email
125
+ Email.send({
126
+ from,
127
+ replyTo,
128
+ to: admin.getEmailTo(),
129
+ subject: "✉️ Uitnodiging beheerder van "+name,
130
+ type: "transactional",
131
+ text: (admin.firstName ? "Dag "+admin.firstName : "Hallo") + `, \n\n${user.firstName ?? 'Iemand'} heeft je uitgenodigd om beheerder te worden van ${what}. Je kan een account aanmaken door op de volgende link te klikken of door deze te kopiëren in de URL-balk van je browser:\n`+recoveryUrl+"\n\nDeze link is geldig tot "+dateTime+".\n\nKen je deze vereniging niet? Dan kan je deze e-mail veilig negeren.\n\nMet vriendelijke groeten,\n"+request.i18n.t("shared.platformName")+"\n"
132
+ });
133
+ }
134
+
135
+ return new Response(UserStruct.create({...admin, hasAccount: admin.hasAccount()}));
136
+ }
137
+ }