change(ui) - password validations

This commit is contained in:
Shekar Siri 2023-04-21 18:24:58 +02:00
parent 2494a98a57
commit 3f5691f485
7 changed files with 340 additions and 270 deletions

View file

@ -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<typeof connector>;
@ -39,12 +40,6 @@ const ChangePassword: React.FC<PropsFromRedux> = ({ 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<HTMLFormElement>) => {
e.preventDefault();
@ -126,12 +121,8 @@ const ChangePassword: React.FC<PropsFromRedux> = ({ passwordErrors, loading, upd
{ERROR_DOESNT_MATCH}
</Message>
<Message error hidden={!newPassword.error}>
Password should contain at least one capital letter, special character, and digit, and
length min 8
{PASSWORD_POLICY}
</Message>
{/* <Message error hidden={!newPasswordRepeat.error}>
Passwords don't match
</Message> */}
<div className="flex items-center pt-3">
<Button type="submit" variant="outline" disabled={isSubmitDisabled()} loading={loading}>
Change Password

View file

@ -0,0 +1,166 @@
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import ReCAPTCHA from 'react-google-recaptcha';
import { Form, Input, Loader, Button, Icon, Message } from 'UI';
import { requestResetPassword, resetPassword, resetErrors } from 'Duck/user';
import stl from './forgotPassword.module.css';
import { validatePassword } from 'App/validate';
import { PASSWORD_POLICY } from 'App/constants';
const recaptchaRef = React.createRef();
const ERROR_DONT_MATCH = "Passwords don't match.";
const CAPTCHA_ENABLED = window.env.CAPTCHA_ENABLED === 'true';
const CAPTCHA_SITE_KEY = window.env.CAPTCHA_SITE_KEY;
interface Props {
errors: any;
resetErrors: any;
loading: boolean;
params: any;
resetPassword: Function;
}
function CreatePassword(props: Props) {
const { loading, params } = props;
const [error, setError] = React.useState<String | null>(null);
const [validationError, setValidationError] = React.useState<String | null>(null);
const [updated, setUpdated] = React.useState(false);
const [requested, setRequested] = React.useState(false);
const [passwordRepeat, setPasswordRepeat] = React.useState('');
const [password, setPassword] = React.useState('');
const [doesntMatch, setDoesntMatch] = React.useState(false);
const pass = params.get('pass');
const invitation = params.get('invitation');
const handleSubmit = (token?: any) => {
if (!validatePassword(password)) {
return;
}
props.resetPassword({ invitation, pass, password }).then((response: any) => {
if (response && response.errors && response.errors.length > 0) {
setError(response.errors[0]);
} else {
setUpdated(true);
}
});
};
const onSubmit = (e: any) => {
e.preventDefault();
if (CAPTCHA_ENABLED && recaptchaRef.current) {
recaptchaRef.current.execute();
} else if (!CAPTCHA_ENABLED) {
handleSubmit();
}
};
const write = (e: any) => {
const { name, value } = e.target;
if (name === 'password') setPassword(value);
if (name === 'passwordRepeat') setPasswordRepeat(value);
};
useEffect(() => {
if (passwordRepeat.length > 0 && passwordRepeat !== password) {
setValidationError(ERROR_DONT_MATCH);
} else if (passwordRepeat.length > 0 && !validatePassword(password)) {
setValidationError(PASSWORD_POLICY);
} else {
setValidationError(null);
}
}, [passwordRepeat, password]);
return (
<Form
onSubmit={onSubmit}
style={{ minWidth: '50%' }}
className="flex flex-col items-center justify-center"
>
{!error && (
<>
<Loader loading={loading}>
<div data-hidden={updated} className="w-full">
{CAPTCHA_ENABLED && (
<div className={stl.recaptcha}>
<ReCAPTCHA
ref={recaptchaRef}
size="invisible"
data-hidden={requested}
sitekey={CAPTCHA_SITE_KEY}
onChange={(token: any) => handleSubmit(token)}
/>
</div>
)}
<React.Fragment>
<Form.Field>
<label>{'New password'}</label>
<Input
autoComplete="new-password"
type="password"
placeholder="Type here..."
name="password"
onChange={write}
className="w-full"
icon="key"
required
/>
</Form.Field>
<Form.Field>
<label>{'Cofirm password'}</label>
<Input
autoComplete="new-password"
type="password"
placeholder="Re-enter your new password"
name="passwordRepeat"
onChange={write}
className="w-full"
icon="key"
required
/>
</Form.Field>
</React.Fragment>
</div>
</Loader>
<div className="mt-4">
<div data-hidden={!updated} className="flex items-center flex-col text-center">
<div className="w-10 h-10 bg-tealx-lightest rounded-full flex items-center justify-center mb-3">
<Icon name="check" size="30" color="tealx" />
</div>
<span>{'Your password has been updated sucessfully.'}</span>
</div>
</div>
{validationError && <Message error>{validationError}</Message>}
<Button type="submit" variant="primary" loading={loading} className="w-full mt-4">
Create
</Button>
</>
)}
{error && (
<div className="flex items-center flex-col text-center">
<div className="w-16 h-16 rounded-full bg-red-lightest flex items-center justify-center mb-2">
<Icon name="envelope-x" size="30" color="red" />
</div>
{error}
</div>
)}
</Form>
);
}
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);

View file

@ -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 (
<div className="flex items-center justify-center h-screen">
<div className="flex flex-col items-center">
<div className="m-10 ">
<img src="/assets/logo.svg" width={200} />
</div>
<div className="border rounded bg-white" style={{ width: '350px' }}>
{!resetting && <h2 className="text-center text-2xl font-medium mb-6 border-b p-5 w-full">Reset Password</h2>}
{resetting && <h2 className="text-center text-lg font-medium mb-6 border-b p-5 w-full">
Welcome, join your organization by <br/> creating a new password
</h2>
}
<div className="px-8">
<Form
onSubmit={this.onSubmit}
style={{ minWidth: '50%' }}
className="flex flex-col items-center justify-center"
>
<Loader loading={loading}>
<div data-hidden={updated} className="w-full">
{CAPTCHA_ENABLED && (
<div className={stl.recaptcha}>
<ReCAPTCHA
ref={recaptchaRef}
size="invisible"
data-hidden={requested}
sitekey={window.env.CAPTCHA_SITE_KEY}
onChange={(token) => this.handleSubmit(token)}
/>
</div>
)}
{!resetting && !requested && (
<Form.Field>
<label>{'Email Address'}</label>
<Input
autoFocus={true}
autoComplete="email"
type="email"
placeholder="Email"
name="email"
onChange={this.write}
className="w-full"
icon="envelope"
required
/>
</Form.Field>
)}
{requested && !errors && (
<div className="flex flex-col items-center justify-center">
<div className="w-16 h-16 rounded-full bg-tealx-light flex items-center justify-center mb-2">
<Icon name="envelope-check" size={30} color="tealx" />
</div>
<div>Alright! a reset link was emailed to {email}. Click on it to reset your account password.</div>
</div>
)}
{resetting && (
<React.Fragment>
<Form.Field>
<label>{'New password'}</label>
{/* <i className={stl.inputIconPassword} /> */}
<Input
autoComplete="new-password"
type="password"
placeholder="Type here..."
name="password"
onChange={this.write}
className="w-full"
icon="key"
required
/>
</Form.Field>
<div className={stl.passwordPolicy} data-hidden={!this.shouldShouwPolicy()}>
{PASSWORD_POLICY}
</div>
<Form.Field>
<label>{'Cofirm password'}</label>
<Input
autoComplete="new-password"
type="password"
placeholder="Re-enter your new password"
name="passwordRepeat"
onChange={this.write}
className="w-full"
icon="key"
required
/>
</Form.Field>
</React.Fragment>
)}
<Message error hidden={!dontMatch}>
{ERROR_DONT_MATCH}
</Message>
</div>
</Loader>
<div className="mt-4">
{errors && errors.map((error, i) => (
<div className="flex items-center flex-col text-center" key={i}>
<div className="w-16 h-16 rounded-full bg-red-lightest flex items-center justify-center mb-2">
<Icon name="envelope-x" size="30" color="red" />
</div>
{error}
</div>
))}
<div data-hidden={!updated} className="flex items-center flex-col text-center">
<div className="w-10 h-10 bg-tealx-lightest rounded-full flex items-center justify-center mb-3">
<Icon name="check" size="30" color="tealx" />
</div>
<span>{'Your password has been updated sucessfully.'}</span>
</div>
</div>
{!(updated || requested) && (
<Button
type="submit"
variant="primary"
loading={loading}
className="w-full"
// disabled={(resetting && this.isSubmitDisabled()) || (!resetting && !validEmail)}
>
{resetting ? 'Create' : 'Email password reset link'}
</Button>
)}
<div className="my-8">
<Link to={LOGIN}>
{updated && (
<Button variant="primary" type="submit" primary>
{'Login'}
</Button>
)}
<div data-hidden={updated} className="link">{'Back to Login'}</div>
</Link>
</div>
</Form>
</div>
</div>
</div>
<Copyright />
</div>
);
}
}

View file

@ -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 (
<div className="flex items-center justify-center h-screen -mt-20">
<div className="flex flex-col items-center">
<div className="m-10 ">
<img src="/assets/logo.svg" width={200} />
</div>
<div className="border rounded bg-white" style={{ width: '350px' }}>
{creatingNewPassword ? (
<h2 className="text-center text-lg font-medium mb-6 border-b p-5 w-full">
Welcome, join your organization by creating a new password
</h2>
) : (
<h2 className="text-center text-2xl font-medium mb-6 border-b p-5 w-full">
Reset Password
</h2>
)}
<div className="w-full px-8">
{creatingNewPassword ? <CreatePassword params={params} /> : <ResetPassword />}
</div>
<div className="flex flex-col items-center justify-center">
<div className="my-8">
<Link to={LOGIN}>
<div className="link">{'Back to Login'}</div>
</Link>
</div>
</div>
</div>
</div>
<Copyright />
</div>
);
}
export default connect((state: any, props: any) => ({
params: new URLSearchParams(props.location.search),
}))(ForgotPassword);

