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