From 0ee512625c9b9473c3ced5129f396df849edd13f Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 22 May 2023 12:41:51 +0200 Subject: [PATCH] feat(ui) - enforce pwd during signup (#1271) --- frontend/app/Router.js | 2 +- frontend/app/components/Signup/Signup.js | 101 -------- frontend/app/components/Signup/Signup.tsx | 91 ++++++++ .../Signup/SignupForm/SignupForm.js | 201 ---------------- .../Signup/SignupForm/SignupForm.tsx | 217 ++++++++++++++++++ .../Signup/SignupForm/{index.js => index.ts} | 0 frontend/app/components/Signup/index.ts | 1 + 7 files changed, 310 insertions(+), 303 deletions(-) delete mode 100644 frontend/app/components/Signup/Signup.js create mode 100644 frontend/app/components/Signup/Signup.tsx delete mode 100644 frontend/app/components/Signup/SignupForm/SignupForm.js create mode 100644 frontend/app/components/Signup/SignupForm/SignupForm.tsx rename frontend/app/components/Signup/SignupForm/{index.js => index.ts} (100%) create mode 100644 frontend/app/components/Signup/index.ts diff --git a/frontend/app/Router.js b/frontend/app/Router.js index 600e4ba1a..10aac6cf8 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -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'; diff --git a/frontend/app/components/Signup/Signup.js b/frontend/app/components/Signup/Signup.js deleted file mode 100644 index c52681c88..000000000 --- a/frontend/app/components/Signup/Signup.js +++ /dev/null @@ -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 }) => ( -
-
- -
-
{text}
-
-); - -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 ( - null} - healthResponse={this.state.healthStatus} - getHealth={this.getHealth} - isLoading={this.state.healthStatusLoading} - setPassed={this.setHealthModalPassed} - /> - ) - } - - return ( -
-
-
- -
-
- - -
- ); - } -} diff --git a/frontend/app/components/Signup/Signup.tsx b/frontend/app/components/Signup/Signup.tsx new file mode 100644 index 000000000..0165fe4da --- /dev/null +++ b/frontend/app/components/Signup/Signup.tsx @@ -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 }) => ( +
+
+ +
+
{text}
+
+); + +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; + +type SignupProps = PropsFromRedux & RouteComponentProps; + +const Signup: React.FC = ({ loading, authDetails, fetchTenants, history }) => { + const [healthModalPassed, setHealthModalPassed] = useState(localStorage.getItem(healthStatusCheck_key) === 'true'); + const [healthStatusLoading, setHealthStatusLoading] = useState(true); + const [healthStatus, setHealthStatus] = useState(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 ( + null} + healthResponse={healthStatus} + getHealth={getHealth} + isLoading={healthStatusLoading} + setPassed={() => setHealthModalPassed(true)} + /> + ); + } + + return ( +
+
+
+ +
+
+ + +
+ ); +}; + +export default connector(withRouter(withPageTitle('Signup - OpenReplay')(Signup))); diff --git a/frontend/app/components/Signup/SignupForm/SignupForm.js b/frontend/app/components/Signup/SignupForm/SignupForm.js deleted file mode 100644 index b8242cc84..000000000 --- a/frontend/app/components/Signup/SignupForm/SignupForm.js +++ /dev/null @@ -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 ( -
-
- -
-
-
-

- Create Account -

-
- <> - {CAPTCHA_ENABLED && ( - this.handleSubmit(token)} - /> - )} -
- {tenants.length > 0 && ( - - - - - - - - - - - - - - - - - - -
-
- By signing up, you agree to our{' '} - - terms of service - {' '} - and{' '} - - privacy policy - - . -
-
-
- - {errors && ( -
- {errors.map((error) => ( -
- - - {error} -
-
-
- ))} -
- )} - - -
- Already having an account?{' '} - - Login - -
-
- ); - } -} diff --git a/frontend/app/components/Signup/SignupForm/SignupForm.tsx b/frontend/app/components/Signup/SignupForm/SignupForm.tsx new file mode 100644 index 000000000..091400647 --- /dev/null +++ b/frontend/app/components/Signup/SignupForm/SignupForm.tsx @@ -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; + +type SignupFormProps = PropsFromRedux; + +const SignupForm: React.FC = ({ 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(null); + const [passwordError, setPasswordError] = useState(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) => + setState({ ...state, [name]: value }); + + const writeOption = ({ name, value }: { name: string; value: { value: string } }) => + setState({ ...state, [name]: value.value }); + + const onSubmit = (e: FormEvent) => { + 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 ( +
+
+ Logo +
+
+
+

+ Create Account +

+
+ <> + {state.CAPTCHA_ENABLED && ( + handleSubmit(token || '')} + /> + )} +
+ {tenants.length > 0 && ( + + + + + + + + + + + + + + + + + + {passwordError && ( + // + + )} + {errors && errors.length && ( + + )} + + +
+
+ By signing up, you agree to our{' '} + + terms of service + {' '} + and{' '} + + privacy policy + + . +
+
+
+ + + +
+ Already having an account?{' '} + + Login + +
+
+ ); +}; + +export default connector(SignupForm); diff --git a/frontend/app/components/Signup/SignupForm/index.js b/frontend/app/components/Signup/SignupForm/index.ts similarity index 100% rename from frontend/app/components/Signup/SignupForm/index.js rename to frontend/app/components/Signup/SignupForm/index.ts diff --git a/frontend/app/components/Signup/index.ts b/frontend/app/components/Signup/index.ts new file mode 100644 index 000000000..6f887af30 --- /dev/null +++ b/frontend/app/components/Signup/index.ts @@ -0,0 +1 @@ +export { default } from './Signup' \ No newline at end of file