add security form, begin work on routing

This commit is contained in:
Rick Watson 2019-05-26 19:09:34 +01:00
parent 353b46c675
commit 6e5b35978a
11 changed files with 263 additions and 120 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,35 @@
import React, { Component } from 'react'; 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 MenuAppBar from '../components/MenuAppBar';
import ManageUsers from './ManageUsers'; import ManageUsers from './ManageUsers';
import SecuritySettings from './SecuritySettings';
class Security extends Component { class Security extends Component {
handleTabChange = (event, path) => {
this.props.history.push(path);
};
render() { render() {
return ( return (
<MenuAppBar sectionTitle="Security"> <MenuAppBar sectionTitle="Security">
<ManageUsers /> <Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
</MenuAppBar> <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;

View File

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

View File

@ -21,7 +21,6 @@ import CloseIcon from '@material-ui/icons/Close';
import CheckIcon from '@material-ui/icons/Check'; import CheckIcon from '@material-ui/icons/Check';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import SectionContent from '../components/SectionContent';
import UserForm from './UserForm'; import UserForm from './UserForm';
import { withAuthenticationContext } from '../authentication/Context'; import { withAuthenticationContext } from '../authentication/Context';
@ -138,108 +137,101 @@ class ManageUsersForm extends React.Component {
const { classes, userData, userDataFetched, errorMessage, onReset } = this.props; const { classes, userData, userDataFetched, errorMessage, onReset } = this.props;
const { user, creating } = this.state; const { user, creating } = this.state;
return ( return (
<SectionContent title="Manage Users"> !userDataFetched ?
{ <div className={classes.loadingSettings}>
!userDataFetched ? <LinearProgress className={classes.loadingSettingsDetails} />
<div className={classes.loadingSettings}> <Typography variant="h4" className={classes.loadingSettingsDetails}>
<LinearProgress className={classes.loadingSettingsDetails} /> Loading...
<Typography variant="h4" className={classes.loadingSettingsDetails}> </Typography>
Loading... </div>
</Typography> :
</div> userData ?
: <Fragment>
userData ? <ValidatorForm onSubmit={this.onSubmit}>
<Fragment> <Table className={classes.table}>
<ValidatorForm onSubmit={this.onSubmit}> <TableHead>
<Table className={classes.table}> <TableRow>
<TableHead> <TableCell>Username</TableCell>
<TableRow> <TableCell align="center">Admin?</TableCell>
<TableCell>Username</TableCell> <TableCell />
<TableCell align="center">Admin?</TableCell> </TableRow>
<TableCell /> </TableHead>
</TableRow> <TableBody>
</TableHead> {userData.users.sort(compareUsers).map(user => (
<TableBody> <TableRow key={user.username}>
{userData.users.sort(compareUsers).map(user => ( <TableCell component="th" scope="row">
<TableRow key={user.username}> {user.username}
<TableCell component="th" scope="row"> </TableCell>
{user.username} <TableCell align="center">
</TableCell> {
<TableCell align="center"> user.admin ? <CheckIcon /> : <CloseIcon />
{ }
user.admin ? <CheckIcon /> : <CloseIcon /> </TableCell>
} <TableCell align="center">
</TableCell> <IconButton aria-label="Delete" onClick={() => this.removeUser(user)}>
<TableCell align="center"> <DeleteIcon />
<IconButton aria-label="Delete" onClick={() => this.removeUser(user)}> </IconButton>
<DeleteIcon /> <IconButton aria-label="Edit" onClick={() => this.startEditingUser(user)}>
</IconButton> <EditIcon />
<IconButton aria-label="Edit" onClick={() => this.startEditingUser(user)}> </IconButton>
<EditIcon /> </TableCell>
</IconButton> </TableRow>
</TableCell> ))}
</TableRow> </TableBody>
))} <TableFooter>
</TableBody> <TableRow>
<TableFooter> <TableCell colSpan={2} />
<TableRow> <TableCell align="center">
<TableCell colSpan={2} /> <Button variant="contained" color="secondary" onClick={this.createUser}>
<TableCell align="center"> Add User
<Button variant="contained" color="secondary" className={classes.button} onClick={this.createUser}> </Button>
Add User </TableCell>
</Button> </TableRow>
</TableCell> </TableFooter>
</TableRow> </Table>
</TableFooter> {
</Table> this.noAdminConfigured() &&
{ <Typography component="div" variant="body1">
this.noAdminConfigured() && <Box bgcolor="error.main" color="error.contrastText" p={2} m={2}>
<Typography component="div" variant="body1"> You must have at least one admin user configured.
<Box bgcolor="error.main" color="error.contrastText" p={2} m={1}> </Box>
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}
</Typography> </Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}> }
Reset <Button variant="contained" color="primary" className={classes.button} type="submit" disabled={this.noAdminConfigured()}>
</Button> Save
</SectionContent> </Button>
} <Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
</SectionContent> 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 = { ManageUsersForm.propTypes = {
authenticationContext: PropTypes.object.isRequired,
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
userData: PropTypes.object, userData: PropTypes.object,
userDataFetched: PropTypes.bool.isRequired, userDataFetched: PropTypes.bool.isRequired,
@ -247,7 +239,8 @@ ManageUsersForm.propTypes = {
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired, onReset: PropTypes.func.isRequired,
setData: 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)); export default withAuthenticationContext(withStyles(styles)(ManageUsersForm));

View File

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

View File

@ -44,7 +44,7 @@ class UserForm extends React.Component {
return ( return (
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}> <ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open={true} scroll="paper"> <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}> <DialogContent dividers={true}>
<TextValidator <TextValidator
validators={creating ? ['required', 'uniqueUsername', 'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'] : []} validators={creating ? ['required', 'uniqueUsername', 'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'] : []}

View File

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

View File

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