Webatrice: Add account validation dialog/form (#4547)
* Add account validation dialog/form * clean up * close registration dialog on token request * remove dupe code * add subtitle styling Co-authored-by: Jeremy Letto <jeremy.letto@datasite.com>
This commit is contained in:
parent
513fcb0908
commit
1d780058c8
12 changed files with 200 additions and 16 deletions
|
@ -9,7 +9,7 @@ import Typography from '@material-ui/core/Typography';
|
||||||
|
|
||||||
|
|
||||||
import { AuthenticationService } from 'api';
|
import { AuthenticationService } from 'api';
|
||||||
import { RegistrationDialog, RequestPasswordResetDialog, ResetPasswordDialog } from 'dialogs';
|
import { RegistrationDialog, RequestPasswordResetDialog, ResetPasswordDialog, AccountActivationDialog } from 'dialogs';
|
||||||
import { LoginForm } from 'forms';
|
import { LoginForm } from 'forms';
|
||||||
import { useReduxEffect, useFireOnce } from 'hooks';
|
import { useReduxEffect, useFireOnce } from 'hooks';
|
||||||
import { Images } from 'images';
|
import { Images } from 'images';
|
||||||
|
@ -65,6 +65,7 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
passwordResetRequestDialog: false,
|
passwordResetRequestDialog: false,
|
||||||
resetPasswordDialog: false,
|
resetPasswordDialog: false,
|
||||||
registrationDialog: false,
|
registrationDialog: false,
|
||||||
|
activationDialog: false,
|
||||||
});
|
});
|
||||||
const [userToResetPassword, setUserToResetPassword] = useState(null);
|
const [userToResetPassword, setUserToResetPassword] = useState(null);
|
||||||
|
|
||||||
|
@ -77,6 +78,24 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
closeResetPasswordDialog();
|
closeResetPasswordDialog();
|
||||||
}, ServerTypes.RESET_PASSWORD_SUCCESS, []);
|
}, ServerTypes.RESET_PASSWORD_SUCCESS, []);
|
||||||
|
|
||||||
|
useReduxEffect(() => {
|
||||||
|
closeActivateAccountDialog();
|
||||||
|
}, ServerTypes.ACCOUNT_ACTIVATION_SUCCESS, []);
|
||||||
|
|
||||||
|
useReduxEffect(() => {
|
||||||
|
closeRegistrationDialog();
|
||||||
|
openActivateAccountDialog();
|
||||||
|
}, ServerTypes.ACCOUNT_AWAITING_ACTIVATION, []);
|
||||||
|
|
||||||
|
useReduxEffect(({ options: { hashedPassword } }) => {
|
||||||
|
if (hostIdToRemember) {
|
||||||
|
HostDTO.get(hostIdToRemember).then(host => {
|
||||||
|
host.hashedPassword = hashedPassword;
|
||||||
|
host.save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, ServerTypes.LOGIN_SUCCESSFUL, [hostIdToRemember]);
|
||||||
|
|
||||||
const showDescription = () => {
|
const showDescription = () => {
|
||||||
return !isConnected && description?.length;
|
return !isConnected && description?.length;
|
||||||
};
|
};
|
||||||
|
@ -112,18 +131,7 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
AuthenticationService.login(options as WebSocketConnectOptions);
|
AuthenticationService.login(options as WebSocketConnectOptions);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [submitButtonDisabled, resetSubmitButton, handleLogin] = useFireOnce(onSubmitLogin)
|
const [submitButtonDisabled, resetSubmitButton, handleLogin] = useFireOnce(onSubmitLogin);
|
||||||
|
|
||||||
useReduxEffect(({ options: { hashedPassword } }) => {
|
|
||||||
if (hostIdToRemember) {
|
|
||||||
HostDTO.get(hostIdToRemember).then(host => {
|
|
||||||
host.hashedPassword = hashedPassword;
|
|
||||||
host.save();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
resetSubmitButton()
|
|
||||||
}, ServerTypes.LOGIN_SUCCESSFUL, [hostIdToRemember]);
|
|
||||||
|
|
||||||
|
|
||||||
const updateHost = ({ selectedHost, userName, hashedPassword, remember }) => {
|
const updateHost = ({ selectedHost, userName, hashedPassword, remember }) => {
|
||||||
HostDTO.get(selectedHost.id).then(hostDTO => {
|
HostDTO.get(selectedHost.id).then(hostDTO => {
|
||||||
|
@ -165,6 +173,10 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
AuthenticationService.resetPassword({ userName, token, newPassword, host, port } as any);
|
AuthenticationService.resetPassword({ userName, token, newPassword, host, port } as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAccountActivationDialogSubmit = ({ token }) => {
|
||||||
|
AuthenticationService.activateAccount({ token } as any);
|
||||||
|
};
|
||||||
|
|
||||||
const skipTokenRequest = (userName) => {
|
const skipTokenRequest = (userName) => {
|
||||||
setUserToResetPassword(userName);
|
setUserToResetPassword(userName);
|
||||||
|
|
||||||
|
@ -198,6 +210,14 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
setDialogState(s => ({ ...s, registrationDialog: true }));
|
setDialogState(s => ({ ...s, registrationDialog: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const closeActivateAccountDialog = () => {
|
||||||
|
setDialogState(s => ({ ...s, activationDialog: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openActivateAccountDialog = () => {
|
||||||
|
setDialogState(s => ({ ...s, activationDialog: true }));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'login overflow-scroll ' + classes.root}>
|
<div className={'login overflow-scroll ' + classes.root}>
|
||||||
{ isConnected && <Redirect from="*" to={RouteEnum.SERVER} />}
|
{ isConnected && <Redirect from="*" to={RouteEnum.SERVER} />}
|
||||||
|
@ -296,6 +316,12 @@ const Login = ({ state, description }: LoginProps) => {
|
||||||
handleClose={closeResetPasswordDialog}
|
handleClose={closeResetPasswordDialog}
|
||||||
userName={userToResetPassword}
|
userName={userToResetPassword}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AccountActivationDialog
|
||||||
|
isOpen={dialogState.activationDialog}
|
||||||
|
onSubmit={handleAccountActivationDialogSubmit}
|
||||||
|
handleClose={closeActivateAccountDialog}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
.dialog-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MuiDialogTitle-root.dialog-title {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
|
||||||
|
import { AccountActivationForm } from 'forms';
|
||||||
|
|
||||||
|
import './AccountActivationDialog.css';
|
||||||
|
|
||||||
|
const AccountActivationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => {
|
||||||
|
const handleOnClose = () => {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog onClose={handleOnClose} open={isOpen}>
|
||||||
|
<DialogTitle disableTypography className="dialog-title">
|
||||||
|
<Typography variant="h6">Account Activation</Typography>
|
||||||
|
|
||||||
|
{handleOnClose ? (
|
||||||
|
<IconButton onClick={handleOnClose}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<div className="content">
|
||||||
|
<Typography variant='subtitle1'>Your account has not been activated yet.</Typography>
|
||||||
|
<Typography variant='subtitle1'>You need to provide the activation token received in the activation email.</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AccountActivationForm onSubmit={onSubmit}></AccountActivationForm>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccountActivationDialog;
|
|
@ -1,3 +1,4 @@
|
||||||
|
export { default as AccountActivationDialog } from './AccountActivationDialog/AccountActivationDialog';
|
||||||
export { default as CardImportDialog } from './CardImportDialog/CardImportDialog';
|
export { default as CardImportDialog } from './CardImportDialog/CardImportDialog';
|
||||||
export { default as KnownHostDialog } from './KnownHostDialog/KnownHostDialog';
|
export { default as KnownHostDialog } from './KnownHostDialog/KnownHostDialog';
|
||||||
export { default as RegistrationDialog } from './RegistrationDialog/RegistrationDialog';
|
export { default as RegistrationDialog } from './RegistrationDialog/RegistrationDialog';
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
.AccountActivationForm {
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccountActivationForm-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccountActivationForm-submit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
// eslint-disable-next-line
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Form, Field } from 'react-final-form';
|
||||||
|
import { OnChange } from 'react-final-form-listeners';
|
||||||
|
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
|
||||||
|
import { InputField, KnownHosts } from 'components';
|
||||||
|
import { FormKey } from 'types';
|
||||||
|
|
||||||
|
import './AccountActivationForm.css';
|
||||||
|
import { useReduxEffect } from 'hooks';
|
||||||
|
import { ServerTypes } from 'store';
|
||||||
|
|
||||||
|
const AccountActivationForm = ({ onSubmit }) => {
|
||||||
|
const [errorMessage, setErrorMessage] = useState(false);
|
||||||
|
|
||||||
|
useReduxEffect(() => {
|
||||||
|
setErrorMessage(true);
|
||||||
|
}, ServerTypes.ACCOUNT_ACTIVATION_FAILED, []);
|
||||||
|
|
||||||
|
const handleOnSubmit = (form) => {
|
||||||
|
setErrorMessage(false);
|
||||||
|
onSubmit(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = values => {
|
||||||
|
const errors: any = {};
|
||||||
|
|
||||||
|
if (!values.token) {
|
||||||
|
errors.token = 'Required';
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleOnSubmit} validate={validate}>
|
||||||
|
{({ handleSubmit, form }) => {
|
||||||
|
return (
|
||||||
|
<form className="AccountActivationForm" onSubmit={handleSubmit}>
|
||||||
|
<div className="AccountActivationForm-item">
|
||||||
|
<Field label="Token" name="token" component={InputField} autoComplete="off" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="AccountActivationForm-error">
|
||||||
|
<Typography color="error">Account activation failed</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button className="AccountActivationForm-submit rounded tall" color="primary" variant="contained" type="submit">
|
||||||
|
Activate Account
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccountActivationForm;
|
|
@ -1,3 +1,4 @@
|
||||||
|
export { default as AccountActivationForm } from './AccountActivationForm/AccountActivationForm';
|
||||||
export { default as CardImportForm } from './CardImportForm/CardImportForm';
|
export { default as CardImportForm } from './CardImportForm/CardImportForm';
|
||||||
export { default as ConnectForm } from './ConnectForm/ConnectForm';
|
export { default as ConnectForm } from './ConnectForm/ConnectForm';
|
||||||
export { default as LoginForm } from './LoginForm/LoginForm';
|
export { default as LoginForm } from './LoginForm/LoginForm';
|
||||||
|
|
|
@ -93,6 +93,15 @@ export const Actions = {
|
||||||
type: Types.REGISTRATION_USERNAME_ERROR,
|
type: Types.REGISTRATION_USERNAME_ERROR,
|
||||||
error
|
error
|
||||||
}),
|
}),
|
||||||
|
accountAwaitingActivation: () => ({
|
||||||
|
type: Types.ACCOUNT_AWAITING_ACTIVATION,
|
||||||
|
}),
|
||||||
|
accountActivationSuccess: () => ({
|
||||||
|
type: Types.ACCOUNT_ACTIVATION_SUCCESS,
|
||||||
|
}),
|
||||||
|
accountActivationFailed: () => ({
|
||||||
|
type: Types.ACCOUNT_ACTIVATION_FAILED,
|
||||||
|
}),
|
||||||
resetPassword: () => ({
|
resetPassword: () => ({
|
||||||
type: Types.RESET_PASSWORD_REQUESTED,
|
type: Types.RESET_PASSWORD_REQUESTED,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -80,6 +80,15 @@ export const Dispatch = {
|
||||||
registrationUserNameError: (error) => {
|
registrationUserNameError: (error) => {
|
||||||
store.dispatch(Actions.registrationUserNameError(error));
|
store.dispatch(Actions.registrationUserNameError(error));
|
||||||
},
|
},
|
||||||
|
accountAwaitingActivation: () => {
|
||||||
|
store.dispatch(Actions.accountAwaitingActivation());
|
||||||
|
},
|
||||||
|
accountActivationSuccess: () => {
|
||||||
|
store.dispatch(Actions.accountActivationSuccess());
|
||||||
|
},
|
||||||
|
accountActivationFailed: () => {
|
||||||
|
store.dispatch(Actions.accountActivationFailed());
|
||||||
|
},
|
||||||
resetPassword: () => {
|
resetPassword: () => {
|
||||||
store.dispatch(Actions.resetPassword());
|
store.dispatch(Actions.resetPassword());
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,6 +22,9 @@ export const Types = {
|
||||||
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
|
||||||
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error',
|
||||||
REGISTRATION_USERNAME_ERROR: '[Server] Registration Username Error',
|
REGISTRATION_USERNAME_ERROR: '[Server] Registration Username Error',
|
||||||
|
ACCOUNT_AWAITING_ACTIVATION: '[Server] Account Awaiting Activation',
|
||||||
|
ACCOUNT_ACTIVATION_SUCCESS: '[Server] Account Activation Success',
|
||||||
|
ACCOUNT_ACTIVATION_FAILED: '[Server] Account Activation Failed',
|
||||||
RESET_PASSWORD_REQUESTED: '[Server] Reset Password Requested',
|
RESET_PASSWORD_REQUESTED: '[Server] Reset Password Requested',
|
||||||
RESET_PASSWORD_FAILED: '[Server] Reset Password Failed',
|
RESET_PASSWORD_FAILED: '[Server] Reset Password Failed',
|
||||||
RESET_PASSWORD_CHALLENGE: '[Server] Reset Password Challenge',
|
RESET_PASSWORD_CHALLENGE: '[Server] Reset Password Challenge',
|
||||||
|
|
|
@ -137,7 +137,7 @@ export class SessionCommands {
|
||||||
webClient.protobuf.sendSessionCommand(sc, raw => {
|
webClient.protobuf.sendSessionCommand(sc, raw => {
|
||||||
switch (raw.responseCode) {
|
switch (raw.responseCode) {
|
||||||
case webClient.protobuf.controller.Response.ResponseCode.RespOk: {
|
case webClient.protobuf.controller.Response.ResponseCode.RespOk: {
|
||||||
const passwordSalt = raw['.Response_PasswordSalt.ext'].passwordSalt;
|
const passwordSalt = raw['.Response_PasswordSalt.ext']?.passwordSalt;
|
||||||
SessionCommands.login(passwordSalt);
|
SessionCommands.login(passwordSalt);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -236,6 +236,7 @@ export class SessionCommands {
|
||||||
|
|
||||||
webClient.protobuf.sendSessionCommand(sc, raw => {
|
webClient.protobuf.sendSessionCommand(sc, raw => {
|
||||||
if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespActivationAccepted) {
|
if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespActivationAccepted) {
|
||||||
|
SessionPersistence.accountActivationSuccess();
|
||||||
SessionCommands.login();
|
SessionCommands.login();
|
||||||
} else {
|
} else {
|
||||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed');
|
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed');
|
||||||
|
|
|
@ -78,11 +78,15 @@ export class SessionPersistence {
|
||||||
}
|
}
|
||||||
|
|
||||||
static accountAwaitingActivation() {
|
static accountAwaitingActivation() {
|
||||||
console.log('Open Modal for Activation Code input');
|
ServerDispatch.accountAwaitingActivation();
|
||||||
|
}
|
||||||
|
|
||||||
|
static accountActivationSuccess() {
|
||||||
|
ServerDispatch.accountActivationSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
static accountActivationFailed() {
|
static accountActivationFailed() {
|
||||||
console.log('Account activation failed, show an action here');
|
ServerDispatch.accountActivationFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
static registrationRequiresEmail() {
|
static registrationRequiresEmail() {
|
||||||
|
|
Loading…
Reference in a new issue