WIP login page and authentication code
This commit is contained in:
		@@ -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