View file

@ -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 (
<Form onSubmit={onSubmit} style={{ minWidth: '50%' }} className="flex flex-col">
<Loader loading={false}>
{CAPTCHA_ENABLED && (
<div className="flex justify-center">
<ReCAPTCHA
ref={recaptchaRef}
size="invisible"
data-hidden={requested}
sitekey={CAPTCHA_SITE_KEY}
onChange={(token: any) => handleSubmit(token)}
/>
</div>
)}
{!requested && (
<>
<Form.Field>
<label>{'Email Address'}</label>
<Input
autoFocus={true}
autoComplete="email"
type="email"
placeholder="Email"
name="email"
onChange={write}
className="w-full"
icon="envelope"
required
/>
</Form.Field>
<Button type="submit" variant="primary" className="mt-4">
Email password reset link
</Button>
</>
)}
{requested && !error && (
<div className="flex flex-col items-center justify-center">
<div className="w-16 h-16 rounded-full bg-tealx-light flex items-center justify-center mb-2">
<Icon name="envelope-check" size={30} color="tealx" />
</div>
<div>
Alright! a reset link was emailed to <span className="font-medium">{email}</span>.
Click on it to reset your account password.
</div>
</div>
)}
{error && (
<div className="flex items-center flex-col text-center">
<div className="w-16 h-16 rounded-full bg-red-lightest flex items-center justify-center mb-2">
<Icon name="envelope-x" size="30" color="red" />
</div>
{error}
</div>
)}
</Loader>
</Form>
);
}
export default connect((state: any) => ({}), { requestResetPassword })(ResetPasswordRequest);

View file

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

View file

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