Skip to content
Snippets Groups Projects
Commit dcdd6a55 authored by Albin Henriksson's avatar Albin Henriksson
Browse files

Resolve "implement login in redux"

parent adb9c371
No related branches found
No related tags found
1 merge request!38Resolve "implement login in redux"
Pipeline #38545 passed
Showing
with 173 additions and 125 deletions
......@@ -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",
......
......@@ -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",
......
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()
})
......@@ -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>
)
}
......
......@@ -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
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
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',
}
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
}
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;
}
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(
......
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>
)
})
......@@ -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)
......@@ -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>
)
......
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>
)
})
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>
)
}
......
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>
)
})
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)
......@@ -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 ? (
......
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;
`
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;
`
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment