feat(ui) - signup password enforce (#1272)

* feat(ui) - enforce pwd during signup (#1271)

* change(ui) - antd dependency
This commit is contained in:
Shekar Siri 2023-05-22 12:54:34 +02:00 committed by GitHub
parent 7847753b55
commit 6bb9181787
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1220 additions and 313 deletions

View file

@ -13,7 +13,7 @@ import { withStore } from 'App/mstore';
import APIClient from './api_client';
import * as routes from './routes';
import { OB_DEFAULT_TAB, isRoute } from 'App/routes';
import Signup from './components/Signup/Signup';
import Signup from 'Components/Signup';
import { fetchTenants } from 'Duck/user';
import { setSessionPath } from 'Duck/sessions';
import { ModalProvider } from './components/Modal';

View file

@ -1,101 +0,0 @@
import React from 'react';
import withPageTitle from 'HOCs/withPageTitle';
import { Icon } from 'UI';
import { connect } from 'react-redux';
import cn from 'classnames';
import SignupForm from './SignupForm';
import RegisterBg from '../../svg/register.svg';
import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
import { getHealthRequest } from 'Components/Header/HealthStatus/getHealth';
import { login } from 'App/routes';
import { withRouter } from 'react-router-dom';
import { fetchTenants } from 'Duck/user';
import Copyright from 'Shared/Copyright';
const LOGIN_ROUTE = login();
const BulletItem = ({ text }) => (
<div className="flex items-center mb-4">
<div className="mr-3 h-8 w-8 rounded-full bg-white shadow flex items-center justify-center">
<Icon name="check" size="26" />
</div>
<div>{text}</div>
</div>
);
const healthStatusCheck_key = '__or__healthStatusCheck_key'
@connect(
(state, props) => ({
loading: state.getIn(['user', 'loginRequest', 'loading']),
authDetails: state.getIn(['user', 'authDetails']),
}), { fetchTenants }
)
@withPageTitle('Signup - OpenReplay')
@withRouter
export default class Signup extends React.Component {
state = {
healthModalPassed: localStorage.getItem(healthStatusCheck_key === 'true'),
healthStatusLoading: true,
healthStatus: null,
}
static getDerivedStateFromProps(nextProps, prevState) {
const { authDetails } = nextProps;
if (Object.keys(authDetails).length === 0) {
return null;
}
if (authDetails.tenants) {
nextProps.history.push(LOGIN_ROUTE);
}
return null;
}
getHealth = async () => {
this.setState({ healthStatusLoading: true });
const { healthMap } = await getHealthRequest(true);
this.setState({ healthStatus: healthMap, healthStatusLoading: false });
}
componentDidMount() {
if (!this.state.healthModalPassed) void this.getHealth();
const { authDetails } = this.props;
if (Object.keys(authDetails).length === 0) {
this.props.fetchTenants();
}
}
setHealthModalPassed = () => {
localStorage.setItem(healthStatusCheck_key, 'true');
this.setState({ healthModalPassed: true });
}
render() {
if (!this.state.healthModalPassed) {
return (
<HealthModal
setShowModal={() => null}
healthResponse={this.state.healthStatus}
getHealth={this.getHealth}
isLoading={this.state.healthStatusLoading}
setPassed={this.setHealthModalPassed}
/>
)
}
return (
<div className="flex justify-center items-center gap-6" style={{ height: '100vh' }}>
<div className="flex items-center justify-center">
<div className="">
<SignupForm />
</div>
</div>
<Copyright />
</div>
);
}
}

View file

@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { connect, ConnectedProps } from 'react-redux';
import { Icon } from 'UI';
import SignupForm from './SignupForm';
import HealthModal from 'Components/Header/HealthStatus/HealthModal/HealthModal';
import { getHealthRequest } from 'Components/Header/HealthStatus/getHealth';
import { fetchTenants } from 'Duck/user';
import withPageTitle from 'HOCs/withPageTitle';
import { login } from 'App/routes';
import Copyright from 'Shared/Copyright';
const LOGIN_ROUTE = login();
const BulletItem: React.FC<{ text: string }> = ({ text }) => (
<div className='flex items-center mb-4'>
<div className='mr-3 h-8 w-8 rounded-full bg-white shadow flex items-center justify-center'>
<Icon name='check' size='26' />
</div>
<div>{text}</div>
</div>
);
const healthStatusCheck_key = '__or__healthStatusCheck_key';
const mapStateToProps = (state: any) => ({
loading: state.getIn(['user', 'loginRequest', 'loading']),
authDetails: state.getIn(['user', 'authDetails'])
});
const mapDispatchToProps = {
fetchTenants
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type PropsFromRedux = ConnectedProps<typeof connector>;
type SignupProps = PropsFromRedux & RouteComponentProps;
const Signup: React.FC<SignupProps> = ({ loading, authDetails, fetchTenants, history }) => {
const [healthModalPassed, setHealthModalPassed] = useState<boolean>(localStorage.getItem(healthStatusCheck_key) === 'true');
const [healthStatusLoading, setHealthStatusLoading] = useState<boolean>(true);
const [healthStatus, setHealthStatus] = useState<any>(null);
const getHealth = async () => {
setHealthStatusLoading(true);
const { healthMap } = await getHealthRequest(true);
setHealthStatus(healthMap);
setHealthStatusLoading(false);
};
useEffect(() => {
if (!healthModalPassed) void getHealth();
if (Object.keys(authDetails).length === 0) {
fetchTenants();
}
}, []);
useEffect(() => {
if (Object.keys(authDetails).length === 0) {
history.push(LOGIN_ROUTE);
}
}, [authDetails]);
if (!healthModalPassed) {
return (
<HealthModal
setShowModal={() => null}
healthResponse={healthStatus}
getHealth={getHealth}
isLoading={healthStatusLoading}
setPassed={() => setHealthModalPassed(true)}
/>
);
}
return (
<div className='flex justify-center items-center gap-6' style={{ height: '100vh' }}>
<div className='flex items-center justify-center'>
<div className=''>
<SignupForm />
</div>
</div>
<Copyright />
</div>
);
};
export default connector(withRouter(withPageTitle('Signup - OpenReplay')(Signup)));

View file

@ -1,201 +0,0 @@
import React from 'react';
import { Form, Input, Icon, Button, Link } from 'UI';
import { login } from 'App/routes';
import ReCAPTCHA from 'react-google-recaptcha';
import stl from './signup.module.css';
import { signup } from 'Duck/user';
import { connect } from 'react-redux';
import Select from 'Shared/Select';
import { SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
const LOGIN_ROUTE = login();
const recaptchaRef = React.createRef();
@connect(
(state) => ({
tenants: state.getIn(['user', 'tenants']),
errors: state.getIn(['user', 'signupRequest', 'errors']),
loading: state.getIn(['user', 'signupRequest', 'loading']),
}),
{ signup }
)
export default class SignupForm extends React.Component {
state = {
tenantId: '',
fullname: '',
password: '',
email: '',
projectName: '',
organizationName: '',
reload: false,
CAPTCHA_ENABLED: window.env.CAPTCHA_ENABLED === 'true',
};
static getDerivedStateFromProps(props, state) {
if (props.errors && props.errors.size > 0 && state.reload) {
recaptchaRef.current.reset();
return {
reload: false,
};
}
return null;
}
handleSubmit = (token) => {
const { tenantId, fullname, password, email, projectName, organizationName, auth } = this.state;
localStorage.removeItem(SITE_ID_STORAGE_KEY);
this.props.signup({
tenantId,
fullname,
password,
email,
projectName,
organizationName,
auth,
'g-recaptcha-response': token,
});
this.setState({ reload: true });
};
write = ({ target: { value, name } }) => this.setState({ [name]: value });
writeOption = ({ name, value }) => this.setState({ [name]: value.value });
onSubmit = (e) => {
e.preventDefault();
const { CAPTCHA_ENABLED } = this.state;
if (CAPTCHA_ENABLED && recaptchaRef.current) {
recaptchaRef.current.execute();
} else if (!CAPTCHA_ENABLED) {
this.handleSubmit();
}
};
render() {
const { loading, errors, tenants } = this.props;
const { CAPTCHA_ENABLED } = this.state;
return (
<div className="flex flex-col items-center">
<div className="m-10 ">
<img src="/assets/logo.svg" width={200} />
</div>
<Form onSubmit={this.onSubmit} className="bg-white border rounded">
<div className="mb-8">
<h2 className="text-center text-2xl font-medium mb-6 border-b p-5 w-full">
Create Account
</h2>
</div>
<>
{CAPTCHA_ENABLED && (
<ReCAPTCHA
ref={recaptchaRef}
size="invisible"
sitekey={window.env.CAPTCHA_SITE_KEY}
onChange={(token) => this.handleSubmit(token)}
/>
)}
<div className="px-8">
{tenants.length > 0 && (
<Form.Field>
<label>Existing Accounts</label>
<Select
className="w-full"
placeholder="Select account"
selection
options={tenants}
name="tenantId"
// value={ instance.currentPeriod }
onChange={this.writeOption}
/>
</Form.Field>
)}
<Form.Field>
<label>Email Address</label>
<Input
autoFocus={true}
autoComplete="username"
type="email"
placeholder="E.g. email@yourcompany.com"
name="email"
onChange={this.write}
required="true"
icon="envelope"
/>
</Form.Field>
<Form.Field>
<label className="mb-2">Password</label>
<Input
type="password"
placeholder="Min 8 Characters"
minLength="8"
name="password"
onChange={this.write}
required="true"
icon="key"
/>
</Form.Field>
<Form.Field>
<label>Name</label>
<Input
type="text"
placeholder="E.g John Doe"
name="fullname"
onChange={this.write}
required="true"
icon="user-alt"
/>
</Form.Field>
<Form.Field>
<label>Organization</label>
<Input
type="text"
placeholder="E.g Uber"
name="organizationName"
onChange={this.write}
required="true"
icon="buildings"
/>
</Form.Field>
<Button type="submit" variant="primary" loading={loading} className="w-full">
Create Account
</Button>
<div className="my-6">
<div className="text-sm">
By signing up, you agree to our{' '}
<a href="https://openreplay.com/terms.html" className="link">
terms of service
</a>{' '}
and{' '}
<a href="https://openreplay.com/privacy.html" className="link">
privacy policy
</a>
.
</div>
</div>
</div>
</>
{errors && (
<div className={stl.errors}>
{errors.map((error) => (
<div className={stl.errorItem}>
<Icon name="info" color="red" size="20" />
<span className="color-red ml-2">
{error}
<br />
</span>
</div>
))}
</div>
)}
</Form>
<div className="text-center py-6">
Already having an account?{' '}
<span className="link">
<Link to={LOGIN_ROUTE}>Login</Link>
</span>
</div>
</div>
);
}
}

View file

@ -0,0 +1,217 @@
import React, { useState, useRef, ChangeEvent, FormEvent, useEffect } from 'react';
import { Form, Input, Button, Link } from 'UI';
import { login } from 'App/routes';
import ReCAPTCHA from 'react-google-recaptcha';
import { signup } from 'Duck/user';
import { connect, ConnectedProps } from 'react-redux';
import Select from 'Shared/Select';
import { SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
import { validatePassword } from 'App/validate';
import { PASSWORD_POLICY } from 'App/constants';
import { Alert, Space } from 'antd';
const LOGIN_ROUTE = login();
const mapState = (state: any) => ({
tenants: state.getIn(['user', 'tenants']),
errors: state.getIn(['user', 'signupRequest', 'errors']),
loading: state.getIn(['user', 'signupRequest', 'loading'])
});
const mapDispatch = {
signup
};
const connector = connect(mapState, mapDispatch);
type PropsFromRedux = ConnectedProps<typeof connector>;
type SignupFormProps = PropsFromRedux;
const SignupForm: React.FC<SignupFormProps> = ({ tenants, errors, loading, signup }) => {
const [state, setState] = useState({
tenantId: '',
fullname: '',
password: '',
email: '',
projectName: '',
organizationName: '',
reload: false,
CAPTCHA_ENABLED: window.env.CAPTCHA_ENABLED === 'true'
});
const recaptchaRef = useRef<ReCAPTCHA>(null);
const [passwordError, setPasswordError] = useState<string | null>(null);
const handleSubmit = (token: string) => {
const { tenantId, fullname, password, email, projectName, organizationName, auth } = state;
if (!validatePassword(password)) return;
localStorage.removeItem(SITE_ID_STORAGE_KEY);
signup({
tenantId,
fullname,
password,
email,
projectName,
organizationName,
auth,
'g-recaptcha-response': token
});
setState({ ...state, reload: true });
};
const write = ({ target: { value, name } }: ChangeEvent<HTMLInputElement>) =>
setState({ ...state, [name]: value });
const writeOption = ({ name, value }: { name: string; value: { value: string } }) =>
setState({ ...state, [name]: value.value });
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { CAPTCHA_ENABLED } = state;
if (CAPTCHA_ENABLED && recaptchaRef.current) {
recaptchaRef.current.execute();
} else if (!CAPTCHA_ENABLED) {
handleSubmit('');
}
};
useEffect(() => {
if (state.password && !validatePassword(state.password)) {
setPasswordError('Password must be at least 8 characters long');
} else {
setPasswordError(null);
}
}, [state.password]);
return (
<div className='flex flex-col items-center'>
<div className='m-10 '>
<img src='/assets/logo.svg' width={200} alt='Logo' />
</div>
<Form onSubmit={onSubmit} className='bg-white border rounded' style={{ maxWidth: '420px' }}>
<div className='mb-8'>
<h2 className='text-center text-2xl font-medium mb-6 border-b p-5 w-full'>
Create Account
</h2>
</div>
<>
{state.CAPTCHA_ENABLED && (
<ReCAPTCHA
ref={recaptchaRef}
size='invisible'
sitekey={window.env.CAPTCHA_SITE_KEY}
onChange={(token) => handleSubmit(token || '')}
/>
)}
<div className='px-8'>
{tenants.length > 0 && (
<Form.Field>
<label>Existing Accounts</label>
<Select
className='w-full'
placeholder='Select account'
selection
options={tenants}
name='tenantId'
// value={ instance.currentPeriod }
onChange={writeOption}
/>
</Form.Field>
)}
<Form.Field>
<label>Email Address</label>
<Input
autoFocus={true}
autoComplete='username'
type='email'
placeholder='E.g. email@yourcompany.com'
name='email'
onChange={write}
required={true}
icon='envelope'
/>
</Form.Field>
<Form.Field>
<label className='mb-2'>Password</label>
<Input
type='password'
placeholder='Min 8 Characters'
minLength={8}
name='password'
onChange={write}
required={true}
icon='key'
/>
</Form.Field>
<Form.Field>
<label>Name</label>
<Input
type='text'
placeholder='E.g John Doe'
name='fullname'
onChange={write}
required={true}
icon='user-alt'
/>
</Form.Field>
<Form.Field>
<label>Organization</label>
<Input
type='text'
placeholder='E.g Uber'
name='organizationName'
onChange={write}
required={true}
icon='buildings'
/>
</Form.Field>
{passwordError && (
// <Alert type='error' message={PASSWORD_POLICY} banner icon={null} />
<Alert
className='my-3'
// message="Error Text"
description={PASSWORD_POLICY}
type='error'
/>
)}
{errors && errors.length && (
<Alert
className='my-3'
// message="Error Text"
description={errors[0]}
type='error'
/>
)}
<Button type='submit' variant='primary' loading={loading} className='w-full'>
Create Account
</Button>
<div className='my-6'>
<div className='text-sm'>
By signing up, you agree to our{' '}
<a href='https://openreplay.com/terms.html' className='link'>
terms of service
</a>{' '}
and{' '}
<a href='https://openreplay.com/privacy.html' className='link'>
privacy policy
</a>
.
</div>
</div>
</div>
</>
</Form>
<div className='text-center py-6'>
Already having an account?{' '}
<span className='link'>
<Link to={LOGIN_ROUTE}>Login</Link>
</span>
</div>
</div>
);
};
export default connector(SignupForm);

View file

@ -0,0 +1 @@
export { default } from './Signup'

View file

@ -27,6 +27,7 @@
"@sentry/browser": "^5.21.1",
"@svg-maps/world": "^1.0.1",
"@svgr/webpack": "^6.2.1",
"antd": "^5.5.0",
"chroma-js": "^2.4.2",
"classnames": "^2.3.1",
"copy-to-clipboard": "^3.3.1",

File diff suppressed because it is too large Load diff