mirror of
https://github.com/pawelmalak/flame.git
synced 2025-07-19 11:39:36 +02:00
Moved auth form. Added auto login and logout functionality
This commit is contained in:
parent
1571981252
commit
d1c61bb393
18 changed files with 311 additions and 98 deletions
5
client/package-lock.json
generated
5
client/package-lock.json
generated
|
@ -9876,6 +9876,11 @@
|
||||||
"object.assign": "^4.1.2"
|
"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": {
|
"killable": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"external-svg-loader": "^1.3.4",
|
"external-svg-loader": "^1.3.4",
|
||||||
"http-proxy-middleware": "^2.0.1",
|
"http-proxy-middleware": "^2.0.1",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-autosuggest": "^10.1.0",
|
"react-autosuggest": "^10.1.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { actionCreators } from './store';
|
||||||
import 'external-svg-loader';
|
import 'external-svg-loader';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { checkVersion } from './utility';
|
import { checkVersion, decodeToken } from './utility';
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
import { Home } from './components/Home/Home';
|
import { Home } from './components/Home/Home';
|
||||||
|
@ -15,23 +15,54 @@ import { useDispatch } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
const App = (): JSX.Element => {
|
export const App = (): JSX.Element => {
|
||||||
const dispath = useDispatch();
|
const dispath = useDispatch();
|
||||||
const { fetchQueries, getConfig, setTheme } = bindActionCreators(
|
const {
|
||||||
actionCreators,
|
fetchQueries,
|
||||||
dispath
|
getConfig,
|
||||||
);
|
setTheme,
|
||||||
|
logout,
|
||||||
|
createNotification,
|
||||||
|
autoLogin,
|
||||||
|
} = bindActionCreators(actionCreators, dispath);
|
||||||
|
|
||||||
useEffect(() => {
|
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();
|
getConfig();
|
||||||
|
|
||||||
|
// set theme
|
||||||
if (localStorage.theme) {
|
if (localStorage.theme) {
|
||||||
setTheme(localStorage.theme);
|
setTheme(localStorage.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check for updated
|
||||||
checkVersion();
|
checkVersion();
|
||||||
|
|
||||||
|
// load custom search queries
|
||||||
fetchQueries();
|
fetchQueries();
|
||||||
|
|
||||||
|
return () => window.clearInterval(tokenIsValid);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -48,5 +79,3 @@ const App = (): JSX.Element => {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
13
client/src/components/Routing/ProtectedRoute.tsx
Normal file
13
client/src/components/Routing/ProtectedRoute.tsx
Normal file
|
@ -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 <Route {...rest} />;
|
||||||
|
} else {
|
||||||
|
return <Redirect to="/settings/app" />;
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,71 +1,14 @@
|
||||||
import { FormEvent, Fragment, useState } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import { Button, SettingsHeadline } from '../../UI';
|
||||||
// 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 classes from './AppDetails.module.css';
|
import classes from './AppDetails.module.css';
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { checkVersion } from '../../../utility';
|
import { checkVersion } from '../../../utility';
|
||||||
|
import { AuthForm } from './AuthForm/AuthForm';
|
||||||
|
|
||||||
export const AppDetails = (): JSX.Element => {
|
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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SettingsHeadline text="Authentication" />
|
<SettingsHeadline text="Authentication" />
|
||||||
{!isAuthenticated ? (
|
<AuthForm />
|
||||||
<form onSubmit={formHandler}>
|
|
||||||
<InputGroup>
|
|
||||||
<label htmlFor="password">Password</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
placeholder="••••••"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
See
|
|
||||||
<a
|
|
||||||
href="https://github.com/pawelmalak/flame/wiki/Authentication"
|
|
||||||
target="blank"
|
|
||||||
>
|
|
||||||
{` project wiki `}
|
|
||||||
</a>
|
|
||||||
to read more about authentication
|
|
||||||
</span>
|
|
||||||
</InputGroup>
|
|
||||||
|
|
||||||
<Button>Login</Button>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<p className={classes.text}>
|
|
||||||
You are logged in. Your session will expire <span>@@@@</span>
|
|
||||||
</p>
|
|
||||||
<Button click={logout}>Logout</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<hr className={classes.separator} />
|
<hr className={classes.separator} />
|
||||||
|
|
||||||
|
|
104
client/src/components/Settings/AppDetails/AuthForm/AuthForm.tsx
Normal file
104
client/src/components/Settings/AppDetails/AuthForm/AuthForm.tsx
Normal file
|
@ -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 (
|
||||||
|
<Fragment>
|
||||||
|
{!isAuthenticated ? (
|
||||||
|
<form onSubmit={formHandler}>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="password">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="••••••"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, password: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
See
|
||||||
|
<a
|
||||||
|
href="https://github.com/pawelmalak/flame/wiki/Authentication"
|
||||||
|
target="blank"
|
||||||
|
>
|
||||||
|
{` project wiki `}
|
||||||
|
</a>
|
||||||
|
to read more about authentication
|
||||||
|
</span>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="duration">Session duration</label>
|
||||||
|
<select
|
||||||
|
id="duration"
|
||||||
|
name="duration"
|
||||||
|
value={formData.duration}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, duration: e.target.value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="5s">dev: 5 seconds</option>
|
||||||
|
<option value="10s">dev: 10 seconds</option>
|
||||||
|
<option value="1h">1 hour</option>
|
||||||
|
<option value="1d">1 day</option>
|
||||||
|
<option value="14d">2 weeks</option>
|
||||||
|
<option value="30d">1 month</option>
|
||||||
|
<option value="1y">1 year</option>
|
||||||
|
</select>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<Button>Login</Button>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<p className={classes.text}>
|
||||||
|
You are logged in. Your session will expire{' '}
|
||||||
|
<span>{tokenExpires}</span>
|
||||||
|
</p>
|
||||||
|
<Button click={logout}>Logout</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +1,9 @@
|
||||||
import { NavLink, Link, Switch, Route } from 'react-router-dom';
|
import { NavLink, Link, Switch, Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Redux
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { State } from '../../store/reducers';
|
||||||
|
|
||||||
// Typescript
|
// Typescript
|
||||||
import { Route as SettingsRoute } from '../../interfaces';
|
import { Route as SettingsRoute } from '../../interfaces';
|
||||||
|
|
||||||
|
@ -14,6 +18,7 @@ import { AppDetails } from './AppDetails/AppDetails';
|
||||||
import { StyleSettings } from './StyleSettings/StyleSettings';
|
import { StyleSettings } from './StyleSettings/StyleSettings';
|
||||||
import { SearchSettings } from './SearchSettings/SearchSettings';
|
import { SearchSettings } from './SearchSettings/SearchSettings';
|
||||||
import { DockerSettings } from './DockerSettings/DockerSettings';
|
import { DockerSettings } from './DockerSettings/DockerSettings';
|
||||||
|
import { ProtectedRoute } from '../Routing/ProtectedRoute';
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
import { Container, Headline } from '../UI';
|
import { Container, Headline } from '../UI';
|
||||||
|
@ -22,13 +27,17 @@ import { Container, Headline } from '../UI';
|
||||||
import { routes } from './settings.json';
|
import { routes } from './settings.json';
|
||||||
|
|
||||||
export const Settings = (): JSX.Element => {
|
export const Settings = (): JSX.Element => {
|
||||||
|
const { isAuthenticated } = useSelector((state: State) => state.auth);
|
||||||
|
|
||||||
|
const tabs = isAuthenticated ? routes : routes.filter((r) => !r.authRequired);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Headline title="Settings" subtitle={<Link to="/">Go back</Link>} />
|
<Headline title="Settings" subtitle={<Link to="/">Go back</Link>} />
|
||||||
<div className={classes.Settings}>
|
<div className={classes.Settings}>
|
||||||
{/* NAVIGATION MENU */}
|
{/* NAVIGATION MENU */}
|
||||||
<nav className={classes.SettingsNav}>
|
<nav className={classes.SettingsNav}>
|
||||||
{routes.map(({ name, dest }: SettingsRoute, idx) => (
|
{tabs.map(({ name, dest }: SettingsRoute, idx) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
className={classes.SettingsNavLink}
|
className={classes.SettingsNavLink}
|
||||||
activeClassName={classes.SettingsNavLinkActive}
|
activeClassName={classes.SettingsNavLinkActive}
|
||||||
|
@ -45,11 +54,20 @@ export const Settings = (): JSX.Element => {
|
||||||
<section className={classes.SettingsContent}>
|
<section className={classes.SettingsContent}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/settings" component={Themer} />
|
<Route exact path="/settings" component={Themer} />
|
||||||
<Route path="/settings/weather" component={WeatherSettings} />
|
<ProtectedRoute
|
||||||
<Route path="/settings/search" component={SearchSettings} />
|
path="/settings/weather"
|
||||||
<Route path="/settings/interface" component={UISettings} />
|
component={WeatherSettings}
|
||||||
<Route path="/settings/docker" component={DockerSettings} />
|
/>
|
||||||
<Route path="/settings/css" component={StyleSettings} />
|
<ProtectedRoute
|
||||||
|
path="/settings/search"
|
||||||
|
component={SearchSettings}
|
||||||
|
/>
|
||||||
|
<ProtectedRoute path="/settings/interface" component={UISettings} />
|
||||||
|
<ProtectedRoute
|
||||||
|
path="/settings/docker"
|
||||||
|
component={DockerSettings}
|
||||||
|
/>
|
||||||
|
<ProtectedRoute path="/settings/css" component={StyleSettings} />
|
||||||
<Route path="/settings/app" component={AppDetails} />
|
<Route path="/settings/app" component={AppDetails} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -2,31 +2,38 @@
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"name": "Theme",
|
"name": "Theme",
|
||||||
"dest": "/settings"
|
"dest": "/settings",
|
||||||
|
"authRequired": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Weather",
|
"name": "Weather",
|
||||||
"dest": "/settings/weather"
|
"dest": "/settings/weather",
|
||||||
|
"authRequired": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Search",
|
"name": "Search",
|
||||||
"dest": "/settings/search"
|
"dest": "/settings/search",
|
||||||
|
"authRequired": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Interface",
|
"name": "Interface",
|
||||||
"dest": "/settings/interface"
|
"dest": "/settings/interface",
|
||||||
|
"authRequired": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Docker",
|
"name": "Docker",
|
||||||
"dest": "/settings/docker"
|
"dest": "/settings/docker",
|
||||||
|
"authRequired": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CSS",
|
"name": "CSS",
|
||||||
"dest": "/settings/css"
|
"dest": "/settings/css",
|
||||||
|
"authRequired": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "App",
|
"name": "App",
|
||||||
"dest": "/settings/app"
|
"dest": "/settings/app",
|
||||||
|
"authRequired": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import './index.css';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { store } from './store/store';
|
import { store } from './store/store';
|
||||||
|
|
||||||
import App from './App';
|
import { App } from './App';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|
|
@ -7,4 +7,10 @@ export interface Model {
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
app: string;
|
||||||
|
exp: number;
|
||||||
|
iat: number;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export interface Route {
|
export interface Route {
|
||||||
name: string;
|
name: string;
|
||||||
dest: string;
|
dest: string;
|
||||||
|
authRequired: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { ApiResponse } from '../../interfaces';
|
import { ApiResponse } from '../../interfaces';
|
||||||
import { ActionType } from '../action-types';
|
import { ActionType } from '../action-types';
|
||||||
import { LoginAction, LogoutAction } from '../actions/auth';
|
import {
|
||||||
|
AuthErrorAction,
|
||||||
|
AutoLoginAction,
|
||||||
|
LoginAction,
|
||||||
|
LogoutAction,
|
||||||
|
} from '../actions/auth';
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
|
|
||||||
export const login =
|
export const login =
|
||||||
(password: string) => async (dispatch: Dispatch<LoginAction>) => {
|
(formData: { password: string; duration: string }) =>
|
||||||
|
async (dispatch: Dispatch<LoginAction>) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.post<ApiResponse<{ token: string }>>(
|
const res = await axios.post<ApiResponse<{ token: string }>>(
|
||||||
'/api/auth',
|
'/api/auth',
|
||||||
{ password }
|
formData
|
||||||
);
|
);
|
||||||
|
|
||||||
localStorage.setItem('token', res.data.data.token);
|
localStorage.setItem('token', res.data.data.token);
|
||||||
|
@ -19,15 +25,7 @@ export const login =
|
||||||
payload: res.data.data.token,
|
payload: res.data.data.token,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const apiError = err as AxiosError;
|
dispatch<any>(authError(err, true));
|
||||||
|
|
||||||
dispatch<any>({
|
|
||||||
type: ActionType.createNotification,
|
|
||||||
payload: {
|
|
||||||
title: 'Error',
|
|
||||||
message: apiError.response?.data.error,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,3 +36,37 @@ export const logout = () => (dispatch: Dispatch<LogoutAction>) => {
|
||||||
type: ActionType.logout,
|
type: ActionType.logout,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const autoLogin = () => async (dispatch: Dispatch<AutoLoginAction>) => {
|
||||||
|
const token: string = localStorage.token;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post<ApiResponse<{ token: { isValid: boolean } }>>(
|
||||||
|
'/api/auth/validate',
|
||||||
|
{ token }
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.autoLogin,
|
||||||
|
payload: token,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
dispatch<any>(authError(err, false));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authError =
|
||||||
|
(error: unknown, showNotification: boolean) =>
|
||||||
|
(dispatch: Dispatch<AuthErrorAction>) => {
|
||||||
|
const apiError = error as AxiosError;
|
||||||
|
|
||||||
|
if (showNotification) {
|
||||||
|
dispatch<any>({
|
||||||
|
type: ActionType.createNotification,
|
||||||
|
payload: {
|
||||||
|
title: 'Error',
|
||||||
|
message: apiError.response?.data.error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -40,4 +40,6 @@ export enum ActionType {
|
||||||
// AUTH
|
// AUTH
|
||||||
login = 'LOGIN',
|
login = 'LOGIN',
|
||||||
logout = 'LOGOUT',
|
logout = 'LOGOUT',
|
||||||
|
autoLogin = 'AUTO_LOGIN',
|
||||||
|
authError = 'AUTH_ERROR',
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,3 +8,12 @@ export interface LoginAction {
|
||||||
export interface LogoutAction {
|
export interface LogoutAction {
|
||||||
type: ActionType.logout;
|
type: ActionType.logout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AutoLoginAction {
|
||||||
|
type: ActionType.autoLogin;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthErrorAction {
|
||||||
|
type: ActionType.authError;
|
||||||
|
}
|
||||||
|
|
|
@ -39,7 +39,12 @@ import {
|
||||||
UpdateBookmarkAction,
|
UpdateBookmarkAction,
|
||||||
} from './bookmark';
|
} from './bookmark';
|
||||||
|
|
||||||
import { LoginAction, LogoutAction } from './auth';
|
import {
|
||||||
|
AuthErrorAction,
|
||||||
|
AutoLoginAction,
|
||||||
|
LoginAction,
|
||||||
|
LogoutAction,
|
||||||
|
} from './auth';
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
// Theme
|
// Theme
|
||||||
|
@ -76,4 +81,6 @@ export type Action =
|
||||||
| UpdateBookmarkAction
|
| UpdateBookmarkAction
|
||||||
// Auth
|
// Auth
|
||||||
| LoginAction
|
| LoginAction
|
||||||
| LogoutAction;
|
| LogoutAction
|
||||||
|
| AutoLoginAction
|
||||||
|
| AuthErrorAction;
|
||||||
|
|
|
@ -28,6 +28,18 @@ export const authReducer = (
|
||||||
token: null,
|
token: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
};
|
};
|
||||||
|
case ActionType.autoLogin:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
token: action.payload,
|
||||||
|
isAuthenticated: true,
|
||||||
|
};
|
||||||
|
case ActionType.authError:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
23
client/src/utility/decodeToken.ts
Normal file
23
client/src/utility/decodeToken.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import jwtDecode from 'jwt-decode';
|
||||||
|
import { parseTime } from '.';
|
||||||
|
import { Token } from '../interfaces';
|
||||||
|
|
||||||
|
export const decodeToken = (token: string): Token => {
|
||||||
|
const decoded = jwtDecode(token) as Token;
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseTokenExpire = (expiresIn: number): string => {
|
||||||
|
const d = new Date(expiresIn * 1000);
|
||||||
|
const p = parseTime;
|
||||||
|
|
||||||
|
const useAmericanDate = localStorage.useAmericanDate === 'true';
|
||||||
|
const time = `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
||||||
|
|
||||||
|
if (useAmericanDate) {
|
||||||
|
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()} ${time}`;
|
||||||
|
} else {
|
||||||
|
return `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()} ${time}`;
|
||||||
|
}
|
||||||
|
};
|
|
@ -9,3 +9,4 @@ export * from './inputHandler';
|
||||||
export * from './storeUIConfig';
|
export * from './storeUIConfig';
|
||||||
export * from './validators';
|
export * from './validators';
|
||||||
export * from './parseTime';
|
export * from './parseTime';
|
||||||
|
export * from './decodeToken';
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue