-
243README.md
-
2data/config/ntpSettings.json
-
15data/config/securitySettings.json
-
2data/www/index.html
-
BINdata/www/js/0.439a.js.gz
-
BINdata/www/js/0.da55.js.gz
-
BINdata/www/js/2.8ca9.js.gz
-
BINdata/www/js/2.9881.js.gz
-
1interface/.env
-
2interface/.env.development
-
2interface/.env.production
-
578interface/package-lock.json
-
7interface/package.json
-
42interface/src/App.js
-
50interface/src/AppRouting.js
-
34interface/src/authentication/AuthenticatedRoute.js
-
58interface/src/authentication/Authentication.js
-
123interface/src/authentication/AuthenticationWrapper.js
-
15interface/src/authentication/Context.js
-
24interface/src/authentication/UnauthenticatedRoute.js
-
210interface/src/components/MenuAppBar.js
-
90interface/src/components/RestComponent.js
-
6interface/src/components/SectionContent.js
-
4interface/src/components/SnackbarNotification.js
-
1interface/src/constants/App.js
-
4interface/src/constants/Endpoints.js
-
2interface/src/constants/WiFiSecurityModes.js
-
39interface/src/containers/APConfiguration.js
-
89interface/src/containers/APStatus.js
-
33interface/src/containers/ManageUsers.js
-
37interface/src/containers/NTPConfiguration.js
-
111interface/src/containers/NTPStatus.js
-
15interface/src/containers/OTAConfiguration.js
-
32interface/src/containers/SecuritySettings.js
-
136interface/src/containers/SignInPage.js
-
135interface/src/containers/SystemStatus.js
-
54interface/src/containers/WiFiConfiguration.js
-
6interface/src/containers/WiFiNetworkScanner.js
-
59interface/src/containers/WiFiStatus.js
-
20interface/src/forms/APSettingsForm.js
-
246interface/src/forms/ManageUsersForm.js
-
12interface/src/forms/NTPSettingsForm.js
-
18interface/src/forms/OTASettingsForm.js
-
97interface/src/forms/SecuritySettingsForm.js
-
102interface/src/forms/UserForm.js
-
10interface/src/forms/WiFiNetworkSelector.js
-
244interface/src/forms/WiFiSettingsForm.js
-
7interface/src/index.js
-
37interface/src/sections/AccessPoint.js
-
37interface/src/sections/NetworkTime.js
-
35interface/src/sections/Security.js
-
37interface/src/sections/System.js
-
74interface/src/sections/WiFiConnection.js
-
2interface/src/validators/optional.js
-
BINmedia/build.png
-
BINmedia/esp12e.jpg
-
BINmedia/esp32.jpg
-
BINmedia/screenshots.png
-
BINmedia/uploadfs.png
-
BINmedia/uploadfw.png
-
12platformio.ini
-
BINscreenshots/screenshots.png
-
4src/APSettingsService.cpp
-
4src/APSettingsService.h
-
6src/APStatus.cpp
-
4src/APStatus.h
-
143src/ArduinoJsonJWT.cpp
-
38src/ArduinoJsonJWT.h
-
6src/AsyncArduinoJson6.h
-
22src/AsyncJsonWebHandler.h
-
45src/AuthenticationService.cpp
-
33src/AuthenticationService.h
-
2src/NTPSettingsService.cpp
-
4src/NTPSettingsService.h
-
6src/NTPStatus.cpp
-
4src/NTPStatus.h
-
4src/OTASettingsService.cpp
-
4src/OTASettingsService.h
-
68src/SecurityManager.cpp
-
110src/SecurityManager.h
-
35src/SecuritySettingsService.cpp
-
26src/SecuritySettingsService.h
-
4src/SettingsPersistence.h
-
94src/SettingsService.h
-
6src/SimpleService.h
-
26src/SystemStatus.cpp
-
35src/SystemStatus.h
-
10src/WiFiScanner.cpp
-
3src/WiFiScanner.h
-
2src/WiFiSettingsService.cpp
-
4src/WiFiSettingsService.h
-
6src/WiFiStatus.cpp
-
4src/WiFiStatus.h
-
37src/main.cpp
@ -1,4 +1,4 @@ |
|||
{ |
|||
"server":"pool.ntp.org", |
|||
"interval":60 |
|||
"interval":3600 |
|||
} |
@ -0,0 +1,15 @@ |
|||
{ |
|||
"jwt_secret":"esp8266-react", |
|||
"users": [ |
|||
{ |
|||
"username": "admin", |
|||
"password": "admin", |
|||
"admin": true |
|||
}, |
|||
{ |
|||
"username": "guest", |
|||
"password": "guest", |
|||
"admin": false |
|||
} |
|||
] |
|||
} |
@ -1 +1 @@ |
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><link rel="stylesheet" href="/css/roboto.css"><link rel="manifest" href="/app/manifest.json"><title>ESP8266 React</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="/js/1.b351.js"></script><script src="/js/2.9881.js"></script><script src="/js/0.da55.js"></script></body></html> |
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><link rel="stylesheet" href="/css/roboto.css"><link rel="manifest" href="/app/manifest.json"><title>ESP8266 React</title></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script src="/js/1.b351.js"></script><script src="/js/2.8ca9.js"></script><script src="/js/0.439a.js"></script></body></html> |
@ -0,0 +1 @@ |
|||
REACT_APP_NAME=ESP8266 React |
@ -1 +1 @@ |
|||
REACT_APP_ENDPOINT_ROOT=http://192.168.0.4/rest/ |
|||
REACT_APP_ENDPOINT_ROOT=http://192.168.0.11/rest/ |
@ -1,2 +1,2 @@ |
|||
REACT_APP_ENDPOINT_ROOT=/rest/ |
|||
GENERATE_SOURCEMAP=false |
|||
GENERATE_SOURCEMAP=false |
@ -1,25 +1,41 @@ |
|||
import React, { Component } from 'react'; |
|||
|
|||
import { Route, Redirect, Switch } from 'react-router'; |
|||
import { Redirect, Switch } from 'react-router'; |
|||
|
|||
// containers
|
|||
import WiFiConfiguration from './containers/WiFiConfiguration'; |
|||
import NTPConfiguration from './containers/NTPConfiguration'; |
|||
import OTAConfiguration from './containers/OTAConfiguration'; |
|||
import APConfiguration from './containers/APConfiguration'; |
|||
import * as Authentication from './authentication/Authentication'; |
|||
import AuthenticationWrapper from './authentication/AuthenticationWrapper'; |
|||
import AuthenticatedRoute from './authentication/AuthenticatedRoute'; |
|||
import UnauthenticatedRoute from './authentication/UnauthenticatedRoute'; |
|||
|
|||
import SignInPage from './containers/SignInPage'; |
|||
|
|||
import WiFiConnection from './sections/WiFiConnection'; |
|||
import AccessPoint from './sections/AccessPoint'; |
|||
import NetworkTime from './sections/NetworkTime'; |
|||
import Security from './sections/Security'; |
|||
import System from './sections/System'; |
|||
|
|||
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> |
|||
<UnauthenticatedRoute exact path="/" component={SignInPage} /> |
|||
<AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} /> |
|||
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} /> |
|||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} /> |
|||
<AuthenticatedRoute exact path="/security/*" component={Security} /> |
|||
<AuthenticatedRoute exact path="/system/*" component={System} /> |
|||
<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.isAuthenticated()) { |
|||
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,58 @@ |
|||
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 || "/wifi/", |
|||
search: (loginPathname && loginSearch) || undefined |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Wraps the normal fetch routene with one with provides the access token if present. |
|||
*/ |
|||
export function authorizedFetch(url, params) { |
|||
const accessToken = localStorage.getItem(ACCESS_TOKEN); |
|||
if (accessToken) { |
|||
params = params || {}; |
|||
params.credentials = 'include'; |
|||
params.headers = params.headers || {}; |
|||
params.headers.Authorization = 'Bearer ' + accessToken; |
|||
} |
|||
return fetch(url, params); |
|||
} |
|||
|
|||
/** |
|||
* Wraps the normal fetch routene which redirects on 401 response. |
|||
*/ |
|||
export function redirectingAuthorizedFetch(url, params) { |
|||
return new Promise(function (resolve, reject) { |
|||
authorizedFetch(url, params).then(response => { |
|||
if (response.status === 401) { |
|||
history.push("/unauthorized"); |
|||
} else { |
|||
resolve(response); |
|||
} |
|||
}).catch(error => { |
|||
reject(error); |
|||
}); |
|||
}); |
|||
} |
@ -0,0 +1,123 @@ |
|||
import * as React from 'react'; |
|||
import history from '../history' |
|||
import { withNotifier } from '../components/SnackbarNotification'; |
|||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../constants/Endpoints'; |
|||
import { ACCESS_TOKEN, authorizedFetch } from './Authentication'; |
|||
import { AuthenticationContext } from './Context'; |
|||
import jwtDecode from 'jwt-decode'; |
|||
import CircularProgress from '@material-ui/core/CircularProgress'; |
|||
import Typography from '@material-ui/core/Typography'; |
|||
import { withStyles } from '@material-ui/core/styles'; |
|||
|
|||
const styles = theme => ({ |
|||
loadingPanel: { |
|||
padding: theme.spacing(2), |
|||
display: "flex", |
|||
alignItems: "center", |
|||
justifyContent: "center", |
|||
height: "100vh", |
|||
flexDirection: "column" |
|||
}, |
|||
progress: { |
|||
margin: theme.spacing(4), |
|||
} |
|||
}); |
|||
|
|||
class AuthenticationWrapper extends React.Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
context: { |
|||
refresh: this.refresh, |
|||
signIn: this.signIn, |
|||
signOut: this.signOut, |
|||
isAuthenticated: this.isAuthenticated, |
|||
isAdmin: this.isAdmin |
|||
}, |
|||
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() { |
|||
const { classes } = this.props; |
|||
return ( |
|||
<div className={classes.loadingPanel}> |
|||
<CircularProgress className={classes.progress} size={100} /> |
|||
<Typography variant="h4" > |
|||
Loading... |
|||
</Typography> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
refresh = () => { |
|||
var accessToken = localStorage.getItem(ACCESS_TOKEN); |
|||
if (accessToken) { |
|||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT) |
|||
.then(response => { |
|||
const user = response.status === 200 ? jwtDecode(accessToken) : undefined; |
|||
this.setState({ initialized: true, context: { ...this.state.context, user } }); |
|||
}).catch(error => { |
|||
this.setState({ initialized: true, context: { ...this.state.context, user: undefined } }); |
|||
this.props.raiseNotification("Error verifying authorization: " + error.message); |
|||
}); |
|||
} else { |
|||
this.setState({ initialized: true, context: { ...this.state.context, user: undefined } }); |
|||
} |
|||
} |
|||
|
|||
signIn = (accessToken) => { |
|||
try { |
|||
localStorage.setItem(ACCESS_TOKEN, accessToken); |
|||
this.setState({ context: { ...this.state.context, user: jwtDecode(accessToken) } }); |
|||
} catch (err) { |
|||
this.setState({ initialized: true, context: { ...this.state.context, user: undefined } }); |
|||
throw new Error("Failed to parse JWT " + err.message); |
|||
} |
|||
} |
|||
|
|||
signOut = () => { |
|||
localStorage.removeItem(ACCESS_TOKEN); |
|||
this.setState({ |
|||
context: { |
|||
...this.state.context, |
|||
user: undefined |
|||
} |
|||
}); |
|||
this.props.raiseNotification("You have signed out."); |
|||
history.push('/'); |
|||
} |
|||
|
|||
isAuthenticated = () => { |
|||
return this.state.context.user; |
|||
} |
|||
|
|||
isAdmin = () => { |
|||
const { context } = this.state; |
|||
return context.user && context.user.admin; |
|||
} |
|||
|
|||
} |
|||
|
|||
export default withStyles(styles)(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,24 @@ |
|||
import * as React from 'react'; |
|||
import { |
|||
Redirect, Route |
|||
} from "react-router-dom"; |
|||
|
|||
import { withAuthenticationContext } from './Context.js'; |
|||
import * as Authentication from './Authentication'; |
|||
|
|||
class UnauthenticatedRoute extends React.Component { |
|||
render() { |
|||
const { authenticationContext, component:Component, ...rest } = this.props; |
|||
const renderComponent = (props) => { |
|||
if (authenticationContext.isAuthenticated()) { |
|||
return (<Redirect to={Authentication.fetchLoginRedirect()} />); |
|||
} |
|||
return (<Component {...props} />); |
|||
} |
|||
return ( |
|||
<Route {...rest} render={renderComponent} /> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default withAuthenticationContext(UnauthenticatedRoute); |
@ -0,0 +1 @@ |
|||
export const APP_NAME = process.env.REACT_APP_NAME; |
@ -1,39 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
|
|||
import Tabs from '@material-ui/core/Tabs'; |
|||
import Tab from '@material-ui/core/Tab'; |
|||
|
|||
import MenuAppBar from '../components/MenuAppBar'; |
|||
import APSettings from './APSettings'; |
|||
import APStatus from './APStatus'; |
|||
|
|||
class APConfiguration extends Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
selectedTab: "apStatus", |
|||
selectedNetwork: null |
|||
}; |
|||
} |
|||
|
|||
handleTabChange = (event, selectedTab) => { |
|||
this.setState({ selectedTab }); |
|||
}; |
|||
|
|||
render() { |
|||
const { selectedTab } = this.state; |
|||
return ( |
|||
<MenuAppBar sectionTitle="AP Configuration"> |
|||
<Tabs value={selectedTab} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="scrollable"> |
|||
<Tab value="apStatus" label="AP Status" /> |
|||
<Tab value="apSettings" label="AP Settings" /> |
|||
</Tabs> |
|||
{selectedTab === "apStatus" && <APStatus />} |
|||
{selectedTab === "apSettings" && <APSettings />} |
|||
</MenuAppBar> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default APConfiguration; |
@ -0,0 +1,33 @@ |
|||
import React, { Component } from 'react'; |
|||
|
|||
import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints'; |
|||
import { restComponent } from '../components/RestComponent'; |
|||
import ManageUsersForm from '../forms/ManageUsersForm'; |
|||
import SectionContent from '../components/SectionContent'; |
|||
|
|||
class ManageUsers extends Component { |
|||
|
|||
componentDidMount() { |
|||
this.props.loadData(); |
|||
} |
|||
|
|||
render() { |
|||
const { data, fetched, errorMessage } = this.props; |
|||
return ( |
|||
<SectionContent title="Manage Users"> |
|||
<ManageUsersForm |
|||
userData={data} |
|||
userDataFetched={fetched} |
|||
errorMessage={errorMessage} |
|||
onSubmit={this.props.saveData} |
|||
onReset={this.props.loadData} |
|||
setData={this.props.setData} |
|||
handleValueChange={this.props.handleValueChange} |
|||
/> |
|||
</SectionContent> |
|||
) |
|||
} |
|||
|
|||
} |
|||
|
|||
export default restComponent(SECURITY_SETTINGS_ENDPOINT, ManageUsers); |
@ -1,37 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
import MenuAppBar from '../components/MenuAppBar'; |
|||
import NTPSettings from './NTPSettings'; |
|||
import NTPStatus from './NTPStatus'; |
|||
|
|||
import Tabs from '@material-ui/core/Tabs'; |
|||
import Tab from '@material-ui/core/Tab'; |
|||
|
|||
class NTPConfiguration extends Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
selectedTab: "ntpStatus" |
|||
}; |
|||
} |
|||
|
|||
handleTabChange = (event, selectedTab) => { |
|||
this.setState({ selectedTab }); |
|||
}; |
|||
|
|||
render() { |
|||
const { selectedTab } = this.state; |
|||
return ( |
|||
<MenuAppBar sectionTitle="NTP Configuration"> |
|||
<Tabs value={selectedTab} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="scrollable"> |
|||
<Tab value="ntpStatus" label="NTP Status" /> |
|||
<Tab value="ntpSettings" label="NTP Settings" /> |
|||
</Tabs> |
|||
{selectedTab === "ntpStatus" && <NTPStatus />} |
|||
{selectedTab === "ntpSettings" && <NTPSettings />} |
|||
</MenuAppBar> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default NTPConfiguration |
@ -1,15 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
import MenuAppBar from '../components/MenuAppBar'; |
|||
import OTASettings from './OTASettings'; |
|||
|
|||
class OTAConfiguration extends Component { |
|||
render() { |
|||
return ( |
|||
<MenuAppBar sectionTitle="OTA Configuration"> |
|||
<OTASettings /> |
|||
</MenuAppBar> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default OTAConfiguration |
@ -0,0 +1,32 @@ |
|||
import React, { Component } from 'react'; |
|||
|
|||
import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints'; |
|||
import { restComponent } from '../components/RestComponent'; |
|||
import SecuritySettingsForm from '../forms/SecuritySettingsForm'; |
|||
import SectionContent from '../components/SectionContent'; |
|||
|
|||
class SecuritySettings extends Component { |
|||
|
|||
componentDidMount() { |
|||
this.props.loadData(); |
|||
} |
|||
|
|||
render() { |
|||
const { data, fetched, errorMessage } = this.props; |
|||
return ( |
|||
<SectionContent title="Security Settings"> |
|||
<SecuritySettingsForm |
|||
securitySettings={data} |
|||
securitySettingsFetched={fetched} |
|||
errorMessage={errorMessage} |
|||
onSubmit={this.props.saveData} |
|||
onReset={this.props.loadData} |
|||
handleValueChange={this.props.handleValueChange} |
|||
/> |
|||
</SectionContent> |
|||
) |
|||
} |
|||
|
|||
} |
|||
|
|||
export default restComponent(SECURITY_SETTINGS_ENDPOINT, SecuritySettings); |
@ -0,0 +1,136 @@ |
|||
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'; |
|||
import { withNotifier } from '../components/SnackbarNotification'; |
|||
import { SIGN_IN_ENDPOINT } from '../constants/Endpoints'; |
|||
import { withAuthenticationContext } from '../authentication/Context'; |
|||
import PasswordValidator from '../components/PasswordValidator'; |
|||
|
|||
const styles = theme => { |
|||
return { |
|||
loginPage: { |
|||
display: "flex", |
|||
height: "100vh", |
|||
margin: "auto", |
|||
padding: theme.spacing(2), |
|||
justifyContent: "center", |
|||
flexDirection: "column", |
|||
maxWidth: theme.breakpoints.values.sm |
|||
}, |
|||
loginPanel: { |
|||
textAlign: "center", |
|||
padding: theme.spacing(2), |
|||
paddingTop: "200px", |
|||
backgroundImage: 'url("/app/icon.png")', |
|||
backgroundRepeat: "no-repeat", |
|||
backgroundPosition: "50% " + theme.spacing(2) + "px", |
|||
backgroundSize: "auto 150px", |
|||
width: "100%" |
|||
}, |
|||
extendedIcon: { |
|||
marginRight: theme.spacing(0.5), |
|||
}, |
|||
textField: { |
|||
width: "100%" |
|||
}, |
|||
button: { |
|||
marginRight: theme.spacing(2), |
|||
marginTop: theme.spacing(2), |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
class SignInPage extends Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
username: '', |
|||
password: '', |
|||
processing: false |
|||
}; |
|||
} |
|||
|
|||
handleValueChange = name => event => { |
|||
this.setState({ [name]: event.target.value }); |
|||
}; |
|||
|
|||
onSubmit = () => { |
|||
const { username, password } = this.state; |
|||
const { authenticationContext } = this.props; |
|||
this.setState({ processing: true }); |
|||
fetch(SIGN_IN_ENDPOINT, { |
|||
method: 'POST', |
|||
body: JSON.stringify({ username, password }), |
|||
headers: new Headers({ |
|||
'Content-Type': 'application/json' |
|||
}) |
|||
}) |
|||
.then(response => { |
|||
if (response.status === 200) { |
|||
return response.json(); |
|||
} else if (response.status === 401) { |
|||
throw Error("Invalid login details."); |
|||
} else { |
|||
throw Error("Invalid status code: " + response.status); |
|||
} |
|||
}).then(json => { |
|||
authenticationContext.signIn(json.access_token); |
|||
}) |
|||
.catch(error => { |
|||
this.props.raiseNotification(error.message); |
|||
this.setState({ processing: false }); |
|||
}); |
|||
}; |
|||
|
|||
render() { |
|||
const { username, password, processing } = 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 |
|||
disabled={processing} |
|||
validators={['required']} |
|||
errorMessages={['Username is required']} |
|||
name="username" |
|||
label="Username" |
|||
className={classes.textField} |
|||
value={username} |
|||
onChange={this.handleValueChange('username')} |
|||
margin="normal" |
|||
/> |
|||
<PasswordValidator |
|||
disabled={processing} |
|||
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" disabled={processing}> |
|||
<ForwardIcon className={classes.extendedIcon} /> |
|||
Sign In |
|||
</Fab> |
|||
</ValidatorForm> |
|||
</Paper> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
} |
|||
|
|||
export default withAuthenticationContext( |
|||
withNotifier(withStyles(styles)(SignInPage)) |
|||
); |
@ -0,0 +1,135 @@ |
|||
import React, { Component, Fragment } from 'react'; |
|||
|
|||
import { withStyles } from '@material-ui/core/styles'; |
|||
import Button from '@material-ui/core/Button'; |
|||
import LinearProgress from '@material-ui/core/LinearProgress'; |
|||
import Typography from '@material-ui/core/Typography'; |
|||
import List from '@material-ui/core/List'; |
|||
import ListItem from '@material-ui/core/ListItem'; |
|||
import ListItemAvatar from '@material-ui/core/ListItemAvatar'; |
|||
import ListItemText from '@material-ui/core/ListItemText'; |
|||
import Avatar from '@material-ui/core/Avatar'; |
|||
import Divider from '@material-ui/core/Divider'; |
|||
import DevicesIcon from '@material-ui/icons/Devices'; |
|||
import MemoryIcon from '@material-ui/icons/Memory'; |
|||
import ShowChartIcon from '@material-ui/icons/ShowChart'; |
|||
import SdStorageIcon from '@material-ui/icons/SdStorage'; |
|||
import DataUsageIcon from '@material-ui/icons/DataUsage'; |
|||
|
|||
|
|||
import { SYSTEM_STATUS_ENDPOINT } from '../constants/Endpoints'; |
|||
import { restComponent } from '../components/RestComponent'; |
|||
import SectionContent from '../components/SectionContent'; |
|||
|
|||
const styles = theme => ({ |
|||
fetching: { |
|||
margin: theme.spacing(4), |
|||
textAlign: "center" |
|||
}, |
|||
button: { |
|||
marginRight: theme.spacing(2), |
|||
marginTop: theme.spacing(2), |
|||
} |
|||
}); |
|||
|
|||
class SystemStatus extends Component { |
|||
|
|||
componentDidMount() { |
|||
this.props.loadData(); |
|||
} |
|||
|
|||
createListItems(data, classes) { |
|||
return ( |
|||
<Fragment> |
|||
<ListItem > |
|||
<ListItemAvatar> |
|||
<Avatar> |
|||
<DevicesIcon /> |
|||
</Avatar> |
|||
</ListItemAvatar> |
|||
<ListItemText primary="Platform" secondary={data.esp_platform} /> |
|||
</ListItem> |
|||
<Divider variant="inset" component="li" /> |
|||
<ListItem > |
|||
<ListItemAvatar> |
|||
<Avatar> |
|||
<ShowChartIcon /> |
|||
</Avatar> |
|||
</ListItemAvatar> |
|||
<ListItemText primary="CPU Frequency" secondary={data.cpu_freq_mhz + ' MHz'} /> |
|||
</ListItem> |
|||
<Divider variant="inset" component="li" /> |
|||
<ListItem > |
|||
<ListItemAvatar> |
|||
<Avatar> |
|||
<MemoryIcon /> |
|||
</Avatar> |
|||
</ListItemAvatar> |
|||
<ListItemText primary="Free Heap" secondary={data.free_heap + ' bytes'} /> |
|||
</ListItem> |
|||
<Divider variant="inset" component="li" /> |
|||
<ListItem > |
|||
<ListItemAvatar> |
|||
<Avatar> |
|||
<DataUsageIcon /> |
|||
</Avatar> |
|||
</ListItemAvatar> |
|||
<ListItemText primary="Sketch Size (used/max)" secondary={data.sketch_size + ' / ' + data.free_sketch_space + ' bytes'} /> |
|||
</ListItem> |
|||
<Divider variant="inset" component="li" /> |
|||
<ListItem > |
|||
<ListItemAvatar> |
|||
<Avatar> |
|||
<SdStorageIcon /> |
|||
</Avatar> |
|||
</ListItemAvatar> |
|||
<ListItemText primary="Flash Chip Size" secondary={data.flash_chip_size + ' bytes'} /> |
|||
</ListItem> |
|||
<Divider variant="inset" component="li" /> |
|||
</Fragment> |
|||
); |
|||
} |
|||
|
|||
renderNTPStatus(data, classes) { |
|||
return ( |
|||
<div> |
|||
<List> |
|||
{this.createListItems(data, classes)} |
|||
</List> |
|||
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}> |
|||
Refresh |
|||
</Button> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
render() { |
|||
const { data, fetched, errorMessage, classes } = this.props; |
|||
return ( |
|||
<SectionContent title="System Status"> |
|||
{ |
|||
!fetched ? |
|||
<div> |
|||
<LinearProgress className={classes.fetching} /> |
|||
<Typography variant="h4" className={classes.fetching}> |
|||
Loading... |
|||
</Typography> |
|||
</div> |
|||
: |
|||
data ? this.renderNTPStatus(data, classes) |
|||
: |
|||
<div> |
|||
<Typography variant="h4" className={classes.fetching}> |
|||
{errorMessage} |
|||
</Typography> |
|||
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}> |
|||
Refresh |
|||
</Button> |
|||
</div> |
|||
} |
|||
</SectionContent> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default restComponent(SYSTEM_STATUS_ENDPOINT, withStyles(styles)(SystemStatus)); |
@ -1,54 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
|
|||
import Tabs from '@material-ui/core/Tabs'; |
|||
import Tab from '@material-ui/core/Tab'; |
|||
|
|||
import MenuAppBar from '../components/MenuAppBar'; |
|||
import WiFiNetworkScanner from './WiFiNetworkScanner'; |
|||
import WiFiSettings from './WiFiSettings'; |
|||
import WiFiStatus from './WiFiStatus'; |
|||
|
|||
class WiFiConfiguration extends Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
selectedTab: "wifiStatus", |
|||
selectedNetwork: null |
|||
}; |
|||
this.selectNetwork = this.selectNetwork.bind(this); |
|||
this.deselectNetwork = this.deselectNetwork.bind(this); |
|||
} |
|||
|
|||
// TODO - slightly inapproperate use of callback ref possibly.
|
|||
selectNetwork(network) { |
|||
this.setState({ selectedTab: "wifiSettings", selectedNetwork:network }); |
|||
} |
|||
|
|||
// deselects the network after the settings component mounts.
|
|||
deselectNetwork(network) { |
|||
this.setState({ selectedNetwork:null }); |
|||
} |
|||
|
|||
handleTabChange = (event, selectedTab) => { |
|||
this.setState({ selectedTab }); |
|||
}; |
|||
|
|||
render() { |
|||
const { selectedTab } = this.state; |
|||
return ( |
|||
<MenuAppBar sectionTitle="WiFi Configuration"> |
|||
<Tabs value={selectedTab} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="scrollable"> |
|||
<Tab value="wifiStatus" label="WiFi Status" /> |
|||
<Tab value="networkScanner" label="Network Scanner" /> |
|||
<Tab value="wifiSettings" label="WiFi Settings" /> |
|||
</Tabs> |
|||
{selectedTab === "wifiStatus" && <WiFiStatus />} |
|||
{selectedTab === "networkScanner" && <WiFiNetworkScanner selectNetwork={this.selectNetwork} />} |
|||
{selectedTab === "wifiSettings" && <WiFiSettings deselectNetwork={this.deselectNetwork} selectedNetwork={this.state.selectedNetwork} />} |
|||
</MenuAppBar> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default WiFiConfiguration; |
@ -0,0 +1,246 @@ |
|||
import React, { Fragment } from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
|
|||
import { ValidatorForm } from 'react-material-ui-form-validator'; |
|||
|
|||
import { withStyles } from '@material-ui/core/styles'; |
|||
import Button from '@material-ui/core/Button'; |
|||
import LinearProgress from '@material-ui/core/LinearProgress'; |
|||
import Typography from '@material-ui/core/Typography'; |
|||
import Table from '@material-ui/core/Table'; |
|||
import TableBody from '@material-ui/core/TableBody'; |
|||
import TableCell from '@material-ui/core/TableCell'; |
|||
import TableFooter from '@material-ui/core/TableFooter'; |
|||
import TableHead from '@material-ui/core/TableHead'; |
|||
import TableRow from '@material-ui/core/TableRow'; |
|||
import Box from '@material-ui/core/Box'; |
|||
|
|||
import EditIcon from '@material-ui/icons/Edit'; |
|||
import DeleteIcon from '@material-ui/icons/Delete'; |
|||
import CloseIcon from '@material-ui/icons/Close'; |
|||
import CheckIcon from '@material-ui/icons/Check'; |
|||
import IconButton from '@material-ui/core/IconButton'; |
|||
|
|||
import UserForm from './UserForm'; |
|||
import { withAuthenticationContext } from '../authentication/Context'; |
|||
|
|||
const styles = theme => ({ |
|||
loadingSettings: { |
|||
margin: theme.spacing(0.5), |
|||
}, |
|||
loadingSettingsDetails: { |
|||
margin: theme.spacing(4), |
|||
textAlign: "center" |
|||
}, |
|||
button: { |
|||
marginRight: theme.spacing(2), |
|||
marginTop: theme.spacing(2), |
|||
}, |
|||
table: { |
|||
'& td, & th': { padding: theme.spacing(0.5) } |
|||
}, |
|||
actions: { |
|||
whiteSpace: "nowrap" |
|||
} |
|||
}); |
|||
|
|||
function compareUsers(a, b) { |
|||
if (a.username < b.username) { |
|||
return -1; |
|||
} |
|||
if (a.username > b.username) { |
|||
return 1; |
|||
} |
|||
return 0; |
|||
} |
|||
|
|||
class ManageUsersForm extends React.Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = {}; |
|||
} |
|||
|
|||
createUser = () => { |
|||
this.setState({ |
|||
creating: true, |
|||
user: { |
|||
username: "", |
|||
password: "", |
|||
admin: true |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
uniqueUsername = username => { |
|||
return !this.props.userData.users.find(u => u.username === username); |
|||
} |
|||
|
|||
noAdminConfigured = () => { |
|||
return !this.props.userData.users.find(u => u.admin); |
|||
} |
|||
|
|||
removeUser = user => { |
|||
const { userData } = this.props; |
|||
const users = userData.users.filter(u => u.username !== user.username); |
|||
this.props.setData({ ...userData, users }); |
|||
} |
|||
|
|||
startEditingUser = user => { |
|||
this.setState({ |
|||
creating: false, |
|||
user |
|||
}); |
|||
}; |
|||
|
|||
cancelEditingUser = () => { |
|||
this.setState({ |
|||
user: undefined |
|||
}); |
|||
} |
|||
|
|||
doneEditingUser = () => { |
|||
const { user } = this.state; |
|||
const { userData } = this.props; |
|||
const users = userData.users.filter(u => u.username !== user.username); |
|||
users.push(user); |
|||
this.props.setData({ ...userData, users }); |
|||
this.setState({ |
|||
user: undefined |
|||
}); |
|||
}; |
|||
|
|||
handleUserValueChange = name => event => { |
|||
const { user } = this.state; |
|||
this.setState({ |
|||
user: { |
|||
...user, [name]: event.target.value |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
handleUserCheckboxChange = name => event => { |
|||
const { user } = this.state; |
|||
this.setState({ |
|||
user: { |
|||
...user, [name]: event.target.checked |
|||
} |
|||
}); |
|||
} |
|||
|
|||
onSubmit = () => { |
|||
this.props.onSubmit(); |
|||
this.props.authenticationContext.refresh(); |
|||
} |
|||
|
|||
render() { |
|||
const { classes, userData, userDataFetched, errorMessage, onReset } = this.props; |
|||
const { user, creating } = this.state; |
|||
return ( |
|||
!userDataFetched ? |
|||
<div className={classes.loadingSettings}> |
|||
<LinearProgress className={classes.loadingSettingsDetails} /> |
|||
<Typography variant="h4" className={classes.loadingSettingsDetails}> |
|||
Loading... |
|||
</Typography> |
|||
</div> |
|||
: |
|||
userData ? |
|||
<Fragment> |
|||
<ValidatorForm onSubmit={this.onSubmit}> |
|||
<Table className={classes.table}> |
|||
<TableHead> |
|||
<TableRow> |
|||
<TableCell>Username</TableCell> |
|||
<TableCell align="center">Admin?</TableCell> |
|||
<TableCell /> |
|||
</TableRow> |
|||
</TableHead> |
|||
<TableBody> |
|||
{userData.users.sort(compareUsers).map(user => ( |
|||
<TableRow key={user.username}> |
|||
<TableCell component="th" scope="row"> |
|||
{user.username} |
|||
</TableCell> |
|||
<TableCell align="center"> |
|||
{ |
|||
user.admin ? <CheckIcon /> : <CloseIcon /> |
|||
} |
|||
</TableCell> |
|||
<TableCell align="center"> |
|||
<IconButton aria-label="Delete" onClick={() => this.removeUser(user)}> |
|||
<DeleteIcon /> |
|||
</IconButton> |
|||
<IconButton aria-label="Edit" onClick={() => this.startEditingUser(user)}> |
|||
<EditIcon /> |
|||
</IconButton> |
|||
</TableCell> |
|||
</TableRow> |
|||
))} |
|||
</TableBody> |
|||
<TableFooter> |
|||
<TableRow> |
|||
<TableCell colSpan={2} /> |
|||
<TableCell align="center"> |
|||
<Button variant="contained" color="secondary" onClick={this.createUser}> |
|||
Add User |
|||
</Button> |
|||
</TableCell> |
|||
</TableRow> |
|||
</TableFooter> |
|||
</Table> |
|||
{ |
|||
this.noAdminConfigured() && |
|||
<Typography component="div" variant="body1"> |
|||
<Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}> |
|||
You must have at least one admin user configured. |
|||
</Box> |
|||
</Typography> |
|||
} |
|||
<Button variant="contained" color="primary" className={classes.button} type="submit" disabled={this.noAdminConfigured()}> |
|||
Save |
|||
</Button> |
|||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}> |
|||
Reset |
|||
</Button> |
|||
</ValidatorForm> |
|||
{ |
|||
user && |
|||
<UserForm |
|||
user={user} |
|||
creating={creating} |
|||
onDoneEditing={this.doneEditingUser} |
|||
onCancelEditing={this.cancelEditingUser} |
|||
handleValueChange={this.handleUserValueChange} |
|||
handleCheckboxChange={this.handleUserCheckboxChange} |
|||
uniqueUsername={this.uniqueUsername} |
|||
/> |
|||
} |
|||
</Fragment> |
|||
: |
|||
<div className={classes.loadingSettings}> |
|||
<Typography variant="h4" className={classes.loadingSettingsDetails}> |
|||
{errorMessage} |
|||
</Typography> |
|||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}> |
|||
Reset |
|||
</Button> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
} |
|||
|
|||
ManageUsersForm.propTypes = { |
|||
classes: PropTypes.object.isRequired, |
|||
userData: PropTypes.object, |
|||
userDataFetched: PropTypes.bool.isRequired, |
|||
errorMessage: PropTypes.string, |
|||
onSubmit: PropTypes.func.isRequired, |
|||
onReset: PropTypes.func.isRequired, |
|||
setData: PropTypes.func.isRequired, |
|||
handleValueChange: PropTypes.func.isRequired, |
|||
authenticationContext: PropTypes.object.isRequired, |
|||
}; |
|||
|
|||
export default withAuthenticationContext(withStyles(styles)(ManageUsersForm)); |
@ -0,0 +1,97 @@ |
|||
import React from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { ValidatorForm } from 'react-material-ui-form-validator'; |
|||
|
|||
import { withStyles } from '@material-ui/core/styles'; |
|||
import Button from '@material-ui/core/Button'; |
|||
import LinearProgress from '@material-ui/core/LinearProgress'; |
|||
import Typography from '@material-ui/core/Typography'; |
|||
import Box from '@material-ui/core/Box'; |
|||
|
|||
import PasswordValidator from '../components/PasswordValidator'; |
|||
import { withAuthenticationContext } from '../authentication/Context'; |
|||
|
|||
const styles = theme => ({ |
|||
loadingSettings: { |
|||
margin: theme.spacing(0.5), |
|||
}, |
|||
loadingSettingsDetails: { |
|||
margin: theme.spacing(4), |
|||
textAlign: "center" |
|||
}, |
|||
textField: { |
|||
width: "100%" |
|||
}, |
|||
button: { |
|||
marginRight: theme.spacing(2), |
|||
marginTop: theme.spacing(2), |
|||
} |
|||
}); |
|||
|
|||
class SecuritySettingsForm extends React.Component { |
|||
|
|||
onSubmit = () => { |
|||
this.props.onSubmit(); |
|||
this.props.authenticationContext.refresh(); |
|||
} |
|||
|
|||
render() { |
|||
const { classes, securitySettingsFetched, securitySettings, errorMessage, handleValueChange, onReset } = this.props; |
|||
return ( |
|||
!securitySettingsFetched ? |
|||
<div className={classes.loadingSettings}> |
|||
<LinearProgress className={classes.loadingSettingsDetails} /> |
|||
<Typography variant="h4" className={classes.loadingSettingsDetails}> |
|||
Loading... |
|||
</Typography> |
|||
</div> |
|||
: |
|||
securitySettings ? |
|||
<ValidatorForm onSubmit={this.onSubmit} ref="SecuritySettingsForm"> |
|||
<PasswordValidator |
|||
validators={['required', 'matchRegexp:^.{1,64}$']} |
|||
errorMessages={['JWT Secret Required', 'JWT Secret must be 64 characters or less']} |
|||
name="jwt_secret" |
|||
label="JWT Secret" |
|||
className={classes.textField} |
|||
value={securitySettings.jwt_secret} |
|||
onChange={handleValueChange('jwt_secret')} |
|||
margin="normal" |
|||
/> |
|||
<Typography component="div" variant="body1"> |
|||
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}> |
|||
If you modify the JWT Secret, all users will be logged out. |
|||
</Box> |
|||
</Typography> |
|||
<Button variant="contained" color="primary" className={classes.button} type="submit"> |
|||
Save |
|||
</Button> |
|||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}> |
|||
Reset |
|||
</Button> |
|||
</ValidatorForm> |
|||
: |
|||
<div className={classes.loadingSettings}> |
|||
<Typography variant="h4" className={classes.loadingSettingsDetails}> |
|||
{errorMessage} |
|||
</Typography> |
|||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}> |
|||
Reset |
|||
</Button> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
SecuritySettingsForm.propTypes = { |
|||
classes: PropTypes.object.isRequired, |
|||
securitySettingsFetched: PropTypes.bool.isRequired, |
|||
securitySettings: PropTypes.object, |
|||
errorMessage: PropTypes.string, |
|||
onSubmit: PropTypes.func.isRequired, |
|||
onReset: PropTypes.func.isRequired, |
|||
handleValueChange: PropTypes.func.isRequired, |
|||
authenticationContext: PropTypes.object.isRequired, |
|||
}; |
|||
|
|||
export default withAuthenticationContext(withStyles(styles)(SecuritySettingsForm)); |
@ -0,0 +1,102 @@ |
|||
import React from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator'; |
|||
|
|||
import { withStyles } from '@material-ui/core/styles'; |
|||
import Button from '@material-ui/core/Button'; |
|||
|
|||
import FormControlLabel from '@material-ui/core/FormControlLabel'; |
|||
import Switch from '@material-ui/core/Switch'; |
|||
import FormGroup from '@material-ui/core/FormGroup'; |
|||
import DialogTitle from '@material-ui/core/DialogTitle'; |
|||
import Dialog from '@material-ui/core/Dialog'; |
|||
import DialogContent from '@material-ui/core/DialogContent'; |
|||
import DialogActions from '@material-ui/core/DialogActions'; |
|||
|
|||
import PasswordValidator from '../components/PasswordValidator'; |
|||
|
|||
const styles = theme => ({ |
|||
textField: { |
|||
width: "100%" |
|||
}, |
|||
button: { |
|||
margin: theme.spacing(0.5) |
|||
} |
|||
}); |
|||
|
|||
class UserForm extends React.Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.formRef = React.createRef(); |
|||
} |
|||
|
|||
componentWillMount() { |
|||
ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername); |
|||
} |
|||
|
|||
submit = () => { |
|||
this.formRef.current.submit(); |
|||
} |
|||
|
|||
render() { |
|||
const { classes, user, creating, handleValueChange, handleCheckboxChange, onDoneEditing, onCancelEditing } = this.props; |
|||
return ( |
|||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}> |
|||
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open={true}> |
|||
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle> |
|||
<DialogContent dividers={true}> |
|||
<TextValidator |
|||
validators={creating ? ['required', 'uniqueUsername', 'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'] : []} |
|||
errorMessages={creating ? ['Username is required', "Username already exists", "Must be 1-24 characters: alpha numeric, '_' or '.'"] : []} |
|||
name="username" |
|||
label="Username" |
|||
className={classes.textField} |
|||
value={user.username} |
|||
disabled={!creating} |
|||
onChange={handleValueChange('username')} |
|||
margin="normal" |
|||
/> |
|||
<PasswordValidator |
|||
validators={['required', 'matchRegexp:^.{1,64}$']} |
|||
errorMessages={['Password is required', 'Password must be 64 characters or less']} |
|||
name="password" |
|||
label="Password" |
|||
className={classes.textField} |
|||
value={user.password} |
|||
onChange={handleValueChange('password')} |
|||
margin="normal" |
|||
/> |
|||
<FormGroup> |
|||
<FormControlLabel |
|||
control={<Switch checked={user.admin} onChange={handleCheckboxChange('admin')} id="admin" />} |
|||
label="Admin?" |
|||
/> |
|||
</FormGroup> |
|||
</DialogContent> |
|||
<DialogActions> |
|||
<Button variant="contained" color="primary" className={classes.button} type="submit" onClick={this.submit}> |
|||
Done |
|||
</Button> |
|||
<Button variant="contained" color="secondary" className={classes.button} type="submit" onClick={onCancelEditing}> |
|||
Cancel |
|||
</Button> |
|||
</DialogActions> |
|||
</Dialog> |
|||
</ValidatorForm> |
|||
); |
|||
} |
|||
} |
|||
|
|||
UserForm.propTypes = { |
|||
classes: PropTypes.object.isRequired, |
|||
user: PropTypes.object.isRequired, |
|||
creating: PropTypes.bool.isRequired, |
|||
onDoneEditing: PropTypes.func.isRequired, |
|||
onCancelEditing: PropTypes.func.isRequired, |
|||
uniqueUsername: PropTypes.func.isRequired, |
|||
handleValueChange: PropTypes.func.isRequired, |
|||
handleCheckboxChange: PropTypes.func.isRequired |
|||
}; |
|||
|
|||
export default withStyles(styles)(UserForm); |
@ -0,0 +1,37 @@ |
|||
import React, { Component } from 'react'; |
|||
import { Redirect, Switch } from 'react-router-dom' |
|||
|
|||
import Tabs from '@material-ui/core/Tabs'; |
|||
import Tab from '@material-ui/core/Tab'; |
|||
|
|||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute'; |
|||
import MenuAppBar from '../components/MenuAppBar'; |
|||
import APSettings from '../containers/APSettings'; |
|||
import APStatus from '../containers/APStatus'; |
|||
import { withAuthenticationContext } from '../authentication/Context.js'; |
|||
|
|||
class AccessPoint extends Component { |
|||
|
|||
handleTabChange = (event, path) => { |
|||
this.props.history.push(path); |
|||
}; |
|||
|
|||
render() { |
|||
const { authenticationContext } = this.props; |
|||
return ( |
|||
<MenuAppBar sectionTitle="Access Point"> |
|||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth"> |
|||
<Tab value="/ap/status" label="Access Point Status" /> |
|||
<Tab value="/ap/settings" label="Access Point Settings" disabled={!authenticationContext.isAdmin()} /> |
|||
</Tabs> |
|||
<Switch> |
|||
<AuthenticatedRoute exact={true} path="/ap/status" component={APStatus} /> |
|||
<AuthenticatedRoute exact={true} path="/ap/settings" component={APSettings} /> |
|||
<Redirect to="/ap/status" /> |
|||
</Switch> |
|||
</MenuAppBar> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default withAuthenticationContext(AccessPoint); |
@ -0,0 +1,37 @@ |
|||
import React, { Component } from 'react'; |
|||
import { Redirect, Switch } from 'react-router-dom' |
|||
|
|||
import Tabs from '@material-ui/core/Tabs'; |
|||
import Tab from '@material-ui/core/Tab'; |
|||
|
|||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute'; |
|||
import MenuAppBar from '../components/MenuAppBar'; |
|||
import NTPSettings from '../containers/NTPSettings'; |
|||
import NTPStatus from '../containers/NTPStatus'; |
|||
import { withAuthenticationContext } from '../authentication/Context.js'; |
|||
|
|||
class NetworkTime extends Component { |
|||
|
|||
handleTabChange = (event, path) => { |
|||
this.props.history.push(path); |
|||
}; |
|||
|
|||
render() { |
|||
const { authenticationContext } = this.props; |
|||
return ( |
|||
<MenuAppBar sectionTitle="Network Time"> |
|||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth"> |
|||
<Tab value="/ntp/status" label="NTP Status" /> |
|||
<Tab value="/ntp/settings" label="NTP Settings" disabled={!authenticationContext.isAdmin()} /> |
|||
</Tabs> |
|||
<Switch> |
|||
<AuthenticatedRoute exact={true} path="/ntp/status" component={NTPStatus} /> |
|||
<AuthenticatedRoute exact={true} path="/ntp/settings" component={NTPSettings} /> |
|||
<Redirect to="/ntp/status" /> |
|||
</Switch> |
|||
</MenuAppBar> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default withAuthenticationContext(NetworkTime) |
@ -0,0 +1,35 @@ |
|||
import React, { Component } from 'react'; |
|||
import { Redirect, Switch } from 'react-router-dom' |
|||
|
|||
import Tabs from '@material-ui/core/Tabs'; |
|||
import Tab from '@material-ui/core/Tab'; |
|||
|
|||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute'; |
|||
import MenuAppBar from '../components/MenuAppBar'; |
|||
import ManageUsers from '../containers/ManageUsers'; |
|||
import SecuritySettings from '../containers/SecuritySettings'; |
|||
|
|||
class Security extends Component { |
|||
|
|||
handleTabChange = (event, path) => { |
|||
this.props.history.push(path); |
|||
}; |
|||
|
|||
render() { |
|||
return ( |
|||
<MenuAppBar sectionTitle="Security"> |
|||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth"> |
|||
<Tab value="/security/users" label="Manage Users" /> |
|||
<Tab value="/security/settings" label="Security Settings" /> |
|||
</Tabs> |
|||
<Switch> |
|||
<AuthenticatedRoute exact={true} path="/security/users" component={ManageUsers} /> |
|||
<AuthenticatedRoute exact={true} path="/security/settings" component={SecuritySettings} /> |
|||
<Redirect to="/security/users" /> |
|||
</Switch> |
|||
</MenuAppBar> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default Security; |
@ -0,0 +1,37 @@ |
|||
import React, { Component } from 'react'; |
|||
import { Redirect, Switch } from 'react-router-dom' |
|||
|
|||
import Tabs from '@material-ui/core/Tabs'; |
|||
import Tab from '@material-ui/core/Tab'; |
|||
|
|||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute'; |
|||
import MenuAppBar from '../components/MenuAppBar'; |
|||
import OTASettings from '../containers/OTASettings'; |
|||
import SystemStatus from '../containers/SystemStatus'; |
|||
import { withAuthenticationContext } from '../authentication/Context.js'; |
|||
|
|||
class System extends Component { |
|||
|
|||
handleTabChange = (event, path) => { |
|||
this.props.history.push(path); |
|||
}; |
|||
|
|||
render() { |
|||
const { authenticationContext } = this.props; |
|||
return ( |
|||
<MenuAppBar sectionTitle="System"> |
|||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth"> |
|||
<Tab value="/system/status" label="System Status" /> |
|||
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticationContext.isAdmin()} /> |
|||
</Tabs> |
|||
<Switch> |
|||
<AuthenticatedRoute exact={true} path="/system/status" component={SystemStatus} /> |
|||
<AuthenticatedRoute exact={true} path="/system/ota" component={OTASettings} /> |
|||
<Redirect to="/system/status" /> |
|||
</Switch> |
|||
</MenuAppBar> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default withAuthenticationContext(System); |
@ -0,0 +1,74 @@ |
|||
import React, { Component } from 'react'; |
|||
import { Redirect, Switch } from 'react-router-dom' |
|||
|
|||
import Tabs from '@material-ui/core/Tabs'; |
|||
import Tab from '@material-ui/core/Tab'; |
|||
|
|||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute'; |
|||
import MenuAppBar from '../components/MenuAppBar'; |
|||
import WiFiNetworkScanner from '../containers/WiFiNetworkScanner'; |
|||
import WiFiSettings from '../containers/WiFiSettings'; |
|||
import WiFiStatus from '../containers/WiFiStatus'; |
|||
import { withAuthenticationContext } from '../authentication/Context.js'; |
|||
|
|||
class WiFiConnection extends Component { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
selectedNetwork: null |
|||
}; |
|||
this.selectNetwork = this.selectNetwork.bind(this); |
|||
this.deselectNetwork = this.deselectNetwork.bind(this); |
|||
} |
|||
|
|||
selectNetwork(network) { |
|||
this.setState({ selectedNetwork: network }); |
|||
this.props.history.push('/wifi/settings'); |
|||
} |
|||
|
|||
deselectNetwork(network) { |
|||
this.setState({ selectedNetwork: null }); |
|||
} |
|||
|
|||
handleTabChange = (event, path) => { |
|||
this.props.history.push(path); |
|||
}; |
|||
|
|||
render() { |
|||
const { authenticationContext } = this.props; |
|||
const ConfiguredWiFiNetworkScanner = (props) => { |
|||
return ( |
|||
<WiFiNetworkScanner |
|||
selectNetwork={this.selectNetwork} |
|||
{...props} |
|||
/> |
|||
); |
|||
}; |
|||
const ConfiguredWiFiSettings = (props) => { |
|||
return ( |
|||
<WiFiSettings |
|||
deselectNetwork={this.deselectNetwork} selectedNetwork={this.state.selectedNetwork} |
|||
{...props} |
|||
/> |
|||
); |
|||
}; |
|||
return ( |
|||
<MenuAppBar sectionTitle="WiFi Connection"> |
|||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth"> |
|||
<Tab value="/wifi/status" label="WiFi Status" /> |
|||
<Tab value="/wifi/scan" label="Scan Networks" disabled={!authenticationContext.isAdmin()} /> |
|||
<Tab value="/wifi/settings" label="WiFi Settings" disabled={!authenticationContext.isAdmin()} /> |
|||
</Tabs> |
|||
<Switch> |
|||
<AuthenticatedRoute exact={true} path="/wifi/status" component={WiFiStatus} /> |
|||
<AuthenticatedRoute exact={true} path="/wifi/scan" component={ConfiguredWiFiNetworkScanner} /> |
|||
<AuthenticatedRoute exact={true} path="/wifi/settings" component={ConfiguredWiFiSettings} /> |
|||
<Redirect to="/wifi/status" /> |
|||
</Switch> |
|||
</MenuAppBar> |
|||
) |
|||
} |
|||
} |
|||
|
|||
export default withAuthenticationContext(WiFiConnection); |
@ -1 +1 @@ |
|||
export default validator => value => !value || validator(value); |
|||
export default validator => value => !value || validator(value); |
After Width: 609 | Height: 146 | Size: 8.4 KiB |
After Width: 310 | Height: 217 | Size: 17 KiB |
After Width: 310 | Height: 287 | Size: 26 KiB |
After Width: 856 | Height: 517 | Size: 51 KiB |
After Width: 609 | Height: 146 | Size: 8.4 KiB |
After Width: 609 | Height: 146 | Size: 8.6 KiB |
Before Width: 856 | Height: 523 | Size: 45 KiB |
@ -0,0 +1,143 @@ |
|||
#include "ArduinoJsonJWT.h"
|
|||
|
|||
ArduinoJsonJWT::ArduinoJsonJWT(String secret) : _secret(secret) { } |
|||
|
|||
void ArduinoJsonJWT::setSecret(String secret){ |
|||
_secret = secret; |
|||
} |
|||
|
|||
String ArduinoJsonJWT::getSecret(){ |
|||
return _secret; |
|||
} |
|||
|
|||
/*
|
|||
* ESP32 uses mbedtls, ESP2866 uses bearssl. |
|||
* |
|||
* Both come with decent HMAC implmentations supporting sha256, as well as others. |
|||
* |
|||
* No need to pull in additional crypto libraries - lets use what we already have. |
|||
*/ |
|||
String ArduinoJsonJWT::sign(String &payload) { |
|||
unsigned char hmacResult[32]; |
|||
{ |
|||
#if defined(ESP_PLATFORM)
|
|||
mbedtls_md_context_t ctx; |
|||
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; |
|||
mbedtls_md_init(&ctx); |
|||
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); |
|||
mbedtls_md_hmac_starts(&ctx, (unsigned char *) _secret.c_str(), _secret.length()); |
|||
mbedtls_md_hmac_update(&ctx, (unsigned char *) payload.c_str(), payload.length()); |
|||
mbedtls_md_hmac_finish(&ctx, hmacResult); |
|||
mbedtls_md_free(&ctx); |
|||
#else
|
|||
br_hmac_key_context keyCtx; |
|||
br_hmac_key_init(&keyCtx, &br_sha256_vtable, _secret.c_str(), _secret.length()); |
|||
br_hmac_context hmacCtx; |
|||
br_hmac_init(&hmacCtx, &keyCtx, 0); |
|||
br_hmac_update(&hmacCtx, payload.c_str(), payload.length()); |
|||
br_hmac_out(&hmacCtx, hmacResult); |
|||
#endif
|
|||
} |
|||
return encode((char *) hmacResult, 32); |
|||
} |
|||
|
|||
String ArduinoJsonJWT::buildJWT(JsonObject &payload) { |
|||
// serialize, then encode payload
|
|||
String jwt; |
|||
serializeJson(payload, jwt); |
|||
jwt = encode(jwt.c_str(), jwt.length()); |
|||
|
|||
// add the header to payload
|
|||
jwt = JWT_HEADER + '.' + jwt; |
|||
|
|||
// add signature
|
|||
jwt += '.' + sign(jwt); |
|||
|
|||
return jwt; |
|||
} |
|||
|
|||
void ArduinoJsonJWT::parseJWT(String jwt, JsonDocument &jsonDocument) { |
|||
// clear json document before we begin, jsonDocument wil be null on failure
|
|||
jsonDocument.clear(); |
|||
|
|||
// must have the correct header and delimiter
|
|||
if (!jwt.startsWith(JWT_HEADER) || jwt.indexOf('.') != JWT_HEADER_SIZE) { |
|||
return; |
|||
} |
|||
|
|||
// check there is a signature delimieter
|
|||
int signatureDelimiterIndex = jwt.lastIndexOf('.'); |
|||
if (signatureDelimiterIndex == JWT_HEADER_SIZE) { |
|||
return; |
|||
} |
|||
|
|||
// check the signature is valid
|
|||
String signature = jwt.substring(signatureDelimiterIndex + 1); |
|||
jwt = jwt.substring(0, signatureDelimiterIndex); |
|||
if (sign(jwt) != signature){ |
|||
return; |
|||
} |
|||
|
|||
// decode payload
|
|||
jwt = jwt.substring(JWT_HEADER_SIZE + 1); |
|||
jwt = decode(jwt); |
|||
|
|||
// parse payload, clearing json document after failure
|
|||
DeserializationError error = deserializeJson(jsonDocument, jwt); |
|||
if (error != DeserializationError::Ok || !jsonDocument.is<JsonObject>()){ |
|||
jsonDocument.clear(); |
|||
} |
|||
} |
|||
|
|||
String ArduinoJsonJWT::encode(const char *cstr, int inputLen) { |
|||
// prepare encoder
|
|||
base64_encodestate _state; |
|||
#if defined(ESP8266)
|
|||
base64_init_encodestate_nonewlines(&_state); |
|||
size_t encodedLength = base64_encode_expected_len_nonewlines(inputLen) + 1; |
|||
#elif defined(ESP_PLATFORM)
|
|||
base64_init_encodestate(&_state); |
|||
size_t encodedLength = base64_encode_expected_len(inputLen) + 1; |
|||
#endif
|
|||
// prepare buffer of correct length, returning an empty string on failure
|
|||
char* buffer = (char*) malloc(encodedLength * sizeof(char)); |
|||
if (buffer == nullptr) { |
|||
return ""; |
|||
} |
|||
|
|||
// encode to buffer
|
|||
int len = base64_encode_block(cstr, inputLen, &buffer[0], &_state); |
|||
len += base64_encode_blockend(&buffer[len], &_state); |
|||
buffer[len] = 0; |
|||
|
|||
// convert to arduino string, freeing buffer
|
|||
String value = String(buffer); |
|||
free(buffer); |
|||
buffer=nullptr; |
|||
|
|||
// remove padding and convert to URL safe form
|
|||
while (value.length() > 0 && value.charAt(value.length() - 1) == '='){ |
|||
value.remove(value.length() - 1); |
|||
} |
|||
value.replace('+', '-'); |
|||
value.replace('/', '_'); |
|||
|
|||
// return as string
|
|||
return value; |
|||
} |
|||
|
|||
String ArduinoJsonJWT::decode(String value) { |
|||
// convert to standard base64
|
|||
value.replace('-', '+'); |
|||
value.replace( '_', '/'); |
|||
|
|||
// prepare buffer of correct length
|
|||
char buffer[base64_decode_expected_len(value.length()) + 1]; |
|||
|
|||
// decode
|
|||
int len = base64_decode_chars(value.c_str(), value.length(), &buffer[0]); |
|||
buffer[len] = 0; |
|||
|
|||
// return as string
|
|||
return String(buffer); |
|||
} |
@ -0,0 +1,38 @@ |
|||
#ifndef ArduinoJsonJWT_H |
|||
#define ArduinoJsonJWT_H |
|||
|
|||
#include <Arduino.h> |
|||
#include <ArduinoJson.h> |
|||
#include <libb64/cdecode.h> |
|||
#include <libb64/cencode.h> |
|||
#if defined(ESP_PLATFORM) |
|||
#include <mbedtls/md.h> |
|||
#else |
|||
#include <bearssl/bearssl_hmac.h> |
|||
#endif |
|||
|
|||
class ArduinoJsonJWT { |
|||
|
|||
private: |
|||
String _secret; |
|||
|
|||
const String JWT_HEADER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; |
|||
const size_t JWT_HEADER_SIZE = JWT_HEADER.length(); |
|||
|
|||
String sign(String &value); |
|||
|
|||
static String encode(const char *cstr, int len); |
|||
static String decode(String value); |
|||
|
|||
public: |
|||
ArduinoJsonJWT(String secret); |
|||
|
|||
void setSecret(String secret); |
|||
String getSecret(); |
|||
|
|||
String buildJWT(JsonObject &payload); |
|||
void parseJWT(String jwt, JsonDocument &jsonDocument); |
|||
}; |
|||
|
|||
|
|||
#endif |
@ -0,0 +1,45 @@ |
|||
#include <AuthenticationService.h>
|
|||
|
|||
AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager): |
|||
_server(server), _securityManager(securityManager) { |
|||
server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, std::bind(&AuthenticationService::verifyAuthorization, this, std::placeholders::_1)); |
|||
|
|||
_signInHandler.setUri(SIGN_IN_PATH); |
|||
_signInHandler.setMethod(HTTP_POST); |
|||
_signInHandler.setMaxContentLength(MAX_AUTHENTICATION_SIZE); |
|||
_signInHandler.onRequest(std::bind(&AuthenticationService::signIn, this, std::placeholders::_1, std::placeholders::_2)); |
|||
server->addHandler(&_signInHandler); |
|||
} |
|||
|
|||
AuthenticationService::~AuthenticationService() {} |
|||
|
|||
/**
|
|||
* Verifys that the request supplied a valid JWT. |
|||
*/ |
|||
void AuthenticationService::verifyAuthorization(AsyncWebServerRequest *request) { |
|||
Authentication authentication = _securityManager->authenticateRequest(request); |
|||
request->send(authentication.isAuthenticated() ? 200: 401); |
|||
} |
|||
|
|||
/**
|
|||
* Signs in a user if the username and password match. Provides a JWT to be used in the Authorization header in subsequent requests. |
|||
*/ |
|||
void AuthenticationService::signIn(AsyncWebServerRequest *request, JsonDocument &jsonDocument){ |
|||
if (jsonDocument.is<JsonObject>()) { |
|||
String username = jsonDocument["username"]; |
|||
String password = jsonDocument["password"]; |
|||
Authentication authentication = _securityManager->authenticate(username, password); |
|||
if (authentication.isAuthenticated()) { |
|||
User* user = authentication.getUser(); |
|||
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_AUTHENTICATION_SIZE); |
|||
JsonObject jsonObject = response->getRoot(); |
|||
jsonObject["access_token"] = _securityManager->generateJWT(user); |
|||
response->setLength(); |
|||
request->send(response); |
|||
return; |
|||
} |
|||
} |
|||
AsyncWebServerResponse *response = request->beginResponse(401); |
|||
request->send(response); |
|||
} |
|||
|
@ -0,0 +1,33 @@ |
|||
#ifndef AuthenticationService_H_ |
|||
#define AuthenticationService_H_ |
|||
|
|||
#include <SecurityManager.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
#include <AsyncJsonWebHandler.h> |
|||
#include <AsyncArduinoJson6.h> |
|||
|
|||
#define VERIFY_AUTHORIZATION_PATH "/rest/verifyAuthorization" |
|||
#define SIGN_IN_PATH "/rest/signIn" |
|||
|
|||
#define MAX_AUTHENTICATION_SIZE 256 |
|||
|
|||
class AuthenticationService { |
|||
|
|||
public: |
|||
|
|||
AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager) ; |
|||
~AuthenticationService(); |
|||
|
|||
private: |
|||
// server instance |
|||
AsyncWebServer* _server; |
|||
SecurityManager* _securityManager; |
|||
AsyncJsonWebHandler _signInHandler; |
|||
|
|||
// endpoint functions |
|||
void signIn(AsyncWebServerRequest *request, JsonDocument &jsonDocument); |
|||
void verifyAuthorization(AsyncWebServerRequest *request); |
|||
|
|||
}; |
|||
|
|||
#endif // end SecurityManager_h |
@ -0,0 +1,68 @@ |
|||
#include <SecurityManager.h>
|
|||
|
|||
Authentication SecurityManager::authenticateRequest(AsyncWebServerRequest *request) { |
|||
AsyncWebHeader* authorizationHeader = request->getHeader(AUTHORIZATION_HEADER); |
|||
if (authorizationHeader) { |
|||
String value = authorizationHeader->value(); |
|||
if (value.startsWith(AUTHORIZATION_HEADER_PREFIX)){ |
|||
value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN); |
|||
return authenticateJWT(value); |
|||
} |
|||
} |
|||
return Authentication(); |
|||
} |
|||
|
|||
Authentication SecurityManager::authenticateJWT(String jwt) { |
|||
DynamicJsonDocument payloadDocument(MAX_JWT_SIZE); |
|||
_jwtHandler.parseJWT(jwt, payloadDocument); |
|||
if (payloadDocument.is<JsonObject>()) { |
|||
JsonObject parsedPayload = payloadDocument.as<JsonObject>(); |
|||
String username = parsedPayload["username"]; |
|||
for (User _user : _users) { |
|||
if (_user.getUsername() == username && validatePayload(parsedPayload, &_user)){ |
|||
return Authentication(_user); |
|||
} |
|||
} |
|||
} |
|||
return Authentication(); |
|||
} |
|||
|
|||
Authentication SecurityManager::authenticate(String username, String password) { |
|||
for (User _user : _users) { |
|||
if (_user.getUsername() == username && _user.getPassword() == password){ |
|||
return Authentication(_user); |
|||
} |
|||
} |
|||
return Authentication(); |
|||
} |
|||
|
|||
inline void populateJWTPayload(JsonObject &payload, User *user) { |
|||
payload["username"] = user->getUsername(); |
|||
payload["admin"] = user -> isAdmin(); |
|||
} |
|||
|
|||
boolean SecurityManager::validatePayload(JsonObject &parsedPayload, User *user) { |
|||
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE); |
|||
JsonObject payload = _jsonDocument.to<JsonObject>(); |
|||
populateJWTPayload(payload, user); |
|||
return payload == parsedPayload; |
|||
} |
|||
|
|||
String SecurityManager::generateJWT(User *user) { |
|||
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE); |
|||
JsonObject payload = _jsonDocument.to<JsonObject>(); |
|||
populateJWTPayload(payload, user); |
|||
return _jwtHandler.buildJWT(payload); |
|||
} |
|||
|
|||
ArRequestHandlerFunction SecurityManager::wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate) { |
|||
return [this, onRequest, predicate](AsyncWebServerRequest *request){ |
|||
Authentication authentication = authenticateRequest(request); |
|||
if (!predicate(authentication)) { |
|||
request->send(401); |
|||
return; |
|||
} |
|||
onRequest(request); |
|||
}; |
|||
} |
|||
|
@ -0,0 +1,110 @@ |
|||
#ifndef SecurityManager_h |
|||
#define SecurityManager_h |
|||
|
|||
#include <list> |
|||
#include <ArduinoJsonJWT.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
|
|||
#define DEFAULT_JWT_SECRET "esp8266-react" |
|||
|
|||
#define AUTHORIZATION_HEADER "Authorization" |
|||
#define AUTHORIZATION_HEADER_PREFIX "Bearer " |
|||
#define AUTHORIZATION_HEADER_PREFIX_LEN 7 |
|||
|
|||
#define MAX_JWT_SIZE 128 |
|||
|
|||
class User { |
|||
private: |
|||
String _username; |
|||
String _password; |
|||
bool _admin; |
|||
public: |
|||
User(String username, String password, bool admin): _username(username), _password(password), _admin(admin) {} |
|||
String getUsername() { |
|||
return _username; |
|||
} |
|||
String getPassword() { |
|||
return _password; |
|||
} |
|||
bool isAdmin() { |
|||
return _admin; |
|||
} |
|||
}; |
|||
|
|||
class Authentication { |
|||
private: |
|||
User *_user; |
|||
boolean _authenticated; |
|||
public: |
|||
Authentication(User& user): _user(new User(user)), _authenticated(true) {} |
|||
Authentication() : _user(nullptr), _authenticated(false) {} |
|||
~Authentication() { |
|||
delete(_user); |
|||
} |
|||
User* getUser() { |
|||
return _user; |
|||
} |
|||
bool isAuthenticated() { |
|||
return _authenticated; |
|||
} |
|||
}; |
|||
|
|||
typedef std::function<boolean(Authentication &authentication)> AuthenticationPredicate; |
|||
|
|||
class AuthenticationPredicates { |
|||
public: |
|||
static bool NONE_REQUIRED(Authentication &authentication) { |
|||
return true; |
|||
}; |
|||
static bool IS_AUTHENTICATED(Authentication &authentication) { |
|||
return authentication.isAuthenticated(); |
|||
}; |
|||
static bool IS_ADMIN(Authentication &authentication) { |
|||
return authentication.isAuthenticated() && authentication.getUser()->isAdmin(); |
|||
}; |
|||
}; |
|||
|
|||
class SecurityManager { |
|||
|
|||
public: |
|||
|
|||
/* |
|||
* Authenticate, returning the user if found |
|||
*/ |
|||
Authentication authenticate(String username, String password); |
|||
|
|||
/* |
|||
* Check the request header for the Authorization token |
|||
*/ |
|||
Authentication authenticateRequest(AsyncWebServerRequest *request); |
|||
|
|||
/* |
|||
* Generate a JWT for the user provided |
|||
*/ |
|||
String generateJWT(User *user); |
|||
|
|||
/** |
|||
* Wrap the provided request to provide validation against an AuthenticationPredicate. |
|||
*/ |
|||
ArRequestHandlerFunction wrapRequest(ArRequestHandlerFunction onRequest, AuthenticationPredicate predicate); |
|||
|
|||
protected: |
|||
|
|||
ArduinoJsonJWT _jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET); |
|||
std::list<User> _users; |
|||
|
|||
private: |
|||
|
|||
/* |
|||
* Lookup the user by JWT |
|||
*/ |
|||
Authentication authenticateJWT(String jwt); |
|||
|
|||
/* |
|||
* Verify the payload is correct |
|||
*/ |
|||
boolean validatePayload(JsonObject &parsedPayload, User *user); |
|||
|
|||
}; |
|||
|
|||
#endif // end SecurityManager_h |
@ -0,0 +1,35 @@ |
|||
#include <SecuritySettingsService.h>
|
|||
|
|||
SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) : AdminSettingsService(server, fs, this, SECURITY_SETTINGS_PATH, SECURITY_SETTINGS_FILE), SecurityManager() {} |
|||
SecuritySettingsService::~SecuritySettingsService() {} |
|||
|
|||
void SecuritySettingsService::readFromJsonObject(JsonObject& root) { |
|||
// secret
|
|||
_jwtHandler.setSecret(root["jwt_secret"] | DEFAULT_JWT_SECRET); |
|||
|
|||
// users
|
|||
_users.clear(); |
|||
if (root["users"].is<JsonArray>()) { |
|||
for (JsonVariant user : root["users"].as<JsonArray>()) { |
|||
_users.push_back(User(user["username"], user["password"], user["admin"])); |
|||
} |
|||
} |
|||
} |
|||
|
|||
void SecuritySettingsService::writeToJsonObject(JsonObject& root) { |
|||
// secret
|
|||
root["jwt_secret"] = _jwtHandler.getSecret(); |
|||
|
|||
// users
|
|||
JsonArray users = root.createNestedArray("users"); |
|||
for (User _user : _users) { |
|||
JsonObject user = users.createNestedObject(); |
|||
user["username"] = _user.getUsername(); |
|||
user["password"] = _user.getPassword(); |
|||
user["admin"] = _user.isAdmin(); |
|||
} |
|||
} |
|||
|
|||
void SecuritySettingsService::begin() { |
|||
readFromFS(); |
|||
} |
@ -0,0 +1,26 @@ |
|||
#ifndef SecuritySettingsService_h |
|||
#define SecuritySettingsService_h |
|||
|
|||
#include <SettingsService.h> |
|||
#include <SecurityManager.h> |
|||
|
|||
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json" |
|||
#define SECURITY_SETTINGS_PATH "/rest/securitySettings" |
|||
|
|||
class SecuritySettingsService : public AdminSettingsService, public SecurityManager { |
|||
|
|||
public: |
|||
|
|||
SecuritySettingsService(AsyncWebServer* server, FS* fs); |
|||
~SecuritySettingsService(); |
|||
|
|||
void begin(); |
|||
|
|||
protected: |
|||
|
|||
void readFromJsonObject(JsonObject& root); |
|||
void writeToJsonObject(JsonObject& root); |
|||
|
|||
}; |
|||
|
|||
#endif // end SecuritySettingsService_h |
@ -0,0 +1,26 @@ |
|||
#include <SystemStatus.h>
|
|||
|
|||
SystemStatus::SystemStatus(AsyncWebServer *server, SecurityManager* securityManager) : _server(server), _securityManager(securityManager) { |
|||
_server->on(SYSTEM_STATUS_SERVICE_PATH, HTTP_GET, |
|||
_securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED) |
|||
); |
|||
} |
|||
|
|||
void SystemStatus::systemStatus(AsyncWebServerRequest *request) { |
|||
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_ESP_STATUS_SIZE); |
|||
JsonObject root = response->getRoot(); |
|||
#if defined(ESP8266)
|
|||
root["esp_platform"] = "esp8266"; |
|||
#elif defined(ESP_PLATFORM)
|
|||
root["esp_platform"] = "esp32"; |
|||
#endif
|
|||
root["cpu_freq_mhz"] = ESP.getCpuFreqMHz(); |
|||
root["free_heap"] = ESP.getFreeHeap(); |
|||
root["sketch_size"] = ESP.getSketchSize(); |
|||
root["free_sketch_space"] = ESP.getFreeSketchSpace(); |
|||
root["sdk_version"] = ESP.getSdkVersion(); |
|||
root["flash_chip_size"] = ESP.getFlashChipSize(); |
|||
root["flash_chip_speed"] = ESP.getFlashChipSpeed(); |
|||
response->setLength(); |
|||
request->send(response); |
|||
} |
@ -0,0 +1,35 @@ |
|||
#ifndef SystemStatus_h |
|||
#define SystemStatus_h |
|||
|
|||
#if defined(ESP8266) |
|||
#include <ESP8266WiFi.h> |
|||
#include <ESPAsyncTCP.h> |
|||
#elif defined(ESP_PLATFORM) |
|||
#include <WiFi.h> |
|||
#include <AsyncTCP.h> |
|||
#endif |
|||
|
|||
#include <ESPAsyncWebServer.h> |
|||
#include <ArduinoJson.h> |
|||
#include <AsyncArduinoJson6.h> |
|||
#include <SecurityManager.h> |
|||
|
|||
#define MAX_ESP_STATUS_SIZE 1024 |
|||
#define SYSTEM_STATUS_SERVICE_PATH "/rest/systemStatus" |
|||
|
|||
class SystemStatus { |
|||
|
|||
public: |
|||
|
|||
SystemStatus(AsyncWebServer *server, SecurityManager* securityManager); |
|||
|
|||
private: |
|||
|
|||
AsyncWebServer* _server; |
|||
SecurityManager* _securityManager; |
|||
|
|||
void systemStatus(AsyncWebServerRequest *request); |
|||
|
|||
}; |
|||
|
|||
#endif // end SystemStatus_h |