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