WIP login page and authentication code
This commit is contained in:
		
							
								
								
									
										25
									
								
								interface/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										25
									
								
								interface/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -6624,8 +6624,7 @@ | ||||
|         }, | ||||
|         "code-point-at": { | ||||
|           "version": "1.1.0", | ||||
|           "bundled": true, | ||||
|           "optional": true | ||||
|           "bundled": true | ||||
|         }, | ||||
|         "concat-map": { | ||||
|           "version": "0.0.1", | ||||
| @@ -6634,8 +6633,7 @@ | ||||
|         }, | ||||
|         "console-control-strings": { | ||||
|           "version": "1.1.0", | ||||
|           "bundled": true, | ||||
|           "optional": true | ||||
|           "bundled": true | ||||
|         }, | ||||
|         "core-util-is": { | ||||
|           "version": "1.0.2", | ||||
| @@ -6738,8 +6736,7 @@ | ||||
|         }, | ||||
|         "inherits": { | ||||
|           "version": "2.0.3", | ||||
|           "bundled": true, | ||||
|           "optional": true | ||||
|           "bundled": true | ||||
|         }, | ||||
|         "ini": { | ||||
|           "version": "1.3.5", | ||||
| @@ -6749,7 +6746,6 @@ | ||||
|         "is-fullwidth-code-point": { | ||||
|           "version": "1.0.0", | ||||
|           "bundled": true, | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "number-is-nan": "^1.0.0" | ||||
|           } | ||||
| @@ -6769,13 +6765,11 @@ | ||||
|         }, | ||||
|         "minimist": { | ||||
|           "version": "0.0.8", | ||||
|           "bundled": true, | ||||
|           "optional": true | ||||
|           "bundled": true | ||||
|         }, | ||||
|         "minipass": { | ||||
|           "version": "2.2.4", | ||||
|           "bundled": true, | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "safe-buffer": "^5.1.1", | ||||
|             "yallist": "^3.0.0" | ||||
| @@ -6792,7 +6786,6 @@ | ||||
|         "mkdirp": { | ||||
|           "version": "0.5.1", | ||||
|           "bundled": true, | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "minimist": "0.0.8" | ||||
|           } | ||||
| @@ -6865,8 +6858,7 @@ | ||||
|         }, | ||||
|         "number-is-nan": { | ||||
|           "version": "1.0.1", | ||||
|           "bundled": true, | ||||
|           "optional": true | ||||
|           "bundled": true | ||||
|         }, | ||||
|         "object-assign": { | ||||
|           "version": "4.1.1", | ||||
| @@ -6876,7 +6868,6 @@ | ||||
|         "once": { | ||||
|           "version": "1.4.0", | ||||
|           "bundled": true, | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "wrappy": "1" | ||||
|           } | ||||
| @@ -6982,7 +6973,6 @@ | ||||
|         "string-width": { | ||||
|           "version": "1.0.2", | ||||
|           "bundled": true, | ||||
|           "optional": true, | ||||
|           "requires": { | ||||
|             "code-point-at": "^1.0.0", | ||||
|             "is-fullwidth-code-point": "^1.0.0", | ||||
| @@ -9357,6 +9347,11 @@ | ||||
|         "array-includes": "^3.0.3" | ||||
|       } | ||||
|     }, | ||||
|     "jwt-decode": { | ||||
|       "version": "2.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", | ||||
|       "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" | ||||
|     }, | ||||
|     "killable": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
|     "@material-ui/core": "^3.9.3", | ||||
|     "@material-ui/icons": "^3.0.2", | ||||
|     "compression-webpack-plugin": "^2.0.0", | ||||
|     "jwt-decode": "^2.2.0", | ||||
|     "moment": "^2.24.0", | ||||
|     "prop-types": "^15.7.2", | ||||
|     "react": "^16.8.6", | ||||
|   | ||||
| @@ -1,25 +1,39 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { Route, Redirect, Switch } from 'react-router'; | ||||
| import { Redirect, Route, Switch } from 'react-router'; | ||||
|  | ||||
| // authentication | ||||
| import * as Authentication from './authentication/Authentication'; | ||||
| import AuthenticationWrapper from './authentication/AuthenticationWrapper'; | ||||
| import AuthenticatedRoute from './authentication/AuthenticatedRoute'; | ||||
|  | ||||
| // containers | ||||
| import WiFiConfiguration from './containers/WiFiConfiguration'; | ||||
| import NTPConfiguration from './containers/NTPConfiguration'; | ||||
| import OTAConfiguration from './containers/OTAConfiguration'; | ||||
| import APConfiguration from './containers/APConfiguration'; | ||||
| import LoginPage from './containers/LoginPage'; | ||||
|  | ||||
| class AppRouting extends Component { | ||||
| 	render() { | ||||
| 	   return ( | ||||
|        <Switch> | ||||
|          <Route exact path="/wifi-configuration" component={WiFiConfiguration} /> | ||||
| 				 <Route exact path="/ap-configuration" component={APConfiguration} /> | ||||
| 				 <Route exact path="/ntp-configuration" component={NTPConfiguration} /> | ||||
| 				 <Route exact path="/ota-configuration" component={OTAConfiguration} /> | ||||
|          <Redirect to="/wifi-configuration" /> | ||||
|        </Switch> | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
|   componentWillMount() { | ||||
|     Authentication.clearLoginRedirect(); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     return ( | ||||
|       <AuthenticationWrapper> | ||||
|         <Switch> | ||||
|           <Route exact path="/" component={LoginPage} /> | ||||
|           <AuthenticatedRoute exact path="/wifi-configuration" component={WiFiConfiguration} /> | ||||
|           <AuthenticatedRoute exact path="/ap-configuration" component={APConfiguration} /> | ||||
|           <AuthenticatedRoute exact path="/ntp-configuration" component={NTPConfiguration} /> | ||||
|           <AuthenticatedRoute exact path="/ota-configuration" component={OTAConfiguration} /> | ||||
|           <Redirect to="/" /> | ||||
|         </Switch> | ||||
|       </AuthenticationWrapper> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default AppRouting; | ||||
|   | ||||
							
								
								
									
										34
									
								
								interface/src/authentication/AuthenticatedRoute.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								interface/src/authentication/AuthenticatedRoute.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import * as React from 'react'; | ||||
| import { | ||||
|   Redirect, Route | ||||
| } from "react-router-dom"; | ||||
|  | ||||
| import { withAuthenticationContext } from './Context.js'; | ||||
| import * as Authentication from './Authentication'; | ||||
| import { withNotifier } from '../components/SnackbarNotification'; | ||||
|  | ||||
| export class AuthenticatedRoute extends React.Component { | ||||
|  | ||||
|   render() { | ||||
|     const { raiseNotification, authenticationContext, component: Component, ...rest } = this.props; | ||||
|     const { location } = this.props; | ||||
|     const renderComponent = (props) => { | ||||
|       if (authenticationContext.jwt) { | ||||
|         return ( | ||||
|           <Component {...props} /> | ||||
|         ); | ||||
|       } | ||||
|       Authentication.storeLoginRedirect(location); | ||||
|       raiseNotification("Please log in to continue."); | ||||
|       return ( | ||||
|         <Redirect to='/' /> | ||||
|       ); | ||||
|     } | ||||
|     return ( | ||||
|       <Route {...rest} render={renderComponent} /> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default withNotifier(withAuthenticationContext(AuthenticatedRoute)); | ||||
							
								
								
									
										56
									
								
								interface/src/authentication/Authentication.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								interface/src/authentication/Authentication.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import history from '../history'; | ||||
|  | ||||
| export const ACCESS_TOKEN = 'access_token'; | ||||
| export const LOGIN_PATHNAME = 'loginPathname'; | ||||
| export const LOGIN_SEARCH = 'loginSearch'; | ||||
|  | ||||
| export function storeLoginRedirect(location) { | ||||
|   if (location) { | ||||
|     localStorage.setItem(LOGIN_PATHNAME, location.pathname); | ||||
|     localStorage.setItem(LOGIN_SEARCH, location.search); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function clearLoginRedirect() { | ||||
|   localStorage.removeItem(LOGIN_PATHNAME); | ||||
|   localStorage.removeItem(LOGIN_SEARCH); | ||||
| } | ||||
|  | ||||
| export function fetchLoginRedirect() { | ||||
|   const loginPathname = localStorage.getItem(LOGIN_PATHNAME); | ||||
|   const loginSearch = localStorage.getItem(LOGIN_SEARCH); | ||||
|   clearLoginRedirect(); | ||||
|   return { | ||||
|     pathname: loginPathname || "/", | ||||
|     search: (loginPathname && loginSearch) || undefined | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Wraps the normal fetch routene with one with provides the access token if present. | ||||
|  */ | ||||
| export function secureFetch(url, params) { | ||||
|   if (localStorage.getItem(ACCESS_TOKEN)) { | ||||
|     params = params || {}; | ||||
|     params.headers = params.headers || new Headers(); | ||||
|     params.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN) | ||||
|   } | ||||
|   return fetch(url, params); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Wraps the normal fetch routene which redirects on 401 response. | ||||
|  */ | ||||
| export function redirectingSecureFetch(url, params) { | ||||
|   return new Promise(function (resolve, reject) { | ||||
|     secureFetch(url, params).then(response => { | ||||
|       if (response.status === 401) { | ||||
|         history.go("/");         | ||||
|       } else { | ||||
|         resolve(response); | ||||
|       } | ||||
|     }).catch(error => { | ||||
|       reject(error); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										91
									
								
								interface/src/authentication/AuthenticationWrapper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								interface/src/authentication/AuthenticationWrapper.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| import * as React from 'react'; | ||||
| import history from '../history' | ||||
| import { withNotifier } from '../components/SnackbarNotification'; | ||||
|  | ||||
| import { ACCESS_TOKEN } from './Authentication'; | ||||
| import { AuthenticationContext } from './Context'; | ||||
| import jwtDecode from 'jwt-decode'; | ||||
|  | ||||
| class AuthenticationWrapper extends React.Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|     this.refresh = this.refresh.bind(this); | ||||
|     this.signIn = this.signIn.bind(this); | ||||
|     this.signOut = this.signOut.bind(this); | ||||
|     this.state = { | ||||
|       context: { | ||||
|         refresh: this.refresh, | ||||
|         signIn: this.signIn, | ||||
|         signOut: this.signOut | ||||
|       }, | ||||
|       initialized: false | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   componentDidMount() { | ||||
|     this.refresh(); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     return ( | ||||
|       <React.Fragment> | ||||
|         {this.state.initialized ? this.renderContent() : this.renderContentLoading()} | ||||
|       </React.Fragment> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   renderContent() { | ||||
|     return ( | ||||
|       <AuthenticationContext.Provider value={this.state.context}> | ||||
|         {this.props.children} | ||||
|       </AuthenticationContext.Provider> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   renderContentLoading() { | ||||
|     return ( | ||||
|       <div>THIS IS WHERE THE LOADING MESSAGE GOES</div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   refresh() { | ||||
|     var accessToken = localStorage.getItem(ACCESS_TOKEN); | ||||
|     if (accessToken) { | ||||
|       try { | ||||
|         this.setState({ initialized: true, context: { ...this.state.context, jwt: jwtDecode(accessToken) } }); | ||||
|       } catch (err) { | ||||
|         localStorage.removeItem(ACCESS_TOKEN); | ||||
|         this.props.raiseNotification("Please log in again."); | ||||
|         history.push('/'); | ||||
|       } | ||||
|     } else { | ||||
|       this.setState({ initialized: true }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   signIn(accessToken) { | ||||
|     try { | ||||
|       this.setState({ context: { ...this.state.context, jwt: jwtDecode(accessToken) } }); | ||||
|       localStorage.setItem(ACCESS_TOKEN, accessToken); | ||||
|     } catch (err) { | ||||
|       this.props.raiseNotification("JWT did not parse."); | ||||
|       history.push('/'); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   signOut() { | ||||
|     localStorage.removeItem(ACCESS_TOKEN); | ||||
|     this.setState({ | ||||
|       context: { | ||||
|         ...this.state.context, | ||||
|         me: undefined | ||||
|       } | ||||
|     }); | ||||
|     this.props.raiseNotification("You have signed out."); | ||||
|     history.push('/'); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default withNotifier(AuthenticationWrapper) | ||||
							
								
								
									
										15
									
								
								interface/src/authentication/Context.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								interface/src/authentication/Context.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import * as React from "react"; | ||||
|  | ||||
| export const AuthenticationContext = React.createContext( | ||||
|   {} | ||||
| ); | ||||
|  | ||||
| export function withAuthenticationContext(Component) { | ||||
|   return function AuthenticationContextComponent(props) { | ||||
|     return ( | ||||
|       <AuthenticationContext.Consumer> | ||||
|         {authenticationContext => <Component {...props} authenticationContext={authenticationContext} />} | ||||
|       </AuthenticationContext.Consumer> | ||||
|     ); | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										104
									
								
								interface/src/containers/LoginPage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								interface/src/containers/LoginPage.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { withStyles } from '@material-ui/core/styles'; | ||||
| import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator'; | ||||
| import Paper from '@material-ui/core/Paper'; | ||||
| import Typography from '@material-ui/core/Typography'; | ||||
| import Fab from '@material-ui/core/Fab'; | ||||
| import { APP_NAME } from '../constants/App'; | ||||
| import ForwardIcon from '@material-ui/icons/Forward'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   loginPage: { | ||||
|     padding: theme.spacing.unit * 2, | ||||
|     height: "100vh", | ||||
|     display: "flex" | ||||
|   }, | ||||
|   loginPanel: { | ||||
|     margin: "auto", | ||||
|     padding: theme.spacing.unit * 2, | ||||
|     paddingTop: "200px", | ||||
|     backgroundImage: 'url("/app/icon.png")', | ||||
|     backgroundRepeat: "no-repeat", | ||||
|     backgroundPosition: "50% "+ theme.spacing.unit * 2 +"px", | ||||
|     backgroundSize: "auto 150px", | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   extendedIcon: { | ||||
|     marginRight: theme.spacing.unit, | ||||
|   }, | ||||
|   loadingSettings: { | ||||
|     margin: theme.spacing.unit, | ||||
|   }, | ||||
|   loadingSettingsDetails: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   textField: { | ||||
|     width: "100%" | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: theme.spacing.unit * 2, | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| class LoginPage extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|     this.state = { | ||||
|       username: '', | ||||
|       password: '' | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   handleValueChange = name => event => { | ||||
|     this.setState({ [name]: event.target.value }); | ||||
|   }; | ||||
|  | ||||
|   onSubmit = event => { | ||||
|     // TODO | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { username, password } = this.state; | ||||
|     const { classes } = this.props; | ||||
|     return ( | ||||
|       <div className={classes.loginPage}> | ||||
|         <Paper className={classes.loginPanel}> | ||||
|           <Typography variant="h4">{APP_NAME}</Typography> | ||||
|           <ValidatorForm onSubmit={this.onSubmit}> | ||||
|             <TextValidator | ||||
|               validators={['required']} | ||||
|               errorMessages={['Username is required']} | ||||
|               name="username" | ||||
|               label="Username" | ||||
|               className={classes.textField} | ||||
|               value={username} | ||||
|               onChange={this.handleValueChange('username')} | ||||
|               margin="normal" | ||||
|             /> | ||||
|             <TextValidator | ||||
|               validators={['required']} | ||||
|               errorMessages={['Password is required']} | ||||
|               name="password" | ||||
|               label="Password" | ||||
|               className={classes.textField} | ||||
|               value={password} | ||||
|               onChange={this.handleValueChange('password')} | ||||
|               margin="normal" | ||||
|             /> | ||||
|             <Fab variant="extended" color="primary" className={classes.button} type="submit"> | ||||
|               <ForwardIcon className={classes.extendedIcon} /> | ||||
|               Login | ||||
|             </Fab> | ||||
|           </ValidatorForm> | ||||
|         </Paper> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default withStyles(styles)(LoginPage); | ||||
		Reference in New Issue
	
	Block a user