From 5d9bfa97af3f320a170ea14a80670ed6370549f7 Mon Sep 17 00:00:00 2001 From: Delucse <46593742+Delucse@users.noreply.github.com> Date: Thu, 3 Dec 2020 11:17:39 +0100 Subject: [PATCH] login --- src/actions/authActions.js | 184 +++++++++++++++++++++++++++++++++++ src/actions/types.js | 11 +++ src/components/Navbar.js | 41 +++++--- src/components/Routes.js | 3 + src/components/User/Login.js | 157 ++++++++++++++++++++++++++++++ src/reducers/authReducer.js | 56 +++++++++++ src/reducers/index.js | 2 + 7 files changed, 440 insertions(+), 14 deletions(-) create mode 100644 src/actions/authActions.js create mode 100644 src/components/User/Login.js create mode 100644 src/reducers/authReducer.js diff --git a/src/actions/authActions.js b/src/actions/authActions.js new file mode 100644 index 0000000..cce0e43 --- /dev/null +++ b/src/actions/authActions.js @@ -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); + } + ); +}; diff --git a/src/actions/types.js b/src/actions/types.js index 32d1056..34d5889 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -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'; diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 3bb1b51..dfd5e6a 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -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) => ( @@ -113,14 +113,25 @@ class Navbar extends Component { ))} - {/* - {[{ text: 'Über uns', icon: faBuilding }, { text: 'Kontakt', icon: faEnvelope }, { text: 'Impressum', icon: faIdCard }].map((item, index) => ( - - - - - ))} - */} + + {[{ 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( + + {item.function(); this.toggleDrawer();} : this.toggleDrawer}> + + + + + ); + } + } + )} + {this.props.tutorialIsLoading || this.props.projectIsLoading ? @@ -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))); diff --git a/src/components/Routes.js b/src/components/Routes.js index 54b4ac7..70bee87 100644 --- a/src/components/Routes.js +++ b/src/components/Routes.js @@ -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 + // User + // settings // privacy diff --git a/src/components/User/Login.js b/src/components/User/Login.js new file mode 100644 index 0000000..81e8963 --- /dev/null +++ b/src/components/User/Login.js @@ -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( + + + + + Anmelden + + + + + + + + }} + onChange={this.onChange} + fullWidth={true} + /> + + + {this.props.progress ? + + : 'Anmelden'} + + + + Passwort vergessen? + + + + Du hast noch kein Konto? Registrieren + + + + ); + } +} + +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)); diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js new file mode 100644 index 0000000..cc48b84 --- /dev/null +++ b/src/reducers/authReducer.js @@ -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; + } +} diff --git a/src/reducers/index.js b/src/reducers/index.js index 5ff8fed..cdedf04 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -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,
+ + {this.props.progress ? + + : 'Anmelden'} + +
+ Passwort vergessen? +
+ Du hast noch kein Konto? Registrieren +