WIP login page and authentication code
This commit is contained in:
parent
f93804c240
commit
c74c287e21
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);
|
Loading…
Reference in New Issue
Block a user