@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.
- package/.env.template.json +63 -0
- package/.eslintrc.js +61 -0
- package/README.md +40 -0
- package/index.ts +172 -0
- package/jest.config.js +11 -0
- package/migrations.ts +33 -0
- package/package.json +48 -0
- package/src/crons.ts +845 -0
- package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +42 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +320 -0
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +171 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +137 -0
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +68 -0
- package/src/endpoints/auth/CreateTokenEndpoint.ts +200 -0
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +31 -0
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +70 -0
- package/src/endpoints/auth/GetUserEndpoint.test.ts +64 -0
- package/src/endpoints/auth/GetUserEndpoint.ts +57 -0
- package/src/endpoints/auth/PatchApiUserEndpoint.ts +90 -0
- package/src/endpoints/auth/PatchUserEndpoint.ts +122 -0
- package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +37 -0
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +41 -0
- package/src/endpoints/auth/SignupEndpoint.ts +107 -0
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +89 -0
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +95 -0
- package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +31 -0
- package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +101 -0
- package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +53 -0
- package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +57 -0
- package/src/endpoints/global/files/UploadFile.ts +147 -0
- package/src/endpoints/global/files/UploadImage.ts +119 -0
- package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +76 -0
- package/src/endpoints/global/members/GetMembersCountEndpoint.ts +43 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +429 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +734 -0
- package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +45 -0
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +105 -0
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +146 -0
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +52 -0
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +80 -0
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +49 -0
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +58 -0
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +62 -0
- package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +153 -0
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +134 -0
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +44 -0
- package/src/endpoints/global/platform/GetPlatformEnpoint.ts +39 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +63 -0
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +68 -0
- package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +39 -0
- package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +80 -0
- package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +41 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +134 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +521 -0
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +37 -0
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +115 -0
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +187 -0
- package/src/endpoints/organization/dashboard/billing/ActivatePackagesEndpoint.ts +424 -0
- package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +67 -0
- package/src/endpoints/organization/dashboard/billing/GetBillingStatusEndpoint.ts +39 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +57 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +129 -0
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +114 -0
- package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +234 -0
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +62 -0
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +85 -0
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +80 -0
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +54 -0
- package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +49 -0
- package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +63 -0
- package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +61 -0
- package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +64 -0
- package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +84 -0
- package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +43 -0
- package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +42 -0
- package/src/endpoints/organization/dashboard/organization/GetOrganizationSSOEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +65 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +281 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +338 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +196 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationSSOEndpoint.ts +50 -0
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +48 -0
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +207 -0
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +202 -0
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +233 -0
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +66 -0
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -0
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +93 -0
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +59 -0
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +78 -0
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +40 -0
- package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +69 -0
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +52 -0
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +73 -0
- package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +60 -0
- package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +47 -0
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +41 -0
- package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +217 -0
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +51 -0
- package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +47 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +83 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +68 -0
- package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +69 -0
- package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +125 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +204 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +278 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +80 -0
- package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +60 -0
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +379 -0
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +54 -0
- package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +45 -0
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +78 -0
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +34 -0
- package/src/endpoints/organization/shared/auth/OpenIDConnectCallbackEndpoint.ts +44 -0
- package/src/endpoints/organization/shared/auth/OpenIDConnectStartEndpoint.ts +82 -0
- package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +59 -0
- package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +51 -0
- package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +40 -0
- package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +124 -0
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +130 -0
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +50 -0
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +450 -0
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +335 -0
- package/src/helpers/AddressValidator.test.ts +40 -0
- package/src/helpers/AddressValidator.ts +256 -0
- package/src/helpers/AdminPermissionChecker.ts +1031 -0
- package/src/helpers/AuthenticatedStructures.ts +158 -0
- package/src/helpers/BuckarooHelper.ts +279 -0
- package/src/helpers/CheckSettlements.ts +215 -0
- package/src/helpers/Context.ts +202 -0
- package/src/helpers/CookieHelper.ts +45 -0
- package/src/helpers/ForwardHandler.test.ts +216 -0
- package/src/helpers/ForwardHandler.ts +140 -0
- package/src/helpers/OpenIDConnectHelper.ts +284 -0
- package/src/helpers/StripeHelper.ts +293 -0
- package/src/helpers/StripePayoutChecker.ts +188 -0
- package/src/middleware/ContextMiddleware.ts +16 -0
- package/src/migrations/1646578856-validate-addresses.ts +60 -0
- package/src/seeds/0000000000-example.ts +13 -0
- package/src/seeds/1715028563-user-permissions.ts +52 -0
- package/tests/e2e/stock.test.ts +2120 -0
- package/tests/e2e/tickets.test.ts +926 -0
- package/tests/helpers/StripeMocker.ts +362 -0
- package/tests/helpers/TestServer.ts +21 -0
- package/tests/jest.global.setup.ts +29 -0
- package/tests/jest.setup.ts +59 -0
- 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
|
+
}
|