From d1c61bb393bad29d4129bd6b8804a3f09b36785a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 11 Nov 2021 14:45:58 +0100 Subject: [PATCH] Moved auth form. Added auto login and logout functionality --- client/package-lock.json | 5 + client/package.json | 1 + client/src/App.tsx | 45 ++++++-- .../src/components/Routing/ProtectedRoute.tsx | 13 +++ .../Settings/AppDetails/AppDetails.tsx | 65 +---------- .../Settings/AppDetails/AuthForm/AuthForm.tsx | 104 ++++++++++++++++++ client/src/components/Settings/Settings.tsx | 30 ++++- client/src/components/Settings/settings.json | 21 ++-- client/src/index.tsx | 2 +- client/src/interfaces/Api.ts | 8 +- client/src/interfaces/Route.ts | 1 + client/src/store/action-creators/auth.ts | 56 ++++++++-- client/src/store/action-types/index.ts | 2 + client/src/store/actions/auth.ts | 9 ++ client/src/store/actions/index.ts | 11 +- client/src/store/reducers/auth.ts | 12 ++ client/src/utility/decodeToken.ts | 23 ++++ client/src/utility/index.ts | 1 + 18 files changed, 311 insertions(+), 98 deletions(-) create mode 100644 client/src/components/Routing/ProtectedRoute.tsx create mode 100644 client/src/components/Settings/AppDetails/AuthForm/AuthForm.tsx create mode 100644 client/src/utility/decodeToken.ts diff --git a/client/package-lock.json b/client/package-lock.json index 706f36a..fe99789 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9876,6 +9876,11 @@ "object.assign": "^4.1.2" } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", diff --git a/client/package.json b/client/package.json index 1c4a25d..7c57f0e 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,7 @@ "axios": "^0.24.0", "external-svg-loader": "^1.3.4", "http-proxy-middleware": "^2.0.1", + "jwt-decode": "^3.1.2", "react": "^17.0.2", "react-autosuggest": "^10.1.0", "react-beautiful-dnd": "^13.1.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index 28c0350..d7668a8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,7 +3,7 @@ import { actionCreators } from './store'; import 'external-svg-loader'; // Utils -import { checkVersion } from './utility'; +import { checkVersion, decodeToken } from './utility'; // Routes import { Home } from './components/Home/Home'; @@ -15,23 +15,54 @@ import { useDispatch } from 'react-redux'; import { bindActionCreators } from 'redux'; import { useEffect } from 'react'; -const App = (): JSX.Element => { +export const App = (): JSX.Element => { const dispath = useDispatch(); - const { fetchQueries, getConfig, setTheme } = bindActionCreators( - actionCreators, - dispath - ); + const { + fetchQueries, + getConfig, + setTheme, + logout, + createNotification, + autoLogin, + } = bindActionCreators(actionCreators, dispath); useEffect(() => { + // login if token exists + if (localStorage.token) { + autoLogin(); + } + + // check if token is valid + const tokenIsValid = setInterval(() => { + if (localStorage.token) { + const expiresIn = decodeToken(localStorage.token).exp * 1000; + const now = new Date().getTime(); + + if (now > expiresIn) { + logout(); + createNotification({ + title: 'Info', + message: 'Session expired. You have been logged out', + }); + } + } + }, 1000); + + // load app config getConfig(); + // set theme if (localStorage.theme) { setTheme(localStorage.theme); } + // check for updated checkVersion(); + // load custom search queries fetchQueries(); + + return () => window.clearInterval(tokenIsValid); }, []); return ( @@ -48,5 +79,3 @@ const App = (): JSX.Element => { ); }; - -export default App; diff --git a/client/src/components/Routing/ProtectedRoute.tsx b/client/src/components/Routing/ProtectedRoute.tsx new file mode 100644 index 0000000..45b6504 --- /dev/null +++ b/client/src/components/Routing/ProtectedRoute.tsx @@ -0,0 +1,13 @@ +import { useSelector } from 'react-redux'; +import { Redirect, Route, RouteProps } from 'react-router'; +import { State } from '../../store/reducers'; + +export const ProtectedRoute = ({ ...rest }: RouteProps) => { + const { isAuthenticated } = useSelector((state: State) => state.auth); + + if (isAuthenticated) { + return ; + } else { + return ; + } +}; diff --git a/client/src/components/Settings/AppDetails/AppDetails.tsx b/client/src/components/Settings/AppDetails/AppDetails.tsx index 420b95c..1829a4d 100644 --- a/client/src/components/Settings/AppDetails/AppDetails.tsx +++ b/client/src/components/Settings/AppDetails/AppDetails.tsx @@ -1,71 +1,14 @@ -import { FormEvent, Fragment, useState } from 'react'; - -// Redux -import { useDispatch, useSelector } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { actionCreators } from '../../../store'; -import { State } from '../../../store/reducers'; - -// UI -import { Button, InputGroup, SettingsHeadline } from '../../UI'; - -// CSS +import { Fragment } from 'react'; +import { Button, SettingsHeadline } from '../../UI'; import classes from './AppDetails.module.css'; - -// Utils import { checkVersion } from '../../../utility'; +import { AuthForm } from './AuthForm/AuthForm'; export const AppDetails = (): JSX.Element => { - const { isAuthenticated } = useSelector((state: State) => state.auth); - - const dispatch = useDispatch(); - const { login, logout } = bindActionCreators(actionCreators, dispatch); - - const [password, setPassword] = useState(''); - - const formHandler = (e: FormEvent) => { - e.preventDefault(); - login(password); - setPassword(''); - }; - return ( - {!isAuthenticated ? ( -
- - - setPassword(e.target.value)} - /> - - See - - {` project wiki `} - - to read more about authentication - - - - -
- ) : ( -
-

- You are logged in. Your session will expire @@@@ -

- -
- )} +
diff --git a/client/src/components/Settings/AppDetails/AuthForm/AuthForm.tsx b/client/src/components/Settings/AppDetails/AuthForm/AuthForm.tsx new file mode 100644 index 0000000..3b54575 --- /dev/null +++ b/client/src/components/Settings/AppDetails/AuthForm/AuthForm.tsx @@ -0,0 +1,104 @@ +import { FormEvent, Fragment, useEffect, useState } from 'react'; + +// Redux +import { useSelector, useDispatch } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../../store'; +import { State } from '../../../../store/reducers'; +import { decodeToken, parseTokenExpire } from '../../../../utility'; + +// Other +import { InputGroup, Button } from '../../../UI'; +import classes from '../AppDetails.module.css'; + +export const AuthForm = (): JSX.Element => { + const { isAuthenticated, token } = useSelector((state: State) => state.auth); + + const dispatch = useDispatch(); + const { login, logout } = bindActionCreators(actionCreators, dispatch); + + const [tokenExpires, setTokenExpires] = useState(''); + const [formData, setFormData] = useState({ + password: '', + duration: '14d', + }); + + useEffect(() => { + if (token) { + const decoded = decodeToken(token); + const expiresIn = parseTokenExpire(decoded.exp); + setTokenExpires(expiresIn); + } + }, [token]); + + const formHandler = (e: FormEvent) => { + e.preventDefault(); + login(formData); + setFormData({ + password: '', + duration: '14d', + }); + }; + + return ( + + {!isAuthenticated ? ( +
+ + + + setFormData({ ...formData, password: e.target.value }) + } + /> + + See + + {` project wiki `} + + to read more about authentication + + + + + + + + + +
+ ) : ( +
+

+ You are logged in. Your session will expire{' '} + {tokenExpires} +

+ +
+ )} +
+ ); +}; diff --git a/client/src/components/Settings/Settings.tsx b/client/src/components/Settings/Settings.tsx index d506b4e..5196b6f 100644 --- a/client/src/components/Settings/Settings.tsx +++ b/client/src/components/Settings/Settings.tsx @@ -1,5 +1,9 @@ import { NavLink, Link, Switch, Route } from 'react-router-dom'; +// Redux +import { useSelector } from 'react-redux'; +import { State } from '../../store/reducers'; + // Typescript import { Route as SettingsRoute } from '../../interfaces'; @@ -14,6 +18,7 @@ import { AppDetails } from './AppDetails/AppDetails'; import { StyleSettings } from './StyleSettings/StyleSettings'; import { SearchSettings } from './SearchSettings/SearchSettings'; import { DockerSettings } from './DockerSettings/DockerSettings'; +import { ProtectedRoute } from '../Routing/ProtectedRoute'; // UI import { Container, Headline } from '../UI'; @@ -22,13 +27,17 @@ import { Container, Headline } from '../UI'; import { routes } from './settings.json'; export const Settings = (): JSX.Element => { + const { isAuthenticated } = useSelector((state: State) => state.auth); + + const tabs = isAuthenticated ? routes : routes.filter((r) => !r.authRequired); + return ( Go back} />
{/* NAVIGATION MENU */}