@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,68 @@
1
+ import { Request } from "@simonbackx/simple-endpoints";
2
+ import { OrganizationFactory, Token, UserFactory } from "@stamhoofd/models";
3
+ import { Token as TokenStruct } from "@stamhoofd/structures";
4
+
5
+ import { testServer } from "../../../tests/helpers/TestServer";
6
+ import { CreateTokenEndpoint } from './CreateTokenEndpoint';
7
+
8
+ describe("Endpoint.CreateToken", () => {
9
+ // Test endpoint
10
+ const endpoint = new CreateTokenEndpoint();
11
+
12
+ test("Can get a token via password flow", async () => {
13
+ const organization = await new OrganizationFactory({}).create()
14
+ // Also check UTF8 passwords
15
+ const password = "54😂test👌🏾86s&é"
16
+ const user = await new UserFactory({ organization, password }).create()
17
+
18
+ const r = Request.buildJson("POST", "/oauth/token", organization.getApiHost(), {
19
+ grant_type: "password",
20
+ username: user.email,
21
+ password: password
22
+ });
23
+
24
+ const response = await testServer.test(endpoint, r);
25
+ expect(response.body).toBeDefined();
26
+
27
+ if (!(response.body instanceof TokenStruct)) {
28
+ throw new Error("Expected TokenStruct")
29
+ }
30
+
31
+ // Check token is valid
32
+ const token = await Token.getByAccessToken(response.body.accessToken)
33
+ expect(token).toBeDefined()
34
+
35
+ const byRefresh = await Token.getByRefreshToken(response.body.refreshToken)
36
+ expect(byRefresh).toBeDefined()
37
+ });
38
+
39
+ test("Can get a token via refresh flow", async () => {
40
+ const organization = await new OrganizationFactory({}).create()
41
+ // Also check UTF8 passwords
42
+ const password = "54😂test👌🏾86s&é"
43
+ const user = await new UserFactory({ organization, password }).create()
44
+ const token = await Token.createToken(user);
45
+
46
+ const r = Request.buildJson("POST", "/oauth/token", organization.getApiHost(), {
47
+ grant_type: "refresh_token",
48
+ refresh_token: token.refreshToken
49
+ });
50
+
51
+ const response = await testServer.test(endpoint, r);
52
+ expect(response.body).toBeDefined();
53
+
54
+ if (!(response.body instanceof TokenStruct)) {
55
+ throw new Error("Expected TokenStruct")
56
+ }
57
+
58
+ expect(response.body.accessToken).not.toEqual(token.accessToken)
59
+ expect(response.body.refreshToken).not.toEqual(token.refreshToken)
60
+
61
+ // Check token is valid
62
+ const byAccess = await Token.getByAccessToken(response.body.accessToken)
63
+ expect(byAccess).toBeDefined()
64
+
65
+ const byRefresh = await Token.getByRefreshToken(response.body.refreshToken)
66
+ expect(byRefresh).toBeDefined()
67
+ });
68
+ });
@@ -0,0 +1,200 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { SimpleError } from '@simonbackx/simple-errors';
3
+ import { EmailVerificationCode, PasswordToken, Token, User } from '@stamhoofd/models';
4
+ import { ChallengeGrantStruct, CreateTokenStruct, PasswordGrantStruct, PasswordTokenGrantStruct, RefreshTokenGrantStruct, RequestChallengeGrantStruct, SignupResponse, Token as TokenStruct } from '@stamhoofd/structures';
5
+
6
+ import { Context } from '../../helpers/Context';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type Body = RequestChallengeGrantStruct | ChallengeGrantStruct | RefreshTokenGrantStruct | PasswordTokenGrantStruct | PasswordGrantStruct;
11
+ type ResponseBody = TokenStruct;
12
+
13
+ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ protected bodyDecoder = CreateTokenStruct;
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, "/oauth/token", {});
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
+ // TODO: add some extra brute force measurements here
31
+ // - add random delay here, increased by the amount of failed attempts (used to slow down). Also on a successfull comparison!
32
+ // - add required CAPTCHA after x failed attempts for a given username (no matter if the username exists or not)
33
+ // - if, even after the CAPTCHAs, the account reaches a given count of failed attempts, the account should be locked out for an hour or even a day (only login endpoint)
34
+ // - check if not multiple attempts for the same username are started in parallel
35
+ // - Limit the amount of failed attemps by IP (will only make it a bit harder)
36
+ // - Detect attacks on random accounts (using email list + most used passwords) and temorary require CAPTCHA on all accounts
37
+ const organization = await Context.setOptionalOrganizationScope()
38
+
39
+ switch (request.body.grantType) {
40
+ case "refresh_token": {
41
+ const oldToken = await Token.getByRefreshToken(request.body.refreshToken)
42
+ if (!oldToken) {
43
+ throw new SimpleError({
44
+ code: "invalid_refresh_token",
45
+ message: "Invalid refresh token",
46
+ statusCode: 400
47
+ });
48
+ }
49
+
50
+ if (oldToken.user.organizationId !== null && oldToken.user.organizationId !== (organization?.id ?? null)) {
51
+ // Invalid scope
52
+ throw new SimpleError({
53
+ code: "invalid_refresh_token",
54
+ message: "Invalid refresh token",
55
+ statusCode: 400
56
+ });
57
+ }
58
+
59
+ // Important to create a new token before adjusting the old token
60
+ const token = await Token.createToken(oldToken.user);
61
+
62
+ // In the rare event our response doesn't reach the client anymore, we don't want the client to sign out...
63
+ // So we give them a second chance and create a new token BUT we expire our existing token in an hour (forever!)
64
+ oldToken.refreshTokenValidUntil = new Date(Date.now() + 60*60*1000)
65
+ oldToken.accessTokenValidUntil = new Date(Date.now() - 60 * 60 * 1000)
66
+
67
+ // Do not delete the old one, only expire it fast so it will get deleted in the future
68
+ await oldToken.save();
69
+
70
+ if (!token) {
71
+ throw new SimpleError({
72
+ code: "error",
73
+ message: "Could not generate token",
74
+ human: "Er ging iets mis bij het aanmelden",
75
+ statusCode: 500
76
+ });
77
+ }
78
+
79
+ const st = new TokenStruct(token);
80
+ return new Response(st);
81
+ }
82
+
83
+ case "password": {
84
+ // Increase timout for legacy
85
+ request.request.request?.setTimeout(30 * 1000);
86
+ const user = await User.login(organization?.id ?? null, request.body.username, request.body.password)
87
+
88
+ const errBody = {
89
+ code: "invalid_username_or_password",
90
+ message: "Invalid username or password",
91
+ human: "Foutief wachtwoord of onbekend emailadres",
92
+ statusCode: 400
93
+ };
94
+
95
+ if (!user) {
96
+ // TODO: increase counter
97
+ throw new SimpleError(errBody);
98
+ }
99
+
100
+ // Yay! Valid password
101
+ // Now check if e-mail is already validated
102
+ // if not: throw a validation error (e-mail validation is required)
103
+ if (!user.verified) {
104
+ const code = await EmailVerificationCode.createFor(user, user.email)
105
+ code.send(user, organization, request.i18n)
106
+
107
+ throw new SimpleError({
108
+ code: "verify_email",
109
+ message: "Your email address needs verification",
110
+ human: "Jouw e-mailadres is nog niet geverifieerd. Verifieer jouw e-mailadres via de link in de e-mail.",
111
+ meta: SignupResponse.create({
112
+ token: code.token
113
+ }).encode({ version: request.request.getVersion() }),
114
+ statusCode: 403
115
+ });
116
+ }
117
+
118
+ const token = await Token.createToken(user);
119
+
120
+ if (!token) {
121
+ throw new SimpleError({
122
+ code: "error",
123
+ message: "Could not generate token",
124
+ human: "Er ging iets mis bij het aanmelden",
125
+ statusCode: 500
126
+ });
127
+ }
128
+
129
+ const st = new TokenStruct(token);
130
+ return new Response(st);
131
+ }
132
+
133
+ case "password_token": {
134
+ const passwordToken = await PasswordToken.getToken(request.body.token)
135
+ if (!passwordToken) {
136
+ throw new SimpleError({
137
+ code: "invalid_token",
138
+ message: "Invalid token",
139
+ human: "Deze link is ongeldig of is al vervallen. Je zal nogmaals een e-mail moeten versturen om je wachtwoord te herstellen.",
140
+ statusCode: 400
141
+ });
142
+ }
143
+
144
+ // Check scope
145
+ if (organization && passwordToken.user.organizationId && passwordToken.user.organizationId != organization.id) {
146
+ // user of a different organization
147
+ throw new SimpleError({
148
+ code: "invalid_token",
149
+ message: "Invalid token",
150
+ human: "Deze link is ongeldig of is al vervallen. Je zal nogmaals een e-mail moeten versturen om je wachtwoord te herstellen.",
151
+ statusCode: 400
152
+ });
153
+ }
154
+
155
+ if (!organization && passwordToken.user.organizationId) {
156
+ // User is scoped to a single organization, while the request is not
157
+ throw new SimpleError({
158
+ code: "invalid_token",
159
+ message: "Invalid token",
160
+ human: "Deze link is ongeldig of is al vervallen. Je zal nogmaals een e-mail moeten versturen om je wachtwoord te herstellen.",
161
+ statusCode: 400
162
+ });
163
+ }
164
+
165
+ // Important to create a new token before adjusting the old token
166
+ const token = await Token.createToken(passwordToken.user);
167
+
168
+ // TODO: make token short lived until renewal
169
+
170
+ if (!token) {
171
+ throw new SimpleError({
172
+ code: "error",
173
+ message: "Could not generate token",
174
+ human: "Er ging iets mis bij het inloggen",
175
+ statusCode: 500
176
+ });
177
+ }
178
+
179
+ // For now we keep the password token because the user might want to reload the page or load it on a different device/browser
180
+ //await passwordToken.delete();
181
+
182
+ // Verify this email address, since the user can't change its email address without being verified
183
+ if (!token.user.verified) {
184
+ token.user.verified = true
185
+ await token.user.save()
186
+ }
187
+
188
+ const st = new TokenStruct(token);
189
+ return new Response(st);
190
+ }
191
+
192
+ default: {
193
+ // t should always be 'never' so we get no compiler error when this compiles
194
+ // if you get a compiler error here, you missed a possible value for grantType
195
+ throw new Error("Grant type " + request.body.grantType + " not supported");
196
+ }
197
+ }
198
+
199
+ }
200
+ }
@@ -0,0 +1,31 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+
3
+ import { Context } from '../../helpers/Context';
4
+
5
+ type Params = Record<string, never>;
6
+ type Query = undefined;
7
+ type Body = undefined;
8
+ type ResponseBody = undefined;
9
+
10
+ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
11
+ protected doesMatch(request: Request): [true, Params] | [false] {
12
+ if (request.method != "DELETE") {
13
+ return [false];
14
+ }
15
+
16
+ const params = Endpoint.parseParameters(request.url, "/oauth/token", {});
17
+
18
+ if (params) {
19
+ return [true, params as Params];
20
+ }
21
+ return [false];
22
+ }
23
+
24
+ async handle(_: DecodedRequest<Params, Query, Body>) {
25
+ await Context.setOptionalOrganizationScope()
26
+ const {token} = await Context.authenticate({allowWithoutAccount: true})
27
+ await token.delete()
28
+
29
+ return new Response(undefined)
30
+ }
31
+ }
@@ -0,0 +1,70 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { Email } from '@stamhoofd/email';
4
+ import { PasswordToken, User } from '@stamhoofd/models';
5
+ import { ForgotPasswordRequest } from '@stamhoofd/structures';
6
+
7
+ import { Context } from '../../helpers/Context';
8
+
9
+ // eslint-disable-next-line @typescript-eslint/ban-types
10
+ type Params = Record<string, never>;
11
+ type Query = undefined;
12
+ type Body = ForgotPasswordRequest;
13
+ type ResponseBody = undefined;
14
+
15
+ export class ForgotPasswordEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
+ protected bodyDecoder = ForgotPasswordRequest as Decoder<ForgotPasswordRequest>;
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, "/forgot-password", {});
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
+ // for now we care more about UX, so we show a mesage if the user doesn't exist
33
+ const organization = await Context.setOptionalOrganizationScope()
34
+ const user = await User.getForAuthentication(organization?.id ?? null, request.body.email, {allowWithoutAccount: true});
35
+
36
+ const { from, replyTo } = {
37
+ from: organization ? organization.getStrongEmail(request.i18n) : Email.getInternalEmailFor(request.i18n),
38
+ replyTo: undefined
39
+ }
40
+ const name = organization ? organization.name : request.i18n.t("shared.platformName");
41
+
42
+ if (!user) {
43
+ // Send email
44
+ Email.send({
45
+ from,
46
+ replyTo,
47
+ to: request.body.email,
48
+ subject: "["+name+"] Wachtwoord vergeten",
49
+ type: "transactional",
50
+ text: "Hallo, \n\nJe gaf aan dat je jouw wachtwoord bent vergeten, maar er bestaat geen account op het e-mailadres dat je hebt ingegeven ("+request.body.email+"). Niet zeker meer welk e-mailadres je kan gebruiken? Wij sturen altijd e-mails naar een e-mailadres waarop je een account hebt. Lukt dat niet? Dan moet je je eerst registreren.\n\nMet vriendelijke groeten,\n"+(name),
51
+ });
52
+
53
+ return new Response(undefined)
54
+ }
55
+
56
+ const recoveryUrl = await PasswordToken.getPasswordRecoveryUrl(user, organization, request.i18n)
57
+
58
+ // Send email
59
+ Email.send({
60
+ from,
61
+ replyTo,
62
+ to: user.email,
63
+ subject: "Wachtwoord vergeten",
64
+ type: "transactional",
65
+ text: (user.firstName ? "Hey "+user.firstName : "Hey") + ", \n\nJe gaf aan dat je jouw wachtwoord bent vergeten. Je kan een nieuw wachtwoord instellen door op de volgende link te klikken of door deze te kopiëren in de URL-balk van je browser:\n"+recoveryUrl+"\n\nWachtwoord al teruggevonden of heb je helemaal niet aangeduid dat je je wachtwoord vergeten bent? Dan mag je deze e-mail gewoon negeren.\n\nMet vriendelijke groeten,\n"+(user.permissions ? request.i18n.t("shared.platformName") : name)
66
+ });
67
+
68
+ return new Response(undefined);
69
+ }
70
+ }
@@ -0,0 +1,64 @@
1
+ import { Request } from "@simonbackx/simple-endpoints";
2
+ import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
3
+ import { NewUser } from '@stamhoofd/structures';
4
+
5
+ import { testServer } from "../../../tests/helpers/TestServer";
6
+ import { GetUserEndpoint } from './GetUserEndpoint';
7
+
8
+
9
+ describe("Endpoint.GetUser", () => {
10
+ // Test endpoint
11
+ const endpoint = new GetUserEndpoint();
12
+
13
+ test("Request user details when signed in", async () => {
14
+ const organization = await new OrganizationFactory({}).create()
15
+ const user = await new UserFactory({ organization }).create()
16
+ const token = await Token.createToken(user)
17
+
18
+ const r = Request.buildJson("GET", "/v1/user", organization.getApiHost());
19
+ r.headers.authorization = "Bearer "+token.accessToken
20
+
21
+ const response = await testServer.test(endpoint, r);
22
+ expect(response.body).toBeDefined();
23
+
24
+ if (!(response.body instanceof NewUser)) {
25
+ throw new Error("Expected NewUser")
26
+ }
27
+
28
+ expect(response.body.id).toEqual(user.id)
29
+ });
30
+
31
+ test("Request user details when not signed in is not working", async () => {
32
+ const organization = await new OrganizationFactory({}).create()
33
+ const user = await new UserFactory({ organization }).create()
34
+ const token = await Token.createToken(user)
35
+
36
+ const r = Request.buildJson("GET", "/v1/user", organization.getApiHost());
37
+
38
+ await expect(testServer.test(endpoint, r)).rejects.toThrow(/missing/i)
39
+ });
40
+
41
+ test("Request user details with invalid token is not working", async () => {
42
+ const organization = await new OrganizationFactory({}).create()
43
+ const user = await new UserFactory({ organization }).create()
44
+ const token = await Token.createToken(user)
45
+
46
+ const r = Request.buildJson("GET", "/v1/user", organization.getApiHost());
47
+ r.headers.authorization = "Bearer " + token.accessToken+"d"
48
+
49
+ await expect(testServer.test(endpoint, r)).rejects.toThrow(/invalid/i)
50
+ });
51
+
52
+ test("Request user details with expired token is not working", async () => {
53
+ const organization = await new OrganizationFactory({}).create()
54
+ const user = await new UserFactory({ organization }).create()
55
+ const token = await Token.createExpiredToken(user)
56
+
57
+ const r = Request.buildJson("GET", "/v1/user", organization.getApiHost());
58
+ r.headers.authorization = "Bearer " + token.accessToken
59
+
60
+ await expect(testServer.test(endpoint, r)).rejects.toThrow(/expired/i)
61
+ });
62
+
63
+
64
+ });
@@ -0,0 +1,57 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { MyUser, User as UserStruct } from '@stamhoofd/structures';
3
+
4
+ import { Context } from '../../helpers/Context';
5
+
6
+ type Params = Record<string, never>;
7
+ type Query = undefined;
8
+ type Body = undefined;
9
+ type ResponseBody = UserStruct|MyUser;
10
+
11
+ export class GetUserEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
12
+
13
+ protected doesMatch(request: Request): [true, Params] | [false] {
14
+ if (request.method != "GET") {
15
+ return [false];
16
+ }
17
+
18
+ const params = Endpoint.parseParameters(request.url, "/user", {});
19
+
20
+ if (params) {
21
+ return [true, params as Params];
22
+ }
23
+ return [false];
24
+ }
25
+
26
+ async handle(request: DecodedRequest<Params, Query, Body>) {
27
+ await Context.setOptionalOrganizationScope()
28
+ const {user} = await Context.authenticate({allowWithoutAccount: true})
29
+
30
+ if (request.request.getVersion() < 243) {
31
+ // Password
32
+ const st = MyUser.create({
33
+ firstName: user.firstName,
34
+ lastName: user.lastName,
35
+ id: user.id,
36
+ organizationId: user.organizationId,
37
+ email: user.email,
38
+ verified: user.verified,
39
+ permissions: user.permissions,
40
+ hasAccount: user.hasAccount()
41
+ })
42
+ return new Response(st);
43
+ }
44
+
45
+ const st = UserStruct.create({
46
+ firstName: user.firstName,
47
+ lastName: user.lastName,
48
+ id: user.id,
49
+ organizationId: user.organizationId,
50
+ email: user.email,
51
+ verified: user.verified,
52
+ permissions: user.permissions,
53
+ hasAccount: user.hasAccount()
54
+ })
55
+ return new Response(st);
56
+ }
57
+ }
@@ -0,0 +1,90 @@
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 { Token, User } from '@stamhoofd/models';
5
+ import { ApiUser, PermissionLevel, 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<ApiUser>
12
+ type ResponseBody = ApiUser
13
+
14
+ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ bodyDecoder = ApiUser.patchType() as Decoder<AutoEncoderPatchType<ApiUser>>
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, "/api-keys/@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.setOrganizationScope();
32
+ const {user} = await Context.authenticate()
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 API-user te wijzigen")
46
+ }
47
+
48
+ editUser.firstName = request.body.name ?? editUser.name
49
+ editUser.lastName = null
50
+
51
+ if (request.body.permissions !== undefined && editUser.permissions) {
52
+ if (!await Context.auth.canAccessUser(editUser, PermissionLevel.Full)) {
53
+ throw new SimpleError({
54
+ code: "permission_denied",
55
+ message: "Je hebt geen rechten om de rechten van deze API-user te wijzigen"
56
+ })
57
+ }
58
+
59
+ if (request.body.permissions) {
60
+ if (organization) {
61
+ editUser.permissions = UserPermissions.limitedPatch(editUser.permissions, request.body.permissions, organization.id)
62
+
63
+ if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization)?.hasFullAccess())) {
64
+ throw new SimpleError({
65
+ code: "permission_denied",
66
+ message: "Je kan jezelf niet verwijderen als hoofdbeheerder"
67
+ })
68
+ }
69
+ } else {
70
+ if (editUser.permissions) {
71
+ editUser.permissions.patchOrPut(request.body.permissions)
72
+ } else {
73
+ editUser.permissions = request.body.permissions.isPut() ? request.body.permissions : null
74
+ }
75
+
76
+ if (editUser.id === user.id && !editUser.permissions?.platform?.hasFullAccess()) {
77
+ throw new SimpleError({
78
+ code: "permission_denied",
79
+ message: "Je kan jezelf niet verwijderen als hoofdbeheerder"
80
+ })
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ await editUser.save();
87
+
88
+ return new Response(await Token.getAPIUserWithToken(editUser));
89
+ }
90
+ }