add security form, begin work on routing
This commit is contained in:
parent
353b46c675
commit
6e5b35978a
@ -1 +1 @@
|
||||
REACT_APP_ENDPOINT_ROOT=http://192.168.0.16/rest/
|
||||
REACT_APP_ENDPOINT_ROOT=http://192.168.0.11/rest/
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
Normal file
32
interface/src/containers/SecuritySettings.js
Normal 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);
|
@ -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
Normal file
97
interface/src/forms/SecuritySettingsForm.js
Normal 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));
|
@ -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}$'] : []}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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…
Reference in New Issue
Block a user