login
This commit is contained in:
parent
4f57e7fd32
commit
5d9bfa97af
184
src/actions/authActions.js
Normal file
184
src/actions/authActions.js
Normal file
@ -0,0 +1,184 @@
|
||||
import { USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types';
|
||||
|
||||
import axios from 'axios';
|
||||
import { returnErrors, returnSuccess } from './messageActions'
|
||||
|
||||
|
||||
// // Check token & load user
|
||||
// export const loadUser = () => (dispatch) => {
|
||||
// // user loading
|
||||
// dispatch({
|
||||
// type: USER_LOADING
|
||||
// });
|
||||
// const config = {
|
||||
// success: res => {
|
||||
// dispatch({
|
||||
// type: USER_LOADED,
|
||||
// payload: res.data.user
|
||||
// });
|
||||
// },
|
||||
// error: err => {
|
||||
// if(err.response){
|
||||
// dispatch(returnErrors(err.response.data.message, err.response.status));
|
||||
// }
|
||||
// dispatch({
|
||||
// type: AUTH_ERROR
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// axios.get('/api/v1/user/me', config, dispatch(authInterceptor()))
|
||||
// .then(res => {
|
||||
// res.config.success(res);
|
||||
// })
|
||||
// .catch(err => {
|
||||
// err.config.error(err);
|
||||
// });
|
||||
// };
|
||||
|
||||
|
||||
var logoutTimerId;
|
||||
const timeToLogout = 14.9*60*1000; // nearly 15 minutes corresponding to the API
|
||||
|
||||
// Login user
|
||||
export const login = ({ email, password }) => (dispatch) => {
|
||||
// Headers
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
// Request Body
|
||||
const body = JSON.stringify({ email, password });
|
||||
axios.post('https://api.opensensemap.org/users/sign-in', body, config)
|
||||
.then(res => {
|
||||
// Logout automatically if refreshToken "expired"
|
||||
const logoutTimer = () => setTimeout(
|
||||
() => dispatch(logout()),
|
||||
timeToLogout
|
||||
);
|
||||
logoutTimerId = logoutTimer();
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'LOGIN_SUCCESS'));
|
||||
dispatch({
|
||||
type: LOGIN_SUCCESS,
|
||||
payload: res.data
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('hier');
|
||||
console.log(err);
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'LOGIN_FAIL'));
|
||||
dispatch({
|
||||
type: LOGIN_FAIL
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Logout User
|
||||
export const logout = () => (dispatch) => {
|
||||
const config = {
|
||||
success: res => {
|
||||
dispatch({
|
||||
type: LOGOUT_SUCCESS
|
||||
});
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'LOGOUT_SUCCESS'));
|
||||
clearTimeout(logoutTimerId);
|
||||
},
|
||||
error: err => {
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'LOGOUT_FAIL'));
|
||||
dispatch({
|
||||
type: LOGOUT_FAIL
|
||||
});
|
||||
clearTimeout(logoutTimerId);
|
||||
}
|
||||
};
|
||||
axios.post('https://api.opensensemap.org/users/sign-out', {}, config, dispatch(authInterceptor()))
|
||||
.then(res => {
|
||||
res.config.success(res);
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response.status !== 401){
|
||||
err.config.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const authInterceptor = () => (dispatch, getState) => {
|
||||
// Add a request interceptor
|
||||
axios.interceptors.request.use(
|
||||
config => {
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
const token = getState().auth.token;
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Add a response interceptor
|
||||
axios.interceptors.response.use(
|
||||
response => {
|
||||
// request was successfull
|
||||
return response;
|
||||
},
|
||||
error => {
|
||||
const originalRequest = error.config;
|
||||
const refreshToken = getState().auth.refreshToken;
|
||||
if(refreshToken){
|
||||
// try to refresh the token failed
|
||||
if (error.response.status === 401 && originalRequest._retry) {
|
||||
// router.push('/login');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
// token was not valid and 1st try to refresh the token
|
||||
if (error.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
const refreshToken = getState().auth.refreshToken;
|
||||
// request to refresh the token, in request-body is the refreshToken
|
||||
axios.post('/api/v1/user/token/refresh', {"refreshToken": refreshToken})
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
clearTimeout(logoutTimerId);
|
||||
const logoutTimer = () => setTimeout(
|
||||
() => dispatch(logout()),
|
||||
timeToLogout
|
||||
);
|
||||
logoutTimerId = logoutTimer();
|
||||
dispatch({
|
||||
type: REFRESH_TOKEN_SUCCESS,
|
||||
payload: res.data
|
||||
});
|
||||
axios.defaults.headers.common['Authorization'] = 'Bearer ' + getState().auth.token;
|
||||
// request was successfull, new request with the old parameters and the refreshed token
|
||||
return axios(originalRequest)
|
||||
.then(res => {
|
||||
originalRequest.success(res);
|
||||
})
|
||||
.catch(err => {
|
||||
originalRequest.error(err);
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
})
|
||||
.catch(err => {
|
||||
// request failed, token could not be refreshed
|
||||
if(err.response){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status));
|
||||
}
|
||||
dispatch({
|
||||
type: AUTH_ERROR
|
||||
});
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
// request status was unequal to 401, no possibility to refresh the token
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
};
|
@ -1,3 +1,14 @@
|
||||
// authentication
|
||||
export const USER_LOADING = 'USER_LOADING';
|
||||
export const USER_LOADED = 'USER_LOADED';
|
||||
export const AUTH_ERROR = 'AUTH_ERROR';
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
export const LOGIN_FAIL = 'LOGIN_FAIL';
|
||||
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
|
||||
export const LOGOUT_FAIL = 'LOGOUT_FAIL';
|
||||
export const REFRESH_TOKEN_FAIL = 'REFRESH_TOKEN_FAIL';
|
||||
export const REFRESH_TOKEN_SUCCESS = 'REFRESH_TOKEN_SUCCESS';
|
||||
|
||||
export const NEW_CODE = 'NEW_CODE';
|
||||
export const CHANGE_WORKSPACE = 'CHANGE_WORKSPACE';
|
||||
export const CREATE_BLOCK = 'CREATE_BLOCK';
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { logout } from '../actions/authActions';
|
||||
|
||||
import senseboxLogo from './sensebox_logo.svg';
|
||||
|
||||
@ -20,7 +21,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
|
||||
import { faBars, faChevronLeft, faLayerGroup, faBuilding, faIdCard, faEnvelope, faCog, faChalkboardTeacher, faFolderPlus, faTools, faLightbulb } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBars, faChevronLeft, faLayerGroup, faSignInAlt, faSignOutAlt, faCertificate, faUserCircle, faIdCard, faEnvelope, faCog, faChalkboardTeacher, faFolderPlus, faTools, faLightbulb } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
@ -102,8 +103,7 @@ class Navbar extends Component {
|
||||
{[{ text: 'Tutorials', icon: faChalkboardTeacher, link: "/tutorial" },
|
||||
{ text: 'Tutorial-Builder', icon: faTools, link: "/tutorial/builder" },
|
||||
{ text: 'Galerie', icon: faLightbulb, link: "/gallery" },
|
||||
{ text: 'Projekte', icon: faLayerGroup, link: "/project" },
|
||||
{ text: 'Einstellungen', icon: faCog, link: "/settings" }].map((item, index) => (
|
||||
{ text: 'Projekte', icon: faLayerGroup, link: "/project" }].map((item, index) => (
|
||||
<Link to={item.link} key={index} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<ListItem button onClick={this.toggleDrawer}>
|
||||
<ListItemIcon><FontAwesomeIcon icon={item.icon} /></ListItemIcon>
|
||||
@ -113,14 +113,25 @@ class Navbar extends Component {
|
||||
))}
|
||||
</List>
|
||||
<Divider classes={{ root: this.props.classes.appBarColor }} style={{ marginTop: 'auto' }} />
|
||||
{/* <List>
|
||||
{[{ text: 'Über uns', icon: faBuilding }, { text: 'Kontakt', icon: faEnvelope }, { text: 'Impressum', icon: faIdCard }].map((item, index) => (
|
||||
<ListItem button key={index} onClick={this.toggleDrawer}>
|
||||
<ListItemIcon><FontAwesomeIcon icon={item.icon} /></ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List> */}
|
||||
<List>
|
||||
{[{ text: 'Anmelden', icon: faSignInAlt, link: '/user/login', restriction: !this.props.isAuthenticated },
|
||||
{ text: 'Konto', icon: faUserCircle, link: '/user', restriction: this.props.isAuthenticated },
|
||||
{ text: 'MyBadges', icon: faCertificate, link: '/user/badge', restriction: this.props.isAuthenticated },
|
||||
{ text: 'Abmelden', icon: faSignOutAlt, function: this.props.logout, restriction: this.props.isAuthenticated },
|
||||
{ text: 'Einstellungen', icon: faCog, link: "/settings" }].map((item, index) => {
|
||||
if(item.restriction || Object.keys(item).filter(attribute => attribute === 'restriction').length === 0){
|
||||
return(
|
||||
<Link to={item.link} key={index} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<ListItem button onClick={item.function ? () => {item.function(); this.toggleDrawer();} : this.toggleDrawer}>
|
||||
<ListItemIcon><FontAwesomeIcon icon={item.icon} /></ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItem>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</List>
|
||||
</Drawer>
|
||||
{this.props.tutorialIsLoading || this.props.projectIsLoading ?
|
||||
<LinearProgress style={{marginBottom: '30px', boxShadow: '0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12)'}}/>
|
||||
@ -132,12 +143,14 @@ class Navbar extends Component {
|
||||
|
||||
Navbar.propTypes = {
|
||||
tutorialIsLoading: PropTypes.bool.isRequired,
|
||||
projectIsLoading: PropTypes.bool.isRequired
|
||||
projectIsLoading: PropTypes.bool.isRequired,
|
||||
isAuthenticated: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
tutorialIsLoading: state.tutorial.progress,
|
||||
projectIsLoading: state.project.progress
|
||||
projectIsLoading: state.project.progress,
|
||||
isAuthenticated: state.auth.isAuthenticated
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(withRouter(Navbar)));
|
||||
export default connect(mapStateToProps, { logout })(withStyles(styles, { withTheme: true })(withRouter(Navbar)));
|
||||
|
@ -15,6 +15,7 @@ import Project from './Project/Project';
|
||||
import Settings from './Settings/Settings';
|
||||
import Impressum from './Impressum';
|
||||
import Privacy from './Privacy';
|
||||
import Login from './User/Login';
|
||||
|
||||
|
||||
class Routes extends Component {
|
||||
@ -40,6 +41,8 @@ class Routes extends Component {
|
||||
// User-Projects
|
||||
<Route path="/project" exact component={ProjectHome} />
|
||||
<Route path="/project/:projectId" exact component={Project} />
|
||||
// User
|
||||
<Route path="/user/login" exact component={Login} />
|
||||
// settings
|
||||
<Route path="/settings" exact component={Settings} />
|
||||
// privacy
|
||||
|
157
src/components/User/Login.js
Normal file
157
src/components/User/Login.js
Normal file
@ -0,0 +1,157 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { login } from '../../actions/authActions'
|
||||
import { clearMessages } from '../../actions/messageActions'
|
||||
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import Snackbar from '../Snackbar';
|
||||
import Breadcrumbs from '../Breadcrumbs';
|
||||
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import Link from '@material-ui/core/Link';
|
||||
|
||||
|
||||
export class Login extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
email: '',
|
||||
password: '',
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: '',
|
||||
showPassword: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(props){
|
||||
const { message } = this.props;
|
||||
if (message !== props.message) {
|
||||
if(message.id === 'LOGIN_SUCCESS'){
|
||||
this.props.history.goBack();
|
||||
}
|
||||
// Check for login error
|
||||
else if(message.id === 'LOGIN_FAIL'){
|
||||
this.setState({ email: '', password: '', snackbar: true, key: Date.now(), message: 'Der Benutzername oder das Passwort ist nicht korrekt.', type: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChange = e => {
|
||||
this.setState({ [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
onSubmit = e => {
|
||||
e.preventDefault();
|
||||
const {email, password} = this.state;
|
||||
if(email !== '' && password !== ''){
|
||||
// create user object
|
||||
const user = {
|
||||
email,
|
||||
password
|
||||
};
|
||||
this.props.login(user);
|
||||
} else {
|
||||
this.setState({ snackbar: true, key: Date.now(), message: 'Gib sowohl ein Benutzername als auch ein Passwort ein.', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
handleClickShowPassword = () => {
|
||||
this.setState({ showPassword: !this.state.showPassword });
|
||||
};
|
||||
|
||||
handleMouseDownPassword = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div>
|
||||
<Breadcrumbs content={[{ link: '/user/login', title: 'Anmelden' }]} />
|
||||
|
||||
<div style={{maxWidth: '500px', marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<h1>Anmelden</h1>
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
<TextField
|
||||
style={{marginBottom: '10px'}}
|
||||
// variant='outlined'
|
||||
type='text'
|
||||
label='E-Mail oder Nutzername'
|
||||
name='email'
|
||||
value={this.state.email}
|
||||
onChange={this.onChange}
|
||||
fullWidth={true}
|
||||
/>
|
||||
<TextField
|
||||
// variant='outlined'
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
label='Passwort'
|
||||
name='password'
|
||||
value={this.state.password}
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment
|
||||
position="end"
|
||||
>
|
||||
<IconButton
|
||||
onClick={this.handleClickShowPassword}
|
||||
onMouseDown={this.handleMouseDownPassword}
|
||||
edge="end"
|
||||
>
|
||||
<FontAwesomeIcon size='xs' icon={this.state.showPassword ? faEyeSlash : faEye} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}}
|
||||
onChange={this.onChange}
|
||||
fullWidth={true}
|
||||
/>
|
||||
<p>
|
||||
<Button color="primary" variant='contained' onClick={this.onSubmit} style={{width: '100%'}}>
|
||||
{this.props.progress ?
|
||||
<div style={{height: '24.5px'}}><CircularProgress color="inherit" size={20}/></div>
|
||||
: 'Anmelden'}
|
||||
</Button>
|
||||
</p>
|
||||
<p style={{textAlign: 'center', fontSize: '0.8rem'}}>
|
||||
<Link rel="noreferrer" target="_blank" href={'https://opensensemap.org/'} color="primary">Passwort vergessen?</Link>
|
||||
</p>
|
||||
<Divider variant='fullWidth'/>
|
||||
<p style={{textAlign: 'center', paddingRight: "34px", paddingLeft: "34px"}}>
|
||||
Du hast noch kein Konto? <Link rel="noreferrer" target="_blank" href={'https://opensensemap.org/'}>Registrieren</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Login.propTypes = {
|
||||
message: PropTypes.object.isRequired,
|
||||
login: PropTypes.func.isRequired,
|
||||
clearMessages: PropTypes.func.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
progress: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
message: state.message,
|
||||
progress: state.auth.progress
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { login, clearMessages })(withRouter(Login));
|
56
src/reducers/authReducer.js
Normal file
56
src/reducers/authReducer.js
Normal file
@ -0,0 +1,56 @@
|
||||
import { USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types';
|
||||
|
||||
|
||||
const initialState = {
|
||||
token: localStorage.getItem('token'),
|
||||
refreshToken: localStorage.getItem('refreshToken'),
|
||||
isAuthenticated: null,
|
||||
progress: false,
|
||||
user: null
|
||||
};
|
||||
|
||||
export default function(state = initialState, action){
|
||||
switch(action.type){
|
||||
case USER_LOADING:
|
||||
return {
|
||||
...state,
|
||||
progress: true
|
||||
};
|
||||
case USER_LOADED:
|
||||
return {
|
||||
...state,
|
||||
isAuthenticated: true,
|
||||
progress: false,
|
||||
user: action.payload
|
||||
};
|
||||
case LOGIN_SUCCESS:
|
||||
case REFRESH_TOKEN_SUCCESS:
|
||||
localStorage.setItem('token', action.payload.token);
|
||||
localStorage.setItem('refreshToken', action.payload.refreshToken);
|
||||
console.log(action.payload);
|
||||
return {
|
||||
...state,
|
||||
user: action.payload.data.user,
|
||||
token: action.payload.token,
|
||||
refreshToken: action.payload.refreshToken,
|
||||
isAuthenticated: true,
|
||||
progress: false
|
||||
};
|
||||
case AUTH_ERROR:
|
||||
case LOGIN_FAIL:
|
||||
case LOGOUT_SUCCESS:
|
||||
case LOGOUT_FAIL:
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
return {
|
||||
...state,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
progress: false
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -5,8 +5,10 @@ import tutorialBuilderReducer from './tutorialBuilderReducer';
|
||||
import generalReducer from './generalReducer';
|
||||
import projectReducer from './projectReducer';
|
||||
import messageReducer from './messageReducer';
|
||||
import authReducer from './authReducer';
|
||||
|
||||
export default combineReducers({
|
||||
auth: authReducer,
|
||||
workspace: workspaceReducer,
|
||||
tutorial: tutorialReducer,
|
||||
builder: tutorialBuilderReducer,
|
||||
|
Loading…
x
Reference in New Issue
Block a user