@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,85 @@
1
+ import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { EmailTemplate } from '@stamhoofd/models';
4
+ import { EmailTemplate as EmailTemplateStruct, PermissionLevel } from '@stamhoofd/structures';
5
+
6
+ import { Context } from '../../../../helpers/Context';
7
+
8
+ type Params = Record<string, never>;
9
+ type Body = PatchableArrayAutoEncoder<EmailTemplateStruct>;
10
+ type Query = undefined;
11
+
12
+ type ResponseBody = EmailTemplateStruct[];
13
+
14
+ export class PatchEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ bodyDecoder = new PatchableArrayDecoder(EmailTemplateStruct as Decoder<EmailTemplateStruct>, EmailTemplateStruct.patchType() as Decoder<AutoEncoderPatchType<EmailTemplateStruct>>, StringDecoder)
16
+
17
+ protected doesMatch(request: Request): [true, Params] | [false] {
18
+ if (request.method != "PATCH") {
19
+ return [false];
20
+ }
21
+
22
+ const params = Endpoint.parseParameters(request.url, "/email-templates", {});
23
+
24
+ if (params) {
25
+ return [true, params as Params];
26
+ }
27
+ return [false];
28
+ }
29
+
30
+ async handle(request: DecodedRequest<Params, Query, Body>) {
31
+ const organization = await Context.setOrganizationScope();
32
+ await Context.authenticate()
33
+
34
+ // Fast throw first (more in depth checking for patches later)
35
+ if (!await Context.auth.canReadEmailTemplates(organization.id)) {
36
+ throw Context.auth.error()
37
+ }
38
+
39
+ const templates: EmailTemplate[] = []
40
+
41
+ // Get all patches
42
+ for (const patch of request.body.getPatches()) {
43
+ const template = await EmailTemplate.getByID(patch.id)
44
+ if (!template || !(await Context.auth.canAccessEmailTemplate(template, PermissionLevel.Write))) {
45
+ throw Context.auth.notFoundOrNoAccess("Je hebt geen toegang om deze emailtemplate te bewerken")
46
+ }
47
+
48
+ template.html = patch.html ?? template.html
49
+ template.subject = patch.subject ?? template.subject
50
+ template.text = patch.text ?? template.text
51
+ template.json = patch.json ?? template.json
52
+
53
+ await template.save()
54
+
55
+ templates.push(template)
56
+ }
57
+
58
+ for (const put of request.body.getPuts()) {
59
+ const struct = put.put
60
+ const template = new EmailTemplate()
61
+ template.id = struct.id
62
+ template.organizationId = organization.id
63
+ template.webshopId = struct.webshopId
64
+ template.groupId = struct.groupId
65
+
66
+ template.html = struct.html
67
+ template.subject = struct.subject
68
+ template.text = struct.text
69
+ template.json = struct.json
70
+
71
+ template.type = struct.type
72
+
73
+ // Check if valid + write permissions
74
+ if (!(await Context.auth.canAccessEmailTemplate(template, PermissionLevel.Write))) {
75
+ throw Context.auth.error("Je hebt geen toegang om deze emailtemplate te maken")
76
+ }
77
+
78
+ await template.save()
79
+
80
+ templates.push(template)
81
+ }
82
+
83
+ return new Response(templates.map(template => EmailTemplateStruct.create(template)))
84
+ }
85
+ }
@@ -0,0 +1,80 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { MollieToken } from '@stamhoofd/models';
3
+ import { CheckMollieResponse, Organization as OrganizationStruct, PermissionLevel } from "@stamhoofd/structures";
4
+
5
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
6
+ import { Context } from '../../../../helpers/Context';
7
+
8
+ type Params = Record<string, never>;
9
+ type Body = undefined
10
+ type Query = undefined
11
+ type ResponseBody = OrganizationStruct|CheckMollieResponse
12
+
13
+ export class CheckMollieEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ protected doesMatch(request: Request): [true, Params] | [false] {
15
+ if (request.method != "POST") {
16
+ return [false];
17
+ }
18
+
19
+ const params = Endpoint.parseParameters(request.url, "/mollie/check", {});
20
+
21
+ if (params) {
22
+ return [true, params as Params];
23
+ }
24
+
25
+ return [false];
26
+ }
27
+
28
+ async handle(request: DecodedRequest<Params, Query, Body>) {
29
+ const organization = await Context.setOrganizationScope();
30
+ await Context.authenticate()
31
+
32
+ // Fast throw first (more in depth checking for patches later)
33
+ if (!await Context.auth.canManagePaymentAccounts(organization.id, PermissionLevel.Full)) {
34
+ throw Context.auth.error()
35
+ }
36
+
37
+ const mollie = await MollieToken.getTokenFor(organization.id)
38
+
39
+ if (!mollie) {
40
+ organization.privateMeta.mollieOnboarding = null
41
+ organization.privateMeta.mollieProfile = null
42
+ await organization.save()
43
+
44
+ if (request.request.getVersion() < 200) {
45
+ return new Response(await AuthenticatedStructures.organization(organization));
46
+ }
47
+
48
+ return new Response(CheckMollieResponse.create({
49
+ organization: await AuthenticatedStructures.organization(organization),
50
+ profiles: []
51
+ }));
52
+ }
53
+ const profiles = await mollie.getProfiles();
54
+
55
+ const status = await mollie.getOnboardingStatus();
56
+ organization.privateMeta.mollieOnboarding = status;
57
+
58
+ // Check profile is still valid
59
+ if (organization.privateMeta.mollieProfile) {
60
+ const s = organization.privateMeta.mollieProfile.id
61
+ const profile = profiles.find(p => p.id === s)
62
+ if (!profile) {
63
+ organization.privateMeta.mollieProfile = null
64
+ } else {
65
+ organization.privateMeta.mollieProfile = profile
66
+ }
67
+ }
68
+
69
+ await organization.save()
70
+
71
+ if (request.request.getVersion() < 200) {
72
+ return new Response(await AuthenticatedStructures.organization(organization));
73
+ }
74
+
75
+ return new Response(CheckMollieResponse.create({
76
+ organization: await AuthenticatedStructures.organization(organization),
77
+ profiles
78
+ }));
79
+ }
80
+ }
@@ -0,0 +1,54 @@
1
+
2
+ import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
3
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
4
+ import { MollieToken } from '@stamhoofd/models';
5
+ import { Organization as OrganizationStruct, PermissionLevel } from "@stamhoofd/structures";
6
+
7
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
8
+ import { checkMollieSettlementsFor } from '../../../../helpers/CheckSettlements';
9
+ import { Context } from '../../../../helpers/Context';
10
+
11
+ type Params = Record<string, never>;
12
+
13
+ class Body extends AutoEncoder {
14
+ @field({ decoder: StringDecoder })
15
+ code: string
16
+ }
17
+
18
+ type Query = undefined
19
+ type ResponseBody = OrganizationStruct
20
+
21
+ export class ConnectMollieEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ bodyDecoder = Body as Decoder<Body>
23
+
24
+ protected doesMatch(request: Request): [true, Params] | [false] {
25
+ if (request.method != "POST") {
26
+ return [false];
27
+ }
28
+
29
+ const params = Endpoint.parseParameters(request.url, "/mollie/connect", {});
30
+
31
+ if (params) {
32
+ return [true, params as Params];
33
+ }
34
+
35
+ return [false];
36
+ }
37
+
38
+ async handle(request: DecodedRequest<Params, Query, Body>) {
39
+ const organization = await Context.setOrganizationScope();
40
+ await Context.authenticate()
41
+
42
+ // Fast throw first (more in depth checking for patches later)
43
+ if (!await Context.auth.canManagePaymentAccounts(organization.id, PermissionLevel.Full)) {
44
+ throw Context.auth.error()
45
+ }
46
+
47
+ const mollieToken = await MollieToken.create(organization, request.body.code)
48
+
49
+ // Check settlements after linking (shouldn't block)
50
+ checkMollieSettlementsFor(mollieToken.accessToken, true).catch(console.error)
51
+
52
+ return new Response(await AuthenticatedStructures.organization(organization));
53
+ }
54
+ }
@@ -0,0 +1,49 @@
1
+
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { MollieToken } from '@stamhoofd/models';
4
+ import { Organization as OrganizationStruct, PermissionLevel } from "@stamhoofd/structures";
5
+
6
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
7
+ import { Context } from '../../../../helpers/Context';
8
+
9
+ type Params = Record<string, never>;
10
+ type Body = undefined
11
+ type Query = undefined
12
+ type ResponseBody = OrganizationStruct
13
+
14
+ export class DisonnectMollieEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ protected doesMatch(request: Request): [true, Params] | [false] {
16
+ if (request.method != "POST") {
17
+ return [false];
18
+ }
19
+
20
+ const params = Endpoint.parseParameters(request.url, "/mollie/disconnect", {});
21
+
22
+ if (params) {
23
+ return [true, params as Params];
24
+ }
25
+
26
+ return [false];
27
+ }
28
+
29
+ async handle(_: DecodedRequest<Params, Query, Body>) {
30
+ const organization = await Context.setOrganizationScope();
31
+ await Context.authenticate()
32
+
33
+ // Fast throw first (more in depth checking for patches later)
34
+ if (!await Context.auth.canManagePaymentAccounts(organization.id, PermissionLevel.Full)) {
35
+ throw Context.auth.error()
36
+ }
37
+
38
+ const mollieToken = await MollieToken.getTokenFor(organization.id)
39
+ await mollieToken?.revoke();
40
+ organization.privateMeta.mollieOnboarding = null;
41
+ organization.privateMeta.mollieProfile = null;
42
+
43
+ await organization.save()
44
+
45
+ // TODO: disable all payment methods that use this method
46
+
47
+ return new Response(await AuthenticatedStructures.organization(organization));
48
+ }
49
+ }
@@ -0,0 +1,63 @@
1
+
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { isSimpleError, isSimpleErrors,SimpleError } from '@simonbackx/simple-errors';
4
+ import { MollieToken } from '@stamhoofd/models';
5
+ import { PermissionLevel } from '@stamhoofd/structures';
6
+
7
+ import { Context } from '../../../../helpers/Context';
8
+
9
+ type Params = Record<string, never>;
10
+ type Body = undefined
11
+ type Query = undefined
12
+ type ResponseBody = string
13
+
14
+ export class GetMollieDashboardEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ protected doesMatch(request: Request): [true, Params] | [false] {
16
+ if (request.method != "GET") {
17
+ return [false];
18
+ }
19
+
20
+ const params = Endpoint.parseParameters(request.url, "/mollie/dashboard", {});
21
+
22
+ if (params) {
23
+ return [true, params as Params];
24
+ }
25
+
26
+ return [false];
27
+ }
28
+
29
+ async handle(_: DecodedRequest<Params, Query, Body>) {
30
+ const organization = await Context.setOrganizationScope();
31
+ await Context.authenticate()
32
+
33
+ // Fast throw first (more in depth checking for patches later)
34
+ if (!await Context.auth.canManagePaymentAccounts(organization.id, PermissionLevel.Full)) {
35
+ throw Context.auth.error()
36
+ }
37
+
38
+ const mollie = await MollieToken.getTokenFor(organization.id)
39
+ if (!mollie) {
40
+ throw new SimpleError({
41
+ code: "not_yet_linked",
42
+ message: "Mollie is nog niet gekoppeld. Koppel Mollie eerst voor je de gegevens aanvult"
43
+ })
44
+ }
45
+
46
+ try {
47
+ const url = await mollie.getOnboardingLink() as string
48
+
49
+ const response = new Response(url)
50
+ response.headers['Content-Type'] = "text/plain"
51
+ return response
52
+ } catch (e) {
53
+ if (isSimpleErrors(e) || isSimpleError(e)) {
54
+ throw e;
55
+ }
56
+ await mollie.delete()
57
+ throw new SimpleError({
58
+ code: "not_yet_linked",
59
+ message: "Mollie is nog niet gekoppeld. Koppel Mollie eerst voor je de gegevens aanvult"
60
+ })
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,61 @@
1
+ import { AutoEncoder, field, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import jwt from 'jsonwebtoken';
4
+
5
+ import { Context } from '../../../../helpers/Context';
6
+
7
+ type Params = Record<string, never>;
8
+ type Query = undefined;
9
+ type Body = undefined;
10
+ class ResponseBody extends AutoEncoder {
11
+ @field({ decoder: StringDecoder })
12
+ jwt: string
13
+ }
14
+
15
+ export class CreateNoltTokenEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
+
17
+ protected doesMatch(request: Request): [true, Params] | [false] {
18
+ if (request.method != "POST") {
19
+ return [false];
20
+ }
21
+
22
+ if (!STAMHOOFD.NOLT_SSO_SECRET_KEY) {
23
+ return [false];
24
+ }
25
+
26
+ const params = Endpoint.parseParameters(request.url, "/nolt/create-token", {});
27
+
28
+ if (params) {
29
+ return [true, params as Params];
30
+ }
31
+ return [false];
32
+ }
33
+
34
+ async handle(_: DecodedRequest<Params, Query, Body>) {
35
+ const organization = await Context.setOrganizationScope();
36
+ const {user} = await Context.authenticate()
37
+
38
+ // Fast throw first (more in depth checking for patches later)
39
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
40
+ throw Context.auth.error()
41
+ }
42
+
43
+ // Create token
44
+ const payload = {
45
+ // The ID that you use in your app for this user
46
+ id: user.id,
47
+ // The user's email address that
48
+ // Nolt should use for notifications
49
+ email: user.email,
50
+ // The display name for this user
51
+ name: user.firstName+" "+user.lastName,
52
+
53
+ // Optional: The URL to the user's avatar picture
54
+ imageUrl: organization.meta.squareLogo?.getPublicPath() ?? organization.meta.horizontalLogo?.getPublicPath() ?? undefined
55
+ }
56
+
57
+ const str = jwt.sign(payload, STAMHOOFD.NOLT_SSO_SECRET_KEY, { algorithm: 'HS256' });
58
+ return new Response(ResponseBody.create({ "jwt": str }));
59
+
60
+ }
61
+ }
@@ -0,0 +1,64 @@
1
+
2
+ import { Request } from "@simonbackx/simple-endpoints";
3
+ import { OrganizationFactory, RegisterCodeFactory, STCredit, Token, UserFactory } from "@stamhoofd/models";
4
+ import { PermissionLevel, Permissions } from "@stamhoofd/structures";
5
+
6
+ import { testServer } from "../../../../../tests/helpers/TestServer";
7
+ import { ApplyRegisterCodeEndpoint } from "./ApplyRegisterCodeEndpoint";
8
+
9
+ describe("Endpoint.ApplyRegisterCodeEndpoint", () => {
10
+ // Test endpoint
11
+ const endpoint = new ApplyRegisterCodeEndpoint();
12
+
13
+ test("Cannot apply a register code if not platform admin", async () => {
14
+ const otherOrganization = await new OrganizationFactory({}).create();
15
+ const code = await new RegisterCodeFactory({organization: otherOrganization}).create();
16
+
17
+ const organization = await new OrganizationFactory({}).create();
18
+ const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create()
19
+ const token = await Token.createToken(user)
20
+
21
+ const r = Request.buildJson(
22
+ "POST",
23
+ "/organization/register-code",
24
+ organization.getApiHost(),
25
+ {
26
+ registerCode: code.code,
27
+ }
28
+ );
29
+ r.headers.authorization = "Bearer "+token.accessToken
30
+
31
+ await expect(testServer.test(endpoint, r)).rejects.toThrow("You do not have permissions for this action");
32
+ });
33
+
34
+ test("Can apply a register code and apply the discount", async () => {
35
+ const otherOrganization = await new OrganizationFactory({}).create();
36
+ const code = await new RegisterCodeFactory({organization: otherOrganization}).create();
37
+
38
+ const organization = await new OrganizationFactory({}).create();
39
+ const user = await new UserFactory({
40
+ organization,
41
+ globalPermissions: Permissions.create({ level: PermissionLevel.Full }),
42
+ email: 'admin@stamhoofd.be'
43
+ }).create()
44
+ const token = await Token.createToken(user)
45
+
46
+ const r = Request.buildJson(
47
+ "POST",
48
+ "/organization/register-code",
49
+ organization.getApiHost(),
50
+ {
51
+ registerCode: code.code,
52
+ }
53
+ );
54
+ r.headers.authorization = "Bearer "+token.accessToken
55
+
56
+ const response = await testServer.test(endpoint, r);
57
+ expect(response.body).toBeUndefined();
58
+
59
+ // Check if this organization has an open register code
60
+ const credits = await STCredit.getForOrganization(organization.id);
61
+ expect(credits.length).toBe(1);
62
+ expect(credits[0].change).toBe(code.value);
63
+ });
64
+ });
@@ -0,0 +1,84 @@
1
+ import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { Email } from '@stamhoofd/email';
4
+ import { RegisterCode, UsedRegisterCode } from '@stamhoofd/models';
5
+
6
+ import { Context } from '../../../../helpers/Context';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type ResponseBody = undefined;
11
+
12
+ class Body extends AutoEncoder {
13
+ @field({ decoder: StringDecoder })
14
+ registerCode: string
15
+ }
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 ApplyRegisterCodeEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ bodyDecoder = Body as Decoder<Body>
23
+
24
+ protected doesMatch(request: Request): [true, Params] | [false] {
25
+ if (request.method != "POST") {
26
+ return [false];
27
+ }
28
+
29
+ const params = Endpoint.parseParameters(request.url, "/organization/register-code", {});
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
+ await Context.authenticate()
40
+
41
+ if (!Context.auth.hasPlatformFullAccess()) {
42
+ throw Context.auth.error()
43
+ }
44
+
45
+ let code = request.body.registerCode;
46
+
47
+ if (code.startsWith('https:')) {
48
+ try {
49
+ const url = new URL(code);
50
+ const codeParam = url.searchParams.get('code');
51
+ if (codeParam) {
52
+ console.log('Parsed code from URL', codeParam)
53
+ code = codeParam;
54
+ }
55
+ } catch (e) {
56
+ console.error('Tried parsing code as URL but failed', code)
57
+ }
58
+ }
59
+
60
+ const {models, emails} = await RegisterCode.applyRegisterCode(organization, code)
61
+
62
+ for (const model of models) {
63
+ await model.save();
64
+ }
65
+
66
+ for (const email of emails) {
67
+ Email.sendInternal(email, organization.i18n)
68
+ }
69
+
70
+ if (organization.meta.packages.isPaid) {
71
+ // Already bought something: apply credit to other organization immediately
72
+ const code = await UsedRegisterCode.getFor(organization.id)
73
+ if (code && !code.creditId) {
74
+ console.log("Rewarding code "+code.id+" for payment")
75
+
76
+ // Deze code werd nog niet beloond
77
+ await code.reward()
78
+ }
79
+ }
80
+
81
+ return new Response(undefined);
82
+ }
83
+ }
84
+
@@ -0,0 +1,43 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { SimpleError } from "@simonbackx/simple-errors";
3
+ import { Group, Token } from '@stamhoofd/models';
4
+ import { Group as GroupStruct, GroupStatus } from "@stamhoofd/structures";
5
+
6
+ import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
7
+ import { Context } from "../../../../helpers/Context";
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type Body = undefined
11
+ type ResponseBody = GroupStruct[]
12
+
13
+ export class GetOrganizationArchivedEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
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, "/organization/archived-groups", {});
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.canAccessArchivedGroups(organization.id)) {
32
+ throw Context.auth.error()
33
+ }
34
+
35
+ // Get all admins
36
+ const groups = await Group.where({ organizationId: organization.id, status: GroupStatus.Archived, deletedAt: null })
37
+ const structures: GroupStruct[] = []
38
+ for (const g of groups) {
39
+ structures.push(await AuthenticatedStructures.group(g))
40
+ }
41
+ return new Response(structures);
42
+ }
43
+ }
@@ -0,0 +1,42 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { Group } from '@stamhoofd/models';
3
+ import { Group as GroupStruct } from "@stamhoofd/structures";
4
+
5
+ import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
6
+ import { Context } from "../../../../helpers/Context";
7
+ type Params = Record<string, never>;
8
+ type Query = undefined;
9
+ type Body = undefined
10
+ type ResponseBody = GroupStruct[]
11
+
12
+ export class GetOrganizationAdminsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
13
+ protected doesMatch(request: Request): [true, Params] | [false] {
14
+ if (request.method != "GET") {
15
+ return [false];
16
+ }
17
+
18
+ const params = Endpoint.parseParameters(request.url, "/organization/deleted-groups", {});
19
+
20
+ if (params) {
21
+ return [true, params as Params];
22
+ }
23
+ return [false];
24
+ }
25
+
26
+ async handle(_: DecodedRequest<Params, Query, Body>) {
27
+ const organization = await Context.setOrganizationScope();
28
+ await Context.authenticate()
29
+
30
+ if (!await Context.auth.canAccessArchivedGroups(organization.id)) {
31
+ throw Context.auth.error()
32
+ }
33
+
34
+ // Get all admins
35
+ const groups = await Group.where({ organizationId: organization.id, deletedAt: { sign: '!=', value: null } })
36
+ const structures: GroupStruct[] = []
37
+ for (const g of groups) {
38
+ structures.push(await AuthenticatedStructures.group(g))
39
+ }
40
+ return new Response(structures);
41
+ }
42
+ }
@@ -0,0 +1,43 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { OpenIDClientConfiguration } from "@stamhoofd/structures";
3
+
4
+ import { Context } from "../../../../helpers/Context";
5
+
6
+ type Params = Record<string, never>;
7
+ type Query = undefined;
8
+ type Body = undefined;
9
+ type ResponseBody = OpenIDClientConfiguration
10
+
11
+ /**
12
+ * 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
13
+ */
14
+
15
+ export class GetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
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, "/organization/sso", {});
22
+
23
+ if (params) {
24
+ return [true, params as Params];
25
+ }
26
+ return [false];
27
+ }
28
+
29
+ async handle(_: DecodedRequest<Params, Query, Body>) {
30
+ const organization = await Context.setOrganizationScope();
31
+ await Context.authenticate()
32
+
33
+ if (!await Context.auth.canManageSSOSettings(organization.id)) {
34
+ throw Context.auth.error()
35
+ }
36
+
37
+ return new Response(organization.serverMeta.ssoConfiguration ?? OpenIDClientConfiguration.create({
38
+ clientId: "",
39
+ clientSecret: "",
40
+ issuer: ""
41
+ }));
42
+ }
43
+ }