-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Configure jwt token with Joken library * Wip on pow auth flow refactoring for jwt * Remove all Pow browser session routes * wip on me endpoint * Me endpoint in session controller * Add OpenApi docs to the session controller * Add Bearer security scheme on main openapi spec * Fix login action endpoint openapi spex * Moved Jwt related authorization to the web module * Configure expiration time of jwt access token * Login endpoint returns access token expiration in seconds * User id as sub claim in access token jwt * Add typ set to Bearer for access token jwt * Refresh token flow in SessionController * Add tests for AccessToken module * Fix AuthenticateAPIKeyPlug test * Add tests for RefreshToken * Add tests for JWTAuthPlug * SessionController tests * User slice state with Login Saga * Login component performs login against api * Refresh token flow in SPA * Add tests for login component * Auth Guard react router component * Upgraded Cypress to version 12. E2E tests with jwt authentication refactored The new tests follows the guideline/directory structure of cypress 12. The use of the new session api allows to integration and testing of the jwt authentication across all the screens. * Host Details exporter status fetching use proper network client * Implemented request_path behavior for redirect to the previous path when session expires * Disabled jwt authentication in testing env * Fix code misspellings * Misc code style refactors * Fix formatting issues * Refactored cypress accept eula procedure to use the direct API * Increase timeout on cypress e2e tests for clusters overview * Address review suggestions about the jwt authentication flow * Removed old pow session login eex template * Address review testing suggestion of the jwt flow * Address frontend comments from review * Addressing review comments about e2e cypress tests * Moved logout function outside the Layout component scope * Removed old trento frontend paths
- Loading branch information
Showing
62 changed files
with
7,637 additions
and
1,179 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import React, { useState, useEffect } from 'react'; | ||
import { useDispatch, useSelector } from 'react-redux'; | ||
import { Outlet, useNavigate } from 'react-router-dom'; | ||
import { setUserAsLogged } from '@state/user'; | ||
import { clearCredentialsFromStore } from '@lib/auth'; | ||
|
||
export default function Guard({ redirectPath, getUser }) { | ||
const [user, setUser] = useState(null); | ||
const [userLoading, setUserLoading] = useState(true); | ||
const dispatch = useDispatch(); | ||
const { loggedIn } = useSelector((state) => state.user); | ||
const navigate = useNavigate(); | ||
|
||
useEffect(() => { | ||
getUser() | ||
.then((trentoUser) => { | ||
setUser(trentoUser); | ||
setUserLoading(false); | ||
// If the user in the store is already loggedIn, means | ||
// that the store is hydrated so the guard is triggered on the spa full loaded | ||
// no dispatching of logged in action needed | ||
if (!loggedIn) { | ||
dispatch(setUserAsLogged()); | ||
} | ||
}) | ||
.catch(() => { | ||
setUserLoading(false); | ||
clearCredentialsFromStore(); | ||
}); | ||
}, [loggedIn]); | ||
|
||
useEffect(() => { | ||
if (!userLoading && !user) { | ||
const currentLocationPath = new URLSearchParams(); | ||
currentLocationPath.append('request_path', window.location.pathname); | ||
navigate(`${redirectPath}?${currentLocationPath.toString()}`, { | ||
replace: true, | ||
}); | ||
} | ||
}, [userLoading, user]); | ||
|
||
if (user) { | ||
return <Outlet />; | ||
} | ||
|
||
return null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import React from 'react'; | ||
|
||
import { screen, waitFor } from '@testing-library/react'; | ||
import 'intersection-observer'; | ||
import '@testing-library/jest-dom'; | ||
import { act } from 'react-dom/test-utils'; | ||
import { Route, Routes } from 'react-router-dom'; | ||
import { renderWithRouter, withState } from '@lib/test-utils'; | ||
import Guard from './Guard'; | ||
|
||
describe('Guard component', () => { | ||
it('should redirect to the redirectPath prop plus the current pathname if present when the user cannot be loaded', async () => { | ||
const badGetUser = () => Promise.reject(Error('the reason is you')); | ||
const [StatefulGuard] = withState( | ||
<Routes> | ||
<Route | ||
element={<Guard redirectPath="/session/new" getUser={badGetUser} />} | ||
> | ||
<Route | ||
path="/" | ||
element={<div data-testid="inner-component"> test </div>} | ||
/> | ||
</Route> | ||
</Routes>, | ||
{ | ||
user: { | ||
loggedIn: false, | ||
}, | ||
} | ||
); | ||
|
||
renderWithRouter(StatefulGuard, '/asd'); | ||
|
||
await act(() => {}); | ||
|
||
expect(window.location.pathname).toEqual('/session/new'); | ||
expect(window.location.search).toEqual('?request_path=%2F'); | ||
}); | ||
|
||
it('should redirect to the redirectPath prop when the user cannot be loaded', async () => { | ||
const badGetUser = () => Promise.reject(Error('the reason is you')); | ||
const [StatefulGuard] = withState( | ||
<Routes> | ||
<Route | ||
element={<Guard redirectPath="/session/new" getUser={badGetUser} />} | ||
> | ||
<Route | ||
path="/" | ||
element={<div data-testid="inner-component"> test </div>} | ||
/> | ||
</Route> | ||
</Routes>, | ||
{ | ||
user: { | ||
loggedIn: false, | ||
}, | ||
} | ||
); | ||
|
||
renderWithRouter(StatefulGuard); | ||
|
||
await act(() => {}); | ||
|
||
expect(window.location.pathname).toEqual('/session/new'); | ||
}); | ||
|
||
it('should render the outlet when the user can be loaded', async () => { | ||
const goodGetUser = () => Promise.resolve({ username: 'admin' }); | ||
const [StatefulGuard] = withState( | ||
<Routes> | ||
<Route | ||
element={<Guard redirectPath="/session/new" getUser={goodGetUser} />} | ||
> | ||
<Route | ||
path="/" | ||
element={<div data-testid="inner-component"> test </div>} | ||
/> | ||
</Route> | ||
</Routes>, | ||
{ | ||
user: { | ||
loggedIn: false, | ||
}, | ||
} | ||
); | ||
|
||
renderWithRouter(StatefulGuard); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByTestId('inner-component')).toBeDefined(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
/* eslint-disable jsx-a11y/label-has-associated-control */ | ||
import React, { useState, useEffect } from 'react'; | ||
import TrentoLogo from '@static/trento.svg'; | ||
import { useDispatch, useSelector } from 'react-redux'; | ||
import { toast } from 'react-hot-toast'; | ||
import { useNavigate, useSearchParams } from 'react-router-dom'; | ||
import { performLogin } from '@state/actions/auth'; | ||
|
||
export default function Login() { | ||
const [username, setUsername] = useState(''); | ||
const [password, setPassword] = useState(''); | ||
const { authError, authInProgress, loggedIn } = useSelector( | ||
(state) => state.user | ||
); | ||
const dispatch = useDispatch(); | ||
const navigate = useNavigate(); | ||
const [searchParams] = useSearchParams(); | ||
|
||
useEffect(() => { | ||
if (authError) { | ||
toast.error('An error occurred during login, try again'); | ||
} | ||
}, [authError]); | ||
|
||
useEffect(() => { | ||
if (loggedIn) { | ||
const destinationURL = searchParams.get('request_path'); | ||
navigate(destinationURL || '/'); | ||
} | ||
}, [loggedIn]); | ||
|
||
const handleLoginSubmit = (e) => { | ||
e.preventDefault(); | ||
dispatch(performLogin({ username, password })); | ||
}; | ||
|
||
return ( | ||
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8"> | ||
<div className="sm:mx-auto sm:w-full sm:max-w-md"> | ||
<img className="mx-auto h-12 w-auto" src={TrentoLogo} alt="Trento" /> | ||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900"> | ||
Login to Trento | ||
</h2> | ||
</div> | ||
|
||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> | ||
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10"> | ||
<form className="space-y-6" onSubmit={handleLoginSubmit}> | ||
<div> | ||
<label | ||
htmlFor="username" | ||
className="block text-sm font-medium text-gray-700" | ||
> | ||
Username | ||
</label> | ||
<div className="mt-1"> | ||
<input | ||
id="username" | ||
type="text" | ||
data-testid="login-username" | ||
disabled={authInProgress} | ||
value={username} | ||
onChange={(e) => setUsername(e.target.value)} | ||
name="username" | ||
autoComplete="username" | ||
required | ||
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-jungle-green-500 focus:border-jungle-green-500 sm:text-sm" | ||
/> | ||
</div> | ||
</div> | ||
|
||
<div> | ||
<label | ||
htmlFor="password" | ||
className="block text-sm font-medium text-gray-700" | ||
> | ||
Password | ||
</label> | ||
<div className="mt-1"> | ||
<input | ||
id="password" | ||
type="password" | ||
data-testid="login-password" | ||
disabled={authInProgress} | ||
value={password} | ||
onChange={(e) => setPassword(e.target.value)} | ||
autoComplete="current-password" | ||
required | ||
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-jungle-green-500 focus:border-jungle-green-500 sm:text-sm" | ||
/> | ||
</div> | ||
</div> | ||
<div> | ||
<button | ||
type="submit" | ||
disabled={authInProgress} | ||
data-testid="login-submit" | ||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-jungle-green-500 hover:opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-jungle-green-500" | ||
> | ||
Login | ||
</button> | ||
</div> | ||
</form> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.