diff --git a/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx b/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx index c299cc5b1..ce385345a 100644 --- a/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx +++ b/frontend/app/components/Client/ProfileSettings/ChangePassword.tsx @@ -4,10 +4,11 @@ import { Button, Message, Form, Input } from 'UI'; import styles from './profileSettings.module.css'; import { updatePassword } from 'Duck/user'; import { toast } from 'react-toastify'; +import { validatePassword } from 'App/validate'; +import { PASSWORD_POLICY } from 'App/constants'; const ERROR_DOESNT_MATCH = "Passwords don't match"; const MIN_LENGTH = 8; -const PASSWORD_POLICY = `Password should contain at least ${MIN_LENGTH} symbols`; type PropsFromRedux = ConnectedProps; @@ -39,12 +40,6 @@ const ChangePassword: React.FC = ({ passwordErrors, loading, upd return false; }, [newPassword, newPasswordRepeat, oldPassword]); - const validatePassword = (password: string) => { - const regex = - /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?])[A-Za-z\d!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]{8,}$/; - return regex.test(password); - }; - const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); @@ -126,12 +121,8 @@ const ChangePassword: React.FC = ({ passwordErrors, loading, upd {ERROR_DOESNT_MATCH} - {/* */}
+ + )} + + {error && ( +
+
+ +
+ {error} +
+ )} + + ); +} + +export default connect( + (state: any) => ({ + errors: state.getIn(['user', 'requestResetPassowrd', 'errors']), + resetErrors: state.getIn(['user', 'resetPassword', 'errors']), + loading: + state.getIn(['user', 'requestResetPassowrd', 'loading']) || + state.getIn(['user', 'resetPassword', 'loading']), + }), + { + requestResetPassword, + resetPassword, + resetErrors, + } +)(CreatePassword); diff --git a/frontend/app/components/ForgotPassword/ForgotPassword.js b/frontend/app/components/ForgotPassword/ForgotPassword.js deleted file mode 100644 index 8c10b1be0..000000000 --- a/frontend/app/components/ForgotPassword/ForgotPassword.js +++ /dev/null @@ -1,257 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ReCAPTCHA from 'react-google-recaptcha'; -import withPageTitle from 'HOCs/withPageTitle'; -import { Form, Input, Loader, Button, Link, Icon, Message } from 'UI'; -import { requestResetPassword, resetPassword, resetErrors } from 'Duck/user'; -import { login as loginRoute } from 'App/routes'; -import { withRouter } from 'react-router-dom'; -import { validateEmail } from 'App/validate'; -import cn from 'classnames'; -import stl from './forgotPassword.module.css'; -import Copyright from 'Shared/Copyright'; - -const LOGIN = loginRoute(); -const recaptchaRef = React.createRef(); -const ERROR_DONT_MATCH = "Passwords don't match."; -const MIN_LENGTH = 8; -const PASSWORD_POLICY = `Password should contain at least ${MIN_LENGTH} symbols.`; - -const checkDontMatch = (newPassword, newPasswordRepeat) => - newPasswordRepeat.length > 0 && newPasswordRepeat !== newPassword; - -@connect( - (state, props) => ({ - errors: state.getIn(['user', 'requestResetPassowrd', 'errors']), - resetErrors: state.getIn(['user', 'resetPassword', 'errors']), - loading: - state.getIn(['user', 'requestResetPassowrd', 'loading']) || - state.getIn(['user', 'resetPassword', 'loading']), - params: new URLSearchParams(props.location.search), - }), - { requestResetPassword, resetPassword, resetErrors } -) -@withPageTitle('Password Reset - OpenReplay') -@withRouter -export default class ForgotPassword extends React.PureComponent { - state = { - email: '', - code: ' ', - password: '', - passwordRepeat: '', - requested: false, - updated: false, - CAPTCHA_ENABLED: window.env.CAPTCHA_ENABLED === 'true', - }; - - handleSubmit = (token) => { - const { email, password } = this.state; - const { params } = this.props; - - const pass = params.get('pass'); - const invitation = params.get('invitation'); - const resetting = pass && invitation; - - if (!resetting) { - this.props - .requestResetPassword({ email: email.trim(), 'g-recaptcha-response': token }) - .then(() => { - const { errors } = this.props; - if (!errors) this.setState({ requested: true }); - }); - } else { - if (this.isSubmitDisabled()) return; - this.props.resetPassword({ email: email.trim(), invitation, pass, password }).then(() => { - const { resetErrors } = this.props; - if (!resetErrors) this.setState({ updated: true }); - }); - } - }; - - isSubmitDisabled() { - const { password, passwordRepeat } = this.state; - if (password !== passwordRepeat || password.length < MIN_LENGTH) return true; - return false; - } - - write = ({ target: { value, name } }) => this.setState({ [name]: value }); - - shouldShouwPolicy() { - const { password } = this.state; - if (password.length > 7) return false; - if (password === '') return false; - return true; - } - - onSubmit = (e) => { - e.preventDefault(); - const { CAPTCHA_ENABLED } = this.state; - if (CAPTCHA_ENABLED && recaptchaRef.current) { - recaptchaRef.current.execute(); - } else if (!CAPTCHA_ENABLED) { - this.handleSubmit(); - } - }; - - componentWillUnmount() { - this.props.resetErrors(); - } - - render() { - const { CAPTCHA_ENABLED } = this.state; - const { errors, loading, params } = this.props; - const { requested, updated, password, passwordRepeat, email } = this.state; - const dontMatch = checkDontMatch(password, passwordRepeat); - - const pass = params.get('pass'); - const invitation = params.get('invitation'); - const resetting = pass && invitation; - const validEmail = validateEmail(email); - - return ( -
-
-
- -
-
- {!resetting &&

Reset Password

} - {resetting &&

- Welcome, join your organization by
creating a new password -

- } - -
-
- -
- {CAPTCHA_ENABLED && ( -
- this.handleSubmit(token)} - /> -
- )} - - {!resetting && !requested && ( - - - - - )} - - {requested && !errors && ( -
-
- -
-
Alright! a reset link was emailed to {email}. Click on it to reset your account password.
-
- )} - - {resetting && ( - - - - {/* */} - - -
- {PASSWORD_POLICY} -
- - - - -
- )} - - -
-
-
- {errors && errors.map((error, i) => ( -
-
- -
- {error} -
- ))} -
-
- -
- {'Your password has been updated sucessfully.'} -
-
- - {!(updated || requested) && ( - - )} - -
- - {updated && ( - - )} -
{'Back to Login'}
- -
- -
-
-
- -
- ); - } -} diff --git a/frontend/app/components/ForgotPassword/ForgotPassword.tsx b/frontend/app/components/ForgotPassword/ForgotPassword.tsx new file mode 100644 index 000000000..cb6aced80 --- /dev/null +++ b/frontend/app/components/ForgotPassword/ForgotPassword.tsx @@ -0,0 +1,58 @@ +import Copyright from 'Shared/Copyright'; +import React from 'react'; +import { Form, Input, Loader, Button, Link, Icon, Message } from 'UI'; +import { login as loginRoute } from 'App/routes'; +import { connect } from 'react-redux'; +import ResetPassword from './ResetPasswordRequest'; +import CreatePassword from './CreatePassword'; + +const LOGIN = loginRoute(); + +interface Props { + params: any; +} +function ForgotPassword(props: Props) { + const { params } = props; + const pass = params.get('pass'); + const invitation = params.get('invitation'); + const creatingNewPassword = pass && invitation; + + return ( +
+
+
+ +
+
+ {creatingNewPassword ? ( +

+ Welcome, join your organization by creating a new password +

+ ) : ( +

+ Reset Password +

+ )} + +
+ {creatingNewPassword ? : } +
+ +
+
+ +
{'Back to Login'}
+ +
+
+
+
+ + +
+ ); +} + +export default connect((state: any, props: any) => ({ + params: new URLSearchParams(props.location.search), +}))(ForgotPassword); diff --git a/frontend/app/components/ForgotPassword/ResetPasswordRequest.tsx b/frontend/app/components/ForgotPassword/ResetPasswordRequest.tsx new file mode 100644 index 000000000..08d354fab --- /dev/null +++ b/frontend/app/components/ForgotPassword/ResetPasswordRequest.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { Form, Input, Loader, Button, Link, Icon, Message } from 'UI'; +import ReCAPTCHA from 'react-google-recaptcha'; +import { connect } from 'react-redux'; +import { requestResetPassword, resetPassword, resetErrors } from 'Duck/user'; + +interface Props { + requestResetPassword: Function; +} +function ResetPasswordRequest(props: Props) { + const recaptchaRef = React.createRef(); + const [requested, setRequested] = React.useState(false); + const [email, setEmail] = React.useState(''); + const [error, setError] = React.useState(null); + const CAPTCHA_ENABLED = window.env.CAPTCHA_ENABLED === 'true'; + const CAPTCHA_SITE_KEY = window.env.CAPTCHA_SITE_KEY; + + const write = (e: any) => { + const { name, value } = e.target; + if (name === 'email') setEmail(value); + }; + + const onSubmit = (e: any) => { + e.preventDefault(); + if (CAPTCHA_ENABLED && recaptchaRef.current) { + recaptchaRef.current.execute(); + } else if (!CAPTCHA_ENABLED) { + handleSubmit(); + } + }; + + const handleSubmit = (token?: any) => { + setError(null); + props + .requestResetPassword({ email: email.trim(), 'g-recaptcha-response': token }) + .then((response: any) => { + setRequested(true); + if (response && response.errors && response.errors.length > 0) { + setError(response.errors[0]); + } else { + } + }); + }; + return ( +
+ + {CAPTCHA_ENABLED && ( +
+ handleSubmit(token)} + /> +
+ )} + {!requested && ( + <> + + + + + + + )} + + {requested && !error && ( +
+
+ +
+
+ Alright! a reset link was emailed to {email}. + Click on it to reset your account password. +
+
+ )} + + {error && ( +
+
+ +
+ {error} +
+ )} +
+
+ ); +} + +export default connect((state: any) => ({}), { requestResetPassword })(ResetPasswordRequest); diff --git a/frontend/app/constants/index.js b/frontend/app/constants/index.js index b1b8c0b46..20673700f 100644 --- a/frontend/app/constants/index.js +++ b/frontend/app/constants/index.js @@ -21,4 +21,5 @@ export { } from './schedule'; export { default } from './filterOptions'; // export { default as storageKeys } from './storageKeys'; -export const ENTERPRISE_REQUEIRED = "This feature requires an enterprise license."; \ No newline at end of file +export const ENTERPRISE_REQUEIRED = "This feature requires an enterprise license."; +export const PASSWORD_POLICY = "The password should have a minimum length of 8 characters and include at least one uppercase letter, one lowercase letter, one digit, and one special character."; \ No newline at end of file diff --git a/frontend/app/validate.js b/frontend/app/validate.js index 771fbd461..5fe96c3d6 100644 --- a/frontend/app/validate.js +++ b/frontend/app/validate.js @@ -82,3 +82,9 @@ export function validateNumber(str, options = {}) { if (max && n > max) return false; return true; } + +export const validatePassword = (password) => { + const regex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?])[A-Za-z\d!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]{8,}$/; + return regex.test(password); +}; \ No newline at end of file