Support Server requests for MFA, Render failed UI statuses to user, C… (#4483)

* Support Server requests for MFA, Render failed UI statuses to user, Connect to KnownHosts component
This commit is contained in:
Zach H 2021-11-23 02:45:08 -05:00 committed by GitHub
parent 73c5956ece
commit 0683d1aced
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 168 additions and 91 deletions

View file

@ -27,7 +27,7 @@ const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit }: any) =>
) : null} ) : null}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<ResetPasswordForm onSubmit={onSubmit}></ResetPasswordForm> <ResetPasswordForm onSubmit={onSubmit}/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View file

@ -1,5 +1,5 @@
// eslint-disable-next-line // eslint-disable-next-line
import React, { useState } from "react"; import React, {useState} from "react";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
@ -13,9 +13,9 @@ import { RequestPasswordResetDialog, ResetPasswordDialog } from 'components';
import { LoginForm } from 'forms'; import { LoginForm } from 'forms';
import { useReduxEffect } from 'hooks'; import { useReduxEffect } from 'hooks';
import { Images } from 'images'; import { Images } from 'images';
import { RouteEnum } from 'types'; import { RouteEnum, StatusEnum } from 'types';
import { ServerSelectors, ServerTypes } from 'store'; import { ServerSelectors, ServerTypes } from 'store';
import { SessionCommands } from 'websocket';
import './Login.css'; import './Login.css';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
@ -60,13 +60,18 @@ const Login = ({ state, description }: LoginProps) => {
const isConnected = AuthenticationService.isConnected(state); const isConnected = AuthenticationService.isConnected(state);
const [dialogState, setDialogState] = useState({ const [dialogState, setDialogState] = useState({
openRequest: false, passwordResetRequestDialog: false,
openReset: false resetPasswordDialog: false
}); });
useReduxEffect(() => { useReduxEffect(() => {
closeRequestPasswordResetDialog();
openResetPasswordDialog(); openResetPasswordDialog();
}, ServerTypes.RESET_PASSWORD, []); }, ServerTypes.RESET_PASSWORD_REQUESTED, []);
useReduxEffect(() => {
closeResetPasswordDialog();
}, ServerTypes.RESET_PASSWORD_SUCCESS, []);
const showDescription = () => { const showDescription = () => {
return !isConnected && description?.length; return !isConnected && description?.length;
@ -82,30 +87,26 @@ const Login = ({ state, description }: LoginProps) => {
} else { } else {
AuthenticationService.resetPasswordRequest({ user, host, port } as any); AuthenticationService.resetPasswordRequest({ user, host, port } as any);
} }
closeRequestPasswordResetDialog();
}; };
const handleResetPasswordDialogSubmit = async ({ user, token, newPassword, passwordAgain, host, port }) => { const handleResetPasswordDialogSubmit = async ({ user, token, newPassword, passwordAgain, host, port }) => {
AuthenticationService.resetPassword({ user, token, newPassword, host, port } as any); AuthenticationService.resetPassword({ user, token, newPassword, host, port } as any);
closeResetPasswordDialog();
}; };
const closeRequestPasswordResetDialog = () => { const closeRequestPasswordResetDialog = () => {
setDialogState(s => ({ ...s, openRequest: false })); setDialogState(s => ({ ...s, passwordResetRequestDialog: false }));
} }
const openRequestPasswordResetDialog = () => { const openRequestPasswordResetDialog = () => {
setDialogState(s => ({ ...s, openRequest: true })); setDialogState(s => ({ ...s, passwordResetRequestDialog: true }));
} }
const closeResetPasswordDialog = () => { const closeResetPasswordDialog = () => {
setDialogState(s => ({ ...s, openReset: false })); setDialogState(s => ({ ...s, resetPasswordDialog: false }));
} }
const openResetPasswordDialog = () => { const openResetPasswordDialog = () => {
setDialogState(s => ({ ...s, openReset: true })); setDialogState(s => ({ ...s, resetPasswordDialog: true }));
} }
return ( return (
@ -184,13 +185,13 @@ const Login = ({ state, description }: LoginProps) => {
</div> </div>
<RequestPasswordResetDialog <RequestPasswordResetDialog
isOpen={dialogState.openRequest} isOpen={dialogState.passwordResetRequestDialog}
onSubmit={handleRequestPasswordResetDialogSubmit} onSubmit={handleRequestPasswordResetDialogSubmit}
handleClose={closeRequestPasswordResetDialog} handleClose={closeRequestPasswordResetDialog}
/> />
<ResetPasswordDialog <ResetPasswordDialog
isOpen={dialogState.openReset} isOpen={dialogState.resetPasswordDialog}
onSubmit={handleResetPasswordDialogSubmit} onSubmit={handleResetPasswordDialogSubmit}
handleClose={closeResetPasswordDialog} handleClose={closeResetPasswordDialog}
/> />

View file

@ -1,66 +1,66 @@
// eslint-disable-next-line // eslint-disable-next-line
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, Field, reduxForm, change } from 'redux-form' import { Form, Field, reduxForm, change } from 'redux-form'
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import { InputField, KnownHosts } from 'components'; import { InputField, KnownHosts } from 'components';
import { FormKey } from 'types'; import { FormKey } from 'types';
import './RegisterForm.css'; import './RegisterForm.css';
const RegisterForm = (props) => { const RegisterForm = (props) => {
const { dispatch, handleSubmit } = props; const { dispatch, handleSubmit } = props;
const onHostChange = ({ host, port }) => { const onHostChange = ({ host, port }) => {
dispatch(change(FormKey.REGISTER, 'host', host)); dispatch(change(FormKey.REGISTER, 'host', host));
dispatch(change(FormKey.REGISTER, 'port', port)); dispatch(change(FormKey.REGISTER, 'port', port));
} }
return ( return (
<Form className="registerForm row" onSubmit={handleSubmit} autoComplete="off"> <Form className="registerForm row" onSubmit={handleSubmit} autoComplete="off">
<div className="leftRegisterForm column" > <div className="leftRegisterForm column" >
<div className="registerForm-item"> <div className="registerForm-item">
<KnownHosts onChange={onHostChange} /> <KnownHosts onChange={onHostChange} />
{ /* Padding is off */ } { /* Padding is off */ }
</div> </div>
<div className="registerForm-item"> <div className="registerForm-item">
<Field label="Country" name="country" component={InputField} /> <Field label="Country" name="country" component={InputField} />
</div> </div>
<div className="registerForm-item"> <div className="registerForm-item">
<Field label="Real Name" name="realName" component={InputField} /> <Field label="Real Name" name="realName" component={InputField} />
</div> </div>
<div className="registerForm-item"> <div className="registerForm-item">
<Field label="Email" name="email" type="email" component={InputField} /> <Field label="Email" name="email" type="email" component={InputField} />
</div> </div>
</div> </div>
<div className="rightRegisterForm column"> <div className="rightRegisterForm column">
<div className="registerForm-item"> <div className="registerForm-item">
<Field label="Player Name" name="user" component={InputField} /> <Field label="Player Name" name="user" component={InputField} />
</div> </div>
<div className="registerForm-item"> <div className="registerForm-item">
<Field label="Password" name="pass" type="password" component={InputField} /> <Field label="Password" name="pass" type="password" component={InputField} />
</div> </div>
<div className="registerForm-item"> <div className="registerForm-item">
<Field label="Password (again)" name="passwordConfirm" type="password" component={InputField} /> <Field label="Password (again)" name="passwordConfirm" type="password" component={InputField} />
</div> </div>
<Button className="registerForm-submit tall" color="primary" variant="contained" type="submit"> <Button className="registerForm-submit tall" color="primary" variant="contained" type="submit">
Register Register
</Button> </Button>
</div> </div>
</Form > </Form >
); );
}; };
const propsMap = { const propsMap = {
form: FormKey.REGISTER, form: FormKey.REGISTER,
}; };
const mapStateToProps = () => ({ const mapStateToProps = () => ({
initialValues: { initialValues: {
} }
}); });
export default connect(mapStateToProps)(reduxForm(propsMap)(RegisterForm)); export default connect(mapStateToProps)(reduxForm(propsMap)(RegisterForm));

View file

@ -2,6 +2,14 @@
width: 100%; width: 100%;
} }
.RequestPasswordResetForm-MFA-Message {
margin-top: -20px;
}
.RequestPasswordResetForm-Error {
margin-bottom: 10px;
}
.RequestPasswordResetForm-item { .RequestPasswordResetForm-item {
margin-bottom: 20px; margin-bottom: 20px;
} }

