WIP login page and authentication code

This commit is contained in:
Rick Watson 2019-05-14 22:47:04 +01:00
parent f93804c240
commit c74c287e21
8 changed files with 337 additions and 27 deletions

View File

@ -6624,8 +6624,7 @@
}, },
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
@ -6634,8 +6633,7 @@
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -6738,8 +6736,7 @@
}, },
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -6749,7 +6746,6 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -6769,13 +6765,11 @@
}, },
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -6792,7 +6786,6 @@
"mkdirp": { "mkdirp": {
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -6865,8 +6858,7 @@
}, },
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -6876,7 +6868,6 @@
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -6982,7 +6973,6 @@
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -9357,6 +9347,11 @@
"array-includes": "^3.0.3" "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": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",

View File

@ -6,6 +6,7 @@
"@material-ui/core": "^3.9.3", "@material-ui/core": "^3.9.3",
"@material-ui/icons": "^3.0.2", "@material-ui/icons": "^3.0.2",
"compression-webpack-plugin": "^2.0.0", "compression-webpack-plugin": "^2.0.0",
"jwt-decode": "^2.2.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.8.6", "react": "^16.8.6",

View File

@ -1,23 +1,37 @@
import React, { Component } from 'react'; 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 // containers
import WiFiConfiguration from './containers/WiFiConfiguration'; import WiFiConfiguration from './containers/WiFiConfiguration';
import NTPConfiguration from './containers/NTPConfiguration'; import NTPConfiguration from './containers/NTPConfiguration';
import OTAConfiguration from './containers/OTAConfiguration'; import OTAConfiguration from './containers/OTAConfiguration';
import APConfiguration from './containers/APConfiguration'; import APConfiguration from './containers/APConfiguration';
import LoginPage from './containers/LoginPage';
class AppRouting extends Component { class AppRouting extends Component {
componentWillMount() {
Authentication.clearLoginRedirect();
}
render() { render() {
return ( return (
<AuthenticationWrapper>
<Switch> <Switch>
<Route exact path="/wifi-configuration" component={WiFiConfiguration} /> <Route exact path="/" component={LoginPage} />
<Route exact path="/ap-configuration" component={APConfiguration} /> <AuthenticatedRoute exact path="/wifi-configuration" component={WiFiConfiguration} />
<Route exact path="/ntp-configuration" component={NTPConfiguration} /> <AuthenticatedRoute exact path="/ap-configuration" component={APConfiguration} />
<Route exact path="/ota-configuration" component={OTAConfiguration} /> <AuthenticatedRoute exact path="/ntp-configuration" component={NTPConfiguration} />
<Redirect to="/wifi-configuration" /> <AuthenticatedRoute exact path="/ota-configuration" component={OTAConfiguration} />
<Redirect to="/" />
</Switch> </Switch>
</AuthenticationWrapper>
) )
} }
} }

View 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));

View 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);
});
});
}

View 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)

View 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>
);
};
}

View 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);