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