View file

@ -1,5 +1,5 @@
// eslint-disable-next-line // eslint-disable-next-line
import React from "react"; import React, { useState } from "react";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, Field, reduxForm, change } from 'redux-form' import { Form, Field, reduxForm, change } from 'redux-form'
@ -9,21 +9,47 @@ import { InputField, KnownHosts } from 'components';
import { FormKey } from 'types'; import { FormKey } from 'types';
import './RequestPasswordResetForm.css'; import './RequestPasswordResetForm.css';
import { useReduxEffect } from 'hooks';
import { ServerTypes } from 'store';
const RequestPasswordResetForm = (props) => { const RequestPasswordResetForm = (props) => {
const { dispatch, handleSubmit } = props; const { dispatch, handleSubmit } = props;
const [errorMessage, setErrorMessage] = useState(false);
const [isMFA, setIsMFA] = useState(false);
const onHostChange = ({ host, port }) => { const onHostChange = ({ host, port }) => {
dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'host', host)); dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'host', host));
dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'port', port)); dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'port', port));
} }
useReduxEffect(() => {
setErrorMessage(true);
}, ServerTypes.RESET_PASSWORD_FAILED, []);
useReduxEffect(() => {
setIsMFA(true);
}, ServerTypes.RESET_PASSWORD_CHALLENGE, []);
const onSubmit = (event) => {
setErrorMessage(false);
handleSubmit(event);
}
return ( return (
<Form className="RequestPasswordResetForm" onSubmit={handleSubmit}> <Form className="RequestPasswordResetForm" onSubmit={onSubmit}>
<div className="RequestPasswordResetForm-items"> <div className="RequestPasswordResetForm-items">
{errorMessage ? (
<div className="RequestPasswordResetForm-Error">Request Password Reset Failed, please try again</div>
) : null}
<div className="RequestPasswordResetForm-item"> <div className="RequestPasswordResetForm-item">
<Field label="Username" name="user" component={InputField} autoComplete="username" /> <Field label="Username" name="user" component={InputField} autoComplete="username" />
</div> </div>
{isMFA ? (
<div className="RequestPasswordResetForm-item">
<div className="RequestPasswordResetForm-MFA-Message">Server has multi-factor authentication enabled</div>
<Field label="Email" name="email" component={InputField} autoComplete="email" />
</div>
) : null}
<div className="RequestPasswordResetForm-item"> <div className="RequestPasswordResetForm-item">
<KnownHosts onChange={onHostChange} /> <KnownHosts onChange={onHostChange} />
</div> </div>

View file

@ -1,5 +1,5 @@
// eslint-disable-next-line // eslint-disable-next-line
import React from "react"; import React, {useState} from "react";
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, Field, reduxForm, change } from 'redux-form' import { Form, Field, reduxForm, change } from 'redux-form'
@ -9,18 +9,31 @@ import { InputField, KnownHosts } from 'components';
import { FormKey } from 'types'; import { FormKey } from 'types';
import './ResetPasswordForm.css'; import './ResetPasswordForm.css';
import { useReduxEffect } from '../../hooks';
import { ServerTypes } from '../../store';
const ResetPasswordForm = (props) => { const ResetPasswordForm = (props) => {
const { dispatch, handleSubmit } = props; const { dispatch, handleSubmit } = props;
const [errorMessage, setErrorMessage] = useState(false);
const onHostChange = ({ host, port }) => { const onHostChange = ({ host, port }) => {
dispatch(change(FormKey.RESET_PASSWORD, 'host', host)); dispatch(change(FormKey.RESET_PASSWORD, 'host', host));
dispatch(change(FormKey.RESET_PASSWORD, 'port', port)); dispatch(change(FormKey.RESET_PASSWORD, 'port', port));
} }
useReduxEffect(() => {
setErrorMessage(true);
}, ServerTypes.RESET_PASSWORD_FAILED, []);
return ( return (
<Form className="ResetPasswordForm" onSubmit={handleSubmit}> <Form className="ResetPasswordForm" onSubmit={handleSubmit}>
<div className="ResetPasswordForm-items"> <div className="ResetPasswordForm-items">
{errorMessage ? (
<div><h3>Password Reset Failed, please try again</h3></div>
) : null}
<div className="ResetPasswordForm-item"> <div className="ResetPasswordForm-item">
<Field label="Username" name="user" component={InputField} autoComplete="username" /> <Field label="Username" name="user" component={InputField} autoComplete="username" />
</div> </div>

View file

@ -1,4 +1,4 @@
/** /**
File is adapted from https://github.com/Qeepsake/use-redux-effect under MIT License File is adapted from https://github.com/Qeepsake/use-redux-effect under MIT License
* @author Aspect Apps Limited * @author Aspect Apps Limited
* @description * @description

View file

@ -1,3 +1,4 @@
import { Type } from 'protobufjs';
import { Types } from './server.types'; import { Types } from './server.types';
export const Actions = { export const Actions = {
@ -68,6 +69,15 @@ export const Actions = {
type: Types.CLEAR_LOGS type: Types.CLEAR_LOGS
}), }),
resetPassword: () => ({ resetPassword: () => ({
type: Types.RESET_PASSWORD type: Types.RESET_PASSWORD_REQUESTED
}),
resetPasswordFailed: () => ({
type: Types.RESET_PASSWORD_FAILED
}),
resetPasswordChallenge: () => ({
type: Types.RESET_PASSWORD_CHALLENGE
}),
resetPasswordSuccess: () => ({
type: Types.RESET_PASSWORD_SUCCESS
}) })
} }

View file

@ -64,5 +64,14 @@ export const Dispatch = {
}, },
resetPassword: () => { resetPassword: () => {
store.dispatch(Actions.resetPassword()); store.dispatch(Actions.resetPassword());
},
resetPasswordFailed: () => {
store.dispatch(Actions.resetPasswordFailed());
},
resetPasswordChallenge: () => {
store.dispatch(Actions.resetPasswordChallenge());
},
resetPasswordSuccess: () => {
store.dispatch(Actions.resetPasswordSuccess());
} }
} }

View file

@ -16,5 +16,8 @@ export const Types = {
USER_LEFT: '[Server] User Left', USER_LEFT: '[Server] User Left',
VIEW_LOGS: '[Server] View Logs', VIEW_LOGS: '[Server] View Logs',
CLEAR_LOGS: '[Server] Clear Logs', CLEAR_LOGS: '[Server] Clear Logs',
RESET_PASSWORD: '[Server] Reset Password' RESET_PASSWORD_REQUESTED: '[Server] Reset Password Requested',
RESET_PASSWORD_FAILED: '[Server] Reset Password Failed',
RESET_PASSWORD_CHALLENGE: '[Server] Reset Password Challenge',
RESET_PASSWORD_SUCCESS: '[Server] Reset Password Success'
}; };

View file

@ -268,11 +268,14 @@ export class SessionCommands {
const resp = raw['.Response_ForgotPasswordRequest.ext']; const resp = raw['.Response_ForgotPasswordRequest.ext'];
if (resp.challengeEmail) { if (resp.challengeEmail) {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Requesting MFA information');
SessionPersistence.resetPasswordChallenge(); SessionPersistence.resetPasswordChallenge();
} else { } else {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password reset in progress');
SessionPersistence.resetPassword(); SessionPersistence.resetPassword();
} }
} else { } else {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password reset failed, please try again');
SessionPersistence.resetPasswordFailed(); SessionPersistence.resetPasswordFailed();
} }
@ -298,8 +301,10 @@ export class SessionCommands {
webClient.protobuf.sendSessionCommand(sc, raw => { webClient.protobuf.sendSessionCommand(sc, raw => {
if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password reset in progress');
SessionPersistence.resetPassword(); SessionPersistence.resetPassword();
} else { } else {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password reset failed, please try again');
SessionPersistence.resetPasswordFailed(); SessionPersistence.resetPasswordFailed();
} }
@ -326,8 +331,10 @@ export class SessionCommands {
webClient.protobuf.sendSessionCommand(sc, raw => { webClient.protobuf.sendSessionCommand(sc, raw => {
if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password successfully updated');
SessionPersistence.resetPasswordSuccess(); SessionPersistence.resetPasswordSuccess();
} else { } else {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password update failed, please try again');
SessionPersistence.resetPasswordFailed(); SessionPersistence.resetPasswordFailed();
} }

View file

@ -82,7 +82,7 @@ export class SessionPersistence {
} }
static resetPasswordChallenge() { static resetPasswordChallenge() {
console.log('Open Modal asking for Email address associated with account'); ServerDispatch.resetPasswordChallenge();
} }
static resetPassword() { static resetPassword() {
@ -90,10 +90,10 @@ export class SessionPersistence {
} }
static resetPasswordSuccess() { static resetPasswordSuccess() {
console.log('User password successfully changed Alert!'); ServerDispatch.resetPasswordSuccess();
} }
static resetPasswordFailed() { static resetPasswordFailed() {
console.log('Open Alert telling user their password request failed for some reason'); ServerDispatch.resetPasswordFailed();
} }
} }