@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,50 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { SimpleError } from "@simonbackx/simple-errors";
3
+ import { Document, DocumentTemplate } from "@stamhoofd/models";
4
+ import { Document as DocumentStruct } 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 = DocumentStruct[]
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 GetDocumentsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
18
+ protected doesMatch(request: Request): [true, Params] | [false] {
19
+ if (request.method != "GET") {
20
+ return [false];
21
+ }
22
+
23
+ const params = Endpoint.parseParameters(request.url, "/organization/document-templates/@id/documents", { id: String});
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
+ const organization = await Context.setOrganizationScope();
33
+ await Context.authenticate()
34
+
35
+ if (!await Context.auth.canManageDocuments(organization.id)) {
36
+ throw Context.auth.error()
37
+ }
38
+
39
+ const template = await DocumentTemplate.getByID(request.params.id)
40
+ if (!template || !await Context.auth.canAccessDocumentTemplate(template)) {
41
+ throw Context.auth.notFoundOrNoAccess("Onbekend document")
42
+ }
43
+
44
+ const documents = await Document.where({ templateId: template.id });
45
+
46
+ return new Response(
47
+ documents.map(t => t.getStructure())
48
+ );
49
+ }
50
+ }
@@ -0,0 +1,129 @@
1
+ import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError } from "@simonbackx/simple-errors";
4
+ import { Document, DocumentTemplate, Group, Member, Registration, Token } from '@stamhoofd/models';
5
+ import { Document as DocumentStruct, DocumentStatus, DocumentTemplatePrivate, PermissionLevel } from "@stamhoofd/structures";
6
+
7
+ import { Context } from '../../../../helpers/Context';
8
+
9
+ type Params = Record<string, never>;
10
+ type Query = undefined;
11
+ type Body = PatchableArrayAutoEncoder<DocumentStruct>
12
+ type ResponseBody = DocumentStruct[]
13
+
14
+ export class PatchDocumentEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ bodyDecoder = new PatchableArrayDecoder(DocumentStruct as Decoder<DocumentStruct>, DocumentStruct.patchType() as Decoder<AutoEncoderPatchType<DocumentStruct>>, StringDecoder)
16
+
17
+ protected doesMatch(request: Request): [true, Params] | [false] {
18
+ if (request.method != "PATCH") {
19
+ return [false];
20
+ }
21
+
22
+ const params = Endpoint.parseParameters(request.url, "/organization/documents", {});
23
+
24
+ if (params) {
25
+ return [true, params as Params];
26
+ }
27
+ return [false];
28
+ }
29
+
30
+ async handle(request: DecodedRequest<Params, Query, Body>) {
31
+ const organization = await Context.setOrganizationScope();
32
+ await Context.authenticate()
33
+
34
+ if (!await Context.auth.canManageDocuments(organization.id ,PermissionLevel.Write)) {
35
+ throw Context.auth.error()
36
+ }
37
+
38
+ const updatedDocuments: DocumentStruct[] = []
39
+
40
+ for (const patch of request.body.getPatches()) {
41
+ const document = await Document.getByID(patch.id)
42
+ if (!document || !(await Context.auth.canAccessDocument(document, PermissionLevel.Write))) {
43
+ throw Context.auth.notFoundOrNoAccess("Onbekend document")
44
+ }
45
+
46
+ if (patch.data) {
47
+ document.data.patchOrPut(patch.data)
48
+ }
49
+
50
+ if (patch.status && (document.status !== DocumentStatus.MissingData || patch.status === DocumentStatus.Deleted)) {
51
+ document.status = patch.status
52
+ }
53
+
54
+ if (document.status === DocumentStatus.Draft || document.status === DocumentStatus.Published) {
55
+ const template = await DocumentTemplate.getByID(document.templateId)
56
+ if (!template) {
57
+ throw new SimpleError({
58
+ code: "not_found",
59
+ message: "Document not found",
60
+ human: "Document niet gevonden"
61
+ })
62
+ }
63
+
64
+ document.status = template.status
65
+ }
66
+
67
+ await document.updateData();
68
+ await document.save();
69
+
70
+ // Return in response
71
+ updatedDocuments.push(document.getStructure())
72
+ }
73
+
74
+ for (const {put} of request.body.getPuts()) {
75
+ // Create a new document
76
+ const template = await DocumentTemplate.getByID(put.templateId)
77
+ if (!template || !await Context.auth.canAccessDocumentTemplate(template, PermissionLevel.Write)) {
78
+ throw new SimpleError({
79
+ code: "not_found",
80
+ message: "Document template not found",
81
+ human: "Document template niet gevonden"
82
+ })
83
+ }
84
+ const document = new Document();
85
+ document.organizationId = organization.id
86
+ document.templateId = template.id
87
+ document.status = put.status
88
+ document.data = put.data
89
+
90
+ if (document.status === DocumentStatus.Draft || document.status === DocumentStatus.Published) {
91
+ document.status = template.status
92
+ }
93
+
94
+ if (put.registrationId) {
95
+ const registration = await Registration.getByID(put.registrationId)
96
+ if (!registration) {
97
+ throw new SimpleError({
98
+ code: "not_found",
99
+ message: "Registration not found",
100
+ human: "Inschrijving niet gevonden"
101
+ })
102
+ }
103
+ document.registrationId = put.registrationId
104
+ put.memberId = registration.memberId
105
+ }
106
+ if (put.memberId) {
107
+ const member = await Member.getWithRegistrations(put.memberId)
108
+ if (!member || !await Context.auth.canAccessMember(member, PermissionLevel.Read)) {
109
+ throw new SimpleError({
110
+ code: "not_found",
111
+ message: "Member not found",
112
+ human: "Lid niet gevonden"
113
+ })
114
+ }
115
+ document.memberId = put.memberId
116
+ }
117
+
118
+ await document.updateData();
119
+ await document.save();
120
+
121
+ // Return in response
122
+ updatedDocuments.push(document.getStructure())
123
+ }
124
+
125
+ return new Response(
126
+ updatedDocuments
127
+ );
128
+ }
129
+ }
@@ -0,0 +1,114 @@
1
+ import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError } from "@simonbackx/simple-errors";
4
+ import { DocumentTemplate, Token } from '@stamhoofd/models';
5
+ import { DocumentTemplatePrivate, PermissionLevel } from "@stamhoofd/structures";
6
+
7
+ import { Context } from '../../../../helpers/Context';
8
+
9
+ type Params = Record<string, never>;
10
+ type Query = undefined;
11
+ type Body = PatchableArrayAutoEncoder<DocumentTemplatePrivate>
12
+ type ResponseBody = DocumentTemplatePrivate[]
13
+
14
+ /**
15
+ * 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
16
+ */
17
+
18
+ export class PatchDocumentTemplateEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+ bodyDecoder = new PatchableArrayDecoder(DocumentTemplatePrivate as Decoder<DocumentTemplatePrivate>, DocumentTemplatePrivate.patchType() as Decoder<AutoEncoderPatchType<DocumentTemplatePrivate>>, StringDecoder)
20
+
21
+ protected doesMatch(request: Request): [true, Params] | [false] {
22
+ if (request.method != "PATCH") {
23
+ return [false];
24
+ }
25
+
26
+ const params = Endpoint.parseParameters(request.url, "/organization/document-templates", {});
27
+
28
+ if (params) {
29
+ return [true, params as Params];
30
+ }
31
+ return [false];
32
+ }
33
+
34
+ async handle(request: DecodedRequest<Params, Query, Body>) {
35
+ const organization = await Context.setOrganizationScope();
36
+ await Context.authenticate()
37
+
38
+ if (!await Context.auth.canManageDocuments(organization.id, PermissionLevel.Write)) {
39
+ throw Context.auth.error()
40
+ }
41
+
42
+ const updatedTemplates: DocumentTemplatePrivate[] = []
43
+
44
+ for (const {put} of request.body.getPuts()) {
45
+ // Creating new templates
46
+ const template = new DocumentTemplate()
47
+ template.privateSettings = put.privateSettings
48
+ template.settings = put.settings
49
+ template.status = put.status
50
+ template.html = put.html
51
+ template.updatesEnabled = put.updatesEnabled
52
+ template.organizationId = organization.id
53
+ await template.save();
54
+
55
+ // todo: Generate documents (maybe in background)
56
+ template.buildAll().catch(console.error)
57
+
58
+ // Return in response
59
+ updatedTemplates.push(template.getPrivateStructure())
60
+ }
61
+
62
+ for (const patch of request.body.getPatches()) {
63
+ const template = await DocumentTemplate.getByID(patch.id)
64
+ if (!template || !await Context.auth.canAccessDocumentTemplate(template, PermissionLevel.Full)) {
65
+ throw Context.auth.notFoundOrNoAccess("Onbekende template")
66
+ }
67
+
68
+ if (patch.privateSettings) {
69
+ template.privateSettings.patchOrPut(patch.privateSettings)
70
+ }
71
+
72
+ if (patch.settings) {
73
+ template.settings.patchOrPut(patch.settings)
74
+ }
75
+
76
+ if (patch.status) {
77
+ template.status = patch.status
78
+ }
79
+
80
+ if (patch.updatesEnabled !== undefined) {
81
+ template.updatesEnabled = patch.updatesEnabled
82
+ }
83
+
84
+ if (patch.html) {
85
+ template.html = patch.html
86
+ }
87
+
88
+ await template.save();
89
+
90
+ // Update documents
91
+ await template.buildAll()
92
+
93
+ // Return in response
94
+ updatedTemplates.push(template.getPrivateStructure())
95
+ }
96
+
97
+ for (const id of request.body.getDeletes()) {
98
+ const template = await DocumentTemplate.getByID(id)
99
+ if (!template || !await Context.auth.canAccessDocumentTemplate(template, PermissionLevel.Full)) {
100
+ throw new SimpleError({
101
+ code: "not_found",
102
+ message: "Template not found",
103
+ human: "Template niet gevonden"
104
+ })
105
+ }
106
+
107
+ await template.delete()
108
+ }
109
+
110
+ return new Response(
111
+ updatedTemplates
112
+ );
113
+ }
114
+ }
@@ -0,0 +1,50 @@
1
+ import { ArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { EmailAddress } from '@stamhoofd/email';
5
+ import { EmailInformation } from '@stamhoofd/structures';
6
+
7
+ import { Context } from '../../../../helpers/Context';
8
+
9
+ type Params = Record<string, never>;
10
+ type Query = undefined;
11
+ type Body = string[]
12
+ type ResponseBody = EmailInformation[];
13
+
14
+ export class CheckEmailBouncesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ bodyDecoder = new ArrayDecoder(StringDecoder)
16
+
17
+ protected doesMatch(request: Request): [true, Params] | [false] {
18
+ if (request.method != "POST") {
19
+ return [false];
20
+ }
21
+
22
+ const params = Endpoint.parseParameters(request.url, "/email/check-bounces", {});
23
+
24
+ if (params) {
25
+ return [true, params as Params];
26
+ }
27
+ return [false];
28
+ }
29
+
30
+ async handle(request: DecodedRequest<Params, Query, Body>) {
31
+ const organization = await Context.setOrganizationScope();
32
+ await Context.authenticate()
33
+
34
+ if (!await Context.auth.canAccessEmailBounces(organization.id)) {
35
+ throw Context.auth.error()
36
+ }
37
+
38
+ if (request.body.length > 10000) {
39
+ throw new SimpleError({
40
+ code: "too_many_recipients",
41
+ message: "Too many recipients",
42
+ human: "Je kan maar maximaal 10.000 adressen tergelijk controleren.",
43
+ field: "recipients"
44
+ })
45
+ }
46
+
47
+ const emails = await EmailAddress.getByEmails(request.body, organization.id)
48
+ return new Response(emails.map(e => EmailInformation.create(e)));
49
+ }
50
+ }
@@ -0,0 +1,234 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { I18n } from '@stamhoofd/backend-i18n';
5
+ import { Email } from '@stamhoofd/email';
6
+ import { getEmailBuilder,RateLimiter } from '@stamhoofd/models';
7
+ import { EmailRequest, Recipient } from "@stamhoofd/structures";
8
+
9
+ import { Context } from '../../../../helpers/Context';
10
+
11
+ type Params = Record<string, never>;
12
+ type Query = undefined;
13
+ type Body = EmailRequest
14
+ type ResponseBody = undefined;
15
+
16
+ export const paidEmailRateLimiter = new RateLimiter({
17
+ limits: [
18
+ {
19
+ // Max 5.000 emails a day
20
+ limit: 5000,
21
+ duration: 24 * 60 * 1000 * 60
22
+ },
23
+ {
24
+ // 10.000 requests per week
25
+ limit: 10000,
26
+ duration: 24 * 60 * 1000 * 60 * 7
27
+ }
28
+ ]
29
+ });
30
+
31
+ export const freeEmailRateLimiter = new RateLimiter({
32
+ limits: [
33
+ {
34
+ // Max 100 a day
35
+ limit: 100,
36
+ duration: 24 * 60 * 1000 * 60
37
+ },
38
+ {
39
+ // Max 200 a week
40
+ limit: 200,
41
+ duration: 7 * 24 * 60 * 1000 * 60
42
+ }
43
+ ]
44
+ });
45
+
46
+ /**
47
+ * 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
48
+ */
49
+
50
+ export class EmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
51
+ bodyDecoder = EmailRequest as Decoder<EmailRequest>
52
+
53
+ protected doesMatch(request: Request): [true, Params] | [false] {
54
+ if (request.method != "POST") {
55
+ return [false];
56
+ }
57
+
58
+ const params = Endpoint.parseParameters(request.url, "/email", {});
59
+
60
+ if (params) {
61
+ return [true, params as Params];
62
+ }
63
+ return [false];
64
+ }
65
+
66
+ async handle(request: DecodedRequest<Params, Query, Body>) {
67
+ const organization = await Context.setOrganizationScope();
68
+ const {user} = await Context.authenticate()
69
+
70
+ if (!Context.auth.canSendEmails()) {
71
+ throw Context.auth.error()
72
+ }
73
+
74
+ if (request.body.recipients.length > 5000) {
75
+ throw new SimpleError({
76
+ code: "too_many_recipients",
77
+ message: "Too many recipients",
78
+ human: "Je kan maar een mail naar maximaal 5000 personen tergelijk versturen. Contacteer ons om deze limiet te verhogen indien dit nodig is.",
79
+ field: "recipients"
80
+ })
81
+ }
82
+
83
+ // For non paid organizations, the limit is 10
84
+ if (request.body.recipients.length > 10 && !organization.meta.packages.isPaid) {
85
+ throw new SimpleError({
86
+ code: "too_many_emails",
87
+ message: "Too many e-mails",
88
+ human: "Zolang je de demo versie van Stamhoofd gebruikt kan je maar maximaal een email sturen naar 10 emailadressen. Als je het pakket aankoopt zal deze limiet er niet zijn. Dit is om misbruik te voorkomen met spammers die spam email versturen via Stamhoofd.",
89
+ field: "recipients"
90
+ })
91
+ }
92
+
93
+ const limiter = organization.meta.packages.isPaid ? paidEmailRateLimiter : freeEmailRateLimiter
94
+
95
+ try {
96
+ limiter.track(organization.id, request.body.recipients.length);
97
+ } catch (e) {
98
+ Email.sendInternal({
99
+ to: "hallo@stamhoofd.be",
100
+ subject: "[Limiet] Limiet bereikt voor aantal e-mails",
101
+ text: "Beste, \nDe limiet werd bereikt voor het aantal e-mails per dag. \nVereniging: "+organization.id+" ("+organization.name+")" + "\n\n" + e.message + "\n\nStamhoofd"
102
+ }, new I18n("nl", "BE"))
103
+
104
+ throw new SimpleError({
105
+ code: "too_many_emails_period",
106
+ message: "Too many e-mails limited",
107
+ human: "Oeps! Om spam te voorkomen limiteren we het aantal emails die je per dag/week kan versturen. Neem contact met ons op om deze limiet te verhogen.",
108
+ field: "recipients"
109
+ })
110
+ }
111
+
112
+
113
+ // Validate email
114
+ const sender = organization.privateMeta.emails.find(e => e.id == request.body.emailId)
115
+ if (!sender) {
116
+ throw new SimpleError({
117
+ code: "invalid_field",
118
+ message: "Invalid emailId",
119
+ human: "Het e-mailadres waarvan je wilt versturen bestaat niet (meer). Kijk je het na?",
120
+ field: "emailId"
121
+ })
122
+ }
123
+
124
+ // Validate attachments
125
+ const size = request.body.attachments.reduce((value: number, attachment) => {
126
+ return value + attachment.content.length
127
+ }, 0)
128
+
129
+ if (size > 9.5*1024*1024) {
130
+ throw new SimpleError({
131
+ code: "too_big_attachments",
132
+ message: "Too big attachments",
133
+ human: "Jouw bericht is te groot. Grote bijlages verstuur je beter niet via e-mail, je plaatst dan best een link naar de locatie in bv. Google Drive. De maximale grootte van een e-mail is 10MB, inclusief het bericht. Als je grote bestanden verstuurt kan je ze ook proberen te verkleinen.",
134
+ field: "attachments"
135
+ })
136
+ }
137
+
138
+ const safeContentTypes = [
139
+ "application/msword",
140
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
141
+ "application/vnd.ms-excel",
142
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
143
+ "application/pdf",
144
+ "image/jpeg",
145
+ "image/png",
146
+ "image/gif"
147
+ ]
148
+
149
+ for (const attachment of request.body.attachments) {
150
+ if (attachment.contentType && !safeContentTypes.includes(attachment.contentType)) {
151
+ throw new SimpleError({
152
+ code: "content_type_not_supported",
153
+ message: "Content-Type not supported",
154
+ human: "Het bestandstype van jouw bijlage wordt niet ondersteund of is onveilig om in een e-mail te plaatsen. Overweeg om je bestand op bv. Google Drive te zetten en de link in jouw e-mail te zetten.",
155
+ field: "attachments"
156
+ })
157
+ }
158
+ }
159
+
160
+ const attachments = request.body.attachments.map((attachment, index) => {
161
+ let filename = "bijlage-"+index;
162
+
163
+ if (attachment.contentType == "application/pdf") {
164
+ // tmp solution for pdf only
165
+ filename += ".pdf"
166
+ }
167
+
168
+ // Correct file name if needed
169
+ if (attachment.filename) {
170
+ filename = attachment.filename.toLowerCase().replace(/[^a-z0-9.]+/g, "-").replace(/^-+/, "").replace(/-+$/, "")
171
+ }
172
+
173
+ return {
174
+ filename: filename,
175
+ content: attachment.content,
176
+ contentType: attachment.contentType ?? undefined,
177
+ encoding: "base64"
178
+ }
179
+ })
180
+
181
+ let from = organization.uri+"@stamhoofd.email";
182
+ let replyTo: string | undefined = sender.email;
183
+
184
+ // Can we send from this e-mail or reply-to?
185
+ if (organization.privateMeta.mailDomain && organization.privateMeta.mailDomainActive && sender.email.endsWith("@"+organization.privateMeta.mailDomain)) {
186
+ from = sender.email
187
+ replyTo = undefined;
188
+ }
189
+
190
+ // Include name in form field
191
+ if (sender.name) {
192
+ from = '"'+sender.name.replaceAll("\"", "\\\"")+"\" <"+from+">"
193
+ } else {
194
+ from = '"'+organization.name.replaceAll("\"", "\\\"")+"\" <"+from+">"
195
+ }
196
+
197
+ const email = request.body
198
+
199
+ // Create e-mail builder
200
+ const builder = await getEmailBuilder(organization, {
201
+ ...email,
202
+ from,
203
+ replyTo,
204
+ attachments,
205
+ defaultReplacements: request.body.defaultReplacements ?? []
206
+ })
207
+
208
+ Email.schedule(builder)
209
+
210
+ // Also send a copy
211
+ const recipient = Recipient.create(email.recipients[0])
212
+ recipient.email = sender.email
213
+ recipient.firstName = sender.name ?? null
214
+ recipient.lastName = null
215
+ recipient.userId = null
216
+
217
+ const prefix = "<p><i>Kopie e-mail verzonden door "+user.firstName+" "+user.lastName+"</i><br /><br /></p>"
218
+ const builder2 = await getEmailBuilder(organization, {
219
+ ...email,
220
+ subject: "[KOPIE] "+email.subject,
221
+ html: email.html?.replace("<body>", "<body>"+prefix) ?? null,
222
+ recipients: [
223
+ recipient
224
+ ],
225
+ from,
226
+ replyTo,
227
+ attachments
228
+ })
229
+
230
+ Email.schedule(builder2)
231
+
232
+ return new Response(undefined);
233
+ }
234
+ }
@@ -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 { SimpleError } from '@simonbackx/simple-errors';
4
+ import { EmailTemplate, Token } from '@stamhoofd/models';
5
+ import { EmailTemplate as EmailTemplateStruct, EmailTemplateType } from '@stamhoofd/structures';
6
+
7
+ import { Context } from '../../../../helpers/Context';
8
+
9
+ type Params = Record<string, never>;
10
+ type Body = undefined;
11
+
12
+ class Query extends AutoEncoder {
13
+ @field({ decoder: StringDecoder, optional: true })
14
+ webshopId?: string
15
+
16
+ @field({ decoder: StringDecoder, optional: true })
17
+ groupId?: string
18
+ }
19
+
20
+ type ResponseBody = EmailTemplateStruct[];
21
+
22
+ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
23
+ queryDecoder = Query as Decoder<Query>;
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, "/email-templates", {});
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
+ const organization = await Context.setOrganizationScope();
40
+ await Context.authenticate()
41
+
42
+ if (!await Context.auth.canReadEmailTemplates(organization.id)) {
43
+ throw Context.auth.error()
44
+ }
45
+
46
+ const types = [
47
+ EmailTemplateType.OrderConfirmationOnline,
48
+ EmailTemplateType.OrderConfirmationTransfer,
49
+ EmailTemplateType.OrderConfirmationPOS,
50
+ EmailTemplateType.OrderReceivedTransfer,
51
+ EmailTemplateType.TicketsConfirmation,
52
+ EmailTemplateType.TicketsConfirmationTransfer,
53
+ EmailTemplateType.TicketsConfirmationPOS,
54
+ EmailTemplateType.TicketsReceivedTransfer,
55
+ EmailTemplateType.RegistrationConfirmation
56
+ ]
57
+
58
+ const templates = await EmailTemplate.where({ organizationId: organization.id, webshopId: request.query.webshopId ?? null, groupId: request.query.groupId ?? null, type: {sign: 'IN', value: types}});
59
+ const defaultTemplates = await EmailTemplate.where({ organizationId: null, type: {sign: 'IN', value: types} });
60
+ return new Response([...templates, ...defaultTemplates].map(template => EmailTemplateStruct.create(template)))
61
+ }
62
+ }