@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,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
+ }