mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
feat: add generate token endpoint and ui for generating tokens for users
This commit is contained in:
@@ -18,5 +18,6 @@ export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
|
||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
||||
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
|
||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
|
||||
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
|
||||
export const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
|
||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
||||
|
||||
77
interface/src/security/GenerateToken.tsx
Normal file
77
interface/src/security/GenerateToken.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, LinearProgress, Typography, TextField } from '@material-ui/core';
|
||||
|
||||
import { FormButton } from '../components';
|
||||
import { redirectingAuthorizedFetch } from '../authentication';
|
||||
import { GENERATE_TOKEN_ENDPOINT } from '../api';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
|
||||
interface GenerateTokenProps extends WithSnackbarProps {
|
||||
username: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface GenerateTokenState {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
class GenerateToken extends React.Component<GenerateTokenProps, 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. This may be used for bearer authentication with the "Authorization" header or using the "access_token" query paramater.
|
||||
</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…
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<FormButton variant="contained" color="primary" type="submit" onClick={onClose}>
|
||||
Close
|
||||
</FormButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withSnackbar(GenerateToken);
|
||||
@@ -11,12 +11,14 @@ import CheckIcon from '@material-ui/icons/Check';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
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 UserForm from './UserForm';
|
||||
import { SecuritySettings, User } from './types';
|
||||
import GenerateToken from './GenerateToken';
|
||||
|
||||
function compareUsers(a: User, b: User) {
|
||||
if (a.username < b.username) {
|
||||
@@ -33,6 +35,7 @@ type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedConte
|
||||
type ManageUsersFormState = {
|
||||
creating: boolean;
|
||||
user?: User;
|
||||
generateTokenFor?: string;
|
||||
}
|
||||
|
||||
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
|
||||
@@ -66,6 +69,18 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
this.props.setData({ ...data, users });
|
||||
}
|
||||
|
||||
closeGenerateToken = () => {
|
||||
this.setState({
|
||||
generateTokenFor: undefined
|
||||
});
|
||||
}
|
||||
|
||||
generateToken = (user: User) => {
|
||||
this.setState({
|
||||
generateTokenFor: user.username
|
||||
});
|
||||
}
|
||||
|
||||
startEditingUser = (user: User) => {
|
||||
this.setState({
|
||||
creating: false,
|
||||
@@ -103,7 +118,7 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
|
||||
render() {
|
||||
const { width, data } = this.props;
|
||||
const { user, creating } = this.state;
|
||||
const { user, creating, generateTokenFor } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<ValidatorForm onSubmit={this.onSubmit}>
|
||||
@@ -125,6 +140,9 @@ 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)}>
|
||||
<VpnKeyIcon />
|
||||
</IconButton>
|
||||
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
@@ -162,6 +180,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
{
|
||||
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
|
||||
}
|
||||
{
|
||||
user &&
|
||||
<UserForm
|
||||
|
||||
@@ -32,7 +32,7 @@ class UserForm extends React.Component<UserFormProps> {
|
||||
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>
|
||||
<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
|
||||
|
||||
@@ -9,3 +9,6 @@ export interface SecuritySettings {
|
||||
jwt_secret: string;
|
||||
}
|
||||
|
||||
export interface GeneratedToken {
|
||||
token: string;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ SecuritySettingsService::SecuritySettingsService(AsyncWebServer * server, FS * f
|
||||
, _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE)
|
||||
, _jwtHandler(FACTORY_JWT_SECRET) {
|
||||
addUpdateHandler([&](const String & originId) { configureJWTHandler(); }, false);
|
||||
server->on(GENERATE_TOKEN_PATH, HTTP_GET, wrapRequest(std::bind(&SecuritySettingsService::generateToken, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN));
|
||||
}
|
||||
|
||||
void SecuritySettingsService::begin() {
|
||||
@@ -109,6 +110,21 @@ ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequest
|
||||
};
|
||||
}
|
||||
|
||||
void SecuritySettingsService::generateToken(AsyncWebServerRequest* request) {
|
||||
AsyncWebParameter* usernameParam = request->getParam("username");
|
||||
for (User _user : _state.users) {
|
||||
if (_user.username == usernameParam->value()) {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, GENERATE_TOKEN_SIZE);
|
||||
JsonObject root = response->getRoot();
|
||||
root["token"] = generateJWT(&_user);
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
request->send(401);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
|
||||
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
|
||||
|
||||
#define GENERATE_TOKEN_SIZE 512
|
||||
#define GENERATE_TOKEN_PATH "/rest/generateToken"
|
||||
|
||||
#if FT_ENABLED(FT_SECURITY)
|
||||
|
||||
class SecuritySettings {
|
||||
@@ -83,6 +86,8 @@ class SecuritySettingsService : public StatefulService<SecuritySettings>, public
|
||||
FSPersistence<SecuritySettings> _fsPersistence;
|
||||
ArduinoJsonJWT _jwtHandler;
|
||||
|
||||
void generateToken(AsyncWebServerRequest * request);
|
||||
|
||||
void configureJWTHandler();
|
||||
|
||||
/*
|
||||
|
||||
1591
mock-api/package-lock.json
generated
1591
mock-api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -87,6 +87,7 @@ const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
|
||||
const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
||||
const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
|
||||
const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
||||
const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
|
||||
const system_status = {
|
||||
"esp_platform": "ESP32", "max_alloc_heap": 113792, "psram_size": 0, "free_psram": 0, "cpu_freq_mhz": 240,
|
||||
"free_heap": 193340, "sdk_version": "v3.3.5-1-g85c43024c", "flash_chip_size": 4194304, "flash_chip_speed": 40000000,
|
||||
@@ -102,6 +103,7 @@ const verify_authentication = { access_token: '1234' };
|
||||
const signin = {
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInZlcnNpb24iOiIzLjAuMmIwIn0.MsHSgoJKI1lyYz77EiT5ZN3ECMrb4mPv9FNy3udq0TU"
|
||||
};
|
||||
const generate_token = { token: '1234' };
|
||||
|
||||
// EMS-ESP Project specific
|
||||
const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
|
||||
@@ -209,7 +211,6 @@ const emsesp_devicedata_2 = {
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
// NETWORK
|
||||
app.get(NETWORK_STATUS_ENDPOINT, (req, res) => { res.json(network_status); });
|
||||
app.get(NETWORK_SETTINGS_ENDPOINT, (req, res) => { res.json(network_settings); });
|
||||
@@ -247,6 +248,7 @@ app.post(RESTART_ENDPOINT, (req, res) => { res.sendStatus(200); });
|
||||
app.post(FACTORY_RESET_ENDPOINT, (req, res) => { res.sendStatus(200); });
|
||||
app.post(UPLOAD_FIRMWARE_ENDPOINT, (req, res) => { res.sendStatus(200); });
|
||||
app.post(SIGN_IN_ENDPOINT, (req, res) => { res.json(signin); });
|
||||
app.get(GENERATE_TOKEN_ENDPOINT, (req, res) => { res.json(generate_token); });
|
||||
|
||||
// EMS-ESP Project stuff
|
||||
app.get(EMSESP_SETTINGS_ENDPOINT, (req, res) => { res.json(emsesp_settings); });
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#define EMSESP_APP_VERSION "3.0.3b2"
|
||||
#define EMSESP_APP_VERSION "3.0.3b3"
|
||||
#define EMSESP_PLATFORM "ESP32"
|
||||
|
||||
Reference in New Issue
Block a user