From 1d780058c870c16291d132f4d514c59eb7e80384 Mon Sep 17 00:00:00 2001 From: Jeremy Letto Date: Sun, 30 Jan 2022 19:42:34 -0600 Subject: [PATCH] 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 --- webclient/src/containers/Login/Login.tsx | 52 +++++++++++---- .../AccountActivationDialog.css | 13 ++++ .../AccountActivationDialog.tsx | 41 ++++++++++++ webclient/src/dialogs/index.ts | 1 + .../AccountActivationForm.css | 12 ++++ .../AccountActivationForm.tsx | 64 +++++++++++++++++++ webclient/src/forms/index.ts | 1 + webclient/src/store/server/server.actions.ts | 9 +++ webclient/src/store/server/server.dispatch.ts | 9 +++ webclient/src/store/server/server.types.ts | 3 + .../src/websocket/commands/SessionCommands.ts | 3 +- .../persistence/SessionPersistence.ts | 8 ++- 12 files changed, 200 insertions(+), 16 deletions(-) create mode 100644 webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.css create mode 100644 webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx create mode 100644 webclient/src/forms/AccountActivationForm/AccountActivationForm.css create mode 100644 webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx diff --git a/webclient/src/containers/Login/Login.tsx b/webclient/src/containers/Login/Login.tsx index 679ccdc0..85c121be 100644 --- a/webclient/src/containers/Login/Login.tsx +++ b/webclient/src/containers/Login/Login.tsx @@ -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 (
{ isConnected && } @@ -296,6 +316,12 @@ const Login = ({ state, description }: LoginProps) => { handleClose={closeResetPasswordDialog} userName={userToResetPassword} /> + +
); } diff --git a/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.css b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.css new file mode 100644 index 00000000..5175ab84 --- /dev/null +++ b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.css @@ -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; +} \ No newline at end of file diff --git a/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx new file mode 100644 index 00000000..970a140f --- /dev/null +++ b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx @@ -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 ( + + + Account Activation + + {handleOnClose ? ( + + + + ) : null} + + +
+ Your account has not been activated yet. + You need to provide the activation token received in the activation email. +
+ + +
+
+ ); +}; + +export default AccountActivationDialog; diff --git a/webclient/src/dialogs/index.ts b/webclient/src/dialogs/index.ts index d2af9ce7..5c5ace1c 100644 --- a/webclient/src/dialogs/index.ts +++ b/webclient/src/dialogs/index.ts @@ -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'; diff --git a/webclient/src/forms/AccountActivationForm/AccountActivationForm.css b/webclient/src/forms/AccountActivationForm/AccountActivationForm.css new file mode 100644 index 00000000..ffb4ecc7 --- /dev/null +++ b/webclient/src/forms/AccountActivationForm/AccountActivationForm.css @@ -0,0 +1,12 @@ +.AccountActivationForm { + width: 100%; + padding-bottom: 15px; +} + +.AccountActivationForm-item { + margin-bottom: 20px; +} + +.AccountActivationForm-submit { + width: 100%; +} diff --git a/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx b/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx new file mode 100644 index 00000000..1a821ada --- /dev/null +++ b/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx @@ -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 ( +
+ {({ handleSubmit, form }) => { + return ( + +
+ +
+ + {errorMessage && ( +
+ Account activation failed +
+ )} + + +
+ ); + }} + + ); +}; + +export default AccountActivationForm; diff --git a/webclient/src/forms/index.ts b/webclient/src/forms/index.ts index 34f36fe4..1740694a 100644 --- a/webclient/src/forms/index.ts +++ b/webclient/src/forms/index.ts @@ -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'; diff --git a/webclient/src/store/server/server.actions.ts b/webclient/src/store/server/server.actions.ts index 720e2ed8..d1a3b2f4 100644 --- a/webclient/src/store/server/server.actions.ts +++ b/webclient/src/store/server/server.actions.ts @@ -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, }), diff --git a/webclient/src/store/server/server.dispatch.ts b/webclient/src/store/server/server.dispatch.ts index 9db1e189..a489edbc 100644 --- a/webclient/src/store/server/server.dispatch.ts +++ b/webclient/src/store/server/server.dispatch.ts @@ -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()); }, diff --git a/webclient/src/store/server/server.types.ts b/webclient/src/store/server/server.types.ts index 4acd8108..5767c3a8 100644 --- a/webclient/src/store/server/server.types.ts +++ b/webclient/src/store/server/server.types.ts @@ -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', diff --git a/webclient/src/websocket/commands/SessionCommands.ts b/webclient/src/websocket/commands/SessionCommands.ts index 00300fed..354d908a 100644 --- a/webclient/src/websocket/commands/SessionCommands.ts +++ b/webclient/src/websocket/commands/SessionCommands.ts @@ -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'); diff --git a/webclient/src/websocket/persistence/SessionPersistence.ts b/webclient/src/websocket/persistence/SessionPersistence.ts index 237a4664..78e385a2 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.ts @@ -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() {