This commit is contained in:
proddy
2021-05-14 12:45:57 +02:00
parent 15df0c0552
commit fec5ff3132
108 changed files with 3508 additions and 2455 deletions

View File

@@ -1,5 +1,14 @@
import React, { Fragment } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, LinearProgress, Typography, TextField } from '@material-ui/core';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box,
LinearProgress,
Typography,
TextField
} from '@material-ui/core';
import { FormButton } from '../components';
import { redirectingAuthorizedFetch } from '../authentication';
@@ -7,71 +16,105 @@ import { GENERATE_TOKEN_ENDPOINT } from '../api';
import { withSnackbar, WithSnackbarProps } from 'notistack';
interface GenerateTokenProps extends WithSnackbarProps {
username: string;
onClose: () => void;
username: string;
onClose: () => void;
}
interface GenerateTokenState {
token?: string;
token?: string;
}
class GenerateToken extends React.Component<GenerateTokenProps, GenerateTokenState> {
class GenerateToken extends React.Component<
GenerateTokenProps,
GenerateTokenState
> {
state: GenerateTokenState = {};
state: GenerateTokenState = {};
componentDidMount() {
const { username } = this.props;
redirectingAuthorizedFetch(GENERATE_TOKEN_ENDPOINT + "?" + new URLSearchParams({ username }), { method: 'GET' })
.then(response => {
if (response.status === 200) {
return response.json();
} else {
throw Error("Error generating token: " + response.status);
}
}).then(generatedToken => {
console.log(generatedToken);
this.setState({ token: generatedToken.token });
})
.catch(error => {
this.props.enqueueSnackbar(error.message || "Problem generating token", { variant: 'error' });
});
}
render() {
const { onClose, username } = this.props;
const { token } = this.state;
return (
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open fullWidth maxWidth="sm">
<DialogTitle id="generate-token-dialog-title">Token for: {username}</DialogTitle>
<DialogContent dividers>
{token ?
<Fragment>
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
<Typography variant="body1">
The token below may be used to access the secured APIs, either as a Bearer authentication in the "Authorization" header or using the "access_token" query parameter.
</Typography>
</Box>
<Box mt={2} mb={2}>
<TextField label="Token" multiline value={token} fullWidth contentEditable={false} />
</Box>
</Fragment>
:
<Box m={4} textAlign="center">
<LinearProgress />
<Typography variant="h6">
Generating token&hellip;
</Typography>
</Box>
}
</DialogContent>
<DialogActions>
<FormButton variant="contained" color="primary" type="submit" onClick={onClose}>
Close
</FormButton>
</DialogActions>
</Dialog>
componentDidMount() {
const { username } = this.props;
redirectingAuthorizedFetch(
GENERATE_TOKEN_ENDPOINT + '?' + new URLSearchParams({ username }),
{ method: 'GET' }
)
.then((response) => {
if (response.status === 200) {
return response.json();
} else {
throw Error('Error generating token: ' + response.status);
}
})
.then((generatedToken) => {
console.log(generatedToken);
this.setState({ token: generatedToken.token });
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || 'Problem generating token',
{ variant: 'error' }
);
}
});
}
render() {
const { onClose, username } = this.props;
const { token } = this.state;
return (
<Dialog
onClose={onClose}
aria-labelledby="generate-token-dialog-title"
open
fullWidth
maxWidth="sm"
>
<DialogTitle id="generate-token-dialog-title">
Token for: {username}
</DialogTitle>
<DialogContent dividers>
{token ? (
<Fragment>
<Box
bgcolor="primary.main"
color="primary.contrastText"
p={2}
mt={2}
mb={2}
>
<Typography variant="body1">
The token below may be used to access the secured APIs, either
as a Bearer authentication in the "Authorization" header or
using the "access_token" query parameter.
</Typography>
</Box>
<Box mt={2} mb={2}>
<TextField
label="Token"
multiline
value={token}
fullWidth
contentEditable={false}
/>
</Box>
</Fragment>
) : (
<Box m={4} textAlign="center">
<LinearProgress />
<Typography variant="h6">Generating token&hellip;</Typography>
</Box>
)}
</DialogContent>
<DialogActions>
<FormButton
variant="contained"
color="primary"
type="submit"
onClick={onClose}
>
Close
</FormButton>
</DialogActions>
</Dialog>
);
}
}
export default withSnackbar(GenerateToken);

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
import ManageUsersForm from './ManageUsersForm';
@@ -9,7 +14,6 @@ import { SecuritySettings } from './types';
type ManageUsersControllerProps = RestControllerProps<SecuritySettings>;
class ManageUsersController extends Component<ManageUsersControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,14 @@ class ManageUsersController extends Component<ManageUsersControllerProps> {
<SectionContent title="Manage Users" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <ManageUsersForm {...formProps} />}
render={(formProps) => <ManageUsersForm {...formProps} />}
/>
</SectionContent>
)
);
}
}
export default restController(SECURITY_SETTINGS_ENDPOINT, ManageUsersController);
export default restController(
SECURITY_SETTINGS_ENDPOINT,
ManageUsersController
);

