More specific access control headers to support cross origin Authorization
Pretty sign in page Verify existing JWT on application mount
This commit is contained in:
		| @@ -1 +1 @@ | ||||
| REACT_APP_ENDPOINT_ROOT=http://192.168.0.4/rest/ | ||||
| REACT_APP_ENDPOINT_ROOT=http://192.168.0.16/rest/ | ||||
|   | ||||
| @@ -1,2 +1,2 @@ | ||||
| REACT_APP_ENDPOINT_ROOT=/rest/ | ||||
| GENERATE_SOURCEMAP=false | ||||
| GENERATE_SOURCEMAP=false | ||||
|   | ||||
| @@ -29,11 +29,13 @@ export function fetchLoginRedirect() { | ||||
| /** | ||||
|  * Wraps the normal fetch routene with one with provides the access token if present. | ||||
|  */ | ||||
| export function secureFetch(url, params) { | ||||
|   if (localStorage.getItem(ACCESS_TOKEN)) { | ||||
| export function authorizedFetch(url, params) { | ||||
|   const accessToken = localStorage.getItem(ACCESS_TOKEN); | ||||
|   if (accessToken) { | ||||
|     params = params || {}; | ||||
|     params.headers = params.headers || new Headers(); | ||||
|     params.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN) | ||||
|     params.credentials = 'include'; | ||||
|     params.headers = params.headers || {}; | ||||
|     params.headers.Authorization = 'Bearer ' + accessToken; | ||||
|   } | ||||
|   return fetch(url, params); | ||||
| } | ||||
| @@ -41,9 +43,9 @@ export function secureFetch(url, params) { | ||||
| /** | ||||
|  * Wraps the normal fetch routene which redirects on 401 response. | ||||
|  */ | ||||
| export function redirectingSecureFetch(url, params) { | ||||
| export function redirectingAuthorizedFetch(url, params) { | ||||
|   return new Promise(function (resolve, reject) { | ||||
|     secureFetch(url, params).then(response => { | ||||
|     authorizedFetch(url, params).then(response => { | ||||
|       if (response.status === 401) { | ||||
|         history.go("/");         | ||||
|       } else { | ||||
|   | ||||
| @@ -1,10 +1,27 @@ | ||||
| import * as React from 'react'; | ||||
| import history from '../history' | ||||
| import { withNotifier } from '../components/SnackbarNotification'; | ||||
|  | ||||
| import { ACCESS_TOKEN } from './Authentication'; | ||||
| import { VERIFY_AUTHORIZATION_ENDPOINT } from '../constants/Endpoints'; | ||||
| import { ACCESS_TOKEN, authorizedFetch } from './Authentication'; | ||||
| import { AuthenticationContext } from './Context'; | ||||
| import jwtDecode from 'jwt-decode'; | ||||
| import CircularProgress from '@material-ui/core/CircularProgress'; | ||||
| import Typography from '@material-ui/core/Typography'; | ||||
| import { withStyles } from '@material-ui/core/styles'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   loadingPanel: { | ||||
|     padding: theme.spacing.unit * 2, | ||||
|     display: "flex", | ||||
|     alignItems: "center", | ||||
|     justifyContent: "center", | ||||
|     height: "100vh", | ||||
|     flexDirection: "column" | ||||
|   }, | ||||
|   progress: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| class AuthenticationWrapper extends React.Component { | ||||
|  | ||||
| @@ -44,23 +61,30 @@ class AuthenticationWrapper extends React.Component { | ||||
|   } | ||||
|  | ||||
|   renderContentLoading() { | ||||
|     const { classes } = this.props; | ||||
|     return ( | ||||
|       <div>THIS IS WHERE THE LOADING MESSAGE GOES</div> | ||||
|       <div className={classes.loadingPanel}> | ||||
|         <CircularProgress className={classes.progress} size={100} /> | ||||
|         <Typography variant="h4" > | ||||
|           Loading... | ||||
|         </Typography> | ||||
|       </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('/'); | ||||
|       } | ||||
|       authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT) | ||||
|         .then(response => { | ||||
|           const jwt = response.status === 200 ? jwtDecode(accessToken) : undefined; | ||||
|           this.setState({ initialized: true, context: { ...this.state.context, jwt } }); | ||||
|         }).catch(error => { | ||||
|           this.setState({ initialized: true, context: { ...this.state.context, jwt: undefined } }); | ||||
|           this.props.raiseNotification("Error verifying authorization: " + error.message); | ||||
|         }); | ||||
|     } else { | ||||
|       this.setState({ initialized: true }); | ||||
|       this.setState({ initialized: true, context: { ...this.state.context, jwt: undefined } }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -69,8 +93,8 @@ class AuthenticationWrapper extends React.Component { | ||||
|       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('/'); | ||||
|       this.setState({ initialized: true, context: { ...this.state.context, jwt: undefined } }); | ||||
|       this.props.raiseNotification("Failed to parse JWT " + err.message); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -79,7 +103,7 @@ class AuthenticationWrapper extends React.Component { | ||||
|     this.setState({ | ||||
|       context: { | ||||
|         ...this.state.context, | ||||
|         me: undefined | ||||
|         jwt: undefined | ||||
|       } | ||||
|     }); | ||||
|     this.props.raiseNotification("You have signed out."); | ||||
| @@ -88,4 +112,4 @@ class AuthenticationWrapper extends React.Component { | ||||
|  | ||||
| } | ||||
|  | ||||
| export default withNotifier(AuthenticationWrapper) | ||||
| export default withStyles(styles)(withNotifier(AuthenticationWrapper)) | ||||
|   | ||||
| @@ -54,7 +54,7 @@ class SnackbarNotification extends React.Component { | ||||
|           open={this.state.open} | ||||
|           autoHideDuration={6000} | ||||
|           onClose={this.handleClose} | ||||
|           SnackbarContentProps={{ | ||||
|           ContentProps={{ | ||||
|             'aria-describedby': 'message-id', | ||||
|           }} | ||||
|           message={<span id="message-id">{this.state.message}</span>} | ||||
|   | ||||
| @@ -9,4 +9,5 @@ export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks"; | ||||
| export const WIFI_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "wifiSettings"; | ||||
| export const WIFI_STATUS_ENDPOINT = ENDPOINT_ROOT + "wifiStatus"; | ||||
| export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings"; | ||||
| export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn"; | ||||
| export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn"; | ||||
| export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization"; | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| 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'; | ||||
| @@ -10,41 +9,42 @@ import ForwardIcon from '@material-ui/icons/Forward'; | ||||
| import { withNotifier } from '../components/SnackbarNotification'; | ||||
| import { SIGN_IN_ENDPOINT } from '../constants/Endpoints'; | ||||
| import { withAuthenticationContext } from '../authentication/Context'; | ||||
| import PasswordValidator from '../components/PasswordValidator'; | ||||
|  | ||||
| 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, | ||||
| const styles = theme => { | ||||
|   return { | ||||
|     loginPage: { | ||||
|       display: "flex", | ||||
|       height: "100vh", | ||||
|       margin: "auto", | ||||
|       padding: theme.spacing.unit * 2, | ||||
|       justifyContent: "center", | ||||
|       flexDirection: "column", | ||||
|       maxWidth: theme.breakpoints.values.sm | ||||
|     }, | ||||
|     loginPanel: { | ||||
|       textAlign: "center", | ||||
|       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", | ||||
|       width: "100%" | ||||
|     }, | ||||
|     extendedIcon: { | ||||
|       marginRight: theme.spacing.unit, | ||||
|     }, | ||||
|     textField: { | ||||
|       width: "100%" | ||||
|     }, | ||||
|     button: { | ||||
|       marginRight: theme.spacing.unit * 2, | ||||
|       marginTop: theme.spacing.unit * 2, | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| } | ||||
|   | ||||
|  | ||||
| class LoginPage extends Component { | ||||
|  | ||||
| @@ -53,7 +53,7 @@ class LoginPage extends Component { | ||||
|     this.state = { | ||||
|       username: '', | ||||
|       password: '', | ||||
|       fetched: false | ||||
|       processing: false | ||||
|     }; | ||||
|   } | ||||
|  | ||||
| @@ -64,7 +64,7 @@ class LoginPage extends Component { | ||||
|   onSubmit = () => { | ||||
|     const { username, password } = this.state; | ||||
|     const { authenticationContext } = this.props; | ||||
|     this.setState({ fetched: false }); | ||||
|     this.setState({ processing: true }); | ||||
|     fetch(SIGN_IN_ENDPOINT, { | ||||
|       method: 'POST', | ||||
|       body: JSON.stringify({ username, password }), | ||||
| @@ -76,21 +76,22 @@ class LoginPage extends Component { | ||||
|         if (response.status === 200) { | ||||
|           return response.json(); | ||||
|         } else if (response.status === 401) { | ||||
|           throw Error("Login details invalid!"); | ||||
|           throw Error("Invalid login details."); | ||||
|         } else { | ||||
|           throw Error("Invalid status code: " + response.status); | ||||
|         } | ||||
|       }).then(json =>{ | ||||
|       }).then(json => { | ||||
|         authenticationContext.signIn(json.access_token); | ||||
|         this.setState({ processing: false }); | ||||
|       }) | ||||
|       .catch(error => { | ||||
|         this.props.raiseNotification(error.message); | ||||
|         this.setState({ fetched: true }); | ||||
|         this.setState({ processing: false }); | ||||
|       }); | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { username, password } = this.state; | ||||
|     const { username, password, processing } = this.state; | ||||
|     const { classes } = this.props; | ||||
|     return ( | ||||
|       <div className={classes.loginPage}> | ||||
| @@ -98,6 +99,7 @@ class LoginPage extends Component { | ||||
|           <Typography variant="h4">{APP_NAME}</Typography> | ||||
|           <ValidatorForm onSubmit={this.onSubmit}> | ||||
|             <TextValidator | ||||
|               disabled={processing} | ||||
|               validators={['required']} | ||||
|               errorMessages={['Username is required']} | ||||
|               name="username" | ||||
| @@ -107,7 +109,8 @@ class LoginPage extends Component { | ||||
|               onChange={this.handleValueChange('username')} | ||||
|               margin="normal" | ||||
|             /> | ||||
|             <TextValidator | ||||
|             <PasswordValidator | ||||
|               disabled={processing} | ||||
|               validators={['required']} | ||||
|               errorMessages={['Password is required']} | ||||
|               name="password" | ||||
| @@ -117,7 +120,7 @@ class LoginPage extends Component { | ||||
|               onChange={this.handleValueChange('password')} | ||||
|               margin="normal" | ||||
|             /> | ||||
|             <Fab variant="extended" color="primary" className={classes.button} type="submit"> | ||||
|             <Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}> | ||||
|               <ForwardIcon className={classes.extendedIcon} /> | ||||
|               Sign In | ||||
|             </Fab> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user