@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,122 @@
1
+ import { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError } from "@simonbackx/simple-errors";
4
+ import { EmailVerificationCode, PasswordToken, Token, User } from '@stamhoofd/models';
5
+ import { NewUser, PermissionLevel, SignupResponse, User as UserStruct,UserPermissions } from "@stamhoofd/structures";
6
+
7
+ import { Context } from '../../helpers/Context';
8
+
9
+ type Params = { id: string };
10
+ type Query = undefined;
11
+ type Body = AutoEncoderPatchType<NewUser>
12
+ type ResponseBody = UserStruct
13
+
14
+ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ bodyDecoder = NewUser.patchType() as Decoder<AutoEncoderPatchType<NewUser>>
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, "/user/@id", { id: String });
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.setOptionalOrganizationScope();
32
+ const {user, token} = await Context.authenticate({allowWithoutAccount: true})
33
+
34
+ if (request.body.id !== request.params.id) {
35
+ throw new SimpleError({
36
+ code: "invalid_request",
37
+ message: "Invalid request: id mismatch",
38
+ statusCode: 400
39
+ })
40
+ }
41
+
42
+ const editUser = request.body.id === user.id ? user : await User.getByID(request.body.id)
43
+
44
+ if (!editUser || !await Context.auth.canAccessUser(editUser, PermissionLevel.Write) || editUser.isApiUser) {
45
+ throw Context.auth.notFoundOrNoAccess("Je hebt geen toegang om deze gebruiker te wijzigen")
46
+ }
47
+
48
+ if (await Context.auth.canEditUserName(editUser)) {
49
+ editUser.firstName = request.body.firstName ?? editUser.firstName
50
+ editUser.lastName = request.body.lastName ?? editUser.lastName
51
+ }
52
+
53
+ if (request.body.permissions !== undefined) {
54
+ if (!await Context.auth.canAccessUser(editUser, PermissionLevel.Full)) {
55
+ throw new SimpleError({
56
+ code: "permission_denied",
57
+ message: "Je hebt geen rechten om de rechten van deze gebruiker te wijzigen"
58
+ })
59
+ }
60
+
61
+ if (request.body.permissions) {
62
+ if (organization) {
63
+ editUser.permissions = UserPermissions.limitedPatch(editUser.permissions, request.body.permissions, organization.id)
64
+
65
+ if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization)?.hasFullAccess())) {
66
+ throw new SimpleError({
67
+ code: "permission_denied",
68
+ message: "Je kan jezelf niet verwijderen als hoofdbeheerder"
69
+ })
70
+ }
71
+ } else {
72
+ if (editUser.permissions) {
73
+ editUser.permissions.patchOrPut(request.body.permissions)
74
+ } else {
75
+ editUser.permissions = request.body.permissions.isPut() ? request.body.permissions : null
76
+ }
77
+
78
+ if (editUser.permissions && editUser.permissions.isEmpty) {
79
+ editUser.permissions = null
80
+ }
81
+
82
+ if (editUser.id === user.id && !editUser.permissions?.platform?.hasFullAccess()) {
83
+ throw new SimpleError({
84
+ code: "permission_denied",
85
+ message: "Je kan jezelf niet verwijderen als hoofdbeheerder"
86
+ })
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ if (editUser.id == user.id && request.body.password) {
93
+ // password changes
94
+ await editUser.changePassword(request.body.password)
95
+ await PasswordToken.clearFor(editUser.id)
96
+ await Token.clearFor(editUser.id, token.accessToken)
97
+ }
98
+
99
+ await editUser.save();
100
+
101
+ if (await Context.auth.canEditUserEmail(editUser)) {
102
+ if (request.body.email && request.body.email !== editUser.email) {
103
+ // Create an validation code
104
+ // We always need the code, to return it. Also on password recovery -> may not be visible to the client whether the user exists or not
105
+ const code = await EmailVerificationCode.createFor(editUser, request.body.email)
106
+ code.send(editUser, organization, request.i18n, editUser.id === user.id)
107
+
108
+ throw new SimpleError({
109
+ code: "verify_email",
110
+ message: "Your email address needs verification",
111
+ human: editUser.id === user.id ? "Verifieer jouw nieuwe e-mailadres via de link in de e-mail, daarna passen we het automatisch aan." : "Er is een verificatie e-mail verstuurd naar "+request.body.email+" om het e-mailadres te verifiëren. Zodra dat is gebeurd, wordt het e-mailadres gewijzigd.",
112
+ meta: SignupResponse.create({
113
+ token: code.token,
114
+ }).encode({ version: request.request.getVersion() }),
115
+ statusCode: 403
116
+ });
117
+ }
118
+ }
119
+
120
+ return new Response(UserStruct.create({...editUser, hasAccount: editUser.hasAccount()}));
121
+ }
122
+ }
@@ -0,0 +1,37 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { EmailVerificationCode } from '@stamhoofd/models';
4
+ import { PollEmailVerificationRequest, PollEmailVerificationResponse } from "@stamhoofd/structures";
5
+
6
+ import { Context } from '../../helpers/Context';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type Body = PollEmailVerificationRequest;
11
+ type ResponseBody = PollEmailVerificationResponse;
12
+
13
+ export class PollEmailVerificationEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ bodyDecoder = PollEmailVerificationRequest as Decoder<PollEmailVerificationRequest>;
15
+
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method != "POST") {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, "/verify-email/poll", {});
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.setOptionalOrganizationScope()
31
+ const valid = await EmailVerificationCode.poll(organization?.id ?? null, request.body.token)
32
+
33
+ return new Response(PollEmailVerificationResponse.create({
34
+ valid
35
+ }));
36
+ }
37
+ }
@@ -0,0 +1,41 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { EmailVerificationCode } from '@stamhoofd/models';
4
+ import { PollEmailVerificationRequest, PollEmailVerificationResponse } from "@stamhoofd/structures";
5
+
6
+ import { Context } from '../../helpers/Context';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type Body = PollEmailVerificationRequest;
11
+ type ResponseBody = PollEmailVerificationResponse;
12
+
13
+ export class PollEmailVerificationEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ bodyDecoder = PollEmailVerificationRequest as Decoder<PollEmailVerificationRequest>;
15
+
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method != "POST") {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, "/verify-email/retry", {});
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.setOptionalOrganizationScope()
31
+ const valid = await EmailVerificationCode.poll(organization?.id ?? null, request.body.token);
32
+
33
+ if (valid) {
34
+ EmailVerificationCode.resend(organization, request.body.token, request.i18n).catch(console.error)
35
+ }
36
+
37
+ return new Response(PollEmailVerificationResponse.create({
38
+ valid
39
+ }));
40
+ }
41
+ }
@@ -0,0 +1,107 @@
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 { Email } from '@stamhoofd/email';
5
+ import { EmailVerificationCode, PasswordToken, User } from '@stamhoofd/models';
6
+ import { NewUser, SignupResponse } from "@stamhoofd/structures";
7
+
8
+ import { Context } from '../../helpers/Context';
9
+
10
+ type Params = Record<string, never>;
11
+ type Query = undefined;
12
+ type Body = NewUser;
13
+ type ResponseBody = SignupResponse;
14
+
15
+ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
+ bodyDecoder = NewUser as Decoder<NewUser>;
17
+
18
+ protected doesMatch(request: Request): [true, Params] | [false] {
19
+ if (request.method != "POST") {
20
+ return [false];
21
+ }
22
+
23
+ const params = Endpoint.parseParameters(request.url, "/sign-up", {});
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.setUserOrganizationScope()
33
+
34
+ const u = await User.getForRegister(organization?.id ?? null, request.body.email)
35
+
36
+ // Don't optimize. Always run two queries atm.
37
+ let user = await User.register(
38
+ organization,
39
+ request.body
40
+ );
41
+
42
+ let sendCode = true
43
+
44
+ if (!user) {
45
+ if (!u) {
46
+ // Fail silently because user did exist, and we don't want to expose that the user doesn't exists
47
+ console.error("Could not register, but user doesn't exist: "+request.body.email)
48
+
49
+ throw new SimpleError({
50
+ code: "unexpected_error",
51
+ message: "Something went wrong",
52
+ human: "Er ging iets mis",
53
+ statusCode: 500
54
+ })
55
+ }
56
+
57
+ user = u
58
+
59
+ if (u.hasAccount()) {
60
+ // Send an e-mail to say you already have an account + follow password forgot flow
61
+ const recoveryUrl = await PasswordToken.getPasswordRecoveryUrl(user, organization, request.i18n)
62
+ const { from, replyTo } = {
63
+ from: (user.permissions || !organization ? Email.getInternalEmailFor(request.i18n) : organization.getStrongEmail(request.i18n)),
64
+ replyTo: undefined
65
+ }
66
+
67
+ const footer = (!user.permissions && organization ? "\n\n—\n\nOnze ledenadministratie werkt via het Stamhoofd platform, op maat van verenigingen. Probeer het ook via https://"+request.i18n.$t("shared.domains.marketing")+"/ledenadministratie\n\n" : '')
68
+
69
+ const name = organization ? organization.name : 'Stamhoofd'
70
+ // Send email
71
+ Email.send({
72
+ from,
73
+ replyTo,
74
+ to: user.email,
75
+ subject: `[${name}] Je hebt al een account`,
76
+ type: "transactional",
77
+ text: (user.firstName ? "Hey "+user.firstName : "Hey") + ", \n\nJe probeerde een account aan te maken, maar je hebt eigenlijk al een account met e-mailadres "+user.email+". Als je jouw wachtwoord niet meer weet, kan je een nieuw wachtwoord instellen door op de volgende link te klikken of door deze te kopiëren in de adresbalk van jouw browser:\n"+recoveryUrl+"\n\nWachtwoord al teruggevonden of heb je helemaal niet proberen te registreren? Dan mag je deze e-mail veilig negeren.\n\nMet vriendelijke groeten,\n"+(user.permissions ? "Stamhoofd" : name)+footer
78
+ });
79
+
80
+ // Don't send the code
81
+ sendCode = false
82
+ } else {
83
+ // This is safe, because we are the first one. There is no password yet.
84
+ // If a hacker tries this, he won't be able to sign in, because he needs to
85
+ // verify the e-mail first (same as if the user didn't exist)
86
+ // If we didn't set the password, we would allow a different kind of attack:
87
+ // a hacker could send an e-mail to the user (try to register again, seindgin a new email which would trigger a different password change), right after the user registered (without verifying yet), when he had set a different password
88
+ // user clicks on second e-mail -> this sets the hackers password instead
89
+ user.verified = false
90
+ await user.changePassword(request.body.password)
91
+ await PasswordToken.clearFor(user.id)
92
+ await user.save()
93
+ }
94
+ }
95
+
96
+ // We always need the code, to return it. Also on password recovery -> may not be visible to the client whether the user exists or not
97
+ const code = await EmailVerificationCode.createFor(user, user.email)
98
+
99
+ if (sendCode) {
100
+ code.send(user, organization, request.i18n)
101
+ }
102
+
103
+ return new Response(SignupResponse.create({
104
+ token: code.token
105
+ }));
106
+ }
107
+ }
@@ -0,0 +1,89 @@
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 { EmailVerificationCode, Token, User } from '@stamhoofd/models';
5
+ import { Token as TokenStruct, VerifyEmailRequest } from "@stamhoofd/structures";
6
+
7
+ import { Context } from '../../helpers/Context';
8
+
9
+ type Params = Record<string, never>;
10
+ type Query = undefined;
11
+ type Body = VerifyEmailRequest;
12
+ type ResponseBody = TokenStruct;
13
+
14
+ export class VerifyEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ bodyDecoder = VerifyEmailRequest as Decoder<VerifyEmailRequest>;
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, "/verify-email", {});
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.setOptionalOrganizationScope();
32
+
33
+ const code = await EmailVerificationCode.verify(organization?.id ?? null, request.body.token, request.body.code)
34
+
35
+ if (!code) {
36
+ throw new SimpleError({
37
+ code: "invalid_code",
38
+ message: "This code is invalid",
39
+ human: "Deze code is ongeldig of vervallen.",
40
+ statusCode: 400
41
+ })
42
+ }
43
+
44
+ const user = await User.getByID(code.userId)
45
+
46
+ if (!user || (user.organizationId !== null && user.organizationId !== (organization?.id ?? null))) {
47
+ throw new SimpleError({
48
+ code: "invalid_code",
49
+ message: "This code is invalid",
50
+ human: "Deze code is ongeldig of vervallen.",
51
+ statusCode: 400
52
+ })
53
+ }
54
+
55
+ if (user.email != code.email) {
56
+ const other = await User.getForAuthentication(user.organizationId, code.email, {allowWithoutAccount: true})
57
+
58
+ if (other) {
59
+ // Delete the other user, but merge data
60
+ await user.merge(other);
61
+ }
62
+
63
+ // change user email
64
+ user.email = code.email
65
+
66
+ // If already in use: will fail, so will verification
67
+ }
68
+
69
+ user.verified = true
70
+
71
+ try {
72
+ await user.save()
73
+ } catch (e) {
74
+ // Duplicate key probably
75
+ if (e.code && e.code == "ER_DUP_ENTRY") {
76
+ throw new SimpleError({
77
+ code: "email_in_use",
78
+ message: "This e-mail is already in use, we cannot set it",
79
+ human: "We kunnen het e-mailadres van deze gebruiker niet instellen naar "+code.email+", omdat die al in gebruik is. Waarschijnlijk heb je meerdere accounts. Probeer met dat e-mailadres in te loggen of contacteer ons ("+request.$t("shared.emails.general")+") als we de gebruikers moeten combineren tot één gebruiker."
80
+ })
81
+ }
82
+ throw e;
83
+ }
84
+
85
+ const token = await Token.createToken(user);
86
+ const st = new TokenStruct(token);
87
+ return new Response(st);
88
+ }
89
+ }
@@ -0,0 +1,95 @@
1
+ import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { City } from '@stamhoofd/models';
4
+ import { Province } from '@stamhoofd/models';
5
+ import { City as CityStruct, Country, Province as ProvinceStruct,SearchRegions } from "@stamhoofd/structures";
6
+ import { StringCompare } from '@stamhoofd/utility';
7
+
8
+ type Params = Record<string, never>;
9
+ class Query extends AutoEncoder {
10
+ @field({ decoder: StringDecoder })
11
+ query: string
12
+ }
13
+ type Body = undefined;
14
+ type ResponseBody = SearchRegions;
15
+
16
+ export class SearchRegionsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
17
+ queryDecoder = Query as Decoder<Query>
18
+
19
+ protected doesMatch(request: Request): [true, Params] | [false] {
20
+ if (request.method != "GET") {
21
+ return [false];
22
+ }
23
+
24
+ const params = Endpoint.parseParameters(request.url, "/address/search", {});
25
+
26
+ if (params) {
27
+ return [true, params as Params];
28
+ }
29
+ return [false];
30
+ }
31
+
32
+ async handle(request: DecodedRequest<Params, Query, Body>) {
33
+ // Escape query
34
+ const query = request.query.query.replace(/([-+><()~*"@\s]+)/g, " ").replace(/[^\w\d]+$/, "")
35
+ if (query.length == 0) {
36
+ // Do not try searching...
37
+ return new Response(SearchRegions.create({
38
+ cities: [],
39
+ provinces: [],
40
+ countries: [],
41
+ }));
42
+ }
43
+
44
+ const match = {
45
+ sign: "MATCH",
46
+ value: query + "*", // We replace special operators in boolean mode with spaces since special characters aren't indexed anyway
47
+ mode: "BOOLEAN"
48
+ };
49
+
50
+ // We had to add an order by in the query to fix the limit. MySQL doesn't want to limit the results correctly if we don't explicitly sort the results on their relevance
51
+ const cities = await City.where({ name: match }, {
52
+ limit: 5,
53
+ sort: [
54
+ {
55
+ column: { name: match },
56
+ direction: "DESC"
57
+ }
58
+ ]
59
+ });
60
+ const loadedCities: (City & { province: Province })[] = []
61
+
62
+ // We had to add an order by in the query to fix the limit. MySQL doesn't want to limit the results correctly if we don't explicitly sort the results on their relevance
63
+ const allProvinces = await Province.all();
64
+
65
+ for (const city of cities) {
66
+ const province = allProvinces.find(p => p.id == city.provinceId);
67
+ if (!province) {
68
+ continue;
69
+ }
70
+ loadedCities.push(city.setRelation(City.province, province))
71
+ }
72
+
73
+ const provinces: Province[] = []
74
+ for (const province of allProvinces) {
75
+ if (StringCompare.typoCount(request.query.query, province.name) < 3) {
76
+ provinces.push(province)
77
+ }
78
+ }
79
+
80
+ const countries: Country[] = []
81
+ if (StringCompare.typoCount(request.query.query, "België") < 3) {
82
+ countries.push(Country.Belgium)
83
+ }
84
+
85
+ if (StringCompare.typoCount(request.query.query, "Nederland") < 3) {
86
+ countries.push(Country.Netherlands)
87
+ }
88
+
89
+ return new Response(SearchRegions.create({
90
+ cities: loadedCities.map(c => CityStruct.create(Object.assign({...c}, { province: ProvinceStruct.create(c.province) }))),
91
+ provinces: provinces.map(c => ProvinceStruct.create(c)),
92
+ countries: countries
93
+ }));
94
+ }
95
+ }
@@ -0,0 +1,31 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { Address, ValidatedAddress } from "@stamhoofd/structures";
4
+
5
+ import { AddressValidator } from '../../../helpers/AddressValidator';
6
+
7
+ type Params = Record<string, never>;
8
+ type Query = undefined;
9
+ type Body = Address
10
+ type ResponseBody = ValidatedAddress;
11
+
12
+ export class ValidateAddressEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
13
+ bodyDecoder = Address as Decoder<Address>
14
+
15
+ protected doesMatch(request: Request): [true, Params] | [false] {
16
+ if (request.method != "POST") {
17
+ return [false];
18
+ }
19
+
20
+ const params = Endpoint.parseParameters(request.url, "/address/validate", {});
21
+
22
+ if (params) {
23
+ return [true, params as Params];
24
+ }
25
+ return [false];
26
+ }
27
+
28
+ async handle(request: DecodedRequest<Params, Query, Body>) {
29
+ return new Response(await AddressValidator.validate(request.body));
30
+ }
31
+ }
@@ -0,0 +1,101 @@
1
+ import { AutoEncoder, Decoder, field, StringDecoder } from "@simonbackx/simple-encoding";
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { Organization, Webshop } from '@stamhoofd/models';
5
+ type Params = Record<string, never>;
6
+
7
+ class Query extends AutoEncoder {
8
+ @field({ decoder: StringDecoder })
9
+ domain: string;
10
+ }
11
+
12
+ type Body = undefined
13
+ type ResponseBody = undefined;
14
+
15
+ export class CheckDomainCertEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
+ queryDecoder = Query as Decoder<Query>;
17
+
18
+ registrationDomains = [... new Set(Object.values(STAMHOOFD.domains.registration ?? {}))]
19
+
20
+ protected doesMatch(request: Request): [true, Params] | [false] {
21
+ if (request.method != "GET") {
22
+ return [false];
23
+ }
24
+
25
+ const params = Endpoint.parseParameters(request.url, "/check-domain-cert", {});
26
+
27
+ if (params) {
28
+ return [true, params as Params];
29
+ }
30
+ return [false];
31
+ }
32
+
33
+ async handle(request: DecodedRequest<Params, Query, Body>) {
34
+ // check if the domain ends on one of our localized registration domains
35
+ for (const domain of this.registrationDomains) {
36
+ if (request.query.domain.endsWith("." + domain)) {
37
+ const strippped = request.query.domain.substr(0, request.query.domain.length - ("." + domain).length )
38
+ if (strippped.includes(".")) {
39
+ throw new SimpleError({
40
+ code: "invalid_domain",
41
+ message: "This domain format is not supported",
42
+ statusCode: 404
43
+ })
44
+ }
45
+
46
+ // Search for the URI
47
+ const organization = await Organization.getByURI(strippped)
48
+
49
+ if (!organization) {
50
+ throw new SimpleError({
51
+ code: "unknown_domain",
52
+ message: "Not known",
53
+ statusCode: 404
54
+ })
55
+ }
56
+ return new Response(undefined);
57
+ }
58
+ }
59
+
60
+ if (STAMHOOFD.domains.legacyWebshop && request.query.domain.endsWith("." + STAMHOOFD.domains.legacyWebshop)) {
61
+ const strippped = request.query.domain.substr(0, request.query.domain.length - ("." + STAMHOOFD.domains.legacyWebshop).length )
62
+ if (strippped.includes(".")) {
63
+ throw new SimpleError({
64
+ code: "invalid_domain",
65
+ message: "This domain format is not supported",
66
+ statusCode: 404
67
+ })
68
+ }
69
+
70
+ // Search for the URI
71
+ const organization = await Organization.getByURI(strippped)
72
+
73
+ if (!organization) {
74
+ throw new SimpleError({
75
+ code: "unknown_domain",
76
+ message: "Not known",
77
+ statusCode: 404
78
+ })
79
+ }
80
+ return new Response(undefined);
81
+ }
82
+
83
+ // Check if we have an organization with a custom domain name
84
+ const organization = await Organization.getByRegisterDomain(request.query.domain)
85
+
86
+ if (organization) {
87
+ return new Response(undefined);
88
+ }
89
+
90
+ const webshops = await Webshop.getByDomainOnly(request.query.domain)
91
+ if (webshops.length > 0) {
92
+ return new Response(undefined);
93
+ }
94
+
95
+ throw new SimpleError({
96
+ code: "unknown_domain",
97
+ message: "Not known",
98
+ statusCode: 404
99
+ })
100
+ }
101
+ }
@@ -0,0 +1,53 @@
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 { EmailAddress } from '@stamhoofd/email';
5
+ import { Organization } from '@stamhoofd/models';
6
+ import { EmailAddressSettings, OrganizationSimple } from '@stamhoofd/structures';
7
+
8
+ type Params = Record<string, never>;
9
+ type Body = undefined;
10
+
11
+ class Query extends AutoEncoder {
12
+ @field({ decoder: StringDecoder })
13
+ id: string
14
+
15
+ @field({ decoder: StringDecoder })
16
+ token: string
17
+ }
18
+
19
+ type ResponseBody = EmailAddressSettings;
20
+
21
+ export class ManageEmailAddressEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ queryDecoder = Query as Decoder<Query>;
23
+
24
+ protected doesMatch(request: Request): [true, Params] | [false] {
25
+ if (request.method != "GET") {
26
+ return [false];
27
+ }
28
+
29
+ const params = Endpoint.parseParameters(request.url, "/email/manage", {});
30
+
31
+ if (params) {
32
+ return [true, params as Params];
33
+ }
34
+ return [false];
35
+ }
36
+
37
+ async handle(request: DecodedRequest<Params, Query, Body>) {
38
+ const email = await EmailAddress.getByID(request.query.id)
39
+ if (!email || email.token !== request.query.token || request.query.token.length < 10 || request.query.id.length < 10) {
40
+ throw new SimpleError({
41
+ code: "invalid_fields",
42
+ message: "Invalid token or id",
43
+ human: "Deze link is vervallen. Probeer het opnieuw in een recentere e-mail"
44
+ })
45
+ }
46
+
47
+ const organization = email.organizationId ? (await Organization.getByID(email.organizationId)) : undefined
48
+ return new Response(EmailAddressSettings.create({
49
+ ...email,
50
+ organization: organization ? OrganizationSimple.create(organization) : null
51
+ }));
52
+ }
53
+ }