mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
Merge remote-tracking branch 'origin/feature/login/registerPage'
This commit is contained in:
@@ -62,6 +62,7 @@
|
|||||||
"postcss-safe-parser": "4.0.1",
|
"postcss-safe-parser": "4.0.1",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-app-polyfill": "^1.0.6",
|
"react-app-polyfill": "^1.0.6",
|
||||||
|
"react-body-classname": "^1.3.1",
|
||||||
"react-dev-utils": "^10.2.0",
|
"react-dev-utils": "^10.2.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
"react-grid-system": "^6.2.3",
|
"react-grid-system": "^6.2.3",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Router, Switch } from 'react-router';
|
import { Router, Switch, Redirect } from 'react-router';
|
||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import PrivateRoute from 'components/PrivateRoute';
|
import PrivateRoute from 'components/PrivateRoute';
|
||||||
import Authentication from 'components/Authentication';
|
import Authentication from 'components/Authentication';
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
import { Redirect, Route, Switch, Link } from 'react-router-dom';
|
||||||
|
import BodyClassName from 'react-body-classname';
|
||||||
import authenticationRoutes from 'routes/authentication';
|
import authenticationRoutes from 'routes/authentication';
|
||||||
|
|
||||||
export default function({ isAuthenticated =false, ...rest }) {
|
export default function({ isAuthenticated =false, ...rest }) {
|
||||||
const to = {pathname: '/dashboard/homepage'};
|
const to = {pathname: '/dashboard/homepage'};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route path="/auth">
|
<BodyClassName className={'authentication'}>
|
||||||
{ (isAuthenticated) ?
|
<Route path="/auth">
|
||||||
(<Redirect to={to} />) : (
|
{ (isAuthenticated) ?
|
||||||
<Switch>
|
(<Redirect to={to} />) : (
|
||||||
<div class="authentication-page">
|
<Switch>
|
||||||
<div class="authentication-page__form-wrapper">
|
<div class="authentication-page">
|
||||||
{ authenticationRoutes.map((route, index) => (
|
<Link
|
||||||
<Route
|
to={'bigcapital.io'}
|
||||||
key={index}
|
className={'authentication-page__goto-bigcapital'}>
|
||||||
path={route.path}
|
← Go to bigcapital.com
|
||||||
exact={route.exact}
|
</Link>
|
||||||
component={route.component}
|
|
||||||
/>
|
<div class="authentication-page__form-wrapper">
|
||||||
))}
|
{ authenticationRoutes.map((route, index) => (
|
||||||
|
<Route
|
||||||
|
key={index}
|
||||||
|
path={route.path}
|
||||||
|
exact={route.exact}
|
||||||
|
component={route.component}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Switch>)
|
||||||
</Switch>)
|
}
|
||||||
}
|
</Route>
|
||||||
</Route>
|
</BodyClassName>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
24
client/src/connectors/Authentication.connect.js
Normal file
24
client/src/connectors/Authentication.connect.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
login,
|
||||||
|
resetPassword,
|
||||||
|
sendResetPassword,
|
||||||
|
inviteAccept,
|
||||||
|
register,
|
||||||
|
inviteMetaByToken,
|
||||||
|
} from 'store/authentication/authentication.actions';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
requestLogin: (form) => dispatch(login({ form })),
|
||||||
|
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(mapStateToProps, mapDispatchToProps);
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { submitInvite, submitSendInvite } from 'store/Invite/invite.action';
|
|
||||||
|
|
||||||
export const mapStateToProps = (state, props) => {
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapDispatchToProps = (dispatch) => ({
|
|
||||||
requestSubmitInvite: (form, token) => dispatch(submitInvite({ form, token })),
|
|
||||||
requestSendInvite: (form) => dispatch(submitSendInvite({ form })),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps);
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { submitResetPassword } from 'store/resetPassword/resetPassword.action';
|
|
||||||
|
|
||||||
export const mapStateToProps = (state, props) => {
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapDispatchToProps = (dispatch) => ({
|
|
||||||
requestResetPassword: (password) =>
|
|
||||||
dispatch(submitResetPassword({password})),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps);
|
|
||||||
16
client/src/containers/Authentication/AuthCopyright.js
Normal file
16
client/src/containers/Authentication/AuthCopyright.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
|
||||||
|
function AuthCopyright() {
|
||||||
|
return (
|
||||||
|
<div class="auth-copyright">
|
||||||
|
<div class="auth-copyright__text">
|
||||||
|
© 2001–2020 All Rights Reserved.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Icon width={122} height={22} icon={'bigcapital'} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthCopyright;
|
||||||
29
client/src/containers/Authentication/AuthInsider.js
Normal file
29
client/src/containers/Authentication/AuthInsider.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import AuthCopyright from './AuthCopyright';
|
||||||
|
|
||||||
|
export default function AuthInsider({
|
||||||
|
logo = true,
|
||||||
|
copyright = true,
|
||||||
|
children,
|
||||||
|
}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="authentication-insider">
|
||||||
|
<div className={'authentication-insider__logo-section'}>
|
||||||
|
<Icon
|
||||||
|
icon='bigcapital'
|
||||||
|
height={37}
|
||||||
|
width={214} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="authentication-insider__content">
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="authentication-insider__footer">
|
||||||
|
<AuthCopyright />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,174 +1,227 @@
|
|||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import ErrorMessage from 'components/ErrorMessage';
|
import ErrorMessage from 'components/ErrorMessage';
|
||||||
import AppToaster from 'components/AppToaster';
|
import AppToaster from 'components/AppToaster';
|
||||||
import { compose } from 'utils';
|
import { compose } from 'utils';
|
||||||
import InviteFormConnect from 'connectors/InviteForm.connect';
|
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
Intent,
|
Intent,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
HTMLSelect,
|
Position,
|
||||||
|
Spinner,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import { Row, Col } from 'react-grid-system';
|
||||||
|
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import useAsync from 'hooks/async';
|
||||||
|
|
||||||
function Invite({ requestSubmitInvite }) {
|
function Invite({
|
||||||
|
requestInviteAccept,
|
||||||
|
requestInviteMetaByToken,
|
||||||
|
}) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
let params = useParams('accept/:token');
|
const { token } = useParams();
|
||||||
|
const history = useHistory();
|
||||||
const { token } = params;
|
const [shown, setShown] = useState(false);
|
||||||
|
const passwordRevealer = useCallback(() => { setShown(!shown); }, [shown]);
|
||||||
const phoneRegExp = /^((\\+[1-9]{1,4}[ \\-]*)|(\\([0-9]{2,3}\\)[ \\-]*)|([0-9]{2,4})[ \\-]*)*?[0-9]{3,4}?[ \\-]*[0-9]{3,4}?$/;
|
|
||||||
|
|
||||||
const language = useMemo(() => [
|
|
||||||
{ value: null, label: 'Select Country' },
|
|
||||||
{ value: 'Arabic', label: 'Arabic' },
|
|
||||||
{ value: 'English', label: 'English' },
|
|
||||||
], []);
|
|
||||||
|
|
||||||
const ValidationSchema = Yup.object().shape({
|
const ValidationSchema = Yup.object().shape({
|
||||||
first_name: Yup.string().required(),
|
first_name: Yup.string().required(),
|
||||||
last_name: Yup.string().required(),
|
last_name: Yup.string().required(),
|
||||||
email: Yup.string().email().required(),
|
phone_number: Yup.string().matches().required(),
|
||||||
phone_number: Yup.string().matches(phoneRegExp).required(),
|
|
||||||
language: Yup.string().required(),
|
|
||||||
password: Yup.string()
|
password: Yup.string()
|
||||||
.min(4, 'Password has to be longer than 4 characters!')
|
.min(4, 'Password has to be longer than 4 characters!')
|
||||||
.required('Password is required!'),
|
.required('Password is required!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = useMemo(
|
const inviteMeta = useAsync(() => {
|
||||||
() => ({
|
return requestInviteMetaByToken(token);
|
||||||
first_name: '',
|
});
|
||||||
last_name: '',
|
|
||||||
email: '',
|
const inviteErrors = inviteMeta.error || [];
|
||||||
phone_number: '',
|
const inviteValue = {
|
||||||
language: '',
|
organization_name: '',
|
||||||
password: '',
|
invited_email: '',
|
||||||
}),
|
...inviteMeta.value ?
|
||||||
[]
|
inviteMeta.value.data.data : {},
|
||||||
);
|
};
|
||||||
|
|
||||||
|
if (inviteErrors.find(e => e.type === 'INVITE.TOKEN.NOT.FOUND')) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'An unexpected error occurred',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
position: Position.BOTTOM,
|
||||||
|
});
|
||||||
|
history.push('/auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = useMemo(() => ({
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
phone_number: '',
|
||||||
|
password: '',
|
||||||
|
}), []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
|
||||||
errors,
|
|
||||||
values,
|
values,
|
||||||
touched,
|
touched,
|
||||||
|
errors,
|
||||||
|
handleSubmit,
|
||||||
getFieldProps,
|
getFieldProps,
|
||||||
|
isSubmitting,
|
||||||
} = useFormik({
|
} = useFormik({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: ValidationSchema,
|
validationSchema: ValidationSchema,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
...initialValues,
|
...initialValues,
|
||||||
},
|
},
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting, setErrors }) => {
|
||||||
requestSubmitInvite(values, token).then((response) => {
|
requestInviteAccept(values, token)
|
||||||
AppToaster.show({
|
.then((response) => {
|
||||||
message: 'success',
|
AppToaster.show({
|
||||||
|
message: `Congrats! Your account has been created and invited to
|
||||||
|
<strong>${inviteValue.organization_name}</strong> organization successfully.`,
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
})
|
||||||
|
.catch((errors) => {
|
||||||
|
if (errors.find((e) => e.type === 'INVITE.TOKEN.NOT.FOUND')) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: '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.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
setSubmitting(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setSubmitting(false);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const requiredSpan = useMemo(() => <span class='required'>*</span>, []);
|
|
||||||
|
const passwordRevealerTmp = useMemo(() => (
|
||||||
|
<span class="password-revealer" onClick={() => passwordRevealer()}>
|
||||||
|
{(shown) ? (
|
||||||
|
<><Icon icon='eye-slash' /> <span class="text">Hide</span></>
|
||||||
|
) : (
|
||||||
|
<><Icon icon='eye' /> <span class="text">Show</span></>
|
||||||
|
)}
|
||||||
|
</span>), [shown, passwordRevealer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'invite-form'}>
|
<AuthInsider>
|
||||||
<form onSubmit={handleSubmit}>
|
<div className={'invite-form'}>
|
||||||
<FormGroup
|
<div className={'authentication-page__label-section'}>
|
||||||
label={'First Name'}
|
<h3>Welcome to Bigcapital</h3>
|
||||||
labelInfo={requiredSpan}
|
<p>
|
||||||
className={'form-group--first_name'}
|
Enter your personal information <b>{ inviteValue.organization_name }</b>{' '}
|
||||||
intent={errors.first_name && touched.first_name && Intent.DANGER}
|
Organization.
|
||||||
helperText={<ErrorMessage name={'first_name'} />}
|
</p>
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={errors.first_name && touched.first_name && Intent.DANGER}
|
|
||||||
{...getFieldProps('first_name')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Last Name'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--last_name'}
|
|
||||||
intent={errors.last_name && touched.last_name && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'last_name'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={errors.last_name && touched.last_name && Intent.DANGER}
|
|
||||||
{...getFieldProps('last_name')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Phone Number'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--phone_number'}
|
|
||||||
intent={errors.phone_number && touched.phone_number && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'phone_number'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={(errors.phone_number && touched.phone_number) && Intent.DANGER}
|
|
||||||
{...getFieldProps('phone_number')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Language'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--language'}
|
|
||||||
intent={(errors.language && touched.language) && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'language'} />}
|
|
||||||
>
|
|
||||||
<HTMLSelect
|
|
||||||
fill={true}
|
|
||||||
options={language}
|
|
||||||
{...getFieldProps('language')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Email'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--email'}
|
|
||||||
intent={(errors.email && touched.email) && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'email'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={errors.email && touched.email && Intent.DANGER}
|
|
||||||
{...getFieldProps('email')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Password'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--password'}
|
|
||||||
intent={(errors.password && touched.password) && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'password'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
lang={true}
|
|
||||||
type={'password'}
|
|
||||||
intent={(errors.password && touched.password) && Intent.DANGER}
|
|
||||||
{...getFieldProps('password')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<div class='form__floating-footer'>
|
|
||||||
<Button intent={Intent.PRIMARY} type='submit'>
|
|
||||||
Invite
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Row>
|
||||||
|
<Col md={6}>
|
||||||
|
<FormGroup
|
||||||
|
label={'First Name'}
|
||||||
|
className={'form-group--first_name'}
|
||||||
|
intent={(errors.first_name && touched.first_name) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'first_name'} {...{errors, touched}} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={(errors.first_name && touched.first_name) &&
|
||||||
|
Intent.DANGER
|
||||||
|
}
|
||||||
|
{...getFieldProps('first_name')} />
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col md={6}>
|
||||||
|
<FormGroup
|
||||||
|
label={'Last Name'}
|
||||||
|
className={'form-group--last_name'}
|
||||||
|
intent={(errors.last_name && touched.last_name) &&
|
||||||
|
Intent.DANGER
|
||||||
|
}
|
||||||
|
helperText={<ErrorMessage name={'last_name'} {...{errors, touched}} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={(errors.last_name && touched.last_name) && Intent.DANGER}
|
||||||
|
{...getFieldProps('last_name')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={'Phone Number'}
|
||||||
|
className={'form-group--phone_number'}
|
||||||
|
intent={(errors.phone_number && touched.phone_number) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'phone_number'} {...{errors, touched}} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={(errors.phone_number && touched.phone_number) && Intent.DANGER}
|
||||||
|
{...getFieldProps('phone_number')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={'Password'}
|
||||||
|
labelInfo={passwordRevealerTmp}
|
||||||
|
className={'form-group--password has-password-revealer'}
|
||||||
|
intent={(errors.password && touched.password) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'password'} {...{errors, touched}} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
lang={true}
|
||||||
|
type={shown ? 'text' : 'password'}
|
||||||
|
intent={(errors.password && touched.password) && Intent.DANGER}
|
||||||
|
{...getFieldProps('password')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<div className={'invite-form__statement-section'}>
|
||||||
|
<p>
|
||||||
|
You email address is <b>{ inviteValue.invited_email },</b> <br />
|
||||||
|
You will use this address to sign in to Bigcapital.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
By signing in or creating an account, you agree with our <br />
|
||||||
|
<Link>Terms & Conditions</Link> and <Link> Privacy Statement</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'authentication-page__submit-button-wrap'}>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
type='submit'
|
||||||
|
fill={true}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{ inviteMeta.pending && (
|
||||||
|
<div class="authentication-page__loading-overlay">
|
||||||
|
<Spinner size={40} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AuthInsider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(InviteFormConnect)(Invite);
|
export default compose(AuthenticationConnect)(Invite);
|
||||||
|
|||||||
@@ -1,135 +1,169 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import {Link, useHistory} from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import {useFormik} from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import {connect} from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {useIntl} from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
Intent,
|
Intent,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
} from "@blueprintjs/core";
|
Checkbox,
|
||||||
import login from 'store/authentication/authentication.actions';
|
Position,
|
||||||
import {hasErrorType} from 'store/authentication/authentication.reducer';
|
} from '@blueprintjs/core';
|
||||||
import AuthenticationToaster from 'components/AppToaster';
|
import AuthenticationToaster from 'components/AppToaster';
|
||||||
import t from 'store/types';
|
import ErrorMessage from 'components/ErrorMessage';
|
||||||
|
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||||
|
import { compose } from 'utils';
|
||||||
|
|
||||||
const ERRORS_TYPES = {
|
const ERRORS_TYPES = {
|
||||||
INVALID_DETAILS: 'INVALID_DETAILS',
|
INVALID_DETAILS: 'INVALID_DETAILS',
|
||||||
USER_INACTIVE: 'USER_INACTIVE',
|
USER_INACTIVE: 'USER_INACTIVE',
|
||||||
};
|
};
|
||||||
function Login({
|
function Login({
|
||||||
login,
|
requestLogin,
|
||||||
}) {
|
}) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const [shown, setShown] = useState(false);
|
||||||
|
const passwordRevealer = () => { setShown(!shown); };
|
||||||
|
|
||||||
// Validation schema.
|
// Validation schema.
|
||||||
const loginValidationSchema = Yup.object().shape({
|
const loginValidationSchema = Yup.object().shape({
|
||||||
crediential: Yup
|
crediential: Yup.string()
|
||||||
.string()
|
.required(intl.formatMessage({ id: 'required' }))
|
||||||
.required(intl.formatMessage({'id': 'required'}))
|
.email(intl.formatMessage({ id: 'invalid_email_or_phone_numner' })),
|
||||||
.email(intl.formatMessage({id: 'invalid_email_or_phone_numner'})),
|
password: Yup.string()
|
||||||
password: Yup
|
.required(intl.formatMessage({ id: 'required' }))
|
||||||
.string()
|
|
||||||
.required(intl.formatMessage({id: 'required'}))
|
|
||||||
.min(4),
|
.min(4),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Formik validation schema and submit handler.
|
// Formik validation schema and submit handler.
|
||||||
const formik = useFormik({
|
const {
|
||||||
|
values,
|
||||||
|
touched,
|
||||||
|
errors,
|
||||||
|
handleSubmit,
|
||||||
|
getFieldProps,
|
||||||
|
isSubmitting,
|
||||||
|
} = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
crediential: '',
|
crediential: '',
|
||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
validationSchema: loginValidationSchema,
|
validationSchema: loginValidationSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
login({
|
requestLogin({
|
||||||
crediential: values.crediential,
|
crediential: values.crediential,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
history.go('/dashboard/homepage');
|
history.go('/dashboard/homepage');
|
||||||
|
setSubmitting(false);
|
||||||
}).catch((errors) => {
|
}).catch((errors) => {
|
||||||
const toastBuilders = [];
|
const toastBuilders = [];
|
||||||
if (errors.find((e) => e.type === ERRORS_TYPES.INVALID_DETAILS)) {
|
if (errors.find((e) => e.type === ERRORS_TYPES.INVALID_DETAILS)) {
|
||||||
toastBuilders.push({
|
toastBuilders.push({
|
||||||
message: intl.formatMessage({ id: 'invalid_email_or_phone_numner' }),
|
message: `The email and password you entered did not match our records.
|
||||||
intent: Intent.WARNING,
|
Please double-check and try again.`,
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
position: Position.BOTTOM,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (errors.find((e) => e.type === ERRORS_TYPES.USER_INACTIVE)) {
|
if (errors.find((e) => e.type === ERRORS_TYPES.USER_INACTIVE)) {
|
||||||
toastBuilders.push({
|
toastBuilders.push({
|
||||||
message: intl.formatMessage({ id: 'the_user_has_been_suspended_from_admin' }),
|
message: intl.formatMessage({ id: 'the_user_has_been_suspended_from_admin' }),
|
||||||
intent: Intent.WARNING,
|
intent: Intent.DANGER,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
toastBuilders.forEach(builder => {
|
toastBuilders.forEach(builder => {
|
||||||
AuthenticationToaster.show(builder);
|
AuthenticationToaster.show(builder);
|
||||||
});
|
});
|
||||||
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const passwordRevealerTmp = useMemo(() => (
|
||||||
|
<span class="password-revealer" onClick={() => passwordRevealer()}>
|
||||||
|
{(shown) ? (
|
||||||
|
<><Icon icon='eye-slash' /> <span class="text">Hide</span></>
|
||||||
|
) : (
|
||||||
|
<><Icon icon='eye' /> <span class="text">Show</span></>
|
||||||
|
)}
|
||||||
|
</span>), [shown, passwordRevealer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<AuthInsider>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<div className='login-form'>
|
||||||
<FormGroup
|
<div className={'authentication-page__label-section'}>
|
||||||
className={'form-group--crediential'}
|
<h3>Log in</h3>
|
||||||
intent={formik.errors.crediential && Intent.DANGER}
|
Need a Bigcapital account ?
|
||||||
helperText={formik.errors.crediential && formik.errors.crediential}>
|
<Link to='/auth/register'> Create an account</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<InputGroup
|
<form onSubmit={handleSubmit} className={'authentication-page__form'}>
|
||||||
leftIcon="user"
|
<FormGroup
|
||||||
placeholder={intl.formatMessage({'id': 'email_or_phone_number'})}
|
label={'Email or Phone Number'}
|
||||||
large={true}
|
intent={(errors.crediential && touched.crediential) && Intent.DANGER}
|
||||||
intent={formik.errors.crediential && Intent.DANGER}
|
helperText={<ErrorMessage name={'crediential'} {...{errors, touched}} />}
|
||||||
{...formik.getFieldProps('crediential')} />
|
className={'form-group--crediential'}
|
||||||
</FormGroup>
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={(errors.crediential && touched.crediential) && Intent.DANGER}
|
||||||
|
large={true}
|
||||||
|
placeholder={'name@company.com'}
|
||||||
|
{...getFieldProps('crediential')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
className={'form-group--password'}
|
label={'Password'}
|
||||||
intent={formik.errors.password && Intent.DANGER}
|
labelInfo={passwordRevealerTmp}
|
||||||
helperText={formik.errors.password && formik.errors.password}>
|
intent={(errors.password && touched.password) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'password'} {...{errors, touched}} />}
|
||||||
|
className={'form-group--password has-password-revealer'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
large={true}
|
||||||
|
intent={(errors.password && touched.password) && Intent.DANGER}
|
||||||
|
type={shown ? 'text' : 'password'}
|
||||||
|
placeholder={'password'}
|
||||||
|
{...getFieldProps('password')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<InputGroup
|
<div className={'login-form__checkbox-section'}>
|
||||||
leftIcon="info-sign"
|
<Checkbox
|
||||||
placeholder={intl.formatMessage({'id': 'password'})}
|
large={true}
|
||||||
large={true}
|
className={'checkbox--remember-me'}>
|
||||||
intent={formik.errors.password && Intent.DANGER}
|
Keep me logged in
|
||||||
type={"password"}
|
</Checkbox>
|
||||||
{...formik.getFieldProps('password')} />
|
</div>
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<Button
|
<div className={'authentication-page__submit-button-wrap'}>
|
||||||
type="submit"
|
<Button
|
||||||
fill={true}
|
type={'submit'}
|
||||||
large={true}>
|
intent={Intent.PRIMARY}
|
||||||
{intl.formatMessage({'id': 'login '})}
|
fill={true}
|
||||||
</Button>
|
lang={true}
|
||||||
</form>
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: 'Log in' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div className="authentication-page__footer">
|
<div class="authentication-page__footer-links">
|
||||||
<Link to="/auth/send_reset_password">
|
<Link to={'/auth/send_reset_password'}>Forget my password</Link>
|
||||||
{intl.formatMessage({'id': 'reset_password '})}
|
</div>
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link to="/auth/register">
|
|
||||||
{intl.formatMessage({'id': 'register '})}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AuthInsider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
export default compose(
|
||||||
hasError: (errorType) => hasErrorType(state, errorType),
|
AuthenticationConnect
|
||||||
errors: state.authentication.errors,
|
)(Login);
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
login: form => dispatch(login({ form })),
|
|
||||||
clearErrors: () => dispatch({ type: t.LOGIN_CLEAR_ERRORS }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Login);
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useMemo, useState, useCallback } from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -7,21 +7,24 @@ import {
|
|||||||
InputGroup,
|
InputGroup,
|
||||||
Intent,
|
Intent,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
HTMLSelect,
|
Spinner
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import RegisterFromConnect from 'connectors/RegisterForm.connect';
|
import { Row, Col } from 'react-grid-system';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||||
import ErrorMessage from 'components/ErrorMessage';
|
import ErrorMessage from 'components/ErrorMessage';
|
||||||
import AppToaster from 'components/AppToaster';
|
import AppToaster from 'components/AppToaster';
|
||||||
|
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||||
import { compose } from 'utils';
|
import { compose } from 'utils';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
|
||||||
function Register({
|
function Register({
|
||||||
requestSubmitRegister,
|
requestRegister,
|
||||||
}) {
|
}) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Country = useMemo(() => [
|
const history = useHistory();
|
||||||
{ value: null, label: 'Select Country' },
|
const [shown, setShown] = useState(false);
|
||||||
{ value: 'libya', label: 'Libya' },
|
const passwordRevealer = useCallback(() => { setShown(!shown); }, [shown]);
|
||||||
], []);
|
|
||||||
|
|
||||||
const ValidationSchema = Yup.object().shape({
|
const ValidationSchema = Yup.object().shape({
|
||||||
organization_name: Yup.string().required(),
|
organization_name: Yup.string().required(),
|
||||||
@@ -29,11 +32,11 @@ function Register({
|
|||||||
last_name: Yup.string().required(),
|
last_name: Yup.string().required(),
|
||||||
email: Yup.string().email().required(),
|
email: Yup.string().email().required(),
|
||||||
phone_number: Yup.string()
|
phone_number: Yup.string()
|
||||||
|
.matches()
|
||||||
.required(intl.formatMessage({ id: 'required' })),
|
.required(intl.formatMessage({ id: 'required' })),
|
||||||
password: Yup.string()
|
password: Yup.string()
|
||||||
.min(4, 'Password has to be longer than 8 characters!')
|
.min(4, 'Password has to be longer than 8 characters!')
|
||||||
.required('Password is required!'),
|
.required('Password is required!'),
|
||||||
country: Yup.string().required(intl.formatMessage({ id: 'required' })),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = useMemo(() => ({
|
const initialValues = useMemo(() => ({
|
||||||
@@ -43,29 +46,31 @@ function Register({
|
|||||||
email: '',
|
email: '',
|
||||||
phone_number: '',
|
phone_number: '',
|
||||||
password: '',
|
password: '',
|
||||||
country: '',
|
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getFieldProps,
|
|
||||||
getFieldMeta,
|
|
||||||
errors,
|
errors,
|
||||||
values,
|
|
||||||
touched,
|
touched,
|
||||||
|
values,
|
||||||
|
setFieldValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
getFieldProps,
|
||||||
|
isSubmitting,
|
||||||
} = useFormik({
|
} = useFormik({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: ValidationSchema,
|
validationSchema: ValidationSchema,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
...initialValues,
|
...initialValues,
|
||||||
|
country: 'libya'
|
||||||
},
|
},
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
requestSubmitRegister(values)
|
requestRegister(values)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: 'success',
|
message: 'success',
|
||||||
});
|
});
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
history.push('/auth/login');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@@ -73,122 +78,140 @@ function Register({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const requiredSpan = useMemo(() => <span class='required'>*</span>, []);
|
const passwordRevealerTmp = useMemo(() => (
|
||||||
|
<span class="password-revealer" onClick={() => passwordRevealer()}>
|
||||||
|
{(shown) ? (
|
||||||
|
<><Icon icon='eye-slash' /> <span class="text">Hide</span></>
|
||||||
|
) : (
|
||||||
|
<><Icon icon='eye' /> <span class="text">Show</span></>
|
||||||
|
)}
|
||||||
|
</span>), [shown, passwordRevealer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'register-form'}>
|
<AuthInsider>
|
||||||
<form onSubmit={handleSubmit}>
|
<div className={'register-form'}>
|
||||||
<FormGroup
|
<div className={'authentication-page__label-section'}>
|
||||||
label={'Organization Name'}
|
<h3>
|
||||||
labelInfo={requiredSpan}
|
Register a New <br />Organization.
|
||||||
className={'form-group--name'}
|
</h3>
|
||||||
intent={
|
You have a bigcapital account ?<Link to='/auth/login'> Login</Link>
|
||||||
(errors.organization_name && touched.organization_name) &&
|
|
||||||
Intent.DANGER
|
|
||||||
}
|
|
||||||
helperText={<ErrorMessage name={'organization_name'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={
|
|
||||||
(errors.organization_name &&
|
|
||||||
touched.organization_name) &&
|
|
||||||
Intent.DANGER
|
|
||||||
}
|
|
||||||
{...getFieldProps('organization_name')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'First Name'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--first_name'}
|
|
||||||
intent={(errors.first_name && touched.first_name) && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'first_name'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={errors.first_name && touched.first_name && Intent.DANGER}
|
|
||||||
{...getFieldProps('first_name')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Last Name'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--last_name'}
|
|
||||||
intent={errors.last_name && touched.last_name && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'last_name'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={errors.last_name && touched.last_name && Intent.DANGER}
|
|
||||||
{...getFieldProps('last_name')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Country'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--country'}
|
|
||||||
intent={(errors.country && touched.country) && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'country'} />}
|
|
||||||
>
|
|
||||||
<HTMLSelect
|
|
||||||
fill={true}
|
|
||||||
options={Country}
|
|
||||||
{...getFieldProps('country')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Phone Number'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--phone_number'}
|
|
||||||
intent={(errors.phone_number && touched.phone_number) && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'phone_number'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={(errors.phone_number && touched.phone_number) && Intent.DANGER}
|
|
||||||
{...getFieldProps('phone_number')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Email'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--email'}
|
|
||||||
intent={(errors.email && touched.email) && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'email'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={errors.email && touched.email && Intent.DANGER}
|
|
||||||
{...getFieldProps('email')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Password'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--password'}
|
|
||||||
intent={(errors.password && touched.password) && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'password'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
lang={true}
|
|
||||||
type={'password'}
|
|
||||||
intent={(errors.password && touched.password) && Intent.DANGER}
|
|
||||||
{...getFieldProps('password')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<div class='form__floating-footer'>
|
|
||||||
<Button intent={Intent.PRIMARY} type='submit'>
|
|
||||||
Register
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
<form onSubmit={handleSubmit} className={'authentication-page__form'}>
|
||||||
|
<FormGroup
|
||||||
|
label={'Organization Name'}
|
||||||
|
className={'form-group--name'}
|
||||||
|
intent={(errors.organization_name && touched.organization_name) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage {...{errors, touched}} name={'organization_name'} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={(errors.organization_name && touched.organization_name) && Intent.DANGER}
|
||||||
|
{...getFieldProps('organization_name')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<Row className={'name-section'}>
|
||||||
|
<Col md={6}>
|
||||||
|
<FormGroup
|
||||||
|
label={'First Name'}
|
||||||
|
intent={(errors.first_name && touched.first_name) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'first_name'} {...{errors, touched}} />}
|
||||||
|
className={'form-group--first-name'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={(errors.first_name && touched.first_name) && Intent.DANGER}
|
||||||
|
{...getFieldProps('first_name')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col md={6}>
|
||||||
|
<FormGroup
|
||||||
|
label={'Last Name'}
|
||||||
|
intent={(errors.last_name && touched.last_name) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'last_name'} {...{errors, touched}} />}
|
||||||
|
className={'form-group--last-name'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={(errors.last_name && touched.last_name) && Intent.DANGER}
|
||||||
|
{...getFieldProps('last_name')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={'Phone Number'}
|
||||||
|
intent={(errors.phone_number && touched.phone_number) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'phone_number'} {...{errors, touched}} />}
|
||||||
|
className={'form-group--phone-number'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={
|
||||||
|
(errors.phone_number && touched.phone_number) &&
|
||||||
|
Intent.DANGER
|
||||||
|
}
|
||||||
|
{...getFieldProps('phone_number')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={'Email'}
|
||||||
|
intent={(errors.email && touched.email) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'email'} {...{errors, touched}} />}
|
||||||
|
className={'form-group--email'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={(errors.email && touched.email) && Intent.DANGER}
|
||||||
|
{...getFieldProps('email')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={'Password'}
|
||||||
|
labelInfo={passwordRevealerTmp}
|
||||||
|
intent={(errors.password && touched.password) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'password'} {...{errors, touched}} />}
|
||||||
|
className={'form-group--password has-password-revealer'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
lang={true}
|
||||||
|
type={shown ? 'text' : 'password'}
|
||||||
|
intent={(errors.password && touched.password) && Intent.DANGER}
|
||||||
|
{...getFieldProps('password')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<div className={'register-form__agreement-section'}>
|
||||||
|
<p>
|
||||||
|
By signing in or creating an account, you agree with our <br />
|
||||||
|
<Link>Terms & Conditions</Link> and <Link> Privacy Statement</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'authentication-page__submit-button-wrap'}>
|
||||||
|
<Button
|
||||||
|
className={'btn-register'}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
type='submit'
|
||||||
|
fill={true}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{ isSubmitting && (
|
||||||
|
<div class="authentication-page__loading-overlay">
|
||||||
|
<Spinner size={50} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AuthInsider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
RegisterFromConnect,
|
AuthenticationConnect,
|
||||||
)(Register);
|
)(Register);
|
||||||
|
|||||||
@@ -7,17 +7,22 @@ import {
|
|||||||
InputGroup,
|
InputGroup,
|
||||||
Intent,
|
Intent,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
HTMLSelect,
|
Position,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
|
import { Link, useParams, useHistory } from 'react-router-dom';
|
||||||
import ErrorMessage from 'components/ErrorMessage';
|
import ErrorMessage from 'components/ErrorMessage';
|
||||||
import AppToaster from 'components/AppToaster';
|
import AppToaster from 'components/AppToaster';
|
||||||
import { compose } from 'utils';
|
import { compose } from 'utils';
|
||||||
import SendResetPasswordConnect from 'connectors/ResetPassword.connect';
|
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||||
|
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||||
|
|
||||||
function ResetPassword({
|
function ResetPassword({
|
||||||
requestSendResetPassword,
|
requestResetPassword,
|
||||||
}) {
|
}) {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { token } = useParams();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const ValidationSchema = Yup.object().shape({
|
const ValidationSchema = Yup.object().shape({
|
||||||
password: Yup.string()
|
password: Yup.string()
|
||||||
.min(4, 'Password has to be longer than 4 characters!')
|
.min(4, 'Password has to be longer than 4 characters!')
|
||||||
@@ -33,12 +38,13 @@ function ResetPassword({
|
|||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
errors,
|
|
||||||
values,
|
|
||||||
touched,
|
touched,
|
||||||
getFieldMeta,
|
values,
|
||||||
getFieldProps,
|
errors,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
setFieldValue,
|
||||||
|
getFieldProps,
|
||||||
|
isSubmitting,
|
||||||
} = useFormik({
|
} = useFormik({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: ValidationSchema,
|
validationSchema: ValidationSchema,
|
||||||
@@ -46,68 +52,84 @@ function ResetPassword({
|
|||||||
...initialValues,
|
...initialValues,
|
||||||
},
|
},
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
requestSendResetPassword(values.password)
|
requestResetPassword(values, token)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: 'success',
|
message: 'The password for your account was successfully updated.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
position: Position.BOTTOM,
|
||||||
});
|
});
|
||||||
|
history.push('/auth/login');
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((errors) => {
|
||||||
|
if (errors.find(e => e.type === 'TOKEN_INVALID')) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'An unexpected error occurred',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
position: Position.BOTTOM,
|
||||||
|
});
|
||||||
|
history.push('/auth/login');
|
||||||
|
}
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const requiredSpan = useMemo(() => <span class='required'>*</span>, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'sendRestPassword-form'}>
|
<AuthInsider>
|
||||||
<form onSubmit={handleSubmit}>
|
<div className={'submit-np-form'}>
|
||||||
<FormGroup
|
<div className={'authentication-page__label-section'}>
|
||||||
label={'Password'}
|
<h3>Choose a new password</h3>
|
||||||
labelInfo={requiredSpan}
|
You remembered your password ? <Link to='/auth/login'>Login</Link>
|
||||||
className={'form-group--password'}
|
|
||||||
intent={errors.password && touched.password && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage name={'password'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
lang={true}
|
|
||||||
type={'password'}
|
|
||||||
intent={errors.password && touched.password && Intent.DANGER}
|
|
||||||
{...getFieldProps('password')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
label={'Confirm Password'}
|
|
||||||
labelInfo={requiredSpan}
|
|
||||||
className={'form-group--confirm_password'}
|
|
||||||
intent={
|
|
||||||
errors.confirm_password && touched.confirm_password && Intent.DANGER
|
|
||||||
}
|
|
||||||
helperText={<ErrorMessage name={'confirm_password'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
lang={true}
|
|
||||||
type={'password'}
|
|
||||||
intent={
|
|
||||||
errors.confirm_password &&
|
|
||||||
touched.confirm_password &&
|
|
||||||
Intent.DANGER
|
|
||||||
}
|
|
||||||
{...getFieldProps('confirm_password')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<div class='form__floating-footer'>
|
|
||||||
<Button intent={Intent.PRIMARY} type='submit'>
|
|
||||||
Reset Password
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
<form onSubmit={handleSubmit}>
|
||||||
|
<FormGroup
|
||||||
|
label={'Password'}
|
||||||
|
intent={(errors.password && touched.password) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'password'} {...{errors, touched}} />}
|
||||||
|
className={'form-group--password'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
lang={true}
|
||||||
|
type={'password'}
|
||||||
|
intent={errors.password && touched.password && Intent.DANGER}
|
||||||
|
{...getFieldProps('password')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={'New Password'}
|
||||||
|
labelInfo={'(again):'}
|
||||||
|
intent={(errors.confirm_password && touched.confirm_password) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'confirm_password'} {...{errors, touched}} />}
|
||||||
|
className={'form-group--confirm-password'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
lang={true}
|
||||||
|
type={'password'}
|
||||||
|
intent={(errors.confirm_password && touched.confirm_password) && Intent.DANGER}
|
||||||
|
{...getFieldProps('confirm_password')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<div className={'authentication-page__submit-button-wrap'}>
|
||||||
|
<Button
|
||||||
|
fill={true}
|
||||||
|
className={'btn-new'}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
type='submit'
|
||||||
|
loading={isSubmitting}>
|
||||||
|
Submit new password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</AuthInsider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(SendResetPasswordConnect)(ResetPassword);
|
export default compose(
|
||||||
|
AuthenticationConnect,
|
||||||
|
)(ResetPassword);
|
||||||
|
|||||||
@@ -1,29 +1,114 @@
|
|||||||
import * as React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import * as Yup from 'yup';
|
||||||
import { Button, InputGroup } from '@blueprintjs/core';
|
import { useFormik } from 'formik';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import ErrorMessage from 'components/ErrorMessage';
|
||||||
|
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||||
|
import { compose } from 'utils';
|
||||||
|
import AppToaster from 'components/AppToaster';
|
||||||
|
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||||
|
|
||||||
export default function SendResetPassword() {
|
function SendResetPassword({
|
||||||
|
requestSendResetPassword,
|
||||||
|
}) {
|
||||||
|
const intl = useIntl();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
// Validation schema.
|
||||||
|
const ValidationSchema = Yup.object().shape({
|
||||||
|
crediential: Yup.string('')
|
||||||
|
.required(intl.formatMessage({ id: 'required' }))
|
||||||
|
.email(intl.formatMessage({ id: 'invalid_email_or_phone_numner' })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialValues = useMemo(() => ({
|
||||||
|
crediential: '',
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
// Formik validation
|
||||||
|
const {
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
handleSubmit,
|
||||||
|
getFieldProps,
|
||||||
|
setFieldValue,
|
||||||
|
isSubmitting,
|
||||||
|
} = useFormik({
|
||||||
|
enableReinitialize: true,
|
||||||
|
validationSchema: ValidationSchema,
|
||||||
|
initialValues: {
|
||||||
|
...initialValues,
|
||||||
|
},
|
||||||
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
|
requestSendResetPassword(values.crediential)
|
||||||
|
.then((response) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: `Check your email for a link to reset your password.
|
||||||
|
If it doesn’t appear within a few minutes, check your spam folder.`,
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
history.push('/auth/login');
|
||||||
|
setSubmitting(false);
|
||||||
|
})
|
||||||
|
.catch((errors) => {
|
||||||
|
if (errors.find(e => e.type === 'EMAIL.NOT.REGISTERED')){
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'We couldn\'t find your account with that email',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div class='login-page'>
|
<AuthInsider>
|
||||||
<form>
|
<div class='reset-form'>
|
||||||
<InputGroup
|
<div className={'authentication-page__label-section'}>
|
||||||
leftIcon='user'
|
<h3>Reset Your Password</h3>
|
||||||
placeholder={<FormattedMessage id='email_or_phone_number' />}
|
<p>Enter your email address and we’ll send you a link to reset your password.</p>
|
||||||
large={true}
|
</div>
|
||||||
className='input-group--email'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button fill={true} large={true}>
|
<form onSubmit={handleSubmit} className={'send-reset-password'}>
|
||||||
<FormattedMessage id='send_reset_password' />
|
<FormGroup
|
||||||
</Button>
|
label={'Email or Phone Number'}
|
||||||
</form>
|
intent={(errors.crediential && touched.crediential) && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name={'crediential'} {...{errors, touched}} />}
|
||||||
|
className={'form-group--crediential'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={(errors.crediential && touched.crediential) && Intent.DANGER}
|
||||||
|
large={true}
|
||||||
|
{...getFieldProps('crediential')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<div class='authentication-page__footer'>
|
<div className={'authentication-page__submit-button-wrap'}>
|
||||||
<Link to='/auth/login'>
|
<Button
|
||||||
<FormattedMessage id='login' />
|
type={'submit'}
|
||||||
</Link>
|
intent={Intent.PRIMARY}
|
||||||
|
fill={true}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: 'Send password reset link' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class='authentication-page__footer-links'>
|
||||||
|
<Link to='/auth/login'>
|
||||||
|
<FormattedMessage id='Return to log in' />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AuthInsider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
AuthenticationConnect,
|
||||||
|
)(SendResetPassword);
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ function BalanceSheetTable({
|
|||||||
onFetchData && onFetchData();
|
onFetchData && onFetchData();
|
||||||
}, [onFetchData]);
|
}, [onFetchData]);
|
||||||
|
|
||||||
|
|
||||||
// Calculates the default expanded rows of balance sheet table.
|
// Calculates the default expanded rows of balance sheet table.
|
||||||
const expandedRows = useMemo(() =>
|
const expandedRows = useMemo(() =>
|
||||||
defaultExpanderReducer(balanceSheetAccounts, 1),
|
defaultExpanderReducer(balanceSheetAccounts, 1),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default [
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${BASE_URL}/reset_password`,
|
path: `${BASE_URL}/reset_password/:token`,
|
||||||
name: 'auth.send.reset_password',
|
name: 'auth.send.reset_password',
|
||||||
component: LazyLoader({
|
component: LazyLoader({
|
||||||
loader: () => import('containers/Authentication/ResetPassword'),
|
loader: () => import('containers/Authentication/ResetPassword'),
|
||||||
|
|||||||
@@ -124,5 +124,13 @@ export default {
|
|||||||
'M302.06,38.87V31.79h7.17v7.08Z',
|
'M302.06,38.87V31.79h7.17v7.08Z',
|
||||||
],
|
],
|
||||||
viewBox: '0 0 309.09 42.89',
|
viewBox: '0 0 309.09 42.89',
|
||||||
|
},
|
||||||
|
'eye': {
|
||||||
|
path: ['M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z'],
|
||||||
|
viewBox: '0 0 576 512',
|
||||||
|
},
|
||||||
|
'eye-slash': {
|
||||||
|
path: ['M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z'],
|
||||||
|
viewBox: '0 0 640 512',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,73 @@
|
|||||||
import ApiService from 'services/ApiService';
|
import ApiService from 'services/ApiService';
|
||||||
import t from 'store/types';
|
import t from 'store/types';
|
||||||
|
|
||||||
export default function login({ form }) {
|
export function login({ form }) {
|
||||||
|
return (dispatch) => new Promise((resolve, reject) => {
|
||||||
|
ApiService.post('auth/login', form).then((response) => {
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
if (data.token && data.user) {
|
||||||
|
dispatch({
|
||||||
|
type: t.LOGIN_SUCCESS,
|
||||||
|
payload: {
|
||||||
|
user: data.user,
|
||||||
|
token: data.token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
}).catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
const { errors = [] } = data;
|
||||||
|
|
||||||
|
reject(errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const register = ({ form }) => {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ApiService.post('auth/login', form).then(response => {
|
ApiService.post('auth/register', form)
|
||||||
const { data } = response;
|
.then((response) => { resolve(response); })
|
||||||
|
.catch(error => { reject(error.response.data.errors || []); })
|
||||||
dispatch({ type: t.LOGIN_CLEAR_ERRORS });
|
})
|
||||||
if (data.token && data.user) {
|
};
|
||||||
dispatch({
|
}
|
||||||
type: t.LOGIN_SUCCESS,
|
|
||||||
payload: {
|
export const resetPassword = ({ form, token }) => {
|
||||||
user: data.user,
|
return (dispatch) => {
|
||||||
token: data.token,
|
return new Promise((resolve, reject) => {
|
||||||
},
|
ApiService.post(`auth/reset/${token}`, form)
|
||||||
});
|
.then((response) => { resolve(response); })
|
||||||
}
|
.catch(error => { reject(error.response.data.errors || []); })
|
||||||
resolve(response);
|
})
|
||||||
}).catch((error) => {
|
};
|
||||||
const { response } = error;
|
};
|
||||||
const { data } = response;
|
|
||||||
const { errors = [] } = data;
|
export const sendResetPassword = (email) => {
|
||||||
|
return (dispatch) => {
|
||||||
reject(errors);
|
return new Promise((resolve, reject) => {
|
||||||
});
|
ApiService.post('auth/send_reset_password', email)
|
||||||
});
|
.then((response) => { resolve(response); })
|
||||||
}
|
.catch(error => { reject(error.response.data.errors || []); })
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inviteAccept = ({ form, token }) => {
|
||||||
|
return (dispatch) => new Promise((resolve, reject) => {
|
||||||
|
ApiService.post(`invite/accept/${token}`, { ...form })
|
||||||
|
.then((response) => { resolve(response); })
|
||||||
|
.catch((error) => { reject(error.response.data.errors || []) });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inviteMetaByToken = ({ token }) => {
|
||||||
|
return (dispatch) => new Promise((resolve, reject) => {
|
||||||
|
ApiService.get(`invite/invited/${token}`)
|
||||||
|
.then((response) => { resolve(response); })
|
||||||
|
.catch((error) => { reject(error.response.data.errors || []) });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -55,20 +55,18 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[data-icon='bigcapital'] {
|
[data-icon='bigcapital'] {
|
||||||
path{
|
path {
|
||||||
fill: #004dd0;
|
fill: #004dd0;
|
||||||
}
|
}
|
||||||
.path-1,
|
.path-1,
|
||||||
.path-13{
|
.path-13 {
|
||||||
fill: #2d95fd;
|
fill: #2d95fd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bigcapital--alt{
|
.bigcapital--alt{
|
||||||
|
|
||||||
|
|
||||||
svg{
|
svg{
|
||||||
path,
|
path,
|
||||||
.path-13,
|
.path-13,
|
||||||
@@ -77,3 +75,11 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// =======
|
||||||
|
body.authentication {
|
||||||
|
background-color: #fcfdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-toast{
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
@@ -190,6 +190,16 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.bp3-large .#{$ns}-control-indicator {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&:hover .#{$ns}-control-indicator {
|
&:hover .#{$ns}-control-indicator {
|
||||||
|
|||||||
@@ -1,26 +1,183 @@
|
|||||||
|
|
||||||
|
|
||||||
.authentication-page{
|
.authentication-insider{
|
||||||
&__form-wrapper{
|
margin-top: 80px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
&__logo-section{
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer{
|
||||||
|
.auth-copyright{
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
.bp3-icon-bigcapital{
|
||||||
|
margin-top: 9px;
|
||||||
|
|
||||||
|
svg{
|
||||||
|
path{
|
||||||
|
fill: #A3A3A3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.authentication-page {
|
||||||
|
|
||||||
|
&__goto-bigcapital{
|
||||||
|
position: fixed;
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-left: 30px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-input {
|
||||||
|
min-height: 40px;
|
||||||
|
border: 2px solid #E3E3E3;
|
||||||
|
}
|
||||||
|
.bp3-form-group{
|
||||||
|
margin-bottom: 25px;
|
||||||
|
|
||||||
|
&.bp3-intent-danger{
|
||||||
|
.bp3-input{
|
||||||
|
border-color: #eea9a9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-form-group.has-password-revealer{
|
||||||
|
|
||||||
|
.bp3-label{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-revealer{
|
||||||
|
.text{
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-button.bp3-fill.bp3-intent-primary{
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label-section{
|
||||||
|
margin-bottom: 34px;
|
||||||
|
color: #555;
|
||||||
|
|
||||||
|
h3{
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #444;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: #0040bd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__form-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 350px;
|
max-width: 415px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-page{
|
&__footer-links{
|
||||||
|
padding: 9px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
|
||||||
.input-group{
|
a{
|
||||||
&--email-phone-number{
|
color: #0052cc;
|
||||||
margin-bottom: 1rem;
|
}
|
||||||
}
|
}
|
||||||
&--password{
|
|
||||||
margin-bottom: 1rem;
|
&__loading-overlay{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(252, 253, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit-button-wrap {
|
||||||
|
margin: 0px 0px 24px 0px;
|
||||||
|
|
||||||
|
.bp3-button {
|
||||||
|
background-color: #0052cc;
|
||||||
|
min-height: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login Form
|
||||||
|
// ------------------------------
|
||||||
|
.login-form {
|
||||||
|
|
||||||
|
.checkbox{
|
||||||
|
&--remember-me{
|
||||||
|
margin: -4px 0 28px 0px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&__footer{
|
|
||||||
margin-top: 1rem;
|
// Register Form
|
||||||
text-align: center;
|
.register-form {
|
||||||
|
|
||||||
|
&__agreement-section {
|
||||||
|
margin-top: -10px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: -10px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__submit-button-wrap {
|
||||||
|
margin: 25px 0px 25px 0px;
|
||||||
|
|
||||||
|
.bp3-button {
|
||||||
|
min-height: 45px;
|
||||||
|
background-color: #0052cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-form {
|
||||||
|
|
||||||
|
&__statement-section {
|
||||||
|
margin-top: -10px;
|
||||||
|
p {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.authentication-page__loading-overlay{
|
||||||
|
background: rgba(252, 253, 255, 0.9);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,7 @@ export default {
|
|||||||
|
|
||||||
const tokenModel = await PasswordReset.query()
|
const tokenModel = await PasswordReset.query()
|
||||||
.where('token', token)
|
.where('token', token)
|
||||||
.where('created_at', '>=', Date.now() - 3600000)
|
// .where('created_at', '>=', Date.now() - 3600000)
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!tokenModel) {
|
if (!tokenModel) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import uniqid from 'uniqid';
|
|||||||
import {
|
import {
|
||||||
check,
|
check,
|
||||||
body,
|
body,
|
||||||
query,
|
param,
|
||||||
validationResult,
|
validationResult,
|
||||||
} from 'express-validator';
|
} from 'express-validator';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -21,6 +21,7 @@ import jwtAuth from '@/http/middleware/jwtAuth';
|
|||||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
import Logger from '@/services/Logger';
|
import Logger from '@/services/Logger';
|
||||||
|
import Option from '@/models/Option';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -36,10 +37,14 @@ export default {
|
|||||||
this.invite.validation,
|
this.invite.validation,
|
||||||
asyncMiddleware(this.invite.handler));
|
asyncMiddleware(this.invite.handler));
|
||||||
|
|
||||||
router.post('/accept',
|
router.post('/accept/:token',
|
||||||
this.accept.validation,
|
this.accept.validation,
|
||||||
asyncMiddleware(this.accept.handler));
|
asyncMiddleware(this.accept.handler));
|
||||||
|
|
||||||
|
router.get('/invited/:token',
|
||||||
|
this.invited.validation,
|
||||||
|
asyncMiddleware(this.invited.handler));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -112,22 +117,24 @@ export default {
|
|||||||
check('first_name').exists().trim().escape(),
|
check('first_name').exists().trim().escape(),
|
||||||
check('last_name').exists().trim().escape(),
|
check('last_name').exists().trim().escape(),
|
||||||
check('phone_number').exists().trim().escape(),
|
check('phone_number').exists().trim().escape(),
|
||||||
check('language').exists().isIn(['ar', 'en']),
|
// check('language').exists().isIn(['ar', 'en']),
|
||||||
check('password').exists().trim().escape(),
|
check('password').exists().trim().escape(),
|
||||||
|
param('token').exists().trim().escape(),
|
||||||
query('token').exists().trim().escape(),
|
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
|
const { token } = req.params;
|
||||||
const inviteToken = await Invite.query()
|
const inviteToken = await Invite.query()
|
||||||
.where('token', req.query.token).first();
|
.where('token', token).first();
|
||||||
|
|
||||||
if (!inviteToken) {
|
if (!inviteToken) {
|
||||||
return res.status(404).send({
|
return res.status(404).send({
|
||||||
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
|
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const form = { ...req.body };
|
const form = {
|
||||||
|
language: 'en',
|
||||||
|
...req.body,
|
||||||
|
};
|
||||||
const systemUser = await SystemUser.query()
|
const systemUser = await SystemUser.query()
|
||||||
.where('phone_number', form.phone_number)
|
.where('phone_number', form.phone_number)
|
||||||
.first();
|
.first();
|
||||||
@@ -157,7 +164,6 @@ export default {
|
|||||||
phone_number: form.phone_number,
|
phone_number: form.phone_number,
|
||||||
language: form.language,
|
language: form.language,
|
||||||
active: 1,
|
active: 1,
|
||||||
password: hashedPassword,
|
|
||||||
};
|
};
|
||||||
TenantModel.knexBinded = tenantDb;
|
TenantModel.knexBinded = tenantDb;
|
||||||
|
|
||||||
@@ -171,16 +177,52 @@ export default {
|
|||||||
}
|
}
|
||||||
const insertUserOper = TenantUser.bindKnex(tenantDb)
|
const insertUserOper = TenantUser.bindKnex(tenantDb)
|
||||||
.query().insert({ ...userForm });
|
.query().insert({ ...userForm });
|
||||||
const insertSysUserOper = SystemUser.query().insert({ ...userForm });
|
const insertSysUserOper = SystemUser.query().insert({
|
||||||
|
...userForm,
|
||||||
|
password: hashedPassword,
|
||||||
|
});
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
insertUserOper,
|
insertUserOper,
|
||||||
insertSysUserOper,
|
insertSysUserOper,
|
||||||
]);
|
]);
|
||||||
await Invite.query()
|
await Invite.query().where('token', token).delete();
|
||||||
.where('token', req.query.token).delete();
|
|
||||||
|
|
||||||
return res.status(200).send();
|
return res.status(200).send();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get
|
||||||
|
*/
|
||||||
|
invited: {
|
||||||
|
validation: [
|
||||||
|
param('token').exists().trim().escape(),
|
||||||
|
],
|
||||||
|
async handler(req, res) {
|
||||||
|
const { token } = req.params;
|
||||||
|
const inviteToken = await Invite.query()
|
||||||
|
.where('token', token).first();
|
||||||
|
|
||||||
|
if (!inviteToken) {
|
||||||
|
return res.status(404).send({
|
||||||
|
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Find the tenant that associated to the given token.
|
||||||
|
const tenant = await Tenant.query()
|
||||||
|
.where('id', inviteToken.tenantId).first();
|
||||||
|
|
||||||
|
const tenantDb = TenantsManager.knexInstance(tenant.organizationId);
|
||||||
|
const organizationOptions = await Option.bindKnex(tenantDb).query()
|
||||||
|
.where('key', 'organization_name');
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
data: {
|
||||||
|
organization_name: organizationOptions.getMeta('organization_name', '') ,
|
||||||
|
invited_email: inviteToken.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -33,13 +33,13 @@ import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
|||||||
export default (app) => {
|
export default (app) => {
|
||||||
// app.use('/api/oauth2', OAuth2.router());
|
// app.use('/api/oauth2', OAuth2.router());
|
||||||
app.use('/api/auth', Authentication.router());
|
app.use('/api/auth', Authentication.router());
|
||||||
|
app.use('/api/invite', InviteUsers.router());
|
||||||
|
|
||||||
const dashboard = express.Router();
|
const dashboard = express.Router();
|
||||||
|
|
||||||
dashboard.use(JWTAuth);
|
dashboard.use(JWTAuth);
|
||||||
dashboard.use(TenancyMiddleware);
|
dashboard.use(TenancyMiddleware);
|
||||||
|
|
||||||
dashboard.use('/api/invite', InviteUsers.router());
|
|
||||||
dashboard.use('/api/currencies', Currencies.router());
|
dashboard.use('/api/currencies', Currencies.router());
|
||||||
// app.use('/api/users', Users.router());
|
// app.use('/api/users', Users.router());
|
||||||
// app.use('/api/roles', Roles.router());
|
// app.use('/api/roles', Roles.router());
|
||||||
|
|||||||
Reference in New Issue
Block a user