@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,60 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
3
|
+
import { User } from '@stamhoofd/models';
|
|
4
|
+
|
|
5
|
+
import { Context } from "../../../../helpers/Context";
|
|
6
|
+
type Params = { id: string };
|
|
7
|
+
type Query = undefined;
|
|
8
|
+
type Body = undefined
|
|
9
|
+
type ResponseBody = undefined
|
|
10
|
+
|
|
11
|
+
export class DeleteUserEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
12
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
13
|
+
if (request.method != "DELETE") {
|
|
14
|
+
return [false];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const params = Endpoint.parseParameters(request.url, "/user/@id", { id: String });
|
|
18
|
+
|
|
19
|
+
if (params) {
|
|
20
|
+
return [true, params as Params];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const params2 = Endpoint.parseParameters(request.url, "/api-keys/@id", { id: String });
|
|
24
|
+
|
|
25
|
+
if (params2) {
|
|
26
|
+
return [true, params2 as Params];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [false];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
33
|
+
const organization = await Context.setOrganizationScope();
|
|
34
|
+
const {user} = await Context.authenticate()
|
|
35
|
+
|
|
36
|
+
// Fast throw first (more in depth checking for patches later)
|
|
37
|
+
if (!await Context.auth.canManageAdmins(organization.id)) {
|
|
38
|
+
throw Context.auth.error()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (user.id == request.params.id) {
|
|
42
|
+
throw new SimpleError({
|
|
43
|
+
code: "permission_denied",
|
|
44
|
+
message: "Je kan jezelf niet verwijderen"
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const editUser = await User.getByID(request.params.id)
|
|
49
|
+
if (!editUser || !Context.auth.checkScope(editUser.organizationId)) {
|
|
50
|
+
throw new SimpleError({
|
|
51
|
+
code: "permission_denied",
|
|
52
|
+
message: "Je hebt geen toegang om deze gebruiker te verwijderen"
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await editUser.delete();
|
|
57
|
+
|
|
58
|
+
return new Response(undefined);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
3
|
+
import { Token, User } from '@stamhoofd/models';
|
|
4
|
+
import { ApiUser } from "@stamhoofd/structures";
|
|
5
|
+
|
|
6
|
+
import { Context } from "../../../../helpers/Context";
|
|
7
|
+
type Params = Record<string, never>;
|
|
8
|
+
type Query = undefined;
|
|
9
|
+
type Body = undefined
|
|
10
|
+
type ResponseBody = ApiUser[]
|
|
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, "/api-keys", {});
|
|
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
|
+
// Fast throw first (more in depth checking for patches later)
|
|
31
|
+
if (!await Context.auth.canManageAdmins(organization.id)) {
|
|
32
|
+
throw Context.auth.error()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Get all admins
|
|
36
|
+
const admins = await User.getApiUsers([organization.id])
|
|
37
|
+
|
|
38
|
+
const mapped: ApiUser[] = []
|
|
39
|
+
for (const admin of admins) {
|
|
40
|
+
mapped.push(await Token.getAPIUserWithToken(admin))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new Response(
|
|
44
|
+
mapped
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { User } from '@stamhoofd/models';
|
|
3
|
+
import { OrganizationAdmins, User as UserStruct } from "@stamhoofd/structures";
|
|
4
|
+
|
|
5
|
+
import { Context } from "../../../../helpers/Context";
|
|
6
|
+
type Params = Record<string, never>;
|
|
7
|
+
type Query = undefined;
|
|
8
|
+
type Body = undefined
|
|
9
|
+
type ResponseBody = OrganizationAdmins
|
|
10
|
+
|
|
11
|
+
export class GetOrganizationAdminsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
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, "/organization/admins", {});
|
|
18
|
+
|
|
19
|
+
if (params) {
|
|
20
|
+
return [true, params as Params];
|
|
21
|
+
}
|
|
22
|
+
return [false];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async handle(_: DecodedRequest<Params, Query, Body>) {
|
|
26
|
+
const organization = await Context.setOrganizationScope();
|
|
27
|
+
await Context.authenticate()
|
|
28
|
+
|
|
29
|
+
// Fast throw first (more in depth checking for patches later)
|
|
30
|
+
if (!await Context.auth.canManageAdmins(organization.id)) {
|
|
31
|
+
throw Context.auth.error()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get all admins
|
|
35
|
+
const admins = await User.getAdmins([organization.id])
|
|
36
|
+
|
|
37
|
+
return new Response(OrganizationAdmins.create({
|
|
38
|
+
users: admins.map(a => UserStruct.create({...a, hasAccount: a.hasAccount()})),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
+
import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
|
|
4
|
+
import { Webshop } from '@stamhoofd/models';
|
|
5
|
+
import { PermissionLevel, PermissionsResourceType, PrivateWebshop, ResourcePermissions, Version, WebshopPrivateMetaData } from "@stamhoofd/structures";
|
|
6
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
7
|
+
|
|
8
|
+
import { Context } from '../../../../helpers/Context';
|
|
9
|
+
|
|
10
|
+
type Params = Record<string, never>;
|
|
11
|
+
type Query = undefined;
|
|
12
|
+
type Body = PrivateWebshop;
|
|
13
|
+
type ResponseBody = PrivateWebshop;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 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
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export class CreateWebshopEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
20
|
+
bodyDecoder = PrivateWebshop as Decoder<PrivateWebshop>
|
|
21
|
+
|
|
22
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
23
|
+
if (request.method != "POST") {
|
|
24
|
+
return [false];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const params = Endpoint.parseParameters(request.url, "/webshop", {});
|
|
28
|
+
|
|
29
|
+
if (params) {
|
|
30
|
+
return [true, params as Params];
|
|
31
|
+
}
|
|
32
|
+
return [false];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
36
|
+
const organization = await Context.setOrganizationScope();
|
|
37
|
+
const {user} = await Context.authenticate()
|
|
38
|
+
|
|
39
|
+
// Fast throw first (more in depth checking for patches later)
|
|
40
|
+
if (!await Context.auth.canCreateWebshops(organization.id)) {
|
|
41
|
+
throw Context.auth.error("Je kan geen webshops maken, vraag aan de hoofdbeheerders om jou toegang te geven.")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const errors = new SimpleErrors()
|
|
45
|
+
|
|
46
|
+
const webshop = new Webshop()
|
|
47
|
+
|
|
48
|
+
webshop.id = request.body.id
|
|
49
|
+
webshop.meta = request.body.meta
|
|
50
|
+
webshop.meta.domainActive = false;
|
|
51
|
+
webshop.privateMeta = request.body.privateMeta
|
|
52
|
+
webshop.products = request.body.products
|
|
53
|
+
webshop.categories = request.body.categories
|
|
54
|
+
webshop.organizationId = organization.id
|
|
55
|
+
webshop.privateMeta.authorId = user.id;
|
|
56
|
+
webshop.privateMeta.dnsRecords = [];
|
|
57
|
+
let updateDNS = false
|
|
58
|
+
|
|
59
|
+
// Check if we can decide the domain
|
|
60
|
+
if (!request.body.domain && !request.body.domainUri) {
|
|
61
|
+
const webshops = await Webshop.where({
|
|
62
|
+
organizationId: organization.id,
|
|
63
|
+
domain: {
|
|
64
|
+
value: null,
|
|
65
|
+
sign: "!="
|
|
66
|
+
},
|
|
67
|
+
domainUri: {
|
|
68
|
+
value: "",
|
|
69
|
+
sign: "!="
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const counters = new Map<string, number>()
|
|
74
|
+
for (const webshop of webshops) {
|
|
75
|
+
if (!webshop.domain || !webshop.meta.domainActive) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const count = (counters.get(webshop.domain) ?? 0) + 1
|
|
79
|
+
counters.set(webshop.domain, count)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (counters.size > 0) {
|
|
83
|
+
const maxDomain = [...counters.entries()].reduce((a, e ) => e[1] > a[1] ? e : a)[0]
|
|
84
|
+
console.log("Choosing default domain for new webshop: ", maxDomain)
|
|
85
|
+
|
|
86
|
+
webshop.domain = maxDomain
|
|
87
|
+
webshop.domainUri = Formatter.slug(webshop.meta.name)
|
|
88
|
+
webshop.privateMeta.dnsRecords = WebshopPrivateMetaData.buildDNSRecords(maxDomain)
|
|
89
|
+
await this.checkDomainUri(webshop)
|
|
90
|
+
updateDNS = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
} else {
|
|
94
|
+
if (request.body.domain) {
|
|
95
|
+
webshop.domain = request.body.domain
|
|
96
|
+
|
|
97
|
+
if (request.body.domainUri) {
|
|
98
|
+
webshop.domainUri = Formatter.slug(request.body.domainUri)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
webshop.privateMeta.dnsRecords = WebshopPrivateMetaData.buildDNSRecords(request.body.domain)
|
|
102
|
+
await this.checkDomainUri(webshop)
|
|
103
|
+
updateDNS = true
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
webshop.uri = request.body.uri.length > 0 ? Formatter.slug(request.body.uri) : Formatter.slug(webshop.meta.name)
|
|
108
|
+
|
|
109
|
+
// Check if this uri is inique
|
|
110
|
+
let original = webshop.uri
|
|
111
|
+
const possibleSuffixes = [new Date().getFullYear().toString(), Formatter.slug(organization.uri)]
|
|
112
|
+
|
|
113
|
+
// Remove possible suffices from original
|
|
114
|
+
for (const suffix of possibleSuffixes) {
|
|
115
|
+
if (original.endsWith("-" + suffix)) {
|
|
116
|
+
original = original.slice(0, -suffix.length - 1)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let tried = 0
|
|
121
|
+
while (webshop.uri.length > 100 || await Webshop.getByURI(webshop.uri) !== undefined) {
|
|
122
|
+
|
|
123
|
+
console.log("Webshop already exists", webshop.uri)
|
|
124
|
+
|
|
125
|
+
let suffix = ""
|
|
126
|
+
if (tried < possibleSuffixes.length) {
|
|
127
|
+
suffix = "-" + possibleSuffixes[tried]
|
|
128
|
+
} else if (tried > 9) {
|
|
129
|
+
suffix = "-" + Math.floor(Math.random() * 100000)
|
|
130
|
+
} else {
|
|
131
|
+
suffix = "-" + (tried - possibleSuffixes.length + 2)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
webshop.uri = original.slice(0, 100 - suffix.length) + suffix;
|
|
135
|
+
|
|
136
|
+
tried++
|
|
137
|
+
if (tried > 15) {
|
|
138
|
+
console.log("Failed to generate unique webshop uri")
|
|
139
|
+
|
|
140
|
+
throw new SimpleError({
|
|
141
|
+
code: "failed_to_generate_unique_uri",
|
|
142
|
+
message: "Failed to generate unique uri",
|
|
143
|
+
human: "Er is een fout opgetreden bij het maken van de webshop, kies een andere naam voor jouw webshop",
|
|
144
|
+
statusCode: 500
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
150
|
+
// Create a temporary permission role for this user
|
|
151
|
+
const organizationPermissions = user.permissions?.organizationPermissions?.get(organization.id)
|
|
152
|
+
if (!organizationPermissions) {
|
|
153
|
+
throw new Error('Unexpected missing permissions')
|
|
154
|
+
}
|
|
155
|
+
const resourcePermissions = ResourcePermissions.create({
|
|
156
|
+
resourceName: webshop.meta.name,
|
|
157
|
+
level: PermissionLevel.Full
|
|
158
|
+
})
|
|
159
|
+
const patch = resourcePermissions.createInsertPatch(PermissionsResourceType.Webshops, webshop.id, organizationPermissions)
|
|
160
|
+
user.permissions!.organizationPermissions.set(organization.id, organizationPermissions.patch(patch))
|
|
161
|
+
console.log('Automatically granted author full permissions to resource', 'webshop', webshop.id, 'user', user.id, 'patch', patch.encode({version: Version}))
|
|
162
|
+
await user.save()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Verify if we have full access
|
|
166
|
+
if (!await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
167
|
+
throw new SimpleError({
|
|
168
|
+
code: "missing_permissions",
|
|
169
|
+
message: "You cannot create a webshop without having full permissions on the created webshop",
|
|
170
|
+
human: "Als je een webshop aanmaakt moet je ervoor zorgen dat jezelf ook volledige toegang hebt."
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await webshop.save()
|
|
175
|
+
|
|
176
|
+
if (updateDNS) {
|
|
177
|
+
await webshop.updateDNSRecords()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
errors.throwIfNotEmpty()
|
|
181
|
+
return new Response(PrivateWebshop.create(webshop));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async checkDomainUri(webshop: Webshop) {
|
|
185
|
+
if (!webshop.domain) {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
// Check if this uri is inique
|
|
189
|
+
const original = webshop.domainUri ? webshop.domainUri + '-' : ''
|
|
190
|
+
const possibleSuffixes = [new Date().getFullYear().toString()]
|
|
191
|
+
let tried = 0
|
|
192
|
+
while (await Webshop.getByDomain(webshop.domain, webshop.domainUri) !== undefined) {
|
|
193
|
+
console.log("Webshop already exists", webshop.domainUri)
|
|
194
|
+
|
|
195
|
+
if (tried < possibleSuffixes.length) {
|
|
196
|
+
webshop.domainUri = original + possibleSuffixes[tried]
|
|
197
|
+
} else if (tried > 9) {
|
|
198
|
+
webshop.domainUri = original + Math.floor(Math.random() * 100000)
|
|
199
|
+
} else {
|
|
200
|
+
webshop.domainUri = original + (tried - possibleSuffixes.length + 2)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
tried++
|
|
204
|
+
|
|
205
|
+
if (tried > 15) {
|
|
206
|
+
console.log("Failed to generate unique webshop domainUri")
|
|
207
|
+
|
|
208
|
+
throw new SimpleError({
|
|
209
|
+
code: "failed_to_generate_unique_domainUri",
|
|
210
|
+
message: "Failed to generate unique domainUri",
|
|
211
|
+
human: "Er is een fout opgetreden bij het maken van de webshop, kies een andere naam voor jouw webshop",
|
|
212
|
+
statusCode: 500
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { BalanceItem, Order, Webshop } from '@stamhoofd/models';
|
|
4
|
+
import { PermissionLevel } from "@stamhoofd/structures";
|
|
5
|
+
|
|
6
|
+
import { Context } from "../../../../helpers/Context";
|
|
7
|
+
|
|
8
|
+
type Params = { id: string };
|
|
9
|
+
type Query = undefined;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = undefined;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 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
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export class DeleteWebshopEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
18
|
+
|
|
19
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
20
|
+
if (request.method != "DELETE") {
|
|
21
|
+
return [false];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const params = Endpoint.parseParameters(request.url, "/webshop/@id", { id: String });
|
|
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
|
+
const organization = await Context.setOrganizationScope();
|
|
34
|
+
await Context.authenticate()
|
|
35
|
+
|
|
36
|
+
// Fast throw first (more in depth checking for patches later)
|
|
37
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
38
|
+
throw Context.auth.error()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
42
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
43
|
+
throw Context.auth.notFoundOrNoAccess()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const orders = await Order.where({ webshopId: webshop.id });
|
|
47
|
+
await BalanceItem.deleteForDeletedOrders(orders.map(o => o.id))
|
|
48
|
+
await webshop.delete()
|
|
49
|
+
return new Response(undefined);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { Webshop, WebshopDiscountCode } from '@stamhoofd/models';
|
|
4
|
+
import { DiscountCode, PermissionLevel } from "@stamhoofd/structures";
|
|
5
|
+
|
|
6
|
+
import { Context } from "../../../../helpers/Context";
|
|
7
|
+
|
|
8
|
+
type Params = { id: string };
|
|
9
|
+
type Query = undefined
|
|
10
|
+
type Body = undefined
|
|
11
|
+
type ResponseBody = DiscountCode[]
|
|
12
|
+
|
|
13
|
+
export class GetWebshopDiscountCodesEndpoint 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, "/webshop/@id/discount-codes", { id: String });
|
|
20
|
+
|
|
21
|
+
if (params) {
|
|
22
|
+
return [true, params as Params];
|
|
23
|
+
}
|
|
24
|
+
return [false];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
28
|
+
const organization = await Context.setOrganizationScope();
|
|
29
|
+
await Context.authenticate()
|
|
30
|
+
|
|
31
|
+
// Fast throw first (more in depth checking for patches later)
|
|
32
|
+
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
33
|
+
throw Context.auth.error()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
37
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
38
|
+
throw Context.auth.notFoundOrNoAccess()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const discountCodes = await WebshopDiscountCode.where({webshopId: request.params.id})
|
|
42
|
+
|
|
43
|
+
return new Response(
|
|
44
|
+
discountCodes.map(d => d.getStructure())
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Decoder } from "@simonbackx/simple-encoding";
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
+
import { Order, Webshop } from '@stamhoofd/models';
|
|
4
|
+
import { PaginatedResponse, PermissionLevel, PrivateOrder, WebshopOrdersQuery } from "@stamhoofd/structures";
|
|
5
|
+
|
|
6
|
+
import { Context } from "../../../../helpers/Context";
|
|
7
|
+
|
|
8
|
+
type Params = { id: string };
|
|
9
|
+
type Query = WebshopOrdersQuery
|
|
10
|
+
type Body = undefined
|
|
11
|
+
type ResponseBody = PaginatedResponse<PrivateOrder[], Query>
|
|
12
|
+
|
|
13
|
+
export class GetWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
+
queryDecoder = WebshopOrdersQuery as Decoder<WebshopOrdersQuery>
|
|
15
|
+
|
|
16
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
+
if (request.method != "GET") {
|
|
18
|
+
return [false];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const params = Endpoint.parseParameters(request.url, "/webshop/@id/orders", { id: String });
|
|
22
|
+
|
|
23
|
+
if (params) {
|
|
24
|
+
return [true, params as Params];
|
|
25
|
+
}
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
+
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.hasSomeAccess(organization.id)) {
|
|
35
|
+
throw Context.auth.error()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
39
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Read)) {
|
|
40
|
+
throw Context.auth.notFoundOrNoAccess("Je hebt geen toegang tot de bestellingen van deze webshop")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let orders: Order[] | undefined = undefined
|
|
44
|
+
const limit = 50
|
|
45
|
+
|
|
46
|
+
if (request.query.updatedSince !== undefined) {
|
|
47
|
+
if (request.query.afterNumber !== undefined) {
|
|
48
|
+
orders = await Order.select("WHERE webshopId = ? AND number is not null AND (updatedAt > ? OR (updatedAt = ? AND number > ?)) ORDER BY updatedAt, number LIMIT ?", [webshop.id, request.query.updatedSince, request.query.updatedSince, request.query.afterNumber, limit])
|
|
49
|
+
} else {
|
|
50
|
+
orders = await Order.select("WHERE webshopId = ? AND number is not null AND updatedAt >= ? ORDER BY updatedAt, number LIMIT ?", [webshop.id, request.query.updatedSince, limit])
|
|
51
|
+
}
|
|
52
|
+
} else if (request.query.afterNumber !== undefined) {
|
|
53
|
+
orders = await Order.select("WHERE webshopId = ? AND number > ? ORDER BY updatedAt, number LIMIT ?", [webshop.id, request.query.afterNumber, limit])
|
|
54
|
+
} else {
|
|
55
|
+
orders = await Order.select("WHERE webshopId = ? AND number is not null ORDER BY updatedAt, number LIMIT ?", [webshop.id, limit])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//const paymentIds = orders.map(o => o.paymentId).filter(p => !!p) as string[]
|
|
59
|
+
//if (paymentIds.length > 0) {
|
|
60
|
+
// const payments = await Payment.getByIDs(...paymentIds)
|
|
61
|
+
// for (const order of orders) {
|
|
62
|
+
// const payment = payments.find(p => p.id === order.paymentId)
|
|
63
|
+
// order.setOptionalRelation(Order.payment, payment ?? null)
|
|
64
|
+
// }
|
|
65
|
+
//} else {
|
|
66
|
+
// for (const order of orders) {
|
|
67
|
+
// order.setOptionalRelation(Order.payment, null)
|
|
68
|
+
// }
|
|
69
|
+
//}
|
|
70
|
+
|
|
71
|
+
const structures = await Order.getPrivateStructures(orders)
|
|
72
|
+
|
|
73
|
+
return new Response(
|
|
74
|
+
new PaginatedResponse({
|
|
75
|
+
results: structures,
|
|
76
|
+
next: orders.length >= limit ? WebshopOrdersQuery.create({
|
|
77
|
+
updatedSince: orders[orders.length - 1].updatedAt ?? undefined,
|
|
78
|
+
afterNumber: orders[orders.length - 1].number ?? undefined
|
|
79
|
+
}) : undefined
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Decoder } from "@simonbackx/simple-encoding";
|
|
2
|
+
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
+
import { Ticket, Webshop } from '@stamhoofd/models';
|
|
4
|
+
import { PaginatedResponse, PermissionLevel, TicketPrivate, WebshopTicketsQuery } from "@stamhoofd/structures";
|
|
5
|
+
|
|
6
|
+
import { Context } from "../../../../helpers/Context";
|
|
7
|
+
|
|
8
|
+
type Params = { id: string };
|
|
9
|
+
type Query = WebshopTicketsQuery
|
|
10
|
+
type Body = undefined
|
|
11
|
+
type ResponseBody = PaginatedResponse<TicketPrivate[], Query>
|
|
12
|
+
|
|
13
|
+
export class GetWebshopTicketsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
+
queryDecoder = WebshopTicketsQuery as Decoder<WebshopTicketsQuery>
|
|
15
|
+
|
|
16
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
17
|
+
if (request.method != "GET") {
|
|
18
|
+
return [false];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const params = Endpoint.parseParameters(request.url, "/webshop/@id/tickets/private", { id: String });
|
|
22
|
+
|
|
23
|
+
if (params) {
|
|
24
|
+
return [true, params as Params];
|
|
25
|
+
}
|
|
26
|
+
return [false];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
|
+
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.hasSomeAccess(organization.id)) {
|
|
35
|
+
throw Context.auth.error()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const webshop = await Webshop.getByID(request.params.id)
|
|
39
|
+
if (!webshop || !await Context.auth.canAccessWebshopTickets(webshop, PermissionLevel.Read)) {
|
|
40
|
+
throw Context.auth.notFoundOrNoAccess("Je hebt geen toegang tot de tickets van deze webshop")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let tickets: Ticket[] | undefined = undefined
|
|
44
|
+
const limit = 150
|
|
45
|
+
|
|
46
|
+
if (request.query.updatedSince !== undefined) {
|
|
47
|
+
if (request.query.lastId !== undefined) {
|
|
48
|
+
tickets = await Ticket.select("WHERE webshopId = ? AND (updatedAt > ? OR (updatedAt = ? AND id > ?)) ORDER BY updatedAt, id LIMIT ?", [webshop.id, request.query.updatedSince, request.query.updatedSince, request.query.lastId, limit])
|
|
49
|
+
} else {
|
|
50
|
+
tickets = await Ticket.select("WHERE webshopId = ? AND updatedAt >= ? ORDER BY updatedAt, id LIMIT ?", [webshop.id, request.query.updatedSince, limit])
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
tickets = await Ticket.select("WHERE webshopId = ? ORDER BY updatedAt, id LIMIT ?", [webshop.id, limit])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const supportsDeletedTickets = request.request.getVersion() >= 229
|
|
57
|
+
|
|
58
|
+
return new Response(
|
|
59
|
+
new PaginatedResponse({
|
|
60
|
+
results: tickets.map(ticket => TicketPrivate.create(ticket)).filter(ticket => supportsDeletedTickets || !ticket.deletedAt),
|
|
61
|
+
next: tickets.length >= limit ? WebshopTicketsQuery.create({
|
|
62
|
+
updatedSince: tickets[tickets.length - 1].updatedAt ?? undefined,
|
|
63
|
+
lastId: tickets[tickets.length - 1].id ?? undefined
|
|
64
|
+
}) : undefined
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|