diff --git a/client/package-lock.json b/client/package-lock.json index babf4067dae1b9bd485c7a0a01acf045b78fb52f..e38069bc1c6ca8f8bf203c6ddcb7dff89f884e39 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10575,6 +10575,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 e5fbde97d3784a1d77fa68eeffce32cbdfea1876..39af2db1ca545f4c7c9e2cfa75cea6f5f4a8675a 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,7 @@ "@types/react-dom": "^17.0.0", "axios": "^0.21.1", "formik": "^2.2.6", + "jwt-decode": "^3.1.2", "react": "^17.0.1", "react-axios": "^2.0.4", "react-dom": "^17.0.1", @@ -24,6 +25,7 @@ "react-scripts": "4.0.2", "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", + "redux-thunk": "^2.3.0", "styled-components": "^5.2.1", "typescript": "^4.1.3", "web-vitals": "^1.1.0", diff --git a/client/src/App.test.tsx b/client/src/App.test.tsx index b9efc05e7e15aadbc6931c5647d37f812269f677..f27dce756f71dd4ac0adff9d490a3fcdc42e0f1b 100644 --- a/client/src/App.test.tsx +++ b/client/src/App.test.tsx @@ -1,9 +1,13 @@ -import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' import App from './App' +import store from './store' -test('renders learn react link', () => { - render(<App />) +test('renders app', () => { + ;<Provider store={store}> + render( + <App />) + </Provider> // const linkElement = screen.getByText(/learn react/i) // expect(linkElement).toBeInTheDocument() }) diff --git a/client/src/App.tsx b/client/src/App.tsx index d3e92bebd8a5449e74ec359e777e793c5c3288cf..61b64bee021b2213107eed48c94505dddfcb3f4f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -18,14 +18,14 @@ const theme = createMuiTheme({ const App: React.FC = () => { return ( <StylesProvider injectFirst> - <Wrapper> - <MuiThemeProvider theme={theme}> - <ThemeProvider theme={theme}> + <MuiThemeProvider theme={theme}> + <ThemeProvider theme={theme}> + <Wrapper> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> <Main /> - </ThemeProvider> - </MuiThemeProvider> - </Wrapper> + </Wrapper> + </ThemeProvider> + </MuiThemeProvider> </StylesProvider> ) } diff --git a/client/src/Main.tsx b/client/src/Main.tsx index 0d85c12a9d357ceed5c4051a0803cecd8f194b3d..c5494c888b84e684299abfba1fdf7d087fc9c924 100644 --- a/client/src/Main.tsx +++ b/client/src/Main.tsx @@ -7,14 +7,15 @@ import AudienceViewPage from './pages/views/AudienceViewPage' import JudgeViewPage from './pages/views/JudgeViewPage' import ParticipantViewPage from './pages/views/ParticipantViewPage' import ViewSelectPage from './pages/views/ViewSelectPage' +import SecureRoute from './utils/SecureRoute' const Main: React.FC = () => { return ( <BrowserRouter> <Switch> - <Route exact path="/" component={LoginPage} /> - <Route path="/admin" component={AdminPage} /> - <Route path="/editor/competition-id=:id" component={PresentationEditorPage} /> + <SecureRoute login exact path="/" component={LoginPage} /> + <SecureRoute path="/admin" component={AdminPage} /> + <SecureRoute path="/editor/competition-id=:id" component={PresentationEditorPage} /> <Route exact path="/view" component={ViewSelectPage} /> <Route exact path="/view/participant" component={ParticipantViewPage} /> <Route exact path="/view/judge" component={JudgeViewPage} /> @@ -23,5 +24,4 @@ const Main: React.FC = () => { </BrowserRouter> ) } - export default Main diff --git a/client/src/actions/login.ts b/client/src/actions/login.ts deleted file mode 100644 index 3ad925a18f8c5853532291a71d5d1e4d0511033c..0000000000000000000000000000000000000000 --- a/client/src/actions/login.ts +++ /dev/null @@ -1,29 +0,0 @@ - -export const login = () => { - return{ - type: 'SIGN_IN' - }; -}; - - - - -/* -// Old code that can be used for comparison - -export function login(name, email, id, token) { - return { - type: Types.USER_LOGIN, - name, - email, - id, - token - } -} - -export function logout() { - return { - type: Types.USER_LOGOUT, - } -} -*/ \ No newline at end of file diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 3d640d40050f5c4a4b3418c8daa679f9e4096ef9..d0fb0d7cda4bd2e192ea554344e12a62859cefaa 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -1,11 +1,15 @@ export default { - AXIOS_GET: "AXIOS_GET", - AXIOS_GET_SUCCESS: "AXIOS_GET_SUCCESS", - AXIOS_GET_ERROR: "AXIOS_GET_ERROR", - - AXIOS_POST: "AXIOS_POST", - AXIOS_POST_SUCCESS: "AXIOS_POST_SUCCESS", - AXIOS_POST_ERROR: "AXIOS_POST_ERROR", - - + LOADING_UI: 'LOADING_UI', + LOADING_USER: 'LOADING_USER', + SET_USER: 'SET_USER', + SET_ERRORS: 'SET_ERRORS', + CLEAR_ERRORS: 'SET_ERRORS', + SET_UNAUTHENTICATED: 'SET_UNAUTHENTICATED', + SET_AUTHENTICATED: 'SET_AUTHENTICATED', + AXIOS_GET: 'AXIOS_GET', + AXIOS_GET_SUCCESS: 'AXIOS_GET_SUCCESS', + AXIOS_GET_ERROR: 'AXIOS_GET_ERROR', + AXIOS_POST: 'AXIOS_POST', + AXIOS_POST_SUCCESS: 'AXIOS_POST_SUCCESS', + AXIOS_POST_ERROR: 'AXIOS_POST_ERROR', } diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..30a9973aba75bb29bb669037deb5f4785d703f83 --- /dev/null +++ b/client/src/actions/user.ts @@ -0,0 +1,47 @@ +import axios from 'axios' +import Types from './types' + +export const loginUser = (userData: any, history: any) => (dispatch: any) => { + dispatch({ type: Types.LOADING_UI }) + axios + .post('/auth/login', userData) + .then((res) => { + const token = `Bearer ${res.data.access_token}` + localStorage.setItem('token', token) //setting token to local storage + axios.defaults.headers.common['Authorization'] = token //setting authorize token to header in axios + dispatch(getUserData()) + dispatch({ type: Types.CLEAR_ERRORS }) // no error + history.push('/admin') //redirecting to admin page after login success + }) + .catch((err) => { + console.error(err) + dispatch({ + type: Types.SET_ERRORS, + payload: err.response.data, + }) + }) +} + +export const getUserData = () => (dispatch: any) => { + dispatch({ type: Types.LOADING_USER }) + axios + .get('/users') + .then((res) => { + dispatch({ + type: Types.SET_USER, + payload: res.data, + }) + }) + .catch((err) => { + console.log(err) + }) +} + +export const logoutUser = () => (dispatch: any) => { + localStorage.removeItem('token') + delete axios.defaults.headers.common['Authorization'] + dispatch({ + type: Types.SET_UNAUTHENTICATED, + }) + window.location.href = '/' //redirect to login page +} diff --git a/client/src/index.css b/client/src/index.css index 7323ae85c542d8da74f197f1395de258e8a4c673..80bfba852206d422d31dd7fbce7c438ef107f361 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,11 +1,17 @@ body { margin: 0; + height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +html, +#root { + height: 100%; +} + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/client/src/index.tsx b/client/src/index.tsx index 6d8b17f95fd464385374041f0d23ecaa07c1e1de..fdd9cf8e2bc1d9655a060c4270091fbf5f13e236 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,29 +1,10 @@ import React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' -import { compose, createStore } from 'redux' import App from './App' import './index.css' -import allReducers from './reducers/allReducers' import reportWebVitals from './reportWebVitals' - -/* - TypeScript does not know the type of the property. - Therefore, you will get the error; Property ‘__REDUX_DEVTOOLS_EXTENSION_COMPOSE__’ - does not exist on type ‘Window’. Hence, you need to add the property to the global window as below. -*/ -declare global { - interface Window { - __REDUX_DEVTOOLS_EXTENSION__: typeof compose - } -} - -// Create an Advanced global store with the name "store" -// const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose // allows Mozilla plugin to view state in a GUI, https://github.com/zalmoxisus/redux-devtools-extension#13-use-redux-devtools-extension-package-from-npm -// const store = createStore(allReducers, composeEnhancers(applyMiddleware())) - -// simple store with plugin -const store = createStore(allReducers, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) +import store from './store' // Provider wraps the app component so that it can access store ReactDOM.render( diff --git a/client/src/pages/admin/AdminPage.test.tsx b/client/src/pages/admin/AdminPage.test.tsx index b29b78f82bb88d7489a9a09ef071693acbfde654..efb56bb24b9ff8726e5a402816b3440f8a6107e6 100644 --- a/client/src/pages/admin/AdminPage.test.tsx +++ b/client/src/pages/admin/AdminPage.test.tsx @@ -1,12 +1,16 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' +import store from '../../store' import AdminPage from './AdminPage' it('renders admin view', () => { render( - <BrowserRouter> - <AdminPage /> - </BrowserRouter> + <Provider store={store}> + <BrowserRouter> + <AdminPage /> + </BrowserRouter> + </Provider> ) }) diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 7f4d4ce002fc49e4b1dffc513d2ab8508f251d5b..5a524470df0933a6643723b7b650c7f94d2843b5 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -9,13 +9,15 @@ import { ListItemIcon, ListItemText, Toolbar, - Typography + Typography, } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import DashboardIcon from '@material-ui/icons/Dashboard' import MailIcon from '@material-ui/icons/Mail' import React from 'react' +import { connect } from 'react-redux' import { Link, Route, Switch, useRouteMatch } from 'react-router-dom' +import { logoutUser } from '../../actions/user' import CompetitionManager from './components/CompetitionManager' import Regions from './components/Regions' @@ -49,10 +51,13 @@ const useStyles = makeStyles((theme: Theme) => }) ) -const AdminView: React.FC = () => { +const AdminView: React.FC = (props: any) => { const classes = useStyles() const [openIndex, setOpenIndex] = React.useState(0) const { path, url } = useRouteMatch() + const handleLogout = () => { + props.logoutUser() + } return ( <div className={classes.root}> <CssBaseline /> @@ -92,7 +97,7 @@ const AdminView: React.FC = () => { <Divider /> <List> <ListItem> - <Button component={Link} to="/" type="submit" fullWidth variant="contained" color="primary"> + <Button onClick={handleLogout} type="submit" fullWidth variant="contained" color="primary"> Logga ut </Button> </ListItem> @@ -123,5 +128,7 @@ const AdminView: React.FC = () => { </div> ) } - -export default AdminView +const mapDispatchToProps = { + logoutUser, +} +export default connect(null, mapDispatchToProps)(AdminView) diff --git a/client/src/pages/admin/components/CompetitionManager.tsx b/client/src/pages/admin/components/CompetitionManager.tsx index b37bb2a2f536384501248f7944d3dc40d45e1ee6..a91b6636d8236f850297cdf20795e8d6bfb1021b 100644 --- a/client/src/pages/admin/components/CompetitionManager.tsx +++ b/client/src/pages/admin/components/CompetitionManager.tsx @@ -195,9 +195,7 @@ const CompetitionManager: React.FC = () => { <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> <MenuItem onClick={handleClose}>Starta</MenuItem> <MenuItem onClick={handleClose}>Duplicera</MenuItem> - <RemoveCompetition onClick={handleClose}> - Ta bort - </RemoveCompetition> + <RemoveCompetition onClick={handleClose}>Ta bort</RemoveCompetition> </Menu> </div> ) diff --git a/client/src/pages/login/LoginPage.test.tsx b/client/src/pages/login/LoginPage.test.tsx index ae41ba025840c938774f018789438941ba368cdc..2492b02673944f39d12a6c04d67a62f664dda7d0 100644 --- a/client/src/pages/login/LoginPage.test.tsx +++ b/client/src/pages/login/LoginPage.test.tsx @@ -1,7 +1,13 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' +import store from '../../store' import LoginPage from './LoginPage' it('renders login form', () => { - render(<LoginPage />) + render( + <Provider store={store}> + <LoginPage /> + </Provider> + ) }) diff --git a/client/src/pages/login/LoginPage.tsx b/client/src/pages/login/LoginPage.tsx index a821a26fe0e6273b7911cf3168eff5731039fc2c..8f8e967c435af9c2611bc9ac3c1365b114a7666a 100644 --- a/client/src/pages/login/LoginPage.tsx +++ b/client/src/pages/login/LoginPage.tsx @@ -1,9 +1,8 @@ import { AppBar, Tab, Tabs } from '@material-ui/core' -import { makeStyles, Theme } from '@material-ui/core/styles' import React from 'react' import AdminLogin from './components/AdminLogin' import CompetitionLogin from './components/CompetitionLogin' -import { LoginPageContainer } from './styled' +import { LoginPageContainer, LoginPaper } from './styled' interface TabPanelProps { activeTab: number @@ -17,18 +16,11 @@ function LoginContent(props: TabPanelProps) { return <CompetitionLogin /> } -const useStyles = makeStyles((theme: Theme) => ({ - root: { - backgroundColor: theme.palette.background.paper, - }, -})) - const LoginPage: React.FC = () => { - const classes = useStyles() const [loginTab, setLoginTab] = React.useState(0) return ( <LoginPageContainer> - <div className={classes.root}> + <LoginPaper elevation={3}> <AppBar position="static"> <Tabs value={loginTab} onChange={(event, selectedTab) => setLoginTab(selectedTab)}> <Tab label="Konto" id="simple-tab-0" /> @@ -36,7 +28,7 @@ const LoginPage: React.FC = () => { </Tabs> </AppBar> <LoginContent activeTab={loginTab} /> - </div> + </LoginPaper> </LoginPageContainer> ) } diff --git a/client/src/pages/login/components/AdminLogin.test.tsx b/client/src/pages/login/components/AdminLogin.test.tsx index 4e966c895cd9deee7b738b6eaee9e979e60ddf0d..3be87593f4e58698597d39fe6d3ac91a77ac133a 100644 --- a/client/src/pages/login/components/AdminLogin.test.tsx +++ b/client/src/pages/login/components/AdminLogin.test.tsx @@ -1,7 +1,13 @@ import { render } from '@testing-library/react' import React from 'react' +import { Provider } from 'react-redux' +import store from '../../../store' import AdminLogin from './AdminLogin' it('renders admin login', () => { - render(<AdminLogin />) + render( + <Provider store={store}> + <AdminLogin /> + </Provider> + ) }) diff --git a/client/src/pages/login/components/AdminLogin.tsx b/client/src/pages/login/components/AdminLogin.tsx index f794862ab341b3adad1d689f9ffed38cdc906fbc..531ae22ea3c11c04be150a6de9ffb200e33f63f0 100644 --- a/client/src/pages/login/components/AdminLogin.tsx +++ b/client/src/pages/login/components/AdminLogin.tsx @@ -1,11 +1,13 @@ import { Button, TextField } from '@material-ui/core' import { Alert, AlertTitle } from '@material-ui/lab' -import axios from 'axios' import { Formik, FormikHelpers } from 'formik' -import React from 'react' +import React, { useEffect, useState } from 'react' +import { connect } from 'react-redux' +import { useHistory } from 'react-router-dom' import * as Yup from 'yup' +import { loginUser } from '../../../actions/user' import { AccountLoginModel } from '../../../interfaces/models' -import { LoginForm } from './styled' +import { CenteredCircularProgress, LoginForm } from './styled' interface AccountLoginFormModel { model: AccountLoginModel @@ -17,6 +19,10 @@ interface ServerResponse { message: string } +interface formError { + message: string +} + const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ model: Yup.object() .shape({ @@ -27,22 +33,20 @@ const accountSchema: Yup.SchemaOf<AccountLoginFormModel> = Yup.object({ error: Yup.string().optional(), }) -const handleAccountSubmit = async (values: AccountLoginFormModel, actions: FormikHelpers<AccountLoginFormModel>) => { - await axios - .post<ServerResponse>(`users/login`, values.model) - .then(() => { - actions.resetForm() - }) - .catch(({ response }) => { - console.log(response.data.message) - actions.setFieldError('error', response.data.message) - }) - .finally(() => { - actions.setSubmitting(false) - }) -} +const AdminLogin: React.FC = (props: any) => { + const [errors, setErrors] = useState({} as formError) + const [loading, setLoading] = useState(false) + useEffect(() => { + if (props.UI.errors) { + setErrors(props.UI.errors) + } + setLoading(props.UI.loading) + }, [props.UI]) + const handleAccountSubmit = (values: AccountLoginFormModel, actions: FormikHelpers<AccountLoginFormModel>) => { + props.loginUser(values.model, history) + } -const AdminLogin: React.FC = () => { + const history = useHistory() const accountInitialValues: AccountLoginFormModel = { model: { email: '', password: '' }, } @@ -73,23 +77,28 @@ const AdminLogin: React.FC = () => { type="submit" fullWidth variant="contained" - color="primary" - disabled={!formik.isValid || !formik.touched.model?.email || !formik.touched.model?.email} + color="secondary" + disabled={!formik.isValid || !formik.touched.model?.email || !formik.touched.model?.email || loading} > Logga in </Button> - {formik.errors.error ? ( + {errors.message && ( <Alert severity="error"> <AlertTitle>Error</AlertTitle> - {formik.errors.error} + {errors.message} </Alert> - ) : ( - <div /> )} + {loading && <CenteredCircularProgress color="secondary" />} </LoginForm> )} </Formik> ) } - -export default AdminLogin +const mapStateToProps = (state: any) => ({ + user: state.user, + UI: state.UI, +}) +const mapDispatchToProps = { + loginUser, +} +export default connect(mapStateToProps, mapDispatchToProps)(AdminLogin) diff --git a/client/src/pages/login/components/CompetitionLogin.tsx b/client/src/pages/login/components/CompetitionLogin.tsx index e70508d45d68176c587f2e6f1ede81fc0fbb8af9..42b9af4e4669c4afd64d4c51c3e1b20024488000 100644 --- a/client/src/pages/login/components/CompetitionLogin.tsx +++ b/client/src/pages/login/components/CompetitionLogin.tsx @@ -66,7 +66,7 @@ const CompetitionLogin: React.FC = () => { onBlur={formik.handleBlur} margin="normal" /> - <Button type="submit" fullWidth variant="contained" color="primary" disabled={!formik.isValid}> + <Button type="submit" fullWidth variant="contained" color="secondary" disabled={!formik.isValid}> Anslut till tävling </Button> {formik.errors.error ? ( diff --git a/client/src/pages/login/components/styled.tsx b/client/src/pages/login/components/styled.tsx index 66be5e0afa4879b9aa222bef9309d68b59d5cddd..a384e7b96d0fb0a396d5a07e9e8fc45d9ad7d0b5 100644 --- a/client/src/pages/login/components/styled.tsx +++ b/client/src/pages/login/components/styled.tsx @@ -1,7 +1,11 @@ +import { CircularProgress } from '@material-ui/core' import styled from 'styled-components' export const LoginForm = styled.form` display: flex; flex-direction: column; ` - +export const CenteredCircularProgress = styled(CircularProgress)` + margin-top: 10px; + align-self: center; +` diff --git a/client/src/pages/login/styled.tsx b/client/src/pages/login/styled.tsx index f00e942d4911ef0e51e68c16a2431902cd7ccfb3..c071747e54a760e2972b8d36201d67780942e47a 100644 --- a/client/src/pages/login/styled.tsx +++ b/client/src/pages/login/styled.tsx @@ -1,9 +1,11 @@ +import { Paper } from '@material-ui/core' import styled from 'styled-components' export const LoginPageContainer = styled.div` display: flex; - height: 100%; justify-content: center; - align-items: center; ` +export const LoginPaper = styled(Paper)` + padding: 10px; +` diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index 4903f9d73f25dcfb29a2fd99ee45bca18aa24895..98260e1663f88dd2e70fb9a5be28e533228b9efe 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -1,10 +1,12 @@ // Combines all the reducers so that we only have to pass "one" reducer to the store in src/index.tsx import { combineReducers } from 'redux' -import loggedInReducer from './isLoggedIn' +import uiReducer from './uiReducer' +import userReducer from './userReducer' const allReducers = combineReducers({ // name: state - isLoggedIn: loggedInReducer, // You can write "loggedInReducer" because its the same as "loggedInReducer: loggedInReducer" + user: userReducer, + UI: uiReducer, }) export default allReducers diff --git a/client/src/reducers/isLoggedIn.ts b/client/src/reducers/isLoggedIn.ts deleted file mode 100644 index cf5d7b43177c6b13113d18ab88d018cc8c9cf724..0000000000000000000000000000000000000000 --- a/client/src/reducers/isLoggedIn.ts +++ /dev/null @@ -1,10 +0,0 @@ -const loggedInReducer = (state = false, action: { type: any }) => { - // isLoggedIn has an initial state of false - switch (action.type) { - case 'SIGN_IN': - return !state - default: - return state - } -} -export default loggedInReducer diff --git a/client/src/reducers/uiReducer.ts b/client/src/reducers/uiReducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..8aac6daf9635d73c420b87b6f98246fa2d3dad9a --- /dev/null +++ b/client/src/reducers/uiReducer.ts @@ -0,0 +1,29 @@ +import Types from '../actions/types' +const initialState = { + loading: false, + errors: null, +} + +export default function (state = initialState, action: any) { + switch (action.type) { + case Types.SET_ERRORS: + return { + ...state, + loading: false, + errors: action.payload, + } + case Types.CLEAR_ERRORS: + return { + ...state, + loading: false, + errors: null, + } + case Types.LOADING_UI: + return { + ...state, + loading: true, + } + default: + return state + } +} diff --git a/client/src/reducers/userReducer.ts b/client/src/reducers/userReducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..fff6875bc69a5fab2bc37dc192127115ff917716 --- /dev/null +++ b/client/src/reducers/userReducer.ts @@ -0,0 +1,33 @@ +//in userReducer.ts +import Types from '../actions/types' + +const initialState = { + authenticated: false, + credentials: {}, + loading: false, +} + +export default function (state = initialState, action: any) { + switch (action.type) { + case Types.SET_AUTHENTICATED: + return { + ...state, + authenticated: true, + } + case Types.SET_UNAUTHENTICATED: + return initialState + case Types.SET_USER: + return { + authenticated: true, + loading: false, + ...action.payload, + } + case Types.LOADING_USER: + return { + ...state, + loading: true, + } + default: + return state + } +} diff --git a/client/src/store.ts b/client/src/store.ts new file mode 100644 index 0000000000000000000000000000000000000000..bab656fad5f8e819338376c8f7b49d9a1caa2d33 --- /dev/null +++ b/client/src/store.ts @@ -0,0 +1,25 @@ +import { applyMiddleware, compose, createStore } from 'redux' +import { composeWithDevTools } from 'redux-devtools-extension' +import thunk from 'redux-thunk' +import allReducers from './reducers/allReducers' +/* + TypeScript does not know the type of the property. + Therefore, you will get the error; Property ‘__REDUX_DEVTOOLS_EXTENSION_COMPOSE__’ + does not exist on type ‘Window’. Hence, you need to add the property to the global window as below. +*/ +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION__: typeof compose + } +} + +const initialState = {} +const middleware = [thunk] +// Create an Advanced global store with the name "store" +// const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose // allows Mozilla plugin to view state in a GUI, https://github.com/zalmoxisus/redux-devtools-extension#13-use-redux-devtools-extension-package-from-npm +// const store = createStore(allReducers, composeEnhancers(applyMiddleware())) + +// simple store with plugin +const store = createStore(allReducers, initialState, composeWithDevTools(applyMiddleware(...middleware))) + +export default store diff --git a/client/src/styled.tsx b/client/src/styled.tsx index d26b4dbbf90a14be7f256b462cb33d8e9a018619..0c17f9cc13be00e9d7d876c726fefc860361d380 100644 --- a/client/src/styled.tsx +++ b/client/src/styled.tsx @@ -1,5 +1,6 @@ import styled from 'styled-components' export const Wrapper = styled.div` - padding: 20px; -` \ No newline at end of file + padding: 10px; + height: 100%; +` diff --git a/client/src/utils/SecureRoute.tsx b/client/src/utils/SecureRoute.tsx new file mode 100644 index 0000000000000000000000000000000000000000..29b5aef5be265826e5991e277bb8debb1b9df2c4 --- /dev/null +++ b/client/src/utils/SecureRoute.tsx @@ -0,0 +1,30 @@ +import React, { useEffect, useState } from 'react' +import { connect } from 'react-redux' +import { Redirect, Route, RouteProps } from 'react-router-dom' +import { CheckAuthentication } from './checkAuthentication' + +interface SecureRouteProps extends RouteProps { + login?: boolean + component: any + authenticated: boolean + rest?: any +} +/** Utility component to use for authentication, replace all routes that should be private with secure routes*/ +const SecureRoute: React.FC<SecureRouteProps> = ({ login, component: Component, authenticated, ...rest }: any) => { + const [isReady, setReady] = useState(false) + useEffect(() => { + CheckAuthentication().then(() => setReady(true)) + }, []) + if (isReady) { + console.log(login, authenticated, Component) + if (login) + return ( + <Route {...rest} render={(props) => (authenticated ? <Redirect to="/admin" /> : <Component {...props} />)} /> + ) + else return <Route {...rest} render={(props) => (authenticated ? <Component {...props} /> : <Redirect to="/" />)} /> + } else return null +} +const mapStateToProps = (state: any) => ({ + authenticated: state.user.authenticated, +}) +export default connect(mapStateToProps)(SecureRoute) diff --git a/client/src/utils/checkAuthentication.ts b/client/src/utils/checkAuthentication.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbc3113a4e4008c9c8c6e594c4eb78d0580d3aef --- /dev/null +++ b/client/src/utils/checkAuthentication.ts @@ -0,0 +1,34 @@ +import axios from 'axios' +import jwtDecode from 'jwt-decode' +import Types from '../actions/types' +import { logoutUser } from '../actions/user' +import store from '../store' + +const UnAuthorized = async () => { + store.dispatch(logoutUser()) +} + +export const CheckAuthentication = async () => { + const authToken = localStorage.token + if (authToken) { + const decodedToken: any = jwtDecode(authToken) + if (decodedToken.exp * 1000 >= Date.now()) { + axios.defaults.headers.common['Authorization'] = authToken + await axios + .get('/users') + .then((res) => { + store.dispatch({ type: Types.SET_AUTHENTICATED }) + store.dispatch({ + type: Types.SET_USER, + payload: res.data, + }) + }) + .catch((error) => { + console.error(error) + UnAuthorized() + }) + } else { + UnAuthorized() + } + } +}