Rick Watson
5 years ago
8 changed files with 337 additions and 27 deletions
-
25interface/package-lock.json
-
1interface/package.json
-
38interface/src/AppRouting.js
-
34interface/src/authentication/AuthenticatedRoute.js
-
56interface/src/authentication/Authentication.js
-
91interface/src/authentication/AuthenticationWrapper.js
-
15interface/src/authentication/Context.js
-
104interface/src/containers/LoginPage.js
@ -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; |
@ -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)); |
@ -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); |
|||
}); |
|||
}); |
|||
} |
@ -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) |
@ -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> |
|||
); |
|||
}; |
|||
} |
@ -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); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue