@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,65 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { Organization, RegisterCode, STCredit, UsedRegisterCode } from '@stamhoofd/models';
3
+ import { RegisterCodeStatus, UsedRegisterCode as UsedRegisterCodeStruct } from '@stamhoofd/structures';
4
+
5
+ import { Context } from '../../../../helpers/Context';
6
+
7
+ type Params = Record<string, never>;
8
+ type Query = undefined;
9
+ type Body = undefined;
10
+ type ResponseBody = RegisterCodeStatus;
11
+
12
+ export class GetRegisterCodeEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
13
+
14
+ protected doesMatch(request: Request): [true, Params] | [false] {
15
+ if (request.method != "GET") {
16
+ return [false];
17
+ }
18
+
19
+ const params = Endpoint.parseParameters(request.url, "/register-code", {});
20
+
21
+ if (params) {
22
+ return [true, params as Params];
23
+ }
24
+ return [false];
25
+ }
26
+
27
+ async handle(_: DecodedRequest<Params, Query, Body>) {
28
+ const organization = await Context.setOrganizationScope();
29
+ await Context.authenticate()
30
+
31
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
32
+ throw Context.auth.error()
33
+ }
34
+
35
+ const codes = await RegisterCode.where({ organizationId: organization.id })
36
+ let code = codes[0]
37
+
38
+ if (codes.length == 0) {
39
+ code = new RegisterCode()
40
+ code.organizationId = organization.id
41
+ code.description = "Doorverwezen door "+ organization.name
42
+ code.value = 2500
43
+ await code.generateCode()
44
+ await code.save()
45
+ }
46
+
47
+ const usedCodes = await UsedRegisterCode.getAll(code.code)
48
+ const allOrganizations = await Organization.getByIDs(...usedCodes.flatMap(u => u.organizationId ? [u.organizationId] : []))
49
+ const allCredits = await STCredit.getByIDs(...usedCodes.flatMap(u => u.creditId ? [u.creditId] : []))
50
+
51
+ return new Response(RegisterCodeStatus.create({
52
+ code: code.code,
53
+ value: code.value,
54
+ invoiceValue: code.invoiceValue,
55
+ usedCodes: usedCodes.map(c => {
56
+ return UsedRegisterCodeStruct.create({
57
+ id: c.id,
58
+ organizationName: allOrganizations.find(o => o.id === c.organizationId)?.name ?? "Onbekend",
59
+ createdAt: c.createdAt,
60
+ creditValue: (c.creditId ? allCredits.find(credit => credit.id === c.creditId)?.change : null) ?? null
61
+ })
62
+ })
63
+ }))
64
+ }
65
+ }
@@ -0,0 +1,281 @@
1
+ import { AutoEncoderPatchType, PatchableArray } from '@simonbackx/simple-encoding';
2
+ import { Request } from "@simonbackx/simple-endpoints";
3
+ import { GroupFactory, OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
4
+ import { Group, GroupGenderType, GroupPatch, GroupPrivateSettings, GroupSettings, GroupSettingsPatch, Organization, PermissionLevel, PermissionRole, PermissionRoleDetailed, Permissions, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
5
+
6
+ import { testServer } from '../../../../../tests/helpers/TestServer';
7
+ import { PatchOrganizationEndpoint } from './PatchOrganizationEndpoint';
8
+
9
+ describe("Endpoint.PatchOrganization", () => {
10
+ // Test endpoint
11
+ const endpoint = new PatchOrganizationEndpoint();
12
+
13
+ test("Change the name of the organization", async () => {
14
+ const organization = await new OrganizationFactory({}).create()
15
+ const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create()
16
+ //const groups = await new GroupFactory({ organization }).createMultiple(2)
17
+ const token = await Token.createToken(user)
18
+
19
+ const r = Request.buildJson("PATCH", "/v2/organization", organization.getApiHost(), {
20
+ id: organization.id,
21
+ name: "My crazy name"
22
+ });
23
+ r.headers.authorization = "Bearer "+token.accessToken
24
+
25
+ const response = await testServer.test(endpoint, r);
26
+ expect(response.body).toBeDefined();
27
+
28
+ if (!(response.body instanceof Organization)) {
29
+ throw new Error("Expected Organization")
30
+ }
31
+
32
+ expect(response.body.id).toEqual(organization.id)
33
+ expect(response.body.name).toEqual("My crazy name")
34
+ });
35
+
36
+ test("Can't change organization as a normal user", async () => {
37
+ const organization = await new OrganizationFactory({}).create()
38
+ const user = await new UserFactory({ organization }).create()
39
+ const token = await Token.createToken(user)
40
+
41
+ const r = Request.buildJson("PATCH", "/organization", organization.getApiHost(), {
42
+ id: organization.id,
43
+ name: "My crazy name"
44
+ });
45
+ r.headers.authorization = "Bearer " + token.accessToken
46
+
47
+ await expect(testServer.test(endpoint, r)).rejects.toThrow(/permissions/i);
48
+ });
49
+
50
+ test("Can't change organization as a user with read access", async () => {
51
+ const organization = await new OrganizationFactory({}).create()
52
+ const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Read }) }).create()
53
+ const token = await Token.createToken(user)
54
+
55
+ const r = Request.buildJson("PATCH", "/organization", organization.getApiHost(), {
56
+ id: organization.id,
57
+ name: "My crazy name"
58
+ });
59
+ r.headers.authorization = "Bearer " + token.accessToken
60
+
61
+ await expect(testServer.test(endpoint, r)).rejects.toThrow(/permissions/i);
62
+ });
63
+
64
+ test("Change the name of a group with access", async () => {
65
+ const organization = await new OrganizationFactory({}).create()
66
+ const role = PermissionRoleDetailed.create({
67
+ name: "Role"
68
+ })
69
+ organization.privateMeta.roles.push(
70
+ role
71
+ )
72
+ const groups = await new GroupFactory({ organization }).createMultiple(2)
73
+
74
+ role.resources.set(PermissionsResourceType.Groups, new Map())
75
+ role.resources.get(PermissionsResourceType.Groups)!.set(groups[0].id, ResourcePermissions.create({
76
+ level: PermissionLevel.Full
77
+ }))
78
+
79
+ await organization.save()
80
+
81
+ const validPermissions = [
82
+ Permissions.create({
83
+ level: PermissionLevel.None,
84
+ roles: [PermissionRole.create(role)]
85
+ }),
86
+ Permissions.create({
87
+ level: PermissionLevel.Full
88
+ }),
89
+ ]
90
+
91
+ for (const permission of validPermissions) {
92
+ const user = await new UserFactory({ organization,
93
+ permissions: permission
94
+ }).create()
95
+ const token = await Token.createToken(user)
96
+
97
+ const changes = new PatchableArray<string, Group, AutoEncoderPatchType<Group>>()
98
+ changes.addPatch(GroupPatch.create({
99
+ id: groups[0].id,
100
+ settings: GroupSettingsPatch.create({
101
+ name: "My crazy group name",
102
+ })
103
+ }))
104
+
105
+ const r = Request.buildJson("PATCH", "/organization", organization.getApiHost(), {
106
+ id: organization.id,
107
+ groups: changes.encode({ version: 2 }),
108
+ });
109
+ r.headers.authorization = "Bearer " + token.accessToken
110
+
111
+ const response = await testServer.test(endpoint, r);
112
+ expect(response.body).toBeDefined();
113
+
114
+ if (!(response.body instanceof Organization)) {
115
+ throw new Error("Expected Organization")
116
+ }
117
+
118
+ expect(response.body.id).toEqual(organization.id)
119
+ expect(response.body.groups.find(g => g.id == groups[0].id)!.settings.name).toEqual("My crazy group name")
120
+ }
121
+ });
122
+
123
+ test("Can't change name of group without access", async () => {
124
+ const organization = await new OrganizationFactory({}).create()
125
+ const role = PermissionRoleDetailed.create({
126
+ name: "Role"
127
+ })
128
+ const role2 = PermissionRoleDetailed.create({
129
+ name: "Role2"
130
+ })
131
+ organization.privateMeta.roles.push(
132
+ role,
133
+ role2
134
+ )
135
+ await organization.save()
136
+ const groups = await new GroupFactory({ organization }).createMultiple(2)
137
+
138
+ groups[0].privateSettings.permissions.write.push(PermissionRole.create(role))
139
+ await groups[0].save()
140
+
141
+ groups[0].privateSettings.permissions.read.push(PermissionRole.create(role2))
142
+ await groups[0].save()
143
+
144
+ const invalidPermissions = [
145
+ Permissions.create({
146
+ level: PermissionLevel.Read,
147
+ roles: [PermissionRole.create(role)]
148
+ }),
149
+ Permissions.create({
150
+ level: PermissionLevel.None,
151
+ roles: [PermissionRole.create(role2)]
152
+ }),
153
+ Permissions.create({
154
+ level: PermissionLevel.Write,
155
+ roles: [PermissionRole.create(role2), PermissionRole.create(role)]
156
+ }),
157
+ Permissions.create({
158
+ level: PermissionLevel.Write
159
+ }),
160
+ Permissions.create({
161
+ level: PermissionLevel.Read
162
+ }),
163
+ null
164
+ ]
165
+
166
+ for (const permission of invalidPermissions) {
167
+ const user = await new UserFactory({
168
+ organization,
169
+ permissions: permission
170
+ }).create()
171
+ const token = await Token.createToken(user)
172
+
173
+ const changes = new PatchableArray<string, Group, AutoEncoderPatchType<Group>>()
174
+ changes.addPatch(GroupPatch.create({
175
+ id: groups[0].id,
176
+ settings: GroupSettingsPatch.create({
177
+ name: "My crazy group name",
178
+ })
179
+ }))
180
+ const r = Request.buildJson("PATCH", "/organization", organization.getApiHost(), {
181
+ id: organization.id,
182
+ groups: changes.encode({ version: 2 }),
183
+ });
184
+ r.headers.authorization = "Bearer " + token.accessToken
185
+ await expect(testServer.test(endpoint, r)).rejects.toThrow(/permissions/i);
186
+ }
187
+
188
+
189
+ });
190
+
191
+
192
+ test("Create a group with access", async () => {
193
+ const organization = await new OrganizationFactory({}).create()
194
+ const groups = await new GroupFactory({ organization }).createMultiple(2)
195
+
196
+ const validPermissions = [
197
+ Permissions.create({
198
+ level: PermissionLevel.Full
199
+ }),
200
+ ]
201
+
202
+ const invalidPermissions = [
203
+ Permissions.create({
204
+ level: PermissionLevel.Write
205
+ }),
206
+ ]
207
+
208
+ for (const permission of validPermissions) {
209
+ const user = await new UserFactory({
210
+ organization,
211
+ permissions: permission
212
+ }).create()
213
+ const token = await Token.createToken(user)
214
+
215
+ const changes = new PatchableArray<string, Group, AutoEncoderPatchType<Group>>()
216
+ const put = Group.create({
217
+ cycle: 0,
218
+ organizationId: organization.id,
219
+ periodId: organization.periodId,
220
+ settings: GroupSettings.create({
221
+ name: "My crazy group name",
222
+ startDate: new Date(),
223
+ endDate: new Date(),
224
+ registrationStartDate: new Date(),
225
+ registrationEndDate: new Date(),
226
+ genderType: GroupGenderType.Mixed,
227
+ }),
228
+ privateSettings: GroupPrivateSettings.create({})
229
+ })
230
+ changes.addPut(put)
231
+
232
+ const r = Request.buildJson("PATCH", "/v140/organization", organization.getApiHost(), {
233
+ id: organization.id,
234
+ groups: changes.encode({ version: 140 }),
235
+ });
236
+ r.headers.authorization = "Bearer " + token.accessToken
237
+
238
+ const response = await testServer.test(endpoint, r);
239
+ expect(response.body).toBeDefined();
240
+
241
+ if (!(response.body instanceof Organization)) {
242
+ throw new Error("Expected Organization")
243
+ }
244
+
245
+ expect(response.body.id).toEqual(organization.id)
246
+ expect(response.body.groups.map(g => g.id)).toContainEqual(put.id)
247
+ }
248
+
249
+ for (const permission of invalidPermissions) {
250
+ const user = await new UserFactory({
251
+ organization,
252
+ permissions: permission
253
+ }).create()
254
+ const token = await Token.createToken(user)
255
+
256
+ const changes = new PatchableArray<string, Group, AutoEncoderPatchType<Group>>()
257
+ const put = Group.create({
258
+ cycle: 0,
259
+ organizationId: organization.id,
260
+ periodId: organization.periodId,
261
+ settings: GroupSettings.create({
262
+ name: "My crazy group name",
263
+ startDate: new Date(),
264
+ endDate: new Date(),
265
+ registrationStartDate: new Date(),
266
+ registrationEndDate: new Date(),
267
+ genderType: GroupGenderType.Mixed,
268
+ })
269
+ })
270
+ changes.addPut(put)
271
+
272
+ const r = Request.buildJson("PATCH", "/v2/organization", organization.getApiHost(), {
273
+ id: organization.id,
274
+ groups: changes.encode({ version: 2 }),
275
+ });
276
+ r.headers.authorization = "Bearer " + token.accessToken
277
+ await expect(testServer.test(endpoint, r)).rejects.toThrow(/permissions/i);
278
+ }
279
+ });
280
+
281
+ });
@@ -0,0 +1,338 @@
1
+ import { AutoEncoderPatchType, Decoder, ObjectData, patchObject } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
4
+ import { Group, Organization,PayconiqPayment, Platform, StripeAccount, Token, User, Webshop } from '@stamhoofd/models';
5
+ import { BuckarooSettings, GroupPrivateSettings, Organization as OrganizationStruct, OrganizationPatch, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, Permissions, PermissionsResourceType,ResourcePermissions, UserPermissions, Version, OrganizationMetaData } from "@stamhoofd/structures";
6
+ import { Formatter } from '@stamhoofd/utility';
7
+
8
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
9
+ import { BuckarooHelper } from '../../../../helpers/BuckarooHelper';
10
+ import { Context } from '../../../../helpers/Context';
11
+
12
+ type Params = Record<string, never>;
13
+ type Query = undefined;
14
+ type Body = AutoEncoderPatchType<OrganizationStruct>;
15
+ type ResponseBody = OrganizationStruct;
16
+
17
+ /**
18
+ * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
19
+ */
20
+
21
+ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ bodyDecoder = OrganizationPatch as Decoder<AutoEncoderPatchType<OrganizationStruct>>
23
+
24
+ protected doesMatch(request: Request): [true, Params] | [false] {
25
+ if (request.method != "PATCH") {
26
+ return [false];
27
+ }
28
+
29
+ const params = Endpoint.parseParameters(request.url, "/organization", {});
30
+
31
+ if (params) {
32
+ return [true, params as Params];
33
+ }
34
+ return [false];
35
+ }
36
+
37
+ async handle(request: DecodedRequest<Params, Query, Body>) {
38
+ const organization = await Context.setOrganizationScope();
39
+ const {user} = await Context.authenticate()
40
+
41
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
42
+ throw Context.auth.error()
43
+ }
44
+
45
+ // check if organization ID matches
46
+ if (request.body.id !== organization.id) {
47
+ throw new SimpleError({
48
+ code: "invalid_id",
49
+ message: "You cannot modify an organization with a different ID than the organization you are signed in for",
50
+ statusCode: 403
51
+ })
52
+ }
53
+
54
+ const errors = new SimpleErrors()
55
+
56
+ if (await Context.auth.hasFullAccess(organization.id)) {
57
+ organization.name = request.body.name ?? organization.name
58
+ if (request.body.website !== undefined) {
59
+ organization.website = request.body.website;
60
+ }
61
+
62
+ if (request.body.address) {
63
+ organization.address = organization.address.patch(request.body.address)
64
+ }
65
+
66
+ if (request.body.uri && request.body.uri !== organization.uri) {
67
+ const slugified = Formatter.slug(request.body.uri);
68
+ if (slugified.length > 100) {
69
+ throw new SimpleError({
70
+ code: "invalid_field",
71
+ message: "Field is too long",
72
+ human: "De URI van de vereniging is te lang",
73
+ field: "uri"
74
+ })
75
+ }
76
+
77
+ if (slugified.length < 3) {
78
+ throw new SimpleError({
79
+ code: "invalid_field",
80
+ message: "Field is too short",
81
+ human: "De URI van de vereniging is te kort",
82
+ field: "uri"
83
+ })
84
+ }
85
+ const alreadyExists = await Organization.getByURI(slugified);
86
+
87
+ if (alreadyExists) {
88
+ throw new SimpleError({
89
+ code: "name_taken",
90
+ message: "An organization with the same URI already exists",
91
+ human: "Er bestaat al een vereniging met dezelfde URI. Voeg bijvoorbeeld de naam van je gemeente toe.",
92
+ field: "uri",
93
+ });
94
+ }
95
+
96
+ organization.uri = slugified
97
+ }
98
+
99
+ if (request.body.privateMeta && request.body.privateMeta.isPatch()) {
100
+ organization.privateMeta.emails = request.body.privateMeta.emails.applyTo(organization.privateMeta.emails)
101
+ organization.privateMeta.roles = request.body.privateMeta.roles.applyTo(organization.privateMeta.roles)
102
+ organization.privateMeta.privateKey = request.body.privateMeta.privateKey ?? organization.privateMeta.privateKey
103
+ organization.privateMeta.featureFlags = patchObject(organization.privateMeta.featureFlags, request.body.privateMeta.featureFlags);
104
+
105
+ if (request.body.privateMeta.mollieProfile !== undefined) {
106
+ organization.privateMeta.mollieProfile = patchObject(organization.privateMeta.mollieProfile, request.body.privateMeta.mollieProfile)
107
+ }
108
+
109
+ if (request.body.privateMeta.useTestPayments !== undefined) {
110
+ organization.privateMeta.useTestPayments = request.body.privateMeta.useTestPayments
111
+ }
112
+
113
+ // Apply payconiq patch
114
+ if (request.body.privateMeta.payconiqAccounts !== undefined) {
115
+ organization.privateMeta.payconiqAccounts = patchObject(organization.privateMeta.payconiqAccounts, request.body.privateMeta.payconiqAccounts)
116
+
117
+ for (const account of organization.privateMeta.payconiqAccounts) {
118
+ if (account.merchantId === null) {
119
+ const payment = await PayconiqPayment.createTest(organization, account)
120
+
121
+ if (!payment) {
122
+ throw new SimpleError({
123
+ code: "invalid_field",
124
+ message: "De API-key voor Payconiq is niet geldig. Kijk eens na of je wel de juiste key hebt ingevuld.",
125
+ field: "payconiqAccounts"
126
+ })
127
+ }
128
+
129
+ // Save merchant id
130
+ const decoded = PayconiqAccount.decode(
131
+ new ObjectData({
132
+ ...(payment as any).creditor,
133
+ id: account.id,
134
+ apiKey: account.apiKey,
135
+ }, {version: 0})
136
+ )
137
+
138
+ account.merchantId = decoded.merchantId
139
+ account.callbackUrl = decoded.callbackUrl
140
+ account.profileId = decoded.profileId
141
+ account.name = decoded.name
142
+ account.iban = decoded.iban
143
+ }
144
+ }
145
+ }
146
+
147
+ if (request.body.privateMeta.buckarooSettings !== undefined) {
148
+ if (request.body.privateMeta.buckarooSettings === null) {
149
+ organization.privateMeta.buckarooSettings = null;
150
+ } else {
151
+ organization.privateMeta.buckarooSettings = organization.privateMeta.buckarooSettings ?? BuckarooSettings.create({})
152
+ organization.privateMeta.buckarooSettings.patchOrPut(request.body.privateMeta.buckarooSettings)
153
+
154
+ // Validate buckaroo settings
155
+ const buckaroo = new BuckarooHelper(organization.privateMeta.buckarooSettings.key, organization.privateMeta.buckarooSettings.secret, organization.privateMeta.useTestPayments ?? STAMHOOFD.environment != 'production')
156
+
157
+ if (!(await buckaroo.createTest())) {
158
+ throw new SimpleError({
159
+ code: "invalid_field",
160
+ message: "De key of secret voor Buckaroo is niet geldig. Kijk eens na of je wel de juiste key hebt ingevuld.",
161
+ field: "buckarooSettings"
162
+ })
163
+ }
164
+ }
165
+ }
166
+
167
+ if (request.body.privateMeta.registrationPaymentConfiguration?.stripeAccountId !== undefined) {
168
+ if (request.body.privateMeta.registrationPaymentConfiguration.stripeAccountId !== null) {
169
+ const account = await StripeAccount.getByID(request.body.privateMeta.registrationPaymentConfiguration.stripeAccountId)
170
+ if (!account || account.organizationId !== organization.id) {
171
+ throw new SimpleError({
172
+ code: "invalid_field",
173
+ message: "Het Stripe account dat je hebt gekozen bestaat niet (meer)",
174
+ field: "registrationPaymentConfiguration.stripeAccountId"
175
+ })
176
+ }
177
+ }
178
+ organization.privateMeta.registrationPaymentConfiguration.stripeAccountId = request.body.privateMeta.registrationPaymentConfiguration.stripeAccountId
179
+ }
180
+ }
181
+
182
+ // Allow admin patches (permissions only atm). No put atm
183
+ if (request.body.admins) {
184
+ for (const patch of request.body.admins.getPatches()) {
185
+ if (patch.permissions) {
186
+ const admin = await User.getByID(patch.id)
187
+ if (!admin || !await Context.auth.canAccessUser(admin, PermissionLevel.Full)) {
188
+ throw new SimpleError({
189
+ code: "invalid_field",
190
+ message: "De beheerder die je wilt wijzigen bestaat niet (meer)",
191
+ field: "admins"
192
+ })
193
+ }
194
+
195
+ admin.permissions = UserPermissions.limitedPatch(admin.permissions, patch.permissions, organization.id)
196
+
197
+ if (admin.id === user.id && (!admin.permissions || !admin.permissions.forOrganization(organization)?.hasFullAccess())) {
198
+ throw new SimpleError({
199
+ code: "permission_denied",
200
+ message: "Je kan jezelf niet verwijderen als hoofdbeheerder"
201
+ })
202
+ }
203
+ await admin.save()
204
+ }
205
+ }
206
+ }
207
+
208
+ if (request.body.meta) {
209
+ const savedPackages = organization.meta.packages
210
+ organization.meta.patchOrPut(request.body.meta)
211
+ organization.meta.packages = savedPackages
212
+
213
+ // check payconiq + mollie
214
+ if (request.body.meta.registrationPaymentConfiguration) {
215
+ if (!organization.privateMeta.payconiqApiKey && !organization.privateMeta.buckarooSettings?.paymentMethods.includes(PaymentMethod.Payconiq)) {
216
+ const i = organization.meta.paymentMethods.findIndex(p => p == PaymentMethod.Payconiq)
217
+ if (i != -1) {
218
+ throw new SimpleError({
219
+ code: "invalid_field",
220
+ message: "Je kan Payconiq niet activeren omdat je geen Payconiq API Key hebt ingesteld. Schakel Payconiq uit voor je verder gaat.",
221
+ field: "paymentMethods"
222
+ })
223
+ }
224
+ }
225
+
226
+ // check payconiq + mollie
227
+ if (!organization.privateMeta.mollieOnboarding || !organization.privateMeta.mollieOnboarding.canReceivePayments) {
228
+ let stripe: StripeAccount | undefined = undefined
229
+ if (organization.privateMeta.registrationPaymentConfiguration.stripeAccountId) {
230
+ stripe = await StripeAccount.getByID(organization.privateMeta.registrationPaymentConfiguration.stripeAccountId)
231
+ }
232
+
233
+ const i = organization.meta.paymentMethods.findIndex(p => {
234
+ if (p === PaymentMethod.Payconiq) return
235
+ if (p === PaymentMethod.Transfer) return
236
+ if (p === PaymentMethod.PointOfSale) return
237
+
238
+ if (!organization.privateMeta.buckarooSettings?.paymentMethods.includes(p)) {
239
+ if (!stripe?.meta.paymentMethods.includes(p)) {
240
+ return true
241
+ }
242
+ }
243
+ })
244
+ if (i != -1) {
245
+ throw new SimpleError({
246
+ code: "invalid_field",
247
+ message: "Je kan "+PaymentMethodHelper.getName(organization.meta.paymentMethods[i])+" niet activeren omdat je daarvoor nog niet aangesloten bent bij een betaalpartner. Schakel "+PaymentMethodHelper.getName(organization.meta.paymentMethods[i])+" uit voor je verder gaat.",
248
+ field: "paymentMethods"
249
+ })
250
+ }
251
+ }
252
+ }
253
+
254
+ if (request.body.meta?.tags) {
255
+ if (!Context.auth.hasPlatformFullAccess()) {
256
+ throw Context.auth.error()
257
+ }
258
+
259
+ const cleanedPatch = OrganizationMetaData.patch({
260
+ tags: request.body.meta.tags as any
261
+ })
262
+ const platform = await Platform.getShared()
263
+ const patchedMeta = organization.meta.patch(cleanedPatch);
264
+ for (const tag of patchedMeta.tags) {
265
+ if (!platform.config.tags.find(t => t.id === tag)) {
266
+ throw new SimpleError({ code: "invalid_tag", message: "Invalid tag", statusCode: 400 });
267
+ }
268
+ }
269
+
270
+ // Sort tags based on platform config order
271
+ patchedMeta.tags.sort((a, b) => {
272
+ const aIndex = platform.config.tags.findIndex(t => t.id === a);
273
+ const bIndex = platform.config.tags.findIndex(t => t.id === b);
274
+ return aIndex - bIndex;
275
+ })
276
+
277
+ organization.meta.tags = patchedMeta.tags;
278
+ }
279
+ }
280
+
281
+ if (request.body.uri) {
282
+ if (!Context.auth.hasPlatformFullAccess()) {
283
+ throw Context.auth.error()
284
+ }
285
+
286
+ const uriExists = await Organization.getByURI(request.body.uri);
287
+
288
+ if (uriExists && uriExists.id !== organization.id) {
289
+ throw new SimpleError({
290
+ code: "name_taken",
291
+ message: "An organization with the same name already exists",
292
+ human: "Er bestaat al een vereniging met dezelfde URI. Pas deze aan zodat deze uniek is, en controleer of deze vereniging niet al bestaat.",
293
+ field: "name",
294
+ });
295
+ }
296
+
297
+ organization.uri = request.body.uri
298
+ }
299
+
300
+ // Save the organization
301
+ await organization.save()
302
+ } else {
303
+ if (request.body.name || request.body.address) {
304
+ throw new SimpleError({
305
+ code: "permission_denied",
306
+ message: "You do not have permissions to edit the organization settings",
307
+ statusCode: 403
308
+ })
309
+ }
310
+ }
311
+
312
+ // Only needed for permissions atm, so no put or delete here
313
+ for (const struct of request.body.webshops.getPatches()) {
314
+ const model = await Webshop.getByID(struct.id)
315
+
316
+ if (!model || !await Context.auth.canAccessWebshop(model, PermissionLevel.Full)) {
317
+ errors.addError(
318
+ Context.auth.error('Je hebt geen toegangsrechten om deze webshop te wijzigen')
319
+ )
320
+ continue;
321
+ }
322
+
323
+ if (struct.meta) {
324
+ model.meta.patchOrPut(struct.meta)
325
+ }
326
+
327
+ if (struct.privateMeta) {
328
+ model.privateMeta.patchOrPut(struct.privateMeta)
329
+ }
330
+
331
+ await model.save();
332
+ }
333
+
334
+ errors.throwIfNotEmpty()
335
+ return new Response(await AuthenticatedStructures.organization(organization));
336
+ }
337
+ }
338
+