diff --git a/webclient/package-lock.json b/webclient/package-lock.json index 87f4c185..89235884 100644 --- a/webclient/package-lock.json +++ b/webclient/package-lock.json @@ -7100,6 +7100,11 @@ "@babel/runtime": "^7.10.0" } }, + "final-form-set-field-touched": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/final-form-set-field-touched/-/final-form-set-field-touched-1.0.1.tgz", + "integrity": "sha512-yvE5AAs9U3OgJQ9YF8NhSF0I0mJEECvOpkaXNqovloxji5Q6gOZ0DCIAyLAKHluGSpsXKUGORyBm8Hq0beZIqQ==" + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -13005,6 +13010,14 @@ "@babel/runtime": "^7.15.4" } }, + "react-final-form-listeners": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-final-form-listeners/-/react-final-form-listeners-1.0.3.tgz", + "integrity": "sha512-OrdCNxSS4JQS/EXD+R530kZKFqaPfa+WcXPgVro/h4BpaBDF/Ja+BtHyCzDezCIb5rWaGGdOJIj+tN2YdtvrXg==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/webclient/package.json b/webclient/package.json index 4cb10da7..d825ce5a 100644 --- a/webclient/package.json +++ b/webclient/package.json @@ -9,6 +9,7 @@ "crypto-js": "^4.1.1", "dexie": "^3.0.3", "final-form": "^4.20.4", + "final-form-set-field-touched": "^1.0.1", "jquery": "^3.6.0", "lodash": "^4.17.21", "prop-types": "^15.7.2", @@ -16,6 +17,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-final-form": "^6.5.7", + "react-final-form-listeners": "^1.0.3", "react-redux": "^7.2.6", "react-router-dom": "^5.3.0", "react-scripts": "4.0.3", diff --git a/webclient/src/components/CheckboxField/CheckboxField.tsx b/webclient/src/components/CheckboxField/CheckboxField.tsx index 8fd1337b..62846706 100644 --- a/webclient/src/components/CheckboxField/CheckboxField.tsx +++ b/webclient/src/components/CheckboxField/CheckboxField.tsx @@ -2,8 +2,8 @@ import React from 'react'; import Checkbox from '@material-ui/core/Checkbox'; import FormControlLabel from '@material-ui/core/FormControlLabel'; -const CheckboxField = ({ input, label }) => { - const { value, onChange } = input; +const CheckboxField = (props) => { + const { input: { value, onChange }, label, ...args } = props; // @TODO this isnt unchecking properly return ( @@ -12,9 +12,10 @@ const CheckboxField = ({ input, label }) => { label={label} control={ onChange(checked)} color="primary" /> } diff --git a/webclient/src/components/InputField/InputField.tsx b/webclient/src/components/InputField/InputField.tsx index ab25bcfd..92207069 100644 --- a/webclient/src/components/InputField/InputField.tsx +++ b/webclient/src/components/InputField/InputField.tsx @@ -17,7 +17,7 @@ const useStyles = makeStyles(theme => ({ }, })); -const InputField = ({ input, label, name, autoComplete, type, meta: { touched, error, warning } }) => { +const InputField = ({ input, meta: { touched, error, warning }, ...args }) => { const classes = useStyles(); return ( @@ -38,15 +38,12 @@ const InputField = ({ input, label, name, autoComplete, type, meta: { touched, e ) } ); diff --git a/webclient/src/components/KnownHosts/KnownHosts.tsx b/webclient/src/components/KnownHosts/KnownHosts.tsx index 6937f3c4..f9fd5a94 100644 --- a/webclient/src/components/KnownHosts/KnownHosts.tsx +++ b/webclient/src/components/KnownHosts/KnownHosts.tsx @@ -29,7 +29,9 @@ const useStyles = makeStyles(theme => ({ }, })); -const KnownHosts = ({ input: { onChange }, meta: { touched, error, warning } }) => { +const KnownHosts = (props) => { + const { input: { onChange }, meta, disabled } = props; + const { touched, error, warning } = meta; const classes = useStyles(); const [hostsState, setHostsState] = useState({ @@ -169,6 +171,7 @@ const KnownHosts = ({ input: { onChange }, meta: { touched, error, warning } }) value={hostsState.selectedHost} fullWidth={true} onChange={e => selectHost(e.target.value)} + disabled={disabled} > + Cockatrice is an open source project. { new Date().getUTCFullYear() } @@ -236,16 +269,24 @@ const Login = ({ state, description }: LoginProps) => { + + ); diff --git a/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.css b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.css index 731927c1..73822f19 100644 --- a/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.css +++ b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.css @@ -3,3 +3,8 @@ justify-content: space-between; align-items: center; } + +.dialog-content { + width: 700px; + max-width: 100%; +} diff --git a/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx index cd1743e9..d8cf89f9 100644 --- a/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx +++ b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx @@ -10,13 +10,13 @@ import { RegisterForm } from 'forms'; import './RegistrationDialog.css'; -const RegistrationDialog = ({ classes, handleClose, isOpen }: any) => { +const RegistrationDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => { const handleOnClose = () => { handleClose(); } return ( - + Create New Account @@ -26,8 +26,8 @@ const RegistrationDialog = ({ classes, handleClose, isOpen }: any) => { ) : null} - - + + ); diff --git a/webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.css b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.css similarity index 100% rename from webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.css rename to webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.css diff --git a/webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx similarity index 86% rename from webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx rename to webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx index 9829ef6e..0744a321 100644 --- a/webclient/src/components/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx +++ b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx @@ -10,7 +10,7 @@ import { RequestPasswordResetForm } from 'forms'; import './RequestPasswordResetDialog.css'; -const RequestPasswordResetDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => { +const RequestPasswordResetDialog = ({ classes, handleClose, isOpen, onSubmit, skipTokenRequest }: any) => { const handleOnClose = () => { handleClose(); } @@ -27,7 +27,7 @@ const RequestPasswordResetDialog = ({ classes, handleClose, isOpen, onSubmit }: ) : null} - + ); diff --git a/webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.css b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.css similarity index 100% rename from webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.css rename to webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.css diff --git a/webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.tsx b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx similarity index 91% rename from webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.tsx rename to webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx index 59799f30..230c19bb 100644 --- a/webclient/src/components/ResetPasswordDialog/ResetPasswordDialog.tsx +++ b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx @@ -10,7 +10,7 @@ import { ResetPasswordForm } from 'forms'; import './ResetPasswordDialog.css'; -const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => { +const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit, userName }: any) => { const handleOnClose = () => { handleClose(); } @@ -27,7 +27,7 @@ const ResetPasswordDialog = ({ classes, handleClose, isOpen, onSubmit }: any) => ) : null} - + ); diff --git a/webclient/src/dialogs/index.ts b/webclient/src/dialogs/index.ts index ec8664bb..d2af9ce7 100644 --- a/webclient/src/dialogs/index.ts +++ b/webclient/src/dialogs/index.ts @@ -1,3 +1,5 @@ export { default as CardImportDialog } from './CardImportDialog/CardImportDialog'; export { default as KnownHostDialog } from './KnownHostDialog/KnownHostDialog'; export { default as RegistrationDialog } from './RegistrationDialog/RegistrationDialog'; +export { default as RequestPasswordResetDialog } from './RequestPasswordResetDialog/RequestPasswordResetDialog'; +export { default as ResetPasswordDialog } from './ResetPasswordDialog/ResetPasswordDialog'; diff --git a/webclient/src/forms/KnownHostForm/KnownHostForm.tsx b/webclient/src/forms/KnownHostForm/KnownHostForm.tsx index 69a134aa..4f0cb9f9 100644 --- a/webclient/src/forms/KnownHostForm/KnownHostForm.tsx +++ b/webclient/src/forms/KnownHostForm/KnownHostForm.tsx @@ -10,9 +10,29 @@ import { InputField } from 'components'; import './KnownHostForm.css'; -function KnownHostForm({ host, onRemove, onSubmit }) { +const KnownHostForm = ({ host, onRemove, onSubmit }) => { const [confirmDelete, setConfirmDelete] = useState(false); + const validate = values => { + const errors: any = {}; + + if (!values.name) { + errors.name = 'Required' + } + + if (!values.host) { + errors.host = 'Required' + } + + if (!values.port) { + errors.port = 'Required' + } + + if (Object.keys(errors).length) { + return errors; + } + }; + return (
{ - const errors: any = {}; - - if (!values.name) { - errors.name = 'Required' - } - - if (!values.host) { - errors.host = 'Required' - } - - if (!values.port) { - errors.port = 'Required' - } - - if (Object.keys(errors).length) { - return errors; - } - }} + validate={validate} > {({ handleSubmit }) => ( @@ -74,7 +76,7 @@ function KnownHostForm({ host, onRemove, onSubmit }) { ) }
); -} +}; const mapStateToProps = () => ({ diff --git a/webclient/src/forms/LoginForm/LoginForm.tsx b/webclient/src/forms/LoginForm/LoginForm.tsx index 10dbed45..5d136195 100644 --- a/webclient/src/forms/LoginForm/LoginForm.tsx +++ b/webclient/src/forms/LoginForm/LoginForm.tsx @@ -1,7 +1,8 @@ // eslint-disable-next-line import React, { Component, useCallback, useEffect, useState, useRef } from 'react'; import { connect } from 'react-redux'; -import { Form, Field, reduxForm, change, FormSubmitHandler } from 'redux-form' +import { Form, Field, useField } from 'react-final-form'; +import { OnChange } from 'react-final-form-listeners'; import Button from '@material-ui/core/Button'; @@ -16,151 +17,142 @@ import './LoginForm.css'; const PASSWORD_LABEL = 'Password'; const STORED_PASSWORD_LABEL = '* SAVED *'; -const LoginForm: any = ({ dispatch, form, submit, handleSubmit }: LoginFormProps) => { - const password: any = useRef(); +const LoginForm = ({ onSubmit, onResetPassword }: LoginFormProps) => { const [host, setHost] = useState(null); - const [remember, setRemember] = useState(false); const [passwordLabel, setPasswordLabel] = useState(PASSWORD_LABEL); - const [hasStoredPassword, useStoredPassword] = useState(false); + const [autoConnect, setAutoConnect] = useAutoConnect(); - const [autoConnect, setAutoConnect] = useAutoConnect(() => { - dispatch(change(form, 'autoConnect', autoConnect)); - - if (autoConnect && !remember) { - setRemember(true); - } - }); - - useEffect(() => { - SettingDTO.get(APP_USER).then((userSetting: SettingDTO) => { - if (userSetting?.autoConnect && !AuthenticationService.connectionAttemptMade()) { - HostDTO.getAll().then(hosts => { - let lastSelectedHost = hosts.find(({ lastSelected }) => lastSelected); - - if (lastSelectedHost?.remember && lastSelectedHost?.hashedPassword) { - dispatch(change(form, 'selectedHost', lastSelectedHost)); - dispatch(change(form, 'userName', lastSelectedHost.userName)); - dispatch(change(form, 'remember', true)); - setPasswordLabel(STORED_PASSWORD_LABEL); - dispatch(submit); - } - }); - } - }); - }, [submit, dispatch, form]); - - useEffect(() => { - dispatch(change(form, 'remember', remember)); - - if (!remember) { - setAutoConnect(false); - } - - if (!remember) { - useStoredPassword(false); - setPasswordLabel(PASSWORD_LABEL); - } else if (host?.hashedPassword) { - useStoredPassword(true); - setPasswordLabel(STORED_PASSWORD_LABEL); - } - }, [remember, dispatch, form]); - - useEffect(() => { - if (!host) { - return - } - - dispatch(change(form, 'userName', host.userName)); - dispatch(change(form, 'password', '')); - - setRemember(host.remember); - setAutoConnect(host.remember && autoConnect); - - if (host.remember && host.hashedPassword) { - // TODO: check if this causes a double render (maybe try combined state) - // try deriving useStoredPassword - useStoredPassword(true); - setPasswordLabel(STORED_PASSWORD_LABEL); - } else { - useStoredPassword(false); - setPasswordLabel(PASSWORD_LABEL); - } - }, [host, dispatch, form]); - - const onRememberChange = event => setRemember(event.target.checked); - const onAutoConnectChange = event => setAutoConnect(event.target.checked); - const onHostChange = h => setHost(h); - - const forgotPassword = () => { - console.log('Show recover password dialog, then AuthService.forgotPasswordRequest'); - }; - - return ( -
-
-
- -
-
- setPasswordLabel(PASSWORD_LABEL)} - onBlur={() => !password.current.value && hasStoredPassword && setPasswordLabel(STORED_PASSWORD_LABEL)} - name='password' - type='password' - component={InputField} - autoComplete='new-password' - /> -
-
- - -
-
- -
-
- -
-
- -
- ); -}; - -const propsMap = { - form: FormKey.LOGIN, - validate: values => { + const validate = values => { const errors: any = {}; - if (!values.user) { - errors.user = 'Required'; + if (!values.userName) { + errors.userName = 'Required'; } - if (!values.password && !values.selectedHost?.hashedPassword) { errors.password = 'Required'; } - if (!values.selectedHost) { errors.selectedHost = 'Required'; } return errors; } + + const useStoredPassword = (remember) => remember && host.hashedPassword; + const togglePasswordLabel = (useStoredLabel) => { + setPasswordLabel(useStoredLabel ? STORED_PASSWORD_LABEL : PASSWORD_LABEL); + }; + + return ( +
+ {({ handleSubmit, form }) => { + const { values } = form.getState(); + + useEffect(() => { + SettingDTO.get(APP_USER).then((userSetting: SettingDTO) => { + if (userSetting?.autoConnect && !AuthenticationService.connectionAttemptMade()) { + HostDTO.getAll().then(hosts => { + let lastSelectedHost = hosts.find(({ lastSelected }) => lastSelected); + + if (lastSelectedHost?.remember && lastSelectedHost?.hashedPassword) { + togglePasswordLabel(true); + + form.change('selectedHost', lastSelectedHost); + form.change('userName', lastSelectedHost.userName); + form.change('remember', true); + form.submit(); + } + }); + } + }); + }, []); + + useEffect(() => { + if (!host) { + return; + } + + form.change('userName', host.userName); + form.change('password', ''); + + onRememberChange(host.remember); + onAutoConnectChange(host.remember && autoConnect); + togglePasswordLabel(useStoredPassword(host.remember)); + }, [host]); + + const onUserNameChange = (userName) => { + const fieldChanged = host.userName?.toLowerCase() !== values.userName?.toLowerCase(); + if (useStoredPassword(values.remember) && fieldChanged) { + setHost(({ hashedPassword, ...s }) => ({ ...s, userName })); + } + } + + const onRememberChange = (checked) => { + form.change('remember', checked); + + if (!checked && values.autoConnect) { + onAutoConnectChange(false); + } + + togglePasswordLabel(useStoredPassword(checked)); + } + + const onAutoConnectChange = (checked) => { + setAutoConnect(checked); + + form.change('autoConnect', checked); + + if (checked && !values.remember) { + form.change('remember', true); + } + } + + return ( + +
+
+ + {onUserNameChange} +
+
+ setPasswordLabel(PASSWORD_LABEL)} + onBlur={() => togglePasswordLabel(useStoredPassword(values.remember))} + name='password' + type='password' + component={InputField} + autoComplete='new-password' + /> +
+
+ + {onRememberChange} + + +
+
+ + {setHost} +
+
+ + {onAutoConnectChange} +
+
+ +
+ ) + }} + + ); }; interface LoginFormProps { - form: string; - dispatch: Function; - submit: Function; - handleSubmit: FormSubmitHandler; + onSubmit: any; + onResetPassword: any; } -const mapStateToProps = (state) => ({ - -}); - -export default connect(mapStateToProps)(reduxForm(propsMap)(LoginForm)); +export default LoginForm; diff --git a/webclient/src/forms/RegisterForm/RegisterForm.css b/webclient/src/forms/RegisterForm/RegisterForm.css index d51d7298..324aa7e6 100644 --- a/webclient/src/forms/RegisterForm/RegisterForm.css +++ b/webclient/src/forms/RegisterForm/RegisterForm.css @@ -1,21 +1,18 @@ -.registerForm { +.RegisterForm { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.RegisterForm-column { + width: 48%; +} + +.RegisterForm-item { + margin-bottom: 20px; +} + +.RegisterForm-submit { width: 100%; } - -.registerForm-submit { - width: 100%; - /*padding is off, something in material-theme is causing it*/ -} - -.row { - display: flex; - flex-direction: row; - width: 100%; - justify-content: space-between; -} - -.column { - width: 48%; - flex: 0 1 auto; - align-self: auto; -} \ No newline at end of file diff --git a/webclient/src/forms/RegisterForm/RegisterForm.tsx b/webclient/src/forms/RegisterForm/RegisterForm.tsx index 8bfbcc4c..342e6024 100644 --- a/webclient/src/forms/RegisterForm/RegisterForm.tsx +++ b/webclient/src/forms/RegisterForm/RegisterForm.tsx @@ -1,67 +1,159 @@ // eslint-disable-next-line -import React, { Component } from 'react'; +import React, { Component, useState } from 'react'; import { connect } from 'react-redux'; -import { Form, Field, reduxForm, change } from 'redux-form' +import { Form, Field } from 'react-final-form'; +import { OnChange } from 'react-final-form-listeners'; +import setFieldTouched from 'final-form-set-field-touched' import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; import { InputField, KnownHosts } from 'components'; +import { useReduxEffect } from 'hooks'; +import { ServerTypes } from 'store'; import { FormKey } from 'types'; import './RegisterForm.css'; -const RegisterForm = (props) => { - const { dispatch, handleSubmit } = props; +const RegisterForm = ({ onSubmit }: RegisterFormProps) => { + const [emailRequired, setEmailRequired] = useState(false); + const [error, setError] = useState(null); + const [emailError, setEmailError] = useState(null); + const [passwordError, setPasswordError] = useState(null); + const [userNameError, setUserNameError] = useState(null); - const onHostChange: any = ({ host, port }) => { - dispatch(change(FormKey.REGISTER, 'host', host)); - dispatch(change(FormKey.REGISTER, 'port', port)); + const onHostChange = (host) => setEmailRequired(false); + const onEmailChange = () => emailError && setEmailError(null); + const onPasswordChange = () => passwordError && setPasswordError(null); + const onUserNameChange = () => userNameError && setUserNameError(null); + + useReduxEffect(() => { + setEmailRequired(true); + }, ServerTypes.REGISTRATION_REQUIRES_EMAIL); + + useReduxEffect(({ error }) => { + setError(error); + }, ServerTypes.REGISTRATION_FAILED); + + useReduxEffect(({ error }) => { + setEmailError(error); + }, ServerTypes.REGISTRATION_EMAIL_ERROR); + + useReduxEffect(({ error }) => { + setPasswordError(error); + }, ServerTypes.REGISTRATION_PASSWORD_ERROR); + + useReduxEffect(({ error }) => { + setUserNameError(error); + }, ServerTypes.REGISTRATION_USERNAME_ERROR); + + const handleOnSubmit = form => { + setError(null); + onSubmit(form); + } + + const validate = values => { + const errors: any = {}; + + if (!values.userName) { + errors.userName = 'Required'; + } else if (userNameError) { + errors.userName = userNameError; + } + + if (!values.password) { + errors.password = 'Required'; + } else if (passwordError) { + errors.password = passwordError; + } + + if (!values.passwordConfirm) { + errors.passwordConfirm = 'Required'; + } else if (values.password !== values.passwordConfirm) { + errors.passwordConfirm = 'Passwords don\'t match' + } + + if (!values.selectedHost) { + errors.selectedHost = 'Required'; + } + + if (emailRequired && !values.email) { + errors.email = 'Required'; + } else if (emailError) { + errors.email = emailError; + } + + return errors; } return ( -
-
-
- - { /* Padding is off */ } -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
- -
+ + {({ handleSubmit, form, ...args }) => { + const { values } = form.getState(); + + if (emailRequired) { + // Allow form render to complete + setTimeout(() => form.mutators.setFieldTouched('email', true)) + } + + return ( + <> + +
+
+ + {onUserNameChange} +
+
+ + {onPasswordChange} +
+
+ +
+
+ + {onHostChange} +
+
+
+
+ +
+
+ + {onEmailChange} +
+
+ +
+ +
+
+ + { error && ( +
+ {error} +
+ )} + + ); + }} ); }; -const propsMap = { - form: FormKey.REGISTER, -}; +interface RegisterFormProps { + onSubmit: any; +} -const mapStateToProps = () => ({ - initialValues: { - - } -}); - -export default connect(mapStateToProps)(reduxForm(propsMap)(RegisterForm)); +export default RegisterForm; diff --git a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css index bfc66182..83ed7420 100644 --- a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css +++ b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.css @@ -1,13 +1,6 @@ .RequestPasswordResetForm { width: 100%; -} - -.RequestPasswordResetForm-MFA-Message { - margin-top: -20px; -} - -.RequestPasswordResetForm-Error { - margin-bottom: 10px; + padding-bottom: 15px; } .RequestPasswordResetForm-item { @@ -26,3 +19,7 @@ .RequestPasswordResetForm-submit { width: 100%; } + +.selectedHost { + margin-top: 40px; +} diff --git a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx index e34a1123..c7cf2706 100644 --- a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx +++ b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx @@ -1,9 +1,11 @@ // eslint-disable-next-line import React, { useState } from "react"; import { connect } from 'react-redux'; -import { Form, Field, reduxForm, change } from 'redux-form' +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'; @@ -12,16 +14,10 @@ import './RequestPasswordResetForm.css'; import { useReduxEffect } from 'hooks'; import { ServerTypes } from 'store'; -const RequestPasswordResetForm = (props) => { - const { dispatch, handleSubmit } = props; +const RequestPasswordResetForm = ({ onSubmit, skipTokenRequest }) => { const [errorMessage, setErrorMessage] = useState(false); const [isMFA, setIsMFA] = useState(false); - const onHostChange: any = ({ host, port }) => { - dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'host', host)); - dispatch(change(FormKey.RESET_PASSWORD_REQUEST, 'port', port)); - } - useReduxEffect(() => { setErrorMessage(true); }, ServerTypes.RESET_PASSWORD_FAILED, []); @@ -30,63 +26,73 @@ const RequestPasswordResetForm = (props) => { setIsMFA(true); }, ServerTypes.RESET_PASSWORD_CHALLENGE, []); - const onSubmit = (event) => { + const handleOnSubmit = (form) => { setErrorMessage(false); - handleSubmit(event); + onSubmit(form); } + const validate = values => { + const errors: any = {}; + + if (!values.userName) { + errors.userName = 'Required'; + } + if (isMFA && !values.email) { + errors.email = 'Required'; + } + if (!values.selectedHost) { + errors.selectedHost = 'Required'; + } + + return errors; + }; + return ( -
-
- {errorMessage ? ( -
Request Password Reset Failed, please try again
- ) : null} -
- -
- {isMFA ? ( -
-
Server has multi-factor authentication enabled
- -
- ) : null} -
- -
-
- + + {({ handleSubmit, form }) => { + const onHostChange: any = ({ userName }) => { + form.change('userName', userName); + setIsMFA(false); + } + + return ( + +
+
+ +
+ {isMFA ? ( +
+ +
Server has multi-factor authentication enabled
+
+ ) : null} +
+ + {onHostChange} +
+ + {errorMessage && ( +
+ Request password reset failed +
+ )} +
+ + + +
+ +
+
+ ); + }} ); }; -const propsMap = { - form: FormKey.RESET_PASSWORD_REQUEST, - validate: values => { - const errors: any = {}; - - if (!values.user) { - errors.user = 'Required'; - } - if (!values.host) { - errors.host = 'Required'; - } - if (!values.port) { - errors.port = 'Required'; - } - - return errors; - } -}; - -const mapStateToProps = () => ({ - initialValues: { - // host: "mtg.tetrarch.co/servatrice", - // port: "443" - // host: "server.cockatrice.us", - // port: "4748" - } -}); - -export default connect(mapStateToProps)(reduxForm(propsMap)(RequestPasswordResetForm)); +export default RequestPasswordResetForm; diff --git a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css index 1a512eef..ad82b7ef 100644 --- a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css +++ b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.css @@ -1,5 +1,6 @@ .ResetPasswordForm { width: 100%; + padding-bottom: 15px; } .ResetPasswordForm-item { diff --git a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx index 589ef454..6d3f52da 100644 --- a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx +++ b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx @@ -1,9 +1,11 @@ // eslint-disable-next-line -import React, {useState} from "react"; +import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; -import { Form, Field, reduxForm, change } from 'redux-form' +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'; @@ -12,58 +14,18 @@ import './ResetPasswordForm.css'; import { useReduxEffect } from '../../hooks'; import { ServerTypes } from '../../store'; -const ResetPasswordForm = (props) => { - const { dispatch, handleSubmit } = props; - +const ResetPasswordForm = ({ onSubmit, userName }) => { const [errorMessage, setErrorMessage] = useState(false); - - const onHostChange: any = ({ host, port }) => { - dispatch(change(FormKey.RESET_PASSWORD, 'host', host)); - dispatch(change(FormKey.RESET_PASSWORD, 'port', port)); - } - useReduxEffect(() => { setErrorMessage(true); }, ServerTypes.RESET_PASSWORD_FAILED, []); - - return ( -
-
- {errorMessage ? ( -

Password Reset Failed, please try again

- ) : null} -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
- ); -}; - -const propsMap = { - form: FormKey.RESET_PASSWORD, - validate: values => { + const validate = values => { const errors: any = {}; - if (!values.user) { - errors.user = 'Required'; + if (!values.userName) { + errors.userName = 'Required'; } if (!values.token) { errors.token = 'Required'; @@ -76,24 +38,47 @@ const propsMap = { } else if (values.newPassword !== values.passwordAgain) { errors.passwordAgain = 'Passwords don\'t match' } - if (!values.host) { - errors.host = 'Required'; - } - if (!values.port) { - errors.port = 'Required'; + if (!values.selectedHost) { + errors.selectedHost = 'Required'; } return errors; - } + }; + + return ( +
+ {({ handleSubmit, form }) => ( + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + {errorMessage && ( +
+ Password reset failed +
+ )} +
+ +
+ )} + + ); }; -const mapStateToProps = () => ({ - initialValues: { - // host: "mtg.tetrarch.co/servatrice", - // port: "443" - // host: "server.cockatrice.us", - // port: "4748" - } -}); - -export default connect(mapStateToProps)(reduxForm(propsMap)(ResetPasswordForm)); +export default ResetPasswordForm; diff --git a/webclient/src/hooks/useAutoConnect.ts b/webclient/src/hooks/useAutoConnect.ts index cb226536..5258f58a 100644 --- a/webclient/src/hooks/useAutoConnect.ts +++ b/webclient/src/hooks/useAutoConnect.ts @@ -6,7 +6,7 @@ import { APP_USER } from 'types'; type OnChange = () => void; -export function useAutoConnect(onChange: OnChange) { +export function useAutoConnect() { const [setting, setSetting] = useState(undefined); const [autoConnect, setAutoConnect] = useState(undefined); @@ -31,8 +31,6 @@ export function useAutoConnect(onChange: OnChange) { if (setting) { setting.autoConnect = autoConnect; setting.save(); - - onChange(); } }, [setting, autoConnect]); diff --git a/webclient/src/store/server/server.actions.ts b/webclient/src/store/server/server.actions.ts index 114f2ac5..720e2ed8 100644 --- a/webclient/src/store/server/server.actions.ts +++ b/webclient/src/store/server/server.actions.ts @@ -72,18 +72,37 @@ export const Actions = { logs }), clearLogs: () => ({ - type: Types.CLEAR_LOGS + type: Types.CLEAR_LOGS, + }), + registrationRequiresEmail: () => ({ + type: Types.REGISTRATION_REQUIRES_EMAIL, + }), + registrationFailed: (error) => ({ + type: Types.REGISTRATION_FAILED, + error + }), + registrationEmailError: (error) => ({ + type: Types.REGISTRATION_EMAIL_ERROR, + error + }), + registrationPasswordError: (error) => ({ + type: Types.REGISTRATION_PASSWORD_ERROR, + error + }), + registrationUserNameError: (error) => ({ + type: Types.REGISTRATION_USERNAME_ERROR, + error }), resetPassword: () => ({ - type: Types.RESET_PASSWORD_REQUESTED + type: Types.RESET_PASSWORD_REQUESTED, }), resetPasswordFailed: () => ({ - type: Types.RESET_PASSWORD_FAILED + type: Types.RESET_PASSWORD_FAILED, }), resetPasswordChallenge: () => ({ - type: Types.RESET_PASSWORD_CHALLENGE + type: Types.RESET_PASSWORD_CHALLENGE, }), resetPasswordSuccess: () => ({ - type: Types.RESET_PASSWORD_SUCCESS + type: Types.RESET_PASSWORD_SUCCESS, }) } diff --git a/webclient/src/store/server/server.dispatch.ts b/webclient/src/store/server/server.dispatch.ts index 67cfd7ca..9db1e189 100644 --- a/webclient/src/store/server/server.dispatch.ts +++ b/webclient/src/store/server/server.dispatch.ts @@ -65,6 +65,21 @@ export const Dispatch = { serverMessage: message => { store.dispatch(Actions.serverMessage(message)); }, + registrationRequiresEmail: () => { + store.dispatch(Actions.registrationRequiresEmail()); + }, + registrationFailed: (error) => { + store.dispatch(Actions.registrationFailed(error)); + }, + registrationEmailError: (error) => { + store.dispatch(Actions.registrationEmailError(error)); + }, + registrationPasswordError: (error) => { + store.dispatch(Actions.registrationPasswordError(error)); + }, + registrationUserNameError: (error) => { + store.dispatch(Actions.registrationUserNameError(error)); + }, resetPassword: () => { store.dispatch(Actions.resetPassword()); }, diff --git a/webclient/src/store/server/server.types.ts b/webclient/src/store/server/server.types.ts index e266df06..4acd8108 100644 --- a/webclient/src/store/server/server.types.ts +++ b/webclient/src/store/server/server.types.ts @@ -17,6 +17,11 @@ export const Types = { USER_LEFT: '[Server] User Left', VIEW_LOGS: '[Server] View Logs', CLEAR_LOGS: '[Server] Clear Logs', + REGISTRATION_REQUIRES_EMAIL: '[Server] Registration Requires Email', + REGISTRATION_FAILED: '[Server] Registration Failed', + REGISTRATION_EMAIL_ERROR: '[Server] Registration Email Error', + REGISTRATION_PASSWORD_ERROR: '[Server] Registration Password Error', + REGISTRATION_USERNAME_ERROR: '[Server] Registration Username Error', 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/types/server.tsx b/webclient/src/types/server.tsx index 2d42156c..e8bfccd6 100644 --- a/webclient/src/types/server.tsx +++ b/webclient/src/types/server.tsx @@ -20,6 +20,8 @@ export interface WebSocketConnectOptions { hashedPassword?: string; newPassword?: string; email?: string; + realName?: string; + country?: string; autojoinrooms?: boolean; keepalive?: number; clientid?: string; diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts index be4449bd..fe6d4ba2 100644 --- a/webclient/src/websocket/WebClient.ts +++ b/webclient/src/websocket/WebClient.ts @@ -38,6 +38,8 @@ export class WebClient { hashedPassword: '', newPassword: '', email: '', + realName: '', + country: '', clientid: null, reason: null, autojoinrooms: true, diff --git a/webclient/src/websocket/commands/SessionCommands.spec.ts b/webclient/src/websocket/commands/SessionCommands.spec.ts index 71d4a39c..83d82215 100644 --- a/webclient/src/websocket/commands/SessionCommands.spec.ts +++ b/webclient/src/websocket/commands/SessionCommands.spec.ts @@ -299,39 +299,300 @@ describe('SessionCommands', () => { sendSessionCommandSpy.mockImplementation((_, callback) => callback(response)); }) - it('should login user if registration accepted without email verification', () => { - jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); - jest.spyOn(SessionPersistence, 'accountAwaitingActivation').mockImplementation(() => {}); + describe('RespRegistrationAccepted', () => { + it('should call SessionCommands.login()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + SessionCommands.register(); - SessionCommands.register(); + expect(SessionCommands.login).toHaveBeenCalled(); - expect(SessionCommands.login).toHaveBeenCalled(); - expect(SessionPersistence.accountAwaitingActivation).not.toHaveBeenCalled(); + }) }); - it('should prompt user if registration accepted with email verification', () => { + describe('RespRegistrationAcceptedNeedsActivation', () => { const RespRegistrationAcceptedNeedsActivation = 'RespRegistrationAcceptedNeedsActivation'; - response.responseCode = RespRegistrationAcceptedNeedsActivation; - webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAcceptedNeedsActivation = - RespRegistrationAcceptedNeedsActivation; - jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); - jest.spyOn(SessionPersistence, 'accountAwaitingActivation').mockImplementation(() => {}); + beforeEach(() => { + response.responseCode = RespRegistrationAcceptedNeedsActivation; + webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAcceptedNeedsActivation = + RespRegistrationAcceptedNeedsActivation; + }); - SessionCommands.register(); + it('should call SessionPersistence.accountAwaitingActivation()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'accountAwaitingActivation').mockImplementation(() => {}); + SessionCommands.register(); - expect(SessionCommands.login).not.toHaveBeenCalled(); - expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalled(); + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalled(); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); }); - it('should disconnect user if registration fails due to registration being disabled', () => { + describe('RespUserAlreadyExists', () => { + const RespUserAlreadyExists = 'RespUserAlreadyExists'; + + beforeEach(() => { + response.responseCode = RespUserAlreadyExists; + webClient.protobuf.controller.Response.ResponseCode.RespUserAlreadyExists = + RespUserAlreadyExists; + }); + + it('should call SessionPersistence.registrationUserNameError()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationUserNameError').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationUserNameError).toHaveBeenCalledWith(expect.any(String)); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); + }); + + describe('RespUsernameInvalid', () => { + const RespUsernameInvalid = 'RespUsernameInvalid'; + + beforeEach(() => { + response.responseCode = RespUsernameInvalid; + webClient.protobuf.controller.Response.ResponseCode.RespUsernameInvalid = + RespUsernameInvalid; + }); + + it('should call SessionPersistence.registrationUserNameError()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationUserNameError').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationUserNameError).toHaveBeenCalledWith(expect.any(String)); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); + }); + + describe('RespPasswordTooShort', () => { + const RespPasswordTooShort = 'RespPasswordTooShort'; + + beforeEach(() => { + response.responseCode = RespPasswordTooShort; + webClient.protobuf.controller.Response.ResponseCode.RespPasswordTooShort = + RespPasswordTooShort; + }); + + it('should call SessionPersistence.registrationPasswordError()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationPasswordError').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationPasswordError).toHaveBeenCalledWith(expect.any(String)); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); + }); + + describe('RespEmailRequiredToRegister', () => { + const RespEmailRequiredToRegister = 'RespEmailRequiredToRegister'; + + beforeEach(() => { + response.responseCode = RespEmailRequiredToRegister; + webClient.protobuf.controller.Response.ResponseCode.RespEmailRequiredToRegister = + RespEmailRequiredToRegister; + }); + + it('should call SessionPersistence.registrationRequiresEmail()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationRequiresEmail').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationRequiresEmail).toHaveBeenCalled(); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); + }); + + describe('RespEmailBlackListed', () => { + const RespEmailBlackListed = 'RespEmailBlackListed'; + + beforeEach(() => { + response.responseCode = RespEmailBlackListed; + webClient.protobuf.controller.Response.ResponseCode.RespEmailBlackListed = + RespEmailBlackListed; + }); + + it('should call SessionPersistence.registrationEmailError()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationEmailError').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationEmailError).toHaveBeenCalledWith(expect.any(String)); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); + }); + + describe('RespTooManyRequests', () => { + const RespTooManyRequests = 'RespTooManyRequests'; + + beforeEach(() => { + response.responseCode = RespTooManyRequests; + webClient.protobuf.controller.Response.ResponseCode.RespTooManyRequests = + RespTooManyRequests; + }); + + it('should call SessionPersistence.registrationEmailError()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationEmailError').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationEmailError).toHaveBeenCalledWith(expect.any(String)); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); + }); + + describe('RespRegistrationDisabled', () => { const RespRegistrationDisabled = 'RespRegistrationDisabled'; - response.responseCode = RespRegistrationDisabled; - webClient.protobuf.controller.Response.ResponseCode.RespRegistrationDisabled = RespRegistrationDisabled; - SessionCommands.register(); + beforeEach(() => { + response.responseCode = RespRegistrationDisabled; + webClient.protobuf.controller.Response.ResponseCode.RespRegistrationDisabled = + RespRegistrationDisabled; + }); - expect(SessionCommands.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.any(String)); + it('should call SessionPersistence.registrationFailed()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationFailed').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith(expect.any(String)); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); + }); + + describe('RespUserIsBanned', () => { + const RespUserIsBanned = 'RespUserIsBanned'; + + beforeEach(() => { + response.responseCode = RespUserIsBanned; + webClient.protobuf.controller.Response.ResponseCode.RespUserIsBanned = + RespUserIsBanned; + }); + + it('should call SessionPersistence.registrationFailed()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationFailed').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith(expect.any(String)); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); + }); + + describe('RespRegistrationFailed', () => { + const RespRegistrationFailed = 'RespRegistrationFailed'; + + beforeEach(() => { + response.responseCode = RespRegistrationFailed; + webClient.protobuf.controller.Response.ResponseCode.RespRegistrationFailed = + RespRegistrationFailed; + }); + + it('should call SessionPersistence.registrationFailed()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationFailed').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith(expect.any(String)); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); + }); + + describe('UnknownFailureReason', () => { + const UnknownFailureReason = 'UnknownFailureReason'; + + beforeEach(() => { + response.responseCode = UnknownFailureReason; + webClient.protobuf.controller.Response.ResponseCode.UnknownFailureReason = + UnknownFailureReason; + }); + + it('should call SessionPersistence.registrationFailed()', () => { + jest.spyOn(SessionCommands, 'login').mockImplementation(() => {}); + jest.spyOn(SessionPersistence, 'registrationFailed').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.login).not.toHaveBeenCalled(); + expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith(expect.any(String)); + }); + + it('should disconnect', () => { + jest.spyOn(SessionCommands, 'disconnect').mockImplementation(() => {}); + SessionCommands.register(); + + expect(SessionCommands.disconnect).toHaveBeenCalled(); + }); }); }); }); diff --git a/webclient/src/websocket/commands/SessionCommands.ts b/webclient/src/websocket/commands/SessionCommands.ts index 6f2d57fa..00300fed 100644 --- a/webclient/src/websocket/commands/SessionCommands.ts +++ b/webclient/src/websocket/commands/SessionCommands.ts @@ -179,48 +179,41 @@ export class SessionCommands { return; } - let error; - switch (raw.responseCode) { case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationAcceptedNeedsActivation: SessionPersistence.accountAwaitingActivation(); break; - case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationDisabled: - error = 'Registration is currently disabled'; - break; case webClient.protobuf.controller.Response.ResponseCode.RespUserAlreadyExists: - error = 'There is already an existing user with this username'; - break; - case webClient.protobuf.controller.Response.ResponseCode.RespEmailRequiredToRegister: - error = 'A valid email address is required to register'; - break; - case webClient.protobuf.controller.Response.ResponseCode.RespEmailBlackListed: - error = 'The email address provider used has been blocked from use'; - break; - case webClient.protobuf.controller.Response.ResponseCode.RespTooManyRequests: - error = 'This email address already has the maximum number of accounts you can register'; - break; - case webClient.protobuf.controller.Response.ResponseCode.RespPasswordTooShort: - error = 'Your password was too short'; - break; - case webClient.protobuf.controller.Response.ResponseCode.RespUserIsBanned: - error = NormalizeService.normalizeBannedUserError(raw.reasonStr, raw.endTime); + SessionPersistence.registrationUserNameError('Username is taken'); break; case webClient.protobuf.controller.Response.ResponseCode.RespUsernameInvalid: console.error('ResponseCode.RespUsernameInvalid', raw.reasonStr); - error = 'Invalid username'; + SessionPersistence.registrationUserNameError('Invalid username'); + break; + case webClient.protobuf.controller.Response.ResponseCode.RespPasswordTooShort: + SessionPersistence.registrationPasswordError('Your password was too short'); + break; + case webClient.protobuf.controller.Response.ResponseCode.RespEmailRequiredToRegister: + SessionPersistence.registrationRequiresEmail(); + break; + case webClient.protobuf.controller.Response.ResponseCode.RespEmailBlackListed: + SessionPersistence.registrationEmailError('This email provider has been blocked'); + break; + case webClient.protobuf.controller.Response.ResponseCode.RespTooManyRequests: + SessionPersistence.registrationEmailError('Max accounts reached for this email'); + break; + case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationDisabled: + SessionPersistence.registrationFailed('Registration is currently disabled'); + break; + case webClient.protobuf.controller.Response.ResponseCode.RespUserIsBanned: + SessionPersistence.registrationFailed(NormalizeService.normalizeBannedUserError(raw.reasonStr, raw.endTime)); break; case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationFailed: default: - console.error('ResponseCode Type', raw.responseCode); - error = 'Registration failed due to a server issue'; + SessionPersistence.registrationFailed('Registration failed due to a server issue'); break; } - if (error) { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Registration Failed: ${error}`); - } - SessionCommands.disconnect(); }); }; @@ -272,14 +265,14 @@ export class SessionCommands { const resp = raw['.Response_ForgotPasswordRequest.ext']; if (resp.challengeEmail) { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Requesting MFA information'); + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordChallenge(); } else { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password reset in progress'); + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPassword(); } } else { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password reset failed, please try again'); + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordFailed(); } @@ -305,10 +298,10 @@ export class SessionCommands { webClient.protobuf.sendSessionCommand(sc, raw => { if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password reset in progress'); + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPassword(); } else { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password reset failed, please try again'); + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordFailed(); } @@ -335,10 +328,10 @@ export class SessionCommands { webClient.protobuf.sendSessionCommand(sc, raw => { if (raw.responseCode === webClient.protobuf.controller.Response.ResponseCode.RespOk) { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password successfully updated'); + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordSuccess(); } else { - SessionCommands.updateStatus(StatusEnum.DISCONNECTED, 'Password update failed, please try again'); + SessionCommands.updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordFailed(); } diff --git a/webclient/src/websocket/persistence/SessionPersistence.ts b/webclient/src/websocket/persistence/SessionPersistence.ts index 2843e91f..237a4664 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.ts @@ -85,6 +85,26 @@ export class SessionPersistence { console.log('Account activation failed, show an action here'); } + static registrationRequiresEmail() { + ServerDispatch.registrationRequiresEmail(); + } + + static registrationFailed(error: string) { + ServerDispatch.registrationFailed(error); + } + + static registrationEmailError(error: string) { + ServerDispatch.registrationEmailError(error); + } + + static registrationPasswordError(error: string) { + ServerDispatch.registrationPasswordError(error); + } + + static registrationUserNameError(error: string) { + ServerDispatch.registrationUserNameError(error); + } + static resetPasswordChallenge() { ServerDispatch.resetPasswordChallenge(); }