fix: move showing version to settings. fixes JWT resetting after each version change

This commit is contained in:
proddy
2021-05-14 12:43:11 +02:00
parent 47eaeba373
commit 1ecee740d3
6 changed files with 354 additions and 148 deletions

View File

@@ -1,12 +1,39 @@
import React, { RefObject, Fragment } from 'react';
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, Box, IconButton } from '@material-ui/core';
import { ClickAwayListener, Popper, Hidden, Typography } from '@material-ui/core';
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
import {
Drawer,
AppBar,
Toolbar,
Avatar,
Divider,
Button,
Box,
IconButton
} from '@material-ui/core';
import {
ClickAwayListener,
Popper,
Hidden,
Typography
} from '@material-ui/core';
import {
List,
ListItem,
ListItemIcon,
ListItemText,
ListItemAvatar
} from '@material-ui/core';
import { Card, CardContent, CardActions } from '@material-ui/core';
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
import {
withStyles,
createStyles,
Theme,
WithTheme,
WithStyles,
withTheme
} from '@material-ui/core/styles';
import SettingsEthernetIcon from '@material-ui/icons/SettingsEthernet';
import SettingsIcon from '@material-ui/icons/Settings';
@@ -19,20 +46,24 @@ import MenuIcon from '@material-ui/icons/Menu';
import ProjectMenu from '../project/ProjectMenu';
import { PROJECT_NAME } from '../api';
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import {
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
const drawerWidth = 290;
const styles = (theme: Theme) => createStyles({
const styles = (theme: Theme) =>
createStyles({
root: {
display: 'flex',
display: 'flex'
},
drawer: {
[theme.breakpoints.up('md')]: {
width: drawerWidth,
flexShrink: 0,
},
flexShrink: 0
}
},
title: {
flexGrow: 1
@@ -40,8 +71,8 @@ const styles = (theme: Theme) => createStyles({
appBar: {
marginLeft: drawerWidth,
[theme.breakpoints.up('md')]: {
width: `calc(100% - ${drawerWidth}px)`,
},
width: `calc(100% - ${drawerWidth}px)`
}
},
toolbarImage: {
[theme.breakpoints.up('xs')]: {
@@ -51,31 +82,31 @@ const styles = (theme: Theme) => createStyles({
[theme.breakpoints.up('sm')]: {
height: 36,
marginRight: theme.spacing(3)
},
}
},
menuButton: {
marginRight: theme.spacing(2),
[theme.breakpoints.up('md')]: {
display: 'none',
},
display: 'none'
}
},
toolbar: theme.mixins.toolbar,
drawerPaper: {
width: drawerWidth,
width: drawerWidth
},
content: {
flexGrow: 1
},
authMenu: {
zIndex: theme.zIndex.tooltip,
maxWidth: 400,
maxWidth: 400
},
authMenuActions: {
padding: theme.spacing(2),
"& > * + *": {
marginLeft: theme.spacing(2),
'& > * + *': {
marginLeft: theme.spacing(2)
}
}
},
});
interface MenuAppBarState {
@@ -83,12 +114,16 @@ interface MenuAppBarState {
authMenuOpen: boolean;
}
interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
interface MenuAppBarProps
extends WithFeaturesProps,
AuthenticatedContextProps,
WithTheme,
WithStyles<typeof styles>,
RouteComponentProps {
sectionTitle: string;
}
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
constructor(props: MenuAppBarProps) {
super(props);
this.state = {
@@ -101,38 +136,48 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
handleToggle = () => {
this.setState({ authMenuOpen: !this.state.authMenuOpen });
}
};
handleClose = (event: React.MouseEvent<Document>) => {
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
if (
this.anchorRef.current &&
this.anchorRef.current.contains(event.currentTarget)
) {
return;
}
this.setState({ authMenuOpen: false });
}
};
handleDrawerToggle = () => {
this.setState({ mobileOpen: !this.state.mobileOpen });
};
render() {
const { classes, theme, children, sectionTitle, authenticatedContext, features } = this.props;
const {
classes,
theme,
children,
sectionTitle,
authenticatedContext,
features
} = this.props;
const { mobileOpen, authMenuOpen } = this.state;
const path = this.props.match.url;
const drawer = (
<div>
<Toolbar>
<Box display="flex">
<img src="/app/icon.png" className={classes.toolbarImage} alt={PROJECT_NAME} />
<img
src="/app/icon.png"
className={classes.toolbarImage}
alt={PROJECT_NAME}
/>
</Box>
<Typography variant="h6" color="textPrimary">
{PROJECT_NAME}
</Typography>
<Typography align="right" variant="caption" color="textPrimary">
&nbsp;&nbsp;v{authenticatedContext.me.version}
</Typography>
<Divider absolute />
</Toolbar>
@@ -144,20 +189,35 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
)}
<List>
<ListItem to='/network/' selected={path.startsWith('/network/')} button component={Link}>
<ListItem
to="/network/"
selected={path.startsWith('/network/')}
button
component={Link}
>
<ListItemIcon>
<SettingsEthernetIcon />
</ListItemIcon>
<ListItemText primary="Network Connection" />
</ListItem>
<ListItem to='/ap/' selected={path.startsWith('/ap/')} button component={Link}>
<ListItem
to="/ap/"
selected={path.startsWith('/ap/')}
button
component={Link}
>
<ListItemIcon>
<SettingsInputAntennaIcon />
</ListItemIcon>
<ListItemText primary="Access Point" />
</ListItem>
{features.ntp && (
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
<ListItem
to="/ntp/"
selected={path.startsWith('/ntp/')}
button
component={Link}
>
<ListItemIcon>
<AccessTimeIcon />
</ListItemIcon>
@@ -165,7 +225,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
</ListItem>
)}
{features.mqtt && (
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
<ListItem
to="/mqtt/"
selected={path.startsWith('/mqtt/')}
button
component={Link}
>
<ListItemIcon>
<DeviceHubIcon />
</ListItemIcon>
@@ -173,14 +238,25 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
</ListItem>
)}
{features.security && (
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
<ListItem
to="/security/"
selected={path.startsWith('/security/')}
button
component={Link}
disabled={!authenticatedContext.me.admin}
>
<ListItemIcon>
<LockIcon />
</ListItemIcon>
<ListItemText primary="Security" />
</ListItem>
)}
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
<ListItem
to="/system/"
selected={path.startsWith('/system/')}
button
component={Link}
>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
@@ -201,7 +277,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
>
<AccountCircleIcon />
</IconButton>
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
<Popper
open={authMenuOpen}
anchorEl={this.anchorRef.current}
transition
className={classes.authMenu}
>
<ClickAwayListener onClickAway={this.handleClose}>
<Card id="menu-list-grow">
<CardContent>
@@ -212,13 +293,27 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
<AccountCircleIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
<ListItemText
primary={
'Signed in as: ' + authenticatedContext.me.username
}
secondary={
authenticatedContext.me.admin ? 'Admin User' : undefined
}
/>
</ListItem>
</List>
</CardContent>
<Divider />
<CardActions className={classes.authMenuActions}>
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
<Button
variant="contained"
fullWidth
color="primary"
onClick={authenticatedContext.signOut}
>
Sign Out
</Button>
</CardActions>
</Card>
</ClickAwayListener>
@@ -239,7 +334,12 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
>
<MenuIcon />
</IconButton>
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
<Typography
variant="h6"
color="inherit"
noWrap
className={classes.title}
>
{sectionTitle}
</Typography>
{features.security && userMenu}
@@ -253,10 +353,10 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
open={mobileOpen}
onClose={this.handleDrawerToggle}
classes={{
paper: classes.drawerPaper,
paper: classes.drawerPaper
}}
ModalProps={{
keepMounted: true,
keepMounted: true
}}
>
{drawer}
@@ -265,7 +365,7 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
<Hidden smDown implementation="css">
<Drawer
classes={{
paper: classes.drawerPaper,
paper: classes.drawerPaper
}}
variant="permanent"
open
@@ -285,10 +385,6 @@ class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
export default withRouter(
withTheme(
withFeatures(
withAuthenticatedContext(
withStyles(styles)(MenuAppBar)
)
)
withFeatures(withAuthenticatedContext(withStyles(styles)(MenuAppBar)))
)
);

View File

@@ -1,7 +1,21 @@
import React, { Component, Fragment } from 'react';
import { Avatar, Button, Divider, Dialog, DialogTitle, DialogContent, DialogActions, Box } from '@material-ui/core';
import { List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
import {
Avatar,
Button,
Divider,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box
} from '@material-ui/core';
import {
List,
ListItem,
ListItemAvatar,
ListItemText
} from '@material-ui/core';
import DevicesIcon from '@material-ui/icons/Devices';
import MemoryIcon from '@material-ui/icons/Memory';
@@ -11,9 +25,14 @@ import AppsIcon from '@material-ui/icons/Apps';
import PowerSettingsNewIcon from '@material-ui/icons/PowerSettingsNew';
import RefreshIcon from '@material-ui/icons/Refresh';
import SettingsBackupRestoreIcon from '@material-ui/icons/SettingsBackupRestore';
import TimerIcon from "@material-ui/icons/Timer";
import TimerIcon from '@material-ui/icons/Timer';
import BuildIcon from '@material-ui/icons/Build';
import { redirectingAuthorizedFetch, AuthenticatedContextProps, withAuthenticatedContext } from '../authentication';
import {
redirectingAuthorizedFetch,
AuthenticatedContextProps,
withAuthenticatedContext
} from '../authentication';
import { RestFormProps, FormButton, ErrorButton } from '../components';
import { FACTORY_RESET_ENDPOINT, RESTART_ENDPOINT } from '../api';
@@ -25,31 +44,48 @@ interface SystemStatusFormState {
processing: boolean;
}
type SystemStatusFormProps = AuthenticatedContextProps & RestFormProps<SystemStatus>;
type SystemStatusFormProps = AuthenticatedContextProps &
RestFormProps<SystemStatus>;
function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusFormState> {
class SystemStatusForm extends Component<
SystemStatusFormProps,
SystemStatusFormState
> {
state: SystemStatusFormState = {
confirmRestart: false,
confirmFactoryReset: false,
processing: false
}
};
createListItems() {
const { data } = this.props
const { data } = this.props;
return (
<Fragment>
<ListItem>
<ListItemAvatar>
<Avatar>
<BuildIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="EMS-ESP Version"
secondary={'v' + data.emsesp_version}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<DevicesIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Device (Platform / SDK)" secondary={data.esp_platform + ' / ' + data.sdk_version} />
<ListItemText
primary="Device (Platform / SDK)"
secondary={data.esp_platform + ' / ' + data.sdk_version}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -67,7 +103,10 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
<ShowChartIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="CPU Frequency" secondary={data.cpu_freq_mhz + ' MHz'} />
<ListItemText
primary="CPU Frequency"
secondary={data.cpu_freq_mhz + ' MHz'}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -76,10 +115,20 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
<MemoryIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Heap (Free / Max Alloc)" secondary={formatNumber(data.free_heap) + ' / ' + formatNumber(data.max_alloc_heap) + ' bytes ' + (data.esp_platform === EspPlatform.ESP8266 ? '(' + data.heap_fragmentation + '% fragmentation)' : '')} />
<ListItemText
primary="Heap (Free / Max Alloc)"
secondary={
formatNumber(data.free_heap) +
' / ' +
formatNumber(data.max_alloc_heap) +
' bytes ' +
(data.esp_platform === EspPlatform.ESP8266
? '(' + data.heap_fragmentation + '% fragmentation)'
: '')
}
/>
</ListItem>
{
(data.esp_platform === EspPlatform.ESP32 && data.psram_size > 0) && (
{data.esp_platform === EspPlatform.ESP32 && data.psram_size > 0 && (
<Fragment>
<Divider variant="inset" component="li" />
<ListItem>
@@ -88,10 +137,18 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
<AppsIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="PSRAM (Size / Free)" secondary={formatNumber(data.psram_size) + ' / ' + formatNumber(data.free_psram) + ' bytes'} />
</ListItem>
</Fragment>)
<ListItemText
primary="PSRAM (Size / Free)"
secondary={
formatNumber(data.psram_size) +
' / ' +
formatNumber(data.free_psram) +
' bytes'
}
/>
</ListItem>
</Fragment>
)}
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
@@ -99,7 +156,17 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
<FolderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="File System (Used / Total)" secondary={formatNumber(data.fs_used) + ' / ' + formatNumber(data.fs_total) + ' bytes (' + formatNumber(data.fs_total - data.fs_used) + '\xa0bytes free)'} />
<ListItemText
primary="File System (Used / Total)"
secondary={
formatNumber(data.fs_used) +
' / ' +
formatNumber(data.fs_total) +
' bytes (' +
formatNumber(data.fs_total - data.fs_used) +
'\xa0bytes free)'
}
/>
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
@@ -119,41 +186,57 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
Are you sure you want to restart the device?
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={this.onRestartRejected} color="secondary">
<Button
variant="contained"
onClick={this.onRestartRejected}
color="secondary"
>
Cancel
</Button>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" onClick={this.onRestartConfirmed} disabled={this.state.processing} color="primary" autoFocus>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="contained"
onClick={this.onRestartConfirmed}
disabled={this.state.processing}
color="primary"
autoFocus
>
Restart
</Button>
</DialogActions>
</Dialog>
)
);
}
onRestart = () => {
this.setState({ confirmRestart: true });
}
};
onRestartRejected = () => {
this.setState({ confirmRestart: false });
}
};
onRestartConfirmed = () => {
this.setState({ processing: true });
redirectingAuthorizedFetch(RESTART_ENDPOINT, { method: 'POST' })
.then(response => {
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar("Device is restarting", { variant: 'info' });
this.props.enqueueSnackbar('Device is restarting', {
variant: 'info'
});
this.setState({ processing: false, confirmRestart: false });
} else {
throw Error("Invalid status code: " + response.status);
throw Error('Invalid status code: ' + response.status);
}
})
.catch(error => {
this.props.enqueueSnackbar(error.message || "Problem restarting device", { variant: 'error' });
.catch((error) => {
this.props.enqueueSnackbar(
error.message || 'Problem restarting device',
{ variant: 'error' }
);
this.setState({ processing: false, confirmRestart: false });
});
}
};
renderFactoryResetDialog() {
return (
@@ -168,72 +251,98 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
Are you sure you want to reset the device to its factory defaults?
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={this.onFactoryResetRejected} color="secondary">
<Button
variant="contained"
onClick={this.onFactoryResetRejected}
color="secondary"
>
Cancel
</Button>
<ErrorButton startIcon={<SettingsBackupRestoreIcon />} variant="contained" onClick={this.onFactoryResetConfirmed} disabled={this.state.processing} autoFocus>
<ErrorButton
startIcon={<SettingsBackupRestoreIcon />}
variant="contained"
onClick={this.onFactoryResetConfirmed}
disabled={this.state.processing}
autoFocus
>
Factory Reset
</ErrorButton>
</DialogActions>
</Dialog>
)
);
}
onFactoryReset = () => {
this.setState({ confirmFactoryReset: true });
}
};
onFactoryResetRejected = () => {
this.setState({ confirmFactoryReset: false });
}
};
onFactoryResetConfirmed = () => {
this.setState({ processing: true });
redirectingAuthorizedFetch(FACTORY_RESET_ENDPOINT, { method: 'POST' })
.then(response => {
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar("Factory reset in progress.", { variant: 'error' });
this.props.enqueueSnackbar('Factory reset in progress.', {
variant: 'error'
});
this.setState({ processing: false, confirmFactoryReset: false });
} else {
throw Error("Invalid status code: " + response.status);
throw Error('Invalid status code: ' + response.status);
}
})
.catch(error => {
this.props.enqueueSnackbar(error.message || "Problem factory resetting device", { variant: 'error' });
.catch((error) => {
this.props.enqueueSnackbar(
error.message || 'Problem factory resetting device',
{ variant: 'error' }
);
this.setState({ processing: false, confirmRestart: false });
});
}
};
render() {
const me = this.props.authenticatedContext.me;
return (
<Fragment>
<List>
{this.createListItems()}
</List>
<List>{this.createListItems()}</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} padding={1}>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
<FormButton
startIcon={<RefreshIcon />}
variant="contained"
color="secondary"
onClick={this.props.loadData}
>
Refresh
</FormButton>
</Box>
{me.admin &&
{me.admin && (
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
<FormButton startIcon={<PowerSettingsNewIcon />} variant="contained" color="primary" onClick={this.onRestart}>
<FormButton
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="primary"
onClick={this.onRestart}
>
Restart
</FormButton>
<ErrorButton startIcon={<SettingsBackupRestoreIcon />} variant="contained" onClick={this.onFactoryReset}>
<ErrorButton
startIcon={<SettingsBackupRestoreIcon />}
variant="contained"
onClick={this.onFactoryReset}
>
Factory reset
</ErrorButton>
</Box>
}
)}
</Box>
{this.renderRestartDialog()}
{this.renderFactoryResetDialog()}
</Fragment>
);
}
}
export default withAuthenticatedContext(SystemStatusForm);

View File

@@ -64,7 +64,6 @@ Authentication SecuritySettingsService::authenticate(const String & username, co
inline void populateJWTPayload(JsonObject & payload, User * user) {
payload["username"] = user->username;
payload["admin"] = user->admin;
payload["version"] = EMSESP_APP_VERSION; // proddy added
}
boolean SecuritySettingsService::validatePayload(JsonObject & parsedPayload, User * user) {

View File

@@ -1,14 +1,19 @@
#include <SystemStatus.h>
#include "../../src/emsesp_stub.hpp" // proddy added
using namespace std::placeholders; // for `_1` etc
SystemStatus::SystemStatus(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(SYSTEM_STATUS_SERVICE_PATH, HTTP_GET, securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(SYSTEM_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
}
void SystemStatus::systemStatus(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_ESP_STATUS_SIZE);
JsonObject root = response->getRoot();
root["emsesp_version"] = EMSESP_APP_VERSION;
root["esp_platform"] = "ESP32";
root["max_alloc_heap"] = ESP.getMaxAllocHeap();
root["psram_size"] = ESP.getPsramSize();

View File

@@ -10,8 +10,6 @@
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <uuid/log.h> // proddy added
#define MAX_ESP_STATUS_SIZE 1024
#define SYSTEM_STATUS_SERVICE_PATH "/rest/systemStatus"

View File

@@ -63,7 +63,6 @@ Authentication SecuritySettingsService::authenticate(const String& username, con
inline void populateJWTPayload(JsonObject& payload, User* user) {
payload["username"] = user->username;
payload["admin"] = user->admin;
payload["version"] = EMSESP_APP_VERSION; // proddy added
}
boolean SecuritySettingsService::validatePayload(JsonObject& parsedPayload, User* user) {