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": {
|
"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",
|
||||||
|
@ -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",
|
||||||
|
@ -1,25 +1,39 @@
|
|||||||
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 {
|
||||||
render() {
|
|
||||||
return (
|
componentWillMount() {
|
||||||
<Switch>
|
Authentication.clearLoginRedirect();
|
||||||
<Route exact path="/wifi-configuration" component={WiFiConfiguration} />
|
}
|
||||||
<Route exact path="/ap-configuration" component={APConfiguration} />
|
|
||||||
<Route exact path="/ntp-configuration" component={NTPConfiguration} />
|
render() {
|
||||||
<Route exact path="/ota-configuration" component={OTAConfiguration} />
|
return (
|
||||||
<Redirect to="/wifi-configuration" />
|
<AuthenticationWrapper>
|
||||||
</Switch>
|
<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;
|
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