mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
re-structure to monorepo.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Icon } from '@/components/Icon';
|
||||
|
||||
export default function AuthCopyright() {
|
||||
return (
|
||||
<div class="auth-copyright">
|
||||
<div class="auth-copyright__text">
|
||||
{intl.get('all_rights_reserved', {
|
||||
pre: moment().subtract(1, 'years').year(),
|
||||
current: moment().get('year'),
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Icon width={122} height={22} icon={'bigcapital'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import AuthCopyright from './AuthCopyright';
|
||||
|
||||
/**
|
||||
* Authentication insider page.
|
||||
*/
|
||||
export default function AuthInsider({
|
||||
logo = true,
|
||||
copyright = true,
|
||||
children,
|
||||
}) {
|
||||
return (
|
||||
<div class="authentication-insider__content">
|
||||
<div class="authentication-insider__form">
|
||||
{ children }
|
||||
</div>
|
||||
|
||||
<div class="authentication-insider__footer">
|
||||
<AuthCopyright />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import InviteAcceptForm from './InviteAcceptForm';
|
||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||
import { InviteAcceptProvider } from './InviteAcceptProvider';
|
||||
|
||||
/**
|
||||
* Authentication invite page.
|
||||
*/
|
||||
export default function Invite() {
|
||||
const { token } = useParams();
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<InviteAcceptProvider token={token}>
|
||||
<InviteAcceptForm />
|
||||
</InviteAcceptProvider>
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Intent, Position } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { useInviteAcceptContext } from './InviteAcceptProvider';
|
||||
import { AppToaster } from '@/components';
|
||||
import { InviteAcceptSchema } from './utils';
|
||||
import InviteAcceptFormContent from './InviteAcceptFormContent';
|
||||
|
||||
export default function InviteAcceptForm() {
|
||||
const history = useHistory();
|
||||
|
||||
// Invite accept context.
|
||||
const { inviteAcceptMutate, inviteMeta, token } = useInviteAcceptContext();
|
||||
|
||||
// Invite value.
|
||||
const inviteValue = {
|
||||
organization_name: '',
|
||||
invited_email: '',
|
||||
...(!isEmpty(inviteMeta)
|
||||
? {
|
||||
invited_email: inviteMeta.email,
|
||||
organization_name: inviteMeta.organizationName,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
// Handle form submitting.
|
||||
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
inviteAcceptMutate([values, token])
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: intl.getHTML(
|
||||
'congrats_your_account_has_been_created_and_invited',
|
||||
{
|
||||
organization_name: inviteValue.organization_name,
|
||||
},
|
||||
),
|
||||
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
history.push('/auth/login');
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) {
|
||||
AppToaster.show({
|
||||
message: intl.get('an_unexpected_error_occurred'),
|
||||
intent: Intent.DANGER,
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
history.push('/auth/login');
|
||||
}
|
||||
if (errors.find((e) => e.type === 'PHONE_MUMNER.ALREADY.EXISTS')) {
|
||||
setErrors({
|
||||
phone_number: 'This phone number is used in another account.',
|
||||
});
|
||||
}
|
||||
if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) {
|
||||
AppToaster.show({
|
||||
message: intl.get('an_unexpected_error_occurred'),
|
||||
intent: Intent.DANGER,
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
history.push('/auth/login');
|
||||
}
|
||||
setSubmitting(false);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'invite-form'}>
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'welcome_to_bigcapital'} />
|
||||
</h3>
|
||||
<p>
|
||||
<T id={'enter_your_personal_information'} />{' '}
|
||||
<b>{inviteValue.organization_name}</b> <T id={'organization'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
validationSchema={InviteAcceptSchema}
|
||||
initialValues={inviteValue}
|
||||
onSubmit={handleSubmit}
|
||||
component={InviteAcceptFormContent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
||||
import { Form, ErrorMessage, FastField, useFormikContext } from 'formik';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Col, Row, FormattedMessage as T } from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
import { useInviteAcceptContext } from './InviteAcceptProvider';
|
||||
import { PasswordRevealer } from './components';
|
||||
|
||||
/**
|
||||
* Invite user form.
|
||||
*/
|
||||
export default function InviteUserFormContent() {
|
||||
// Invite accept context.
|
||||
const { inviteMeta } = useInviteAcceptContext();
|
||||
|
||||
// Formik context.
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
const [passwordType, setPasswordType] = React.useState('password');
|
||||
|
||||
// Handle password revealer changing.
|
||||
const handlePasswordRevealerChange = React.useCallback(
|
||||
(shown) => {
|
||||
const type = shown ? 'text' : 'password';
|
||||
setPasswordType(type);
|
||||
},
|
||||
[setPasswordType],
|
||||
);
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
<FastField name={'first_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'first_name'} />}
|
||||
className={'form-group--first_name'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'first_name'} />}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
</Col>
|
||||
|
||||
<Col md={6}>
|
||||
<FastField name={'last_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'last_name'} />}
|
||||
className={'form-group--last_name'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'last_name'} />}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<FastField name={'phone_number'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'phone_number'} />}
|
||||
className={'form-group--phone_number'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'phone_number'} />}
|
||||
>
|
||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
<FastField name={'password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'password'} />}
|
||||
labelInfo={
|
||||
<PasswordRevealer onChange={handlePasswordRevealerChange} />
|
||||
}
|
||||
className={'form-group--password has-password-revealer'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'password'} />}
|
||||
>
|
||||
<InputGroup
|
||||
lang={true}
|
||||
type={passwordType}
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
<div className={'invite-form__statement-section'}>
|
||||
<p>
|
||||
<T id={'you_email_address_is'} /> <b>{inviteMeta.email},</b> <br />
|
||||
<T id={'you_will_use_this_address_to_sign_in_to_bigcapital'} />
|
||||
</p>
|
||||
<p>
|
||||
{intl.getHTML('signing_in_or_creating', {
|
||||
terms: (msg) => <Link>{msg}</Link>,
|
||||
privacy: (msg) => <Link>{msg}</Link>,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
fill={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'create_account'} />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useInviteMetaByToken, useAuthInviteAccept } from '@/hooks/query';
|
||||
import { InviteAcceptLoading } from './components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
const InviteAcceptContext = createContext();
|
||||
|
||||
/**
|
||||
* Invite accept provider.
|
||||
*/
|
||||
function InviteAcceptProvider({ token, ...props }) {
|
||||
// Invite meta by token.
|
||||
const {
|
||||
data: inviteMeta,
|
||||
error: inviteMetaError,
|
||||
isError: isInviteMetaError,
|
||||
isFetching: isInviteMetaLoading,
|
||||
} = useInviteMetaByToken(token, { retry: false });
|
||||
|
||||
// Invite accept mutate.
|
||||
const { mutateAsync: inviteAcceptMutate } = useAuthInviteAccept({
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// History context.
|
||||
const history = useHistory();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (inviteMetaError) { history.push('/auth/login'); }
|
||||
}, [history, inviteMetaError]);
|
||||
|
||||
// Provider payload.
|
||||
const provider = {
|
||||
token,
|
||||
inviteMeta,
|
||||
inviteMetaError,
|
||||
isInviteMetaError,
|
||||
isInviteMetaLoading,
|
||||
inviteAcceptMutate
|
||||
};
|
||||
|
||||
if (inviteMetaError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InviteAcceptLoading isLoading={isInviteMetaLoading}>
|
||||
{ isInviteMetaError }
|
||||
<InviteAcceptContext.Provider value={provider} {...props} />
|
||||
</InviteAcceptLoading>
|
||||
);
|
||||
}
|
||||
|
||||
const useInviteAcceptContext = () => useContext(InviteAcceptContext);
|
||||
|
||||
export { InviteAcceptProvider, useInviteAcceptContext };
|
||||
71
packages/webapp/src/containers/Authentication/Login.tsx
Normal file
71
packages/webapp/src/containers/Authentication/Login.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Formik } from 'formik';
|
||||
import { AppToaster as Toaster, FormattedMessage as T } from '@/components';
|
||||
|
||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||
import { useAuthLogin } from '@/hooks/query';
|
||||
|
||||
import LoginForm from './LoginForm';
|
||||
import { LoginSchema, transformLoginErrorsToToasts } from './utils';
|
||||
|
||||
/**
|
||||
* Login page.
|
||||
*/
|
||||
export default function Login() {
|
||||
const { mutateAsync: loginMutate } = useAuthLogin();
|
||||
|
||||
const handleSubmit = (values, { setSubmitting }) => {
|
||||
loginMutate({
|
||||
crediential: values.crediential,
|
||||
password: values.password,
|
||||
}).catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
const toastBuilders = transformLoginErrorsToToasts(errors);
|
||||
|
||||
toastBuilders.forEach((builder) => {
|
||||
Toaster.show(builder);
|
||||
});
|
||||
setSubmitting(false);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div className="login-form">
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'log_in'} />
|
||||
</h3>
|
||||
<T id={'need_bigcapital_account'} />
|
||||
<Link to="/auth/register">
|
||||
{' '}
|
||||
<T id={'create_an_account'} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
crediential: '',
|
||||
password: '',
|
||||
}}
|
||||
validationSchema={LoginSchema}
|
||||
onSubmit={handleSubmit}
|
||||
component={LoginForm}
|
||||
/>
|
||||
|
||||
<div class="authentication-page__footer-links">
|
||||
<Link to={'/auth/send_reset_password'}>
|
||||
<T id={'forget_my_password'} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
89
packages/webapp/src/containers/Authentication/LoginForm.tsx
Normal file
89
packages/webapp/src/containers/Authentication/LoginForm.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
InputGroup,
|
||||
Intent,
|
||||
FormGroup,
|
||||
Checkbox,
|
||||
} from '@blueprintjs/core';
|
||||
import { Form, ErrorMessage, Field } from 'formik';
|
||||
import { T } from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
import { PasswordRevealer } from './components';
|
||||
|
||||
/**
|
||||
* Login form.
|
||||
*/
|
||||
export default function LoginForm({ isSubmitting }) {
|
||||
const [passwordType, setPasswordType] = React.useState('password');
|
||||
|
||||
// Handle password revealer changing.
|
||||
const handlePasswordRevealerChange = React.useCallback(
|
||||
(shown) => {
|
||||
const type = shown ? 'text' : 'password';
|
||||
setPasswordType(type);
|
||||
},
|
||||
[setPasswordType],
|
||||
);
|
||||
|
||||
return (
|
||||
<Form className={'authentication-page__form'}>
|
||||
<Field name={'crediential'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'email_or_phone_number'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'crediential'} />}
|
||||
className={'form-group--crediential'}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
large={true}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field name={'password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'password'} />}
|
||||
labelInfo={
|
||||
<PasswordRevealer onChange={handlePasswordRevealerChange} />
|
||||
}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'password'} />}
|
||||
className={'form-group--password has-password-revealer'}
|
||||
>
|
||||
<InputGroup
|
||||
large={true}
|
||||
intent={inputIntent({ error, touched })}
|
||||
type={passwordType}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div className={'login-form__checkbox-section'}>
|
||||
<Checkbox large={true} className={'checkbox--remember-me'}>
|
||||
<T id={'keep_me_logged_in'} />
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
type={'submit'}
|
||||
intent={Intent.PRIMARY}
|
||||
fill={true}
|
||||
lang={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'log_in'} />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
89
packages/webapp/src/containers/Authentication/Register.tsx
Normal file
89
packages/webapp/src/containers/Authentication/Register.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
// @ts-nocheck
|
||||
import React, { useMemo } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
|
||||
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||
import { useAuthLogin, useAuthRegister } from '@/hooks/query/authentication';
|
||||
|
||||
import RegisterForm from './RegisterForm';
|
||||
import { RegisterSchema, transformRegisterErrorsToForm } from './utils';
|
||||
|
||||
/**
|
||||
* Register form.
|
||||
*/
|
||||
export default function RegisterUserForm() {
|
||||
const { mutateAsync: authLoginMutate } = useAuthLogin();
|
||||
const { mutateAsync: authRegisterMutate } = useAuthRegister();
|
||||
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
phone_number: '',
|
||||
password: '',
|
||||
country: 'LY',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
authRegisterMutate(values)
|
||||
.then((response) => {
|
||||
authLoginMutate({
|
||||
crediential: values.email,
|
||||
password: values.password,
|
||||
}).catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
AppToaster.show({
|
||||
message: intl.get('something_wentwrong'),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
const formErrors = transformRegisterErrorsToForm(errors);
|
||||
|
||||
setErrors(formErrors);
|
||||
setSubmitting(false);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div className={'register-form'}>
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'register_a_new_organization'} />
|
||||
</h3>
|
||||
<T id={'you_have_a_bigcapital_account'} />
|
||||
<Link to="/auth/login">
|
||||
<T id={'login'} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={RegisterSchema}
|
||||
onSubmit={handleSubmit}
|
||||
component={RegisterForm}
|
||||
/>
|
||||
</div>
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
148
packages/webapp/src/containers/Authentication/RegisterForm.tsx
Normal file
148
packages/webapp/src/containers/Authentication/RegisterForm.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import {
|
||||
Button,
|
||||
InputGroup,
|
||||
Intent,
|
||||
FormGroup,
|
||||
Spinner,
|
||||
} from '@blueprintjs/core';
|
||||
import { ErrorMessage, Field, Form } from 'formik';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Row, Col, If } from '@/components';
|
||||
import { PasswordRevealer } from './components';
|
||||
import { inputIntent } from '@/utils';
|
||||
|
||||
/**
|
||||
* Register form.
|
||||
*/
|
||||
export default function RegisterForm({ isSubmitting }) {
|
||||
const [passwordType, setPasswordType] = React.useState('password');
|
||||
|
||||
// Handle password revealer changing.
|
||||
const handlePasswordRevealerChange = React.useCallback(
|
||||
(shown) => {
|
||||
const type = shown ? 'text' : 'password';
|
||||
setPasswordType(type);
|
||||
},
|
||||
[setPasswordType],
|
||||
);
|
||||
|
||||
return (
|
||||
<Form className={'authentication-page__form'}>
|
||||
<Row className={'name-section'}>
|
||||
<Col md={6}>
|
||||
<Field name={'first_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'first_name'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'first_name'} />}
|
||||
className={'form-group--first-name'}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
</Col>
|
||||
|
||||
<Col md={6}>
|
||||
<Field name={'last_name'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'last_name'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'last_name'} />}
|
||||
className={'form-group--last-name'}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Field name={'phone_number'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'phone_number'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'phone_number'} />}
|
||||
className={'form-group--phone-number'}
|
||||
>
|
||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field name={'email'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'email'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'email'} />}
|
||||
className={'form-group--email'}
|
||||
>
|
||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field name={'password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'password'} />}
|
||||
labelInfo={
|
||||
<PasswordRevealer onChange={handlePasswordRevealerChange} />
|
||||
}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'password'} />}
|
||||
className={'form-group--password has-password-revealer'}
|
||||
>
|
||||
<InputGroup
|
||||
lang={true}
|
||||
type={passwordType}
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<div className={'register-form__agreement-section'}>
|
||||
<p>
|
||||
{intl.getHTML('signing_in_or_creating', {
|
||||
terms: (msg) => <Link>{msg}</Link>,
|
||||
privacy: (msg) => <Link>{msg}</Link>,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
className={'btn-register'}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
fill={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'register'} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<If condition={isSubmitting}>
|
||||
<div class="authentication-page__loading-overlay">
|
||||
<Spinner size={50} />
|
||||
</div>
|
||||
</If>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// @ts-nocheck
|
||||
import React, { useMemo } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Intent, Position } from '@blueprintjs/core';
|
||||
import { Link, useParams, useHistory } from 'react-router-dom';
|
||||
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||
|
||||
import { useAuthResetPassword } from '@/hooks/query';
|
||||
|
||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||
|
||||
import ResetPasswordForm from './ResetPasswordForm';
|
||||
import { ResetPasswordSchema } from './utils';
|
||||
/**
|
||||
* Reset password page.
|
||||
*/
|
||||
export default function ResetPassword() {
|
||||
const { token } = useParams();
|
||||
const history = useHistory();
|
||||
|
||||
// Authentication reset password.
|
||||
const { mutateAsync: authResetPasswordMutate } = useAuthResetPassword();
|
||||
|
||||
// Initial values of the form.
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Handle the form submitting.
|
||||
const handleSubmit = (values, { setSubmitting }) => {
|
||||
authResetPasswordMutate([token, values])
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: intl.get('password_successfully_updated'),
|
||||
intent: Intent.DANGER,
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
history.push('/auth/login');
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
if (errors.find((e) => e.type === 'TOKEN_INVALID')) {
|
||||
AppToaster.show({
|
||||
message: intl.get('an_unexpected_error_occurred'),
|
||||
intent: Intent.DANGER,
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
history.push('/auth/login');
|
||||
}
|
||||
setSubmitting(false);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div className={'submit-np-form'}>
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'choose_a_new_password'} />
|
||||
</h3>
|
||||
<T id={'you_remembered_your_password'} />{' '}
|
||||
<Link to="/auth/login">
|
||||
<T id={'login'} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={ResetPasswordSchema}
|
||||
onSubmit={handleSubmit}
|
||||
component={ResetPasswordForm}
|
||||
/>
|
||||
</div>
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
||||
import { Form, ErrorMessage, FastField } from 'formik';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
|
||||
/**
|
||||
* Reset password form.
|
||||
*/
|
||||
export default function ResetPasswordForm({ isSubmitting }) {
|
||||
return (
|
||||
<Form>
|
||||
<FastField name={'password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'new_password'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'password'} />}
|
||||
className={'form-group--password'}
|
||||
>
|
||||
<InputGroup
|
||||
lang={true}
|
||||
type={'password'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
<FastField name={'confirm_password'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'new_password'} />}
|
||||
labelInfo={'(again):'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'confirm_password'} />}
|
||||
className={'form-group--confirm-password'}
|
||||
>
|
||||
<InputGroup
|
||||
lang={true}
|
||||
type={'password'}
|
||||
intent={inputIntent({ error, touched })}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
fill={true}
|
||||
className={'btn-new'}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'submit'} />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// @ts-nocheck
|
||||
import React, { useMemo } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
|
||||
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||
import { useAuthSendResetPassword } from '@/hooks/query';
|
||||
|
||||
import SendResetPasswordForm from './SendResetPasswordForm';
|
||||
import {
|
||||
SendResetPasswordSchema,
|
||||
transformSendResetPassErrorsToToasts,
|
||||
} from './utils';
|
||||
|
||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||
|
||||
/**
|
||||
* Send reset password page.
|
||||
*/
|
||||
export default function SendResetPassword({ requestSendResetPassword }) {
|
||||
const history = useHistory();
|
||||
|
||||
const { mutateAsync: sendResetPasswordMutate } = useAuthSendResetPassword();
|
||||
|
||||
// Initial values.
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
crediential: '',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Handle form submitting.
|
||||
const handleSubmit = (values, { setSubmitting }) => {
|
||||
sendResetPasswordMutate({ email: values.crediential })
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: intl.get('check_your_email_for_a_link_to_reset'),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
history.push('/auth/login');
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
const toastBuilders = transformSendResetPassErrorsToToasts(errors);
|
||||
|
||||
toastBuilders.forEach((builder) => {
|
||||
AppToaster.show(builder);
|
||||
});
|
||||
setSubmitting(false);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div className="reset-form">
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'you_can_t_login'} />
|
||||
</h3>
|
||||
<p>
|
||||
<T id={'we_ll_send_a_recovery_link_to_your_email'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={SendResetPasswordSchema}
|
||||
component={SendResetPasswordForm}
|
||||
/>
|
||||
<div class="authentication-page__footer-links">
|
||||
<Link to="/auth/login">
|
||||
<T id={'return_to_log_in'} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
||||
import { Form, ErrorMessage, FastField } from 'formik';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { inputIntent } from '@/utils';
|
||||
|
||||
/**
|
||||
* Send reset password form.
|
||||
*/
|
||||
export default function SendResetPasswordForm({ isSubmitting }) {
|
||||
return (
|
||||
<Form className={'send-reset-password'}>
|
||||
<FastField name={'crediential'}>
|
||||
{({ form, field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'email_or_phone_number'} />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name={'crediential'} />}
|
||||
className={'form-group--crediential'}
|
||||
>
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
large={true}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
<div className={'authentication-page__submit-button-wrap'}>
|
||||
<Button
|
||||
type={'submit'}
|
||||
intent={Intent.PRIMARY}
|
||||
fill={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'send_reset_password_mail'} />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
57
packages/webapp/src/containers/Authentication/components.tsx
Normal file
57
packages/webapp/src/containers/Authentication/components.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import ContentLoader from 'react-content-loader';
|
||||
import { If, Icon, FormattedMessage as T } from '@/components';
|
||||
import { saveInvoke } from '@/utils';
|
||||
|
||||
export function PasswordRevealer({ defaultShown = false, onChange }) {
|
||||
const [shown, setShown] = React.useState(defaultShown);
|
||||
|
||||
const handleClick = () => {
|
||||
setShown(!shown);
|
||||
saveInvoke(onChange, !shown);
|
||||
};
|
||||
|
||||
return (
|
||||
<span class="password-revealer" onClick={handleClick}>
|
||||
<If condition={shown}>
|
||||
<Icon icon="eye-slash" />{' '}
|
||||
<span class="text">
|
||||
<T id={'hide'} />
|
||||
</span>
|
||||
</If>
|
||||
<If condition={!shown}>
|
||||
<Icon icon="eye" />{' '}
|
||||
<span class="text">
|
||||
<T id={'show'} />
|
||||
</span>
|
||||
</If>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite accept loading space.
|
||||
*/
|
||||
export function InviteAcceptLoading({ isLoading, children, ...props }) {
|
||||
return isLoading ? (
|
||||
<ContentLoader
|
||||
speed={2}
|
||||
width={400}
|
||||
height={280}
|
||||
viewBox="0 0 400 280"
|
||||
backgroundColor="#f3f3f3"
|
||||
foregroundColor="#e6e6e6"
|
||||
{...props}
|
||||
>
|
||||
<rect x="0" y="80" rx="2" ry="2" width="200" height="20" />
|
||||
<rect x="0" y="0" rx="2" ry="2" width="250" height="30" />
|
||||
<rect x="0" y="38" rx="2" ry="2" width="300" height="15" />
|
||||
<rect x="0" y="175" rx="2" ry="2" width="200" height="20" />
|
||||
<rect x="1" y="205" rx="2" ry="2" width="385" height="38" />
|
||||
<rect x="0" y="110" rx="2" ry="2" width="385" height="38" />
|
||||
</ContentLoader>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
}
|
||||
133
packages/webapp/src/containers/Authentication/utils.tsx
Normal file
133
packages/webapp/src/containers/Authentication/utils.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
|
||||
export const LOGIN_ERRORS = {
|
||||
INVALID_DETAILS: 'INVALID_DETAILS',
|
||||
USER_INACTIVE: 'USER_INACTIVE',
|
||||
LOGIN_TO_MANY_ATTEMPTS: 'LOGIN_TO_MANY_ATTEMPTS',
|
||||
};
|
||||
|
||||
const REGISTER_ERRORS = {
|
||||
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
|
||||
EMAIL_EXISTS: 'EMAIL.EXISTS',
|
||||
};
|
||||
|
||||
export const LoginSchema = Yup.object().shape({
|
||||
crediential: Yup.string()
|
||||
.required()
|
||||
.email()
|
||||
.label(intl.get('email')),
|
||||
password: Yup.string()
|
||||
.required()
|
||||
.min(4)
|
||||
.label(intl.get('password')),
|
||||
});
|
||||
|
||||
export const RegisterSchema = Yup.object().shape({
|
||||
first_name: Yup.string()
|
||||
.required()
|
||||
.label(intl.get('first_name_')),
|
||||
last_name: Yup.string()
|
||||
.required()
|
||||
.label(intl.get('last_name_')),
|
||||
email: Yup.string()
|
||||
.email()
|
||||
.required()
|
||||
.label(intl.get('email')),
|
||||
phone_number: Yup.string()
|
||||
.matches()
|
||||
.required()
|
||||
.label(intl.get('phone_number_')),
|
||||
password: Yup.string()
|
||||
.min(4)
|
||||
.required()
|
||||
.label(intl.get('password')),
|
||||
});
|
||||
|
||||
export const ResetPasswordSchema = Yup.object().shape({
|
||||
password: Yup.string()
|
||||
.min(4)
|
||||
.required()
|
||||
.label(intl.get('password')),
|
||||
confirm_password: Yup.string()
|
||||
.oneOf([Yup.ref('password'), null])
|
||||
.required()
|
||||
.label(intl.get('confirm_password')),
|
||||
});
|
||||
|
||||
// Validation schema.
|
||||
export const SendResetPasswordSchema = Yup.object().shape({
|
||||
crediential: Yup.string()
|
||||
.required()
|
||||
.email()
|
||||
.label(intl.get('email')),
|
||||
});
|
||||
|
||||
export const InviteAcceptSchema = Yup.object().shape({
|
||||
first_name: Yup.string()
|
||||
.required()
|
||||
.label(intl.get('first_name_')),
|
||||
last_name: Yup.string()
|
||||
.required()
|
||||
.label(intl.get('last_name_')),
|
||||
phone_number: Yup.string()
|
||||
.matches()
|
||||
.required()
|
||||
.label(intl.get('phone_number')),
|
||||
password: Yup.string()
|
||||
.min(4)
|
||||
.required()
|
||||
.label(intl.get('password')),
|
||||
});
|
||||
|
||||
export const transformSendResetPassErrorsToToasts = (errors) => {
|
||||
const toastBuilders = [];
|
||||
|
||||
if (errors.find((e) => e.type === 'EMAIL.NOT.REGISTERED')) {
|
||||
toastBuilders.push({
|
||||
message: intl.get('we_couldn_t_find_your_account_with_that_email'),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
return toastBuilders;
|
||||
}
|
||||
|
||||
export const transformLoginErrorsToToasts = (errors) => {
|
||||
const toastBuilders = [];
|
||||
|
||||
if (errors.find((e) => e.type === LOGIN_ERRORS.INVALID_DETAILS)) {
|
||||
toastBuilders.push({
|
||||
message: intl.get('email_and_password_entered_did_not_match'),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
if (errors.find((e) => e.type === LOGIN_ERRORS.USER_INACTIVE)) {
|
||||
toastBuilders.push({
|
||||
message: intl.get('the_user_has_been_suspended_from_admin'),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
if (
|
||||
errors.find((e) => e.type === LOGIN_ERRORS.LOGIN_TO_MANY_ATTEMPTS)
|
||||
) {
|
||||
toastBuilders.push({
|
||||
message: intl.get('your_account_has_been_locked'),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
return toastBuilders;
|
||||
}
|
||||
|
||||
export const transformRegisterErrorsToForm = (errors) => {
|
||||
const formErrors = {};
|
||||
|
||||
if (errors.some((e) => e.type === REGISTER_ERRORS.PHONE_NUMBER_EXISTS)) {
|
||||
formErrors.phone_number = intl.get('the_phone_number_already_used_in_another_account');
|
||||
}
|
||||
if (errors.some((e) => e.type === REGISTER_ERRORS.EMAIL_EXISTS)) {
|
||||
formErrors.email = intl.get('the_email_already_used_in_another_account');
|
||||
}
|
||||
return formErrors;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// @ts-nocheck
|
||||
import { isAuthenticated } from '@/store/authentication/authentication.reducer';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
export default (mapState) => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
isAuthorized: isAuthenticated(state),
|
||||
authenticatedUserId: state.authentication.userId,
|
||||
currentOrganizationId: state.authentication?.organizationId,
|
||||
currentTenantId: state.authentication?.tenantId,
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
return connect(mapStateToProps);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
// requestLogin: (form) => dispatch(login({ form })),
|
||||
// requestLogout: () => dispatch({ type: t.LOGOUT }),
|
||||
// requestRegister: (form) => dispatch(register({ form })),
|
||||
// requestSendResetPassword: (email) => dispatch(sendResetPassword({ email })),
|
||||
// requestResetPassword: (form, token) => dispatch(resetPassword({ form, token })),
|
||||
// requestInviteAccept: (form, token) => dispatch(inviteAccept({ form, token })),
|
||||
// requestInviteMetaByToken: (token) => dispatch(inviteMetaByToken({ token })),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
Reference in New Issue
Block a user