@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,45 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints'
2
+ import { SimpleError } from '@simonbackx/simple-errors';
3
+ import { Organization, RegisterCode } from '@stamhoofd/models';
4
+ import {RegisterCode as RegisterCodeStruct } from "@stamhoofd/structures"
5
+ type Params = { code: string };
6
+ type Query = undefined;
7
+ type Body = undefined;
8
+ type ResponseBody = RegisterCodeStruct;
9
+
10
+ export class CheckRegisterCodeEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
11
+
12
+ protected doesMatch(request: Request): [true, Params] | [false] {
13
+ if (request.method != "GET") {
14
+ return [false];
15
+ }
16
+
17
+ const params = Endpoint.parseParameters(request.url, "/register-code/@code", {code: String});
18
+
19
+ if (params) {
20
+ return [true, params as Params];
21
+ }
22
+ return [false];
23
+ }
24
+
25
+ async handle(request: DecodedRequest<Params, Query, Body>) {
26
+
27
+ const code = await RegisterCode.getByID(request.params.code)
28
+ if (code) {
29
+ return new Response(RegisterCodeStruct.create({
30
+ code: code.code,
31
+ description: code.description,
32
+ customMessage: code.customMessage,
33
+ organizationName: code.organizationId ? ((await Organization.getByID(code.organizationId))!.name) : null,
34
+ value: code.value
35
+ }))
36
+ }
37
+
38
+ throw new SimpleError({
39
+ code: "invalid_code",
40
+ message: "Invalid code",
41
+ human: "Deze code is niet geldig",
42
+ field: "registerCode"
43
+ })
44
+ }
45
+ }
@@ -0,0 +1,105 @@
1
+
2
+ import { Request } from "@simonbackx/simple-endpoints";
3
+ import { Organization, OrganizationFactory, RegisterCodeFactory, STCredit } from "@stamhoofd/models";
4
+ import { Address, Country, CreateOrganization, NewUser, Organization as OrganizationStruct, Version } from "@stamhoofd/structures";
5
+
6
+ import { testServer } from "../../../../tests/helpers/TestServer";
7
+ import { CreateOrganizationEndpoint } from "./CreateOrganizationEndpoint";
8
+
9
+ function expect_toBeDefined<T>(arg: T): asserts arg is NonNullable<T> {
10
+ expect(arg).toBeDefined();
11
+ }
12
+
13
+ describe("Endpoint.CreateOrganization", () => {
14
+ // Test endpoint
15
+ const endpoint = new CreateOrganizationEndpoint();
16
+
17
+ test("Can create a new organization", async () => {
18
+ const r = Request.buildJson("POST", `/v${Version}/organizations`, "todo-host.be", CreateOrganization.create({
19
+ organization: OrganizationStruct.create({
20
+ name: "My endpoint test organization",
21
+ uri: "my-endpoint-test-organization",
22
+ address: Address.create({
23
+ street: "My street",
24
+ number: "1",
25
+ postalCode: "9000",
26
+ city: "Gent",
27
+ country: Country.Belgium
28
+ }),
29
+
30
+ }),
31
+ user: NewUser.create({
32
+ email: "voorbeeld@stamhoofd.be",
33
+ password: "My user password",
34
+ }),
35
+ }).encode({version: Version}));
36
+
37
+ const response = await testServer.test(endpoint, r);
38
+ expect(response.body.token).not.toBeEmpty();
39
+ });
40
+
41
+ test("Creating an organization with an in-use URI throws", async () => {
42
+ const r = Request.buildJson("POST", `/v${Version}/organizations`, "todo-host.be", CreateOrganization.create({
43
+ organization: OrganizationStruct.create({
44
+ name: "My endpoint test organization",
45
+ uri: "my-endpoint-test-organization",
46
+ address: Address.create({
47
+ street: "My street",
48
+ number: "1",
49
+ postalCode: "9000",
50
+ city: "Gent",
51
+ country: Country.Belgium
52
+ }),
53
+
54
+ }),
55
+ user: NewUser.create({
56
+ email: "voorbeeld@stamhoofd.be",
57
+ password: "My user password",
58
+ })
59
+ }).encode({version: Version}));
60
+
61
+ await expect(testServer.test(endpoint, r)).rejects.toThrow(/name/);
62
+ });
63
+
64
+ test("Can create an organization with a register code and apply the discount", async () => {
65
+ const otherOrganization = await new OrganizationFactory({}).create();
66
+ const code = await new RegisterCodeFactory({organization: otherOrganization}).create();
67
+ const uri = 'my-organization-with-a-discount';
68
+
69
+ const r = Request.buildJson(
70
+ "POST",
71
+ "/organizations",
72
+ "todo-host.be",
73
+ CreateOrganization.create({
74
+ organization: OrganizationStruct.create({
75
+ name: "My organization with a discount",
76
+ uri,
77
+ address: Address.create({
78
+ street: "My street",
79
+ number: "1",
80
+ postalCode: "9000",
81
+ city: "Gent",
82
+ country: Country.Belgium
83
+ }),
84
+ }),
85
+ user: NewUser.create({
86
+ email: "voorbeeld@stamhoofd.be",
87
+ password: "My user password",
88
+ }),
89
+ registerCode: code.code
90
+ })
91
+ );
92
+
93
+ const response = await testServer.test(endpoint, r);
94
+ expect(response.body.token).not.toBeEmpty();
95
+
96
+ const organization = await Organization.getByURI(uri);
97
+ expect_toBeDefined(organization);
98
+
99
+ // Check if this organization has an open register code
100
+ const credits = await STCredit.getForOrganization(organization.id);
101
+ expect(credits.length).toBe(1);
102
+ expect(credits[0].change).toBe(code.value);
103
+ });
104
+
105
+ });
@@ -0,0 +1,146 @@
1
+ import { Model } from '@simonbackx/simple-database';
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 { Email, EmailInterfaceBase } from '@stamhoofd/email';
6
+ import { EmailVerificationCode, Organization, RegisterCode, User } from '@stamhoofd/models';
7
+ import { CreateOrganization, PermissionLevel, Permissions, SignupResponse, UserPermissions } from "@stamhoofd/structures";
8
+ import { Formatter } from "@stamhoofd/utility";
9
+
10
+ type Params = Record<string, never>;
11
+ type Query = undefined;
12
+ type Body = CreateOrganization;
13
+ type ResponseBody = SignupResponse;
14
+
15
+ export class CreateOrganizationEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
+ bodyDecoder = CreateOrganization as Decoder<CreateOrganization>;
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, "/organizations", {});
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
+ if (STAMHOOFD.userMode === 'platform') {
33
+ throw new SimpleError({
34
+ code: "invalid_field",
35
+ message: "Not allowed",
36
+ human: "Je kan geen vereniging aanmaken"
37
+ })
38
+ }
39
+
40
+ if (request.body.organization.name.length < 4) {
41
+ if (request.body.organization.name.length == 0) {
42
+ throw new SimpleError({
43
+ code: "invalid_field",
44
+ message: "Should not be empty",
45
+ human: "Je bent de naam van je organisatie vergeten in te vullen",
46
+ field: "organization.name"
47
+ })
48
+ }
49
+
50
+ throw new SimpleError({
51
+ code: "invalid_field",
52
+ message: "Field is too short",
53
+ human: "Kijk de naam van je organisatie na, deze is te kort. Vul eventueel aan met de gemeente.",
54
+ field: "organization.name"
55
+ })
56
+ }
57
+
58
+ const uri = Formatter.slug(request.body.organization.name);
59
+
60
+ if (uri.length > 100) {
61
+ throw new SimpleError({
62
+ code: "invalid_field",
63
+ message: "Field is too long",
64
+ human: "De naam van de vereniging is te lang. Probeer de naam wat te verkorten en probeer opnieuw.",
65
+ field: "organization.name"
66
+ })
67
+ }
68
+ const alreadyExists = await Organization.getByURI(uri);
69
+
70
+ if (alreadyExists) {
71
+ throw new SimpleError({
72
+ code: "name_taken",
73
+ message: "An organization with the same name already exists",
74
+ human: "Er bestaat al een vereniging met dezelfde naam. Voeg bijvoorbeeld de naam van je gemeente toe.",
75
+ field: "name",
76
+ });
77
+ }
78
+
79
+ // First create the organization
80
+ // TODO: add some duplicate validation
81
+ const organization = new Organization();
82
+ organization.id = request.body.organization.id;
83
+ organization.name = request.body.organization.name;
84
+
85
+ // Delay save until after organization is saved, but do validations before the organization is saved
86
+ let registerCodeModels: Model[] = []
87
+ let delayEmails: EmailInterfaceBase[] = []
88
+
89
+ if (request.body.registerCode) {
90
+ const applied = await RegisterCode.applyRegisterCode(organization, request.body.registerCode)
91
+ registerCodeModels = applied.models
92
+ delayEmails = applied.emails
93
+ }
94
+
95
+ organization.uri = uri;
96
+ organization.meta = request.body.organization.meta
97
+ organization.address = request.body.organization.address
98
+ organization.privateMeta.acquisitionTypes = request.body.organization.privateMeta?.acquisitionTypes ?? []
99
+
100
+ try {
101
+ await organization.save();
102
+ } catch (e) {
103
+ console.error(e);
104
+ throw new SimpleError({
105
+ code: "creating_organization",
106
+ message: "Something went wrong while creating the organization. Please try again later or contact us.",
107
+ statusCode: 500
108
+ });
109
+ }
110
+
111
+ const user = await User.register(
112
+ organization,
113
+ request.body.user
114
+ );
115
+ if (!user) {
116
+ // This user already exists, well that is pretty impossible
117
+ throw new SimpleError({
118
+ code: "creating_user",
119
+ message: "Something went wrong while creating the user. Please contact us to resolve this issue.",
120
+ statusCode: 500
121
+ });
122
+ }
123
+
124
+ // Should prevent this extra save
125
+ user.permissions = UserPermissions.create({})
126
+ user.permissions.organizationPermissions.set(organization.id, Permissions.create({ level: PermissionLevel.Full }))
127
+ await user.save()
128
+
129
+ for (const model of registerCodeModels) {
130
+ await model.save()
131
+ }
132
+
133
+ const code = await EmailVerificationCode.createFor(user, user.email)
134
+ code.send(user, organization, request.i18n)
135
+
136
+ for (const email of delayEmails) {
137
+ Email.sendInternal(email, organization.i18n)
138
+ }
139
+
140
+ return new Response(SignupResponse.create({
141
+ token: code.token
142
+ }));
143
+ }
144
+
145
+
146
+ }
@@ -0,0 +1,52 @@
1
+ import { Request } from "@simonbackx/simple-endpoints";
2
+ import { GroupFactory, OrganizationFactory } from '@stamhoofd/models';
3
+ import { Organization } from '@stamhoofd/structures';
4
+
5
+ import { testServer } from "../../../../tests/helpers/TestServer";
6
+ import { GetOrganizationFromDomainEndpoint } from './GetOrganizationFromDomainEndpoint';
7
+
8
+ describe("Endpoint.GetOrganizationFromDomain", () => {
9
+ // Test endpoint
10
+ const endpoint = new GetOrganizationFromDomainEndpoint();
11
+
12
+ test("Get organization from default uri", async () => {
13
+ const organization = await new OrganizationFactory({}).create()
14
+ const groups = await new GroupFactory({ organization }).createMultiple(2)
15
+
16
+ const r = Request.buildJson("GET", "/v2/organization-from-domain");
17
+ r.query = {
18
+ "domain": organization.uri+".stamhoofd.dev",
19
+ }
20
+
21
+ const response = await testServer.test(endpoint, r);
22
+ expect(response.body).toBeDefined();
23
+
24
+ if (!(response.body instanceof Organization)) {
25
+ throw new Error("Expected Organization")
26
+ }
27
+
28
+ expect(response.body.id).toEqual(organization.id)
29
+ expect(response.body.groups.map(g => g.id).sort()).toEqual(groups.map(g => g.id).sort())
30
+ });
31
+
32
+ test("Get organization from custom domain", async () => {
33
+ const organization = await new OrganizationFactory({ domain: "inschrijven.mijnscouts.be"}).create()
34
+ const groups = await new GroupFactory({ organization }).createMultiple(2)
35
+
36
+ const r = Request.buildJson("GET", "/v2/organization-from-domain");
37
+ r.query = {
38
+ "domain": "inschrijven.mijnscouts.be",
39
+ }
40
+
41
+ const response = await testServer.test(endpoint, r);
42
+ expect(response.body).toBeDefined();
43
+
44
+ if (!(response.body instanceof Organization)) {
45
+ throw new Error("Expected Organization")
46
+ }
47
+
48
+ expect(response.body.id).toEqual(organization.id)
49
+ expect(response.body.groups.map(g => g.id).sort()).toEqual(groups.map(g => g.id).sort())
50
+ });
51
+
52
+ });
@@ -0,0 +1,80 @@
1
+ import { AutoEncoder, Decoder, field, 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 } from '@stamhoofd/models';
5
+ import { Organization as OrganizationStruct } from "@stamhoofd/structures";
6
+ import { GoogleTranslateHelper } from "@stamhoofd/utility";
7
+ type Params = Record<string, never>;
8
+
9
+ class Query extends AutoEncoder {
10
+ @field({ decoder: StringDecoder })
11
+ domain: string;
12
+ }
13
+
14
+ type Body = undefined
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 GetOrganizationFromDomainEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ queryDecoder = Query as Decoder<Query>
23
+ registrationDomains = [... new Set(Object.values(STAMHOOFD.domains.registration ?? {}))]
24
+
25
+ protected doesMatch(request: Request): [true, Params] | [false] {
26
+ if (request.method != "GET") {
27
+ return [false];
28
+ }
29
+
30
+ const params = Endpoint.parseParameters(request.url, "/organization-from-domain", {});
31
+
32
+ if (params) {
33
+ return [true, params as Params];
34
+ }
35
+ return [false];
36
+ }
37
+
38
+ async handle(request: DecodedRequest<Params, Query, Body>) {
39
+ // Clean up google translate domains -> make sure we can translate register pages
40
+ request.query.domain = GoogleTranslateHelper.getDomainFromTranslateDomain(request.query.domain)
41
+
42
+ for (const domain of this.registrationDomains) {
43
+ if (request.query.domain.endsWith("." + domain)) {
44
+ const strippped = request.query.domain.substr(0, request.query.domain.length - ("." + domain).length )
45
+ if (strippped.includes(".")) {
46
+ throw new SimpleError({
47
+ code: "invalid_domain",
48
+ message: "This domain format is not supported",
49
+ statusCode: 404
50
+ })
51
+ }
52
+
53
+ // Search for the URI
54
+ const organization = await Organization.getByURI(strippped)
55
+
56
+ if (!organization) {
57
+ throw new SimpleError({
58
+ code: "unknown_organization",
59
+ message: "No organization registered with this domain name",
60
+ statusCode: 404
61
+ })
62
+ }
63
+ return new Response(await organization.getStructure());
64
+ }
65
+ }
66
+
67
+ // Check if we have an organization with a custom domain name
68
+
69
+ const organization = await Organization.getByRegisterDomain(request.query.domain)
70
+
71
+ if (!organization) {
72
+ throw new SimpleError({
73
+ code: "unknown_organization",
74
+ message: "No organization registered with this domain name",
75
+ statusCode: 404
76
+ })
77
+ }
78
+ return new Response(await organization.getStructure());
79
+ }
80
+ }
@@ -0,0 +1,49 @@
1
+ import { AutoEncoder, Decoder, field, 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 } from '@stamhoofd/models';
5
+ import { Organization as OrganizationStruct } from "@stamhoofd/structures";
6
+ import { GoogleTranslateHelper } from "@stamhoofd/utility";
7
+ type Params = Record<string, never>;
8
+
9
+ class Query extends AutoEncoder {
10
+ @field({ decoder: StringDecoder })
11
+ uri: string;
12
+ }
13
+
14
+ type Body = undefined
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 GetOrganizationFromUriEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ queryDecoder = Query as Decoder<Query>
23
+
24
+ protected doesMatch(request: Request): [true, Params] | [false] {
25
+ if (request.method != "GET") {
26
+ return [false];
27
+ }
28
+
29
+ const params = Endpoint.parseParameters(request.url, "/organization-from-uri", {});
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 Organization.getByURI(request.query.uri)
39
+
40
+ if (!organization) {
41
+ throw new SimpleError({
42
+ code: "unknown_organization",
43
+ message: "No organization registered with this uri",
44
+ statusCode: 404
45
+ })
46
+ }
47
+ return new Response(await organization.getStructure());
48
+ }
49
+ }
@@ -0,0 +1,58 @@
1
+ import { Request } from "@simonbackx/simple-endpoints";
2
+ import { OrganizationFactory } from '@stamhoofd/models';
3
+ import { OrganizationSimple } from '@stamhoofd/structures';
4
+ import { v4 as uuidv4 } from "uuid";
5
+
6
+ import { testServer } from '../../../../tests/helpers/TestServer';
7
+ import { SearchOrganizationEndpoint } from "./SearchOrganizationEndpoint";
8
+
9
+ describe("Endpoint.SearchOrganization", () => {
10
+ // Test endpoint
11
+ const endpoint = new SearchOrganizationEndpoint();
12
+
13
+ test("Search for a given organization using exact search", async () => {
14
+ const organization = await new OrganizationFactory({
15
+ name: (uuidv4()).replace(/-/g, "")
16
+ }).create()
17
+
18
+ const r = Request.buildJson("GET", "/v19/organizations/search");
19
+ r.query = {
20
+ query: organization.name
21
+ };
22
+
23
+ const response = await testServer.test(endpoint, r);
24
+ expect(response.body).toBeDefined();
25
+ expect(response.body).toHaveLength(1)
26
+
27
+ // Access token should be expired
28
+ expect(response.body[0]).toBeInstanceOf(OrganizationSimple);
29
+ expect(response.status).toEqual(200);
30
+ expect(response.body[0]).toMatchObject({
31
+ id: organization.id,
32
+ name: organization.name,
33
+ address: organization.address
34
+ })
35
+ });
36
+
37
+ test("Search for a given organization on city name using exact search", async () => {
38
+ const city = uuidv4()
39
+ const organizations = await new OrganizationFactory({
40
+ city
41
+ }).createMultiple(2)
42
+
43
+ const r = Request.buildJson("GET", "/v1/organizations/search");
44
+ r.query = {
45
+ query: city
46
+ };
47
+
48
+ const response = await testServer.test(endpoint, r);
49
+ expect(response.body).toBeDefined();
50
+ expect(response.body).toHaveLength(2)
51
+
52
+ // Access token should be expired
53
+ expect(response.body[0]).toBeInstanceOf(OrganizationSimple);
54
+ expect(response.body[1]).toBeInstanceOf(OrganizationSimple);
55
+ expect(response.status).toEqual(200);
56
+ expect(response.body.map(o => o.id).sort()).toEqual(organizations.map(o => o.id).sort())
57
+ });
58
+ });
@@ -0,0 +1,62 @@
1
+ import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { Organization } from "@stamhoofd/models";
4
+ import { Organization as OrganizationStruct,OrganizationSimple } from "@stamhoofd/structures";
5
+
6
+ type Params = Record<string, never>;
7
+
8
+ class Query extends AutoEncoder {
9
+ @field({ decoder: StringDecoder })
10
+ query: string
11
+ }
12
+
13
+ type Body = undefined;
14
+ type ResponseBody = (OrganizationSimple | OrganizationStruct)[]
15
+
16
+ export class SearchOrganizationEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
17
+ protected queryDecoder = Query as Decoder<Query>
18
+
19
+ protected doesMatch(request: Request): [true, Params] | [false] {
20
+ if (request.method != "GET") {
21
+ return [false];
22
+ }
23
+
24
+ const params = Endpoint.parseParameters(request.url, "/organizations/search", {});
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
+ // Escape query
34
+ const query = request.query.query.replace(/([-+><()~*"@\s]+)/g, " ").replace(/[^\w\d]+$/, "")
35
+ if (query.length == 0) {
36
+ // Do not try searching...
37
+ return new Response([])
38
+ }
39
+
40
+ const match = {
41
+ sign: "MATCH",
42
+ value: query + "*", // We replace special operators in boolean mode with spaces since special characters aren't indexed anyway
43
+ mode: "BOOLEAN"
44
+ };
45
+
46
+ // We had to add an order by in the query to fix the limit. MySQL doesn't want to limit the results correctly if we don't explicitly sort the results on their relevance
47
+ const organizations = await Organization.where({ searchIndex: match }, {
48
+ limit: 15,
49
+ sort: [
50
+ {
51
+ column: { searchIndex: match },
52
+ direction: "DESC"
53
+ }
54
+ ]
55
+ });
56
+
57
+ if (request.request.getVersion() < 169) {
58
+ return new Response(organizations.map(o => OrganizationSimple.create(o)));
59
+ }
60
+ return new Response(await Promise.all(organizations.map(o => o.getStructure())));
61
+ }
62
+ }