Browse Source

add security form, begin work on routing

master
Rick Watson 5 years ago
parent
commit
6e5b35978a
  1. 2
      interface/.env.development
  2. 2
      interface/src/AppRouting.js
  3. 2
      interface/src/components/MenuAppBar.js
  4. 21
      interface/src/containers/ManageUsers.js
  5. 28
      interface/src/containers/Security.js
  6. 32
      interface/src/containers/SecuritySettings.js
  7. 187
      interface/src/forms/ManageUsersForm.js
  8. 97
      interface/src/forms/SecuritySettingsForm.js
  9. 2
      interface/src/forms/UserForm.js
  10. 8
      src/SecurityManager.cpp
  11. 2
      src/SecurityManager.h

2
interface/.env.development

@ -1 +1 @@
REACT_APP_ENDPOINT_ROOT=http://192.168.0.16/rest/
REACT_APP_ENDPOINT_ROOT=http://192.168.0.11/rest/

2
interface/src/AppRouting.js

@ -31,7 +31,7 @@ class AppRouting extends Component {
<AuthenticatedRoute exact path="/ap-configuration" component={APConfiguration} />
<AuthenticatedRoute exact path="/ntp-configuration" component={NTPConfiguration} />
<AuthenticatedRoute exact path="/ota-configuration" component={OTAConfiguration} />
<AuthenticatedRoute exact path="/security" component={Security} />
<AuthenticatedRoute exact path="/security/*" component={Security} />
<Redirect to="/" />
</Switch>
</AuthenticationWrapper>

2
interface/src/components/MenuAppBar.js

@ -137,7 +137,7 @@ class MenuAppBar extends React.Component {
</ListItemIcon>
<ListItemText primary="OTA Updates" />
</ListItem>
<ListItem button component={Link} to='/security'>
<ListItem button component={Link} to='/security/'>
<ListItemIcon>
<LockIcon />
</ListItemIcon>

21
interface/src/containers/ManageUsers.js

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { USERS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent';
import ManageUsersForm from '../forms/ManageUsersForm';
import SectionContent from '../components/SectionContent';
class ManageUsers extends Component {
@ -13,15 +14,17 @@ class ManageUsers extends Component {
render() {
const { data, fetched, errorMessage } = this.props;
return (
<ManageUsersForm
userData={data}
userDataFetched={fetched}
errorMessage={errorMessage}
onSubmit={this.props.saveData}
onReset={this.props.loadData}
setData={this.props.setData}
handleValueChange={this.props.handleValueChange}
/>
<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>
)
}

28
interface/src/containers/Security.js

@ -1,15 +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 './ManageUsers';
import SecuritySettings from './SecuritySettings';
class Security extends Component {
handleTabChange = (event, path) => {
this.props.history.push(path);
};
render() {
return (
<MenuAppBar sectionTitle="Security">
<ManageUsers />
</MenuAppBar>
<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
export default Security;

32
interface/src/containers/SecuritySettings.js

@ -0,0 +1,32 @@
import React, { Component } from 'react';
import { USERS_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(USERS_ENDPOINT, SecuritySettings);

187
interface/src/forms/ManageUsersForm.js

@ -21,7 +21,6 @@ import CloseIcon from '@material-ui/icons/Close';
import CheckIcon from '@material-ui/icons/Check';
import IconButton from '@material-ui/core/IconButton';
import SectionContent from '../components/SectionContent';
import UserForm from './UserForm';
import { withAuthenticationContext } from '../authentication/Context';
@ -138,108 +137,101 @@ class ManageUsersForm extends React.Component {
const { classes, userData, userDataFetched, errorMessage, onReset } = this.props;
const { user, creating } = this.state;
return (
<SectionContent title="Manage Users">
{
!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" className={classes.button} 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} m={1}>
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>
:
<SectionContent title="Manage Users">
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
!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} m={2}>
You must have at least one admin user configured.
</Box>
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</SectionContent>
}
</SectionContent>
}
<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 = {
authenticationContext: PropTypes.object.isRequired,
classes: PropTypes.object.isRequired,
userData: PropTypes.object,
userDataFetched: PropTypes.bool.isRequired,
@ -247,7 +239,8 @@ ManageUsersForm.propTypes = {
onSubmit: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
setData: PropTypes.func.isRequired,
handleValueChange: PropTypes.func.isRequired
handleValueChange: PropTypes.func.isRequired,
authenticationContext: PropTypes.object.isRequired,
};
export default withAuthenticationContext(withStyles(styles)(ManageUsersForm));

97
interface/src/forms/SecuritySettingsForm.js

@ -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.unit,
},
loadingSettingsDetails: {
margin: theme.spacing.unit * 4,
textAlign: "center"
},
textField: {
width: "100%"
},
button: {
marginRight: theme.spacing.unit * 2,
marginTop: theme.spacing.unit * 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:^.{0,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} m={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));

2
interface/src/forms/UserForm.js

@ -44,7 +44,7 @@ class UserForm extends React.Component {
return (
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open={true} scroll="paper">
<DialogTitle id="user-form-dialog-title">{creating ? 'Create' : 'Modify'} User</DialogTitle>
<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}$'] : []}

8
src/SecurityManager.cpp

@ -6,6 +6,7 @@ SecurityManager::~SecurityManager() {}
void SecurityManager::readFromJsonObject(JsonObject& root) {
// secret
_jwtSecret = root["jwt_secret"] | DEFAULT_JWT_SECRET;
_jwtHandler.setSecret(_jwtSecret);
// users
_users.clear();
@ -34,9 +35,6 @@ void SecurityManager::writeToJsonObject(JsonObject& root) {
void SecurityManager::begin() {
// read config
readFromFS();
// configure secret
jwtHandler.setSecret(_jwtSecret);
}
Authentication SecurityManager::authenticateRequest(AsyncWebServerRequest *request) {
@ -53,7 +51,7 @@ Authentication SecurityManager::authenticateRequest(AsyncWebServerRequest *reque
Authentication SecurityManager::authenticateJWT(String jwt) {
DynamicJsonDocument payloadDocument(MAX_JWT_SIZE);
jwtHandler.parseJWT(jwt, payloadDocument);
_jwtHandler.parseJWT(jwt, payloadDocument);
if (payloadDocument.is<JsonObject>()) {
JsonObject parsedPayload = payloadDocument.as<JsonObject>();
String username = parsedPayload["username"];
@ -91,5 +89,5 @@ String SecurityManager::generateJWT(User *user) {
DynamicJsonDocument _jsonDocument(MAX_JWT_SIZE);
JsonObject payload = _jsonDocument.to<JsonObject>();
populateJWTPayload(payload, user);
return jwtHandler.buildJWT(payload);
return _jwtHandler.buildJWT(payload);
}

2
src/SecurityManager.h

@ -93,7 +93,7 @@ class SecurityManager : public SettingsService {
private:
// jwt handler
ArduinoJsonJWT jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET);
ArduinoJsonJWT _jwtHandler = ArduinoJsonJWT(DEFAULT_JWT_SECRET);
// access point settings
String _jwtSecret;

Loading…
Cancel
Save