View File

@@ -1,8 +1,18 @@
import React, { Fragment } from 'react';
import { ValidatorForm } from 'react-material-ui-form-validator';
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow, withWidth, WithWidthProps, isWidthDown } from '@material-ui/core';
import { Box, Button, Typography, } from '@material-ui/core';
import {
Table,
TableBody,
TableCell,
TableHead,
TableFooter,
TableRow,
withWidth,
WithWidthProps,
isWidthDown
} from '@material-ui/core';
import { Box, Button, Typography } from '@material-ui/core';
import EditIcon from '@material-ui/icons/Edit';
import DeleteIcon from '@material-ui/icons/Delete';
@@ -13,8 +23,16 @@ import SaveIcon from '@material-ui/icons/Save';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
import VpnKeyIcon from '@material-ui/icons/VpnKey';
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import { RestFormProps, FormActions, FormButton, extractEventValue } from '../components';
import {
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import {
RestFormProps,
FormActions,
FormButton,
extractEventValue
} from '../components';
import UserForm from './UserForm';
import { SecuritySettings, User } from './types';
@@ -30,16 +48,20 @@ function compareUsers(a: User, b: User) {
return 0;
}
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps & WithWidthProps;
type ManageUsersFormProps = RestFormProps<SecuritySettings> &
AuthenticatedContextProps &
WithWidthProps;
type ManageUsersFormState = {
creating: boolean;
user?: User;
generateTokenFor?: string;
}
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
};
class ManageUsersForm extends React.Component<
ManageUsersFormProps,
ManageUsersFormState
> {
state: ManageUsersFormState = {
creating: false
};
@@ -48,38 +70,38 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
this.setState({
creating: true,
user: {
username: "",
password: "",
username: '',
password: '',
admin: true
}
});
};
uniqueUsername = (username: string) => {
return !this.props.data.users.find(u => u.username === username);
}
return !this.props.data.users.find((u) => u.username === username);
};
noAdminConfigured = () => {
return !this.props.data.users.find(u => u.admin);
}
return !this.props.data.users.find((u) => u.admin);
};
removeUser = (user: User) => {
const { data } = this.props;
const users = data.users.filter(u => u.username !== user.username);
const users = data.users.filter((u) => u.username !== user.username);
this.props.setData({ ...data, users });
}
};
closeGenerateToken = () => {
this.setState({
generateTokenFor: undefined
});
}
};
generateToken = (user: User) => {
this.setState({
generateTokenFor: user.username
});
}
};
startEditingUser = (user: User) => {
this.setState({
@@ -92,13 +114,13 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
this.setState({
user: undefined
});
}
};
doneEditingUser = () => {
const { user } = this.state;
if (user) {
const { data } = this.props;
const users = data.users.filter(u => u.username !== user.username);
const users = data.users.filter((u) => u.username !== user.username);
users.push(user);
this.props.setData({ ...data, users });
this.setState({
@@ -107,14 +129,18 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
}
};
handleUserValueChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ user: { ...this.state.user!, [name]: extractEventValue(event) } });
handleUserValueChange = (name: keyof User) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({
user: { ...this.state.user!, [name]: extractEventValue(event) }
});
};
onSubmit = () => {
this.props.saveData();
this.props.authenticatedContext.refresh();
}
};
render() {
const { width, data } = this.props;
@@ -122,7 +148,10 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
return (
<Fragment>
<ValidatorForm onSubmit={this.onSubmit}>
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
<Table
size="small"
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
>
<TableHead>
<TableRow>
<TableCell>Username</TableCell>
@@ -131,7 +160,7 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
</TableRow>
</TableHead>
<TableBody>
{data.users.sort(compareUsers).map(user => (
{data.users.sort(compareUsers).map((user) => (
<TableRow key={user.username}>
<TableCell component="th" scope="row">
{user.username}
@@ -140,51 +169,79 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
{user.admin ? <CheckIcon /> : <CloseIcon />}
</TableCell>
<TableCell align="center">
<IconButton size="small" aria-label="Generate Token" onClick={() => this.generateToken(user)}>
<IconButton
size="small"
aria-label="Generate Token"
onClick={() => this.generateToken(user)}
>
<VpnKeyIcon />
</IconButton>
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
<IconButton
size="small"
aria-label="Delete"
onClick={() => this.removeUser(user)}
>
<DeleteIcon />
</IconButton>
<IconButton size="small" aria-label="Edit" onClick={() => this.startEditingUser(user)}>
<IconButton
size="small"
aria-label="Edit"
onClick={() => this.startEditingUser(user)}
>
<EditIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter >
<TableFooter>
<TableRow>
<TableCell colSpan={2} />
<TableCell align="center" padding="default">
<Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}>
<Button
startIcon={<PersonAddIcon />}
variant="contained"
color="secondary"
onClick={this.createUser}
>
Add
</Button>
</TableCell>
</TableRow>
</TableFooter>
</Table>
{
this.noAdminConfigured() &&
(
<Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}>
<Typography variant="body1">
You must have at least one admin user configured.
</Typography>
</Box>
)
}
{this.noAdminConfigured() && (
<Box
bgcolor="error.main"
color="error.contrastText"
p={2}
mt={2}
mb={2}
>
<Typography variant="body1">
You must have at least one admin user configured.
</Typography>
</Box>
)}
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit" disabled={this.noAdminConfigured()}>
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
disabled={this.noAdminConfigured()}
>
Save
</FormButton>
</FormActions>
</ValidatorForm>
{
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
}
{
user &&
{generateTokenFor && (
<GenerateToken
username={generateTokenFor}
onClose={this.closeGenerateToken}
/>
)}
{user && (
<UserForm
user={user}
creating={creating}
@@ -193,11 +250,10 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
handleValueChange={this.handleUserValueChange}
uniqueUsername={this.uniqueUsername}
/>
}
)}
</Fragment>
);
}
}
export default withAuthenticatedContext(withWidth()(ManageUsersForm));

View File

@@ -1,9 +1,12 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
import { AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
import {
AuthenticatedContextProps,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import ManageUsersController from './ManageUsersController';
@@ -12,25 +15,36 @@ import SecuritySettingsController from './SecuritySettingsController';
type SecurityProps = AuthenticatedContextProps & RouteComponentProps;
class Security extends Component<SecurityProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
render() {
return (
<MenuAppBar sectionTitle="Security">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/security/users" label="Manage Users" />
<Tab value="/security/settings" label="Security Settings" />
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/security/users" component={ManageUsersController} />
<AuthenticatedRoute exact path="/security/settings" component={SecuritySettingsController} />
<AuthenticatedRoute
exact
path="/security/users"
component={ManageUsersController}
/>
<AuthenticatedRoute
exact
path="/security/settings"
component={SecuritySettingsController}
/>
<Redirect to="/security/users" />
</Switch>
</MenuAppBar>
)
);
}
}

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
import SecuritySettingsForm from './SecuritySettingsForm';
@@ -9,7 +14,6 @@ import { SecuritySettings } from './types';
type SecuritySettingsControllerProps = RestControllerProps<SecuritySettings>;
class SecuritySettingsController extends Component<SecuritySettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,14 @@ class SecuritySettingsController extends Component<SecuritySettingsControllerPro
<SectionContent title="Security Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <SecuritySettingsForm {...formProps} />}
render={(formProps) => <SecuritySettingsForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(SECURITY_SETTINGS_ENDPOINT, SecuritySettingsController);
export default restController(
SECURITY_SETTINGS_ENDPOINT,
SecuritySettingsController
);

View File

@@ -4,19 +4,27 @@ import { ValidatorForm } from 'react-material-ui-form-validator';
import { Box, Typography } from '@material-ui/core';
import SaveIcon from '@material-ui/icons/Save';
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import { RestFormProps, PasswordValidator, FormActions, FormButton } from '../components';
import {
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import {
RestFormProps,
PasswordValidator,
FormActions,
FormButton
} from '../components';
import { SecuritySettings } from './types';
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> &
AuthenticatedContextProps;
class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
onSubmit = () => {
this.props.saveData();
this.props.authenticatedContext.refresh();
}
};
render() {
const { data, handleValueChange } = this.props;
@@ -24,7 +32,10 @@ class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
<ValidatorForm onSubmit={this.onSubmit}>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,64}$']}
errorMessages={['Password Required', 'Password must be 64 characters or less']}
errorMessages={[
'Password Required',
'Password must be 64 characters or less'
]}
name="jwt_secret"
label="Super User Password"
fullWidth
@@ -33,20 +44,32 @@ class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
onChange={handleValueChange('jwt_secret')}
margin="normal"
/>
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
<Box
bgcolor="primary.main"
color="primary.contrastText"
p={2}
mt={2}
mb={2}
>
<Typography variant="body1">
The Super User password is used to sign authentication tokens and is also the Console's `su` password. If you modify this all users will be signed out.
The Super User password is used to sign authentication tokens and is
also the Console's `su` password. If you modify this all users will
be signed out.
</Typography>
</Box>
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>
</ValidatorForm>
);
}
}
export default withAuthenticatedContext(SecuritySettingsForm);

View File

@@ -1,9 +1,19 @@
import React, { RefObject } from 'react';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { Dialog, DialogTitle, DialogContent, DialogActions, Checkbox } from '@material-ui/core';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Checkbox
} from '@material-ui/core';
import { PasswordValidator, BlockFormControlLabel, FormButton } from '../components';
import {
PasswordValidator,
BlockFormControlLabel,
FormButton
} from '../components';
import { User } from './types';
@@ -11,33 +21,67 @@ interface UserFormProps {
creating: boolean;
user: User;
uniqueUsername: (value: any) => boolean;
handleValueChange: (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => void;
handleValueChange: (
name: keyof User
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
onDoneEditing: () => void;
onCancelEditing: () => void;
}
class UserForm extends React.Component<UserFormProps> {
formRef: RefObject<any> = React.createRef();
componentDidMount() {
ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername);
ValidatorForm.addValidationRule(
'uniqueUsername',
this.props.uniqueUsername
);
}
submit = () => {
this.formRef.current.submit();
}
};
render() {
const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
const {
user,
creating,
handleValueChange,
onDoneEditing,
onCancelEditing
} = this.props;
return (
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open fullWidth maxWidth="sm">
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
<Dialog
onClose={onCancelEditing}
aria-labelledby="user-form-dialog-title"
open
fullWidth
maxWidth="sm"
>
<DialogTitle id="user-form-dialog-title">
{creating ? 'Add' : 'Modify'} User
</DialogTitle>
<DialogContent dividers>
<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 '.'"] : []}
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"
fullWidth
@@ -49,7 +93,10 @@ class UserForm extends React.Component<UserFormProps> {
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,64}$']}
errorMessages={['Password is required', 'Password must be 64 characters or less']}
errorMessages={[
'Password is required',
'Password must be 64 characters or less'
]}
name="password"
label="Password"
fullWidth
@@ -70,10 +117,19 @@ class UserForm extends React.Component<UserFormProps> {
/>
</DialogContent>
<DialogActions>
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>
<FormButton
variant="contained"
color="secondary"
onClick={onCancelEditing}
>
Cancel
</FormButton>
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
<FormButton
variant="contained"
color="primary"
type="submit"
onClick={this.submit}
>
Done
</FormButton>
</DialogActions>

View File

@@ -1,14 +1,14 @@
export interface User {
username: string
password: string
admin: boolean
username: string;
password: string;
admin: boolean;
}
export interface SecuritySettings {
users: User[]
jwt_secret: string
users: User[];
jwt_secret: string;
}
export interface GeneratedToken {
token: string
token: string;
}