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:
		@@ -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