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:
Jeremy Letto 2022-01-30 19:42:34 -06:00 committed by GitHub
parent 513fcb0908
commit 1d780058c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 200 additions and 16 deletions

View file

@ -9,7 +9,7 @@ import Typography from '@material-ui/core/Typography';
import { AuthenticationService } from 'api';
import { RegistrationDialog, RequestPasswordResetDialog, ResetPasswordDialog } from 'dialogs';
import { RegistrationDialog, RequestPasswordResetDialog, ResetPasswordDialog, AccountActivationDialog } from 'dialogs';
import { LoginForm } from 'forms';
import { useReduxEffect, useFireOnce } from 'hooks';
import { Images } from 'images';
@ -65,6 +65,7 @@ const Login = ({ state, description }: LoginProps) => {
passwordResetRequestDialog: false,
resetPasswordDialog: false,
registrationDialog: false,
activationDialog: false,
});
const [userToResetPassword, setUserToResetPassword] = useState(null);
@ -77,6 +78,24 @@ const Login = ({ state, description }: LoginProps) => {
closeResetPasswordDialog();
}, 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 = () => {
return !isConnected && description?.length;
};
@ -112,18 +131,7 @@ const Login = ({ state, description }: LoginProps) => {
AuthenticationService.login(options as WebSocketConnectOptions);
}, []);
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 [submitButtonDisabled, resetSubmitButton, handleLogin] = useFireOnce(onSubmitLogin);
const updateHost = ({ selectedHost, userName, hashedPassword, remember }) => {
HostDTO.get(selectedHost.id).then(hostDTO => {
@ -165,6 +173,10 @@ const Login = ({ state, description }: LoginProps) => {
AuthenticationService.resetPassword({ userName, token, newPassword, host, port } as any);
};
const handleAccountActivationDialogSubmit = ({ token }) => {
AuthenticationService.activateAccount({ token } as any);
};
const skipTokenRequest = (userName) => {
setUserToResetPassword(userName);
@ -198,6 +210,14 @@ const Login = ({ state, description }: LoginProps) => {
setDialogState(s => ({ ...s, registrationDialog: true }));
}
const closeActivateAccountDialog = () => {
setDialogState(s => ({ ...s, activationDialog: false }));
};
const openActivateAccountDialog = () => {
setDialogState(s => ({ ...s, activationDialog: true }));
};
return (
<div className={'login overflow-scroll ' + classes.root}>
{ isConnected && <Redirect from="*" to={RouteEnum.SERVER} />}
@ -296,6 +316,12 @@ const Login = ({ state, description }: LoginProps) => {
handleClose={closeResetPasswordDialog}
userName={userToResetPassword}
/>
<AccountActivationDialog
isOpen={dialogState.activationDialog}
onSubmit={handleAccountActivationDialogSubmit}
handleClose={closeActivateAccountDialog}
/>
</div>
);
}

View file

@ -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;
}

View file

@ -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;

View file

@ -1,3 +1,4 @@
export { default as AccountActivationDialog } from './AccountActivationDialog/AccountActivationDialog';
export { default as CardImportDialog } from './CardImportDialog/CardImportDialog';
export { default as KnownHostDialog } from './KnownHostDialog/KnownHostDialog';
export { default as RegistrationDialog } from './RegistrationDialog/RegistrationDialog';

View file

@ -0,0 +1,12 @@
.AccountActivationForm {
width: 100%;
padding-bottom: 15px;
}
.AccountActivationForm-item {
margin-bottom: 20px;
}
.AccountActivationForm-submit {
width: 100%;
}

View file

@ -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;

View file

@ -1,3 +1,4 @@
export { default as AccountActivationForm } from './AccountActivationForm/AccountActivationForm';
export { default as CardImportForm } from './CardImportForm/CardImportForm';
export { default as ConnectForm } from './ConnectForm/ConnectForm';
export { default as LoginForm } from './LoginForm/LoginForm';

View file

@ -93,6 +93,15 @@ export const Actions = {
type: Types.REGISTRATION_USERNAME_ERROR,
error
}),
accountAwaitingActivation: () => ({
type: Types.ACCOUNT_AWAITING_ACTIVATION,
}),
accountActivationSuccess: () => ({
type: Types.ACCOUNT_ACTIVATION_SUCCESS,
}),
accountActivationFailed: () => ({
type: Types.ACCOUNT_ACTIVATION_FAILED,
}),
resetPassword: () => ({
type: Types.RESET_PASSWORD_REQUESTED,
}),

View file

@ -80,6 +80,15 @@ export const Dispatch = {
registrationUserNameError: (error) => {
store.dispatch(Actions.registrationUserNameError(error));
},
accountAwaitingActivation: () => {
store.dispatch(Actions.accountAwaitingActivation());
},
accountActivationSuccess: () => {
store.dispatch(Actions.accountActivationSuccess());
},
accountActivationFailed: () => {
store.dispatch(Actions.accountActivationFailed());
},
resetPassword: () => {
store.dispatch(Actions.resetPassword());
},

View file

@ -22,6 +22,9 @@ export const Types = {
REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error',
REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password 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_FAILED: '[Server] Reset Password Failed',
RESET_PASSWORD_CHALLENGE: '[Server] Reset Password Challenge',

View file

@ -137,7 +137,7 @@ export class SessionCommands {
webClient.protobuf.sendSessionCommand(sc, raw => {
switch (raw.responseCode) {
case webClient.protobuf.controller.Response.ResponseCode.RespOk: {
const passwordSalt = raw['.Response_PasswordSalt.ext'].passwordSalt;
const passwordSalt = raw['.Response_PasswordSalt.ext']?.passwordSalt;
SessionCommands.login(passwordSalt);
break;
}
@ -236,6 +236,7 @@ export class SessionCommands {
webClient.protobuf.sendSessionCommand(sc, raw => {
if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespActivationAccepted) {
SessionPersistence.accountActivationSuccess();
SessionCommands.login();
} else {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed');

View file

@ -78,11 +78,15 @@ export class SessionPersistence {
}
static accountAwaitingActivation() {
console.log('Open Modal for Activation Code input');
ServerDispatch.accountAwaitingActivation();
}
static accountActivationSuccess() {
ServerDispatch.accountActivationSuccess();
}
static accountActivationFailed() {
console.log('Account activation failed, show an action here');
ServerDispatch.accountActivationFailed();
}
static registrationRequiresEmail() {