mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
Merge pull request #426 from bigcapitalhq/big-163-user-email-verification-after-signing-up
feat: User email verification after signing-up.
This commit is contained in:
@@ -9,13 +9,24 @@ import 'moment/locale/ar-ly';
|
||||
import 'moment/locale/es-us';
|
||||
|
||||
import AppIntlLoader from './AppIntlLoader';
|
||||
import PrivateRoute from '@/components/Guards/PrivateRoute';
|
||||
import { EnsureAuthenticated } from '@/components/Guards/EnsureAuthenticated';
|
||||
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
||||
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
||||
import { Authentication } from '@/containers/Authentication/Authentication';
|
||||
|
||||
import LazyLoader from '@/components/LazyLoader';
|
||||
import { SplashScreen, DashboardThemeProvider } from '../components';
|
||||
import { queryConfig } from '../hooks/query/base';
|
||||
import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified';
|
||||
import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated';
|
||||
import { EnsureUserEmailNotVerified } from './Guards/EnsureUserEmailNotVerified';
|
||||
|
||||
const EmailConfirmation = LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/EmailConfirmation'),
|
||||
});
|
||||
const RegisterVerify = LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/RegisterVerify'),
|
||||
});
|
||||
|
||||
/**
|
||||
* App inner.
|
||||
@@ -26,9 +37,30 @@ function AppInsider({ history }) {
|
||||
<DashboardThemeProvider>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={'/auth'} component={Authentication} />
|
||||
<Route path={'/auth/register/verify'}>
|
||||
<EnsureAuthenticated>
|
||||
<EnsureUserEmailNotVerified>
|
||||
<RegisterVerify />
|
||||
</EnsureUserEmailNotVerified>
|
||||
</EnsureAuthenticated>
|
||||
</Route>
|
||||
|
||||
<Route path={'/auth/email_confirmation'}>
|
||||
<EmailConfirmation />
|
||||
</Route>
|
||||
|
||||
<Route path={'/auth'}>
|
||||
<EnsureAuthNotAuthenticated>
|
||||
<Authentication />
|
||||
</EnsureAuthNotAuthenticated>
|
||||
</Route>
|
||||
|
||||
<Route path={'/'}>
|
||||
<PrivateRoute component={DashboardPrivatePages} />
|
||||
<EnsureAuthenticated>
|
||||
<EnsureUserEmailVerified>
|
||||
<DashboardPrivatePages />
|
||||
</EnsureUserEmailVerified>
|
||||
</EnsureAuthenticated>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
useAuthenticatedAccount,
|
||||
useCurrentOrganization,
|
||||
@@ -116,6 +116,14 @@ export function useApplicationBoot() {
|
||||
isBooted.current = true;
|
||||
},
|
||||
);
|
||||
// Reset the loading states once the hook unmount.
|
||||
useEffect(
|
||||
() => () => {
|
||||
isAuthUserLoading && !isBooted.current && stopLoading();
|
||||
isOrgLoading && !isBooted.current && stopLoading();
|
||||
},
|
||||
[isAuthUserLoading, isOrgLoading, stopLoading],
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading: isOrgLoading || isAuthUserLoading,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
|
||||
interface EnsureAuthNotAuthenticatedProps {
|
||||
children: React.ReactNode;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export function EnsureAuthNotAuthenticated({
|
||||
children,
|
||||
redirectTo = '/',
|
||||
}: EnsureAuthNotAuthenticatedProps) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return !isAuthenticated ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<Redirect to={{ pathname: redirectTo }} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
|
||||
interface EnsureAuthenticatedProps {
|
||||
children: React.ReactNode;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export function EnsureAuthenticated({
|
||||
children,
|
||||
redirectTo = '/auth/login',
|
||||
}: EnsureAuthenticatedProps) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return isAuthenticated ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<Redirect to={{ pathname: redirectTo }} />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useAuthUserVerified } from '@/hooks/state';
|
||||
|
||||
interface EnsureUserEmailNotVerifiedProps {
|
||||
children: React.ReactNode;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher Order Component to ensure that the user's email is not verified.
|
||||
* If is verified, redirects to the inner setup page.
|
||||
*/
|
||||
export function EnsureUserEmailNotVerified({
|
||||
children,
|
||||
redirectTo = '/',
|
||||
}: EnsureUserEmailNotVerifiedProps) {
|
||||
const isAuthVerified = useAuthUserVerified();
|
||||
|
||||
if (isAuthVerified) {
|
||||
return <Redirect to={{ pathname: redirectTo }} />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useAuthUserVerified } from '@/hooks/state';
|
||||
|
||||
interface EnsureUserEmailVerifiedProps {
|
||||
children: React.ReactNode;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher Order Component to ensure that the user's email is verified.
|
||||
* If not verified, redirects to the email verification page.
|
||||
*/
|
||||
export function EnsureUserEmailVerified({
|
||||
children,
|
||||
redirectTo = '/auth/register/verify',
|
||||
}: EnsureUserEmailVerifiedProps) {
|
||||
const isAuthVerified = useAuthUserVerified();
|
||||
|
||||
if (!isAuthVerified) {
|
||||
return <Redirect to={{ pathname: redirectTo }} />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
|
||||
export default function PrivateRoute({ component: Component, ...rest }) {
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
return (
|
||||
<BodyClassName className={''}>
|
||||
{isAuthenticated ? (
|
||||
<Component />
|
||||
) : (
|
||||
<Redirect to={{ pathname: '/auth/login' }} />
|
||||
)}
|
||||
</BodyClassName>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// @ts-nocheck
|
||||
import styled from 'styled-components';
|
||||
import { Icon, FormattedMessage as T } from '@/components';
|
||||
|
||||
interface AuthContainerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function AuthContainer({ children }: AuthContainerProps) {
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthInsider>
|
||||
<AuthLogo>
|
||||
<Icon icon="bigcapital" height={37} width={214} />
|
||||
</AuthLogo>
|
||||
|
||||
{children}
|
||||
</AuthInsider>
|
||||
</AuthPage>
|
||||
);
|
||||
}
|
||||
|
||||
const AuthPage = styled.div``;
|
||||
const AuthInsider = styled.div`
|
||||
width: 384px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 40px;
|
||||
padding-top: 80px;
|
||||
`;
|
||||
|
||||
const AuthLogo = styled.div`
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
@@ -1,24 +1,16 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch, useLocation } from 'react-router-dom';
|
||||
import { Route, Switch, useLocation } from 'react-router-dom';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import styled from 'styled-components';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
|
||||
import authenticationRoutes from '@/routes/authentication';
|
||||
import { Icon, FormattedMessage as T } from '@/components';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
import { AuthMetaBootProvider } from './AuthMetaBoot';
|
||||
|
||||
import '@/style/pages/Authentication/Auth.scss';
|
||||
|
||||
export function Authentication() {
|
||||
const to = { pathname: '/' };
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Redirect to={to} />;
|
||||
}
|
||||
return (
|
||||
<BodyClassName className={'authentication'}>
|
||||
<AuthPage>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// @ts-nocheck
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import { useAuthSignUpVerify } from '@/hooks/query';
|
||||
import { AppToaster } from '@/components';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
|
||||
function useQuery() {
|
||||
const { search } = useLocation();
|
||||
return useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
export default function EmailConfirmation() {
|
||||
const { mutateAsync: authSignupVerify } = useAuthSignUpVerify();
|
||||
const history = useHistory();
|
||||
const query = useQuery();
|
||||
|
||||
const token = query.get('token');
|
||||
const email = query.get('email');
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || !email) {
|
||||
history.push('/auth/login');
|
||||
}
|
||||
}, [history, token, email]);
|
||||
|
||||
useEffect(() => {
|
||||
authSignupVerify({ token, email })
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: 'Your email has been verified, Congrats!',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
history.push('/');
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
message: 'Something went wrong',
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
history.push('/');
|
||||
});
|
||||
}, [token, email, authSignupVerify, history]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
.root {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title{
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #252A31;
|
||||
}
|
||||
|
||||
.description{
|
||||
margin-bottom: 1rem;
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
color: #404854;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// @ts-nocheck
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import AuthInsider from './AuthInsider';
|
||||
import { AuthInsiderCard } from './_components';
|
||||
import styles from './RegisterVerify.module.scss';
|
||||
import { AppToaster, Stack } from '@/components';
|
||||
import { useAuthActions } from '@/hooks/state';
|
||||
import { useAuthSignUpVerifyResendMail } from '@/hooks/query';
|
||||
import { AuthContainer } from './AuthContainer';
|
||||
|
||||
export default function RegisterVerify() {
|
||||
const { setLogout } = useAuthActions();
|
||||
const { mutateAsync: resendSignUpVerifyMail, isLoading } =
|
||||
useAuthSignUpVerifyResendMail();
|
||||
|
||||
const handleResendMailBtnClick = () => {
|
||||
resendSignUpVerifyMail()
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
intent: Intent.SUCCESS,
|
||||
message: 'The verification mail has sent successfully.',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
AppToaster.show({
|
||||
intent: Intent.DANGER,
|
||||
message: 'Something went wrong.',
|
||||
});
|
||||
});
|
||||
};
|
||||
const handleSignOutBtnClick = () => {
|
||||
setLogout();
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContainer>
|
||||
<AuthInsider>
|
||||
<AuthInsiderCard className={styles.root}>
|
||||
<h2 className={styles.title}>Please verify your email</h2>
|
||||
<p className={styles.description}>
|
||||
We sent an email to <strong>asdahmed@gmail.com</strong> Click the
|
||||
link inside to get started.
|
||||
</p>
|
||||
|
||||
<Stack spacing={4}>
|
||||
<Button
|
||||
large
|
||||
fill
|
||||
loading={isLoading}
|
||||
intent={Intent.NONE}
|
||||
onClick={handleResendMailBtnClick}
|
||||
>
|
||||
Resend email
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
large
|
||||
fill
|
||||
minimal
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleSignOutBtnClick}
|
||||
>
|
||||
Not my email
|
||||
</Button>
|
||||
</Stack>
|
||||
</AuthInsiderCard>
|
||||
</AuthInsider>
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export const useAuthResetPassword = (props) => {
|
||||
*/
|
||||
export const useAuthMetadata = (props) => {
|
||||
return useRequestQuery(
|
||||
[t.AUTH_METADATA_PAGE,],
|
||||
[t.AUTH_METADATA_PAGE],
|
||||
{
|
||||
method: 'get',
|
||||
url: `auth/meta`,
|
||||
@@ -88,5 +88,35 @@ export const useAuthMetadata = (props) => {
|
||||
defaultData: {},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const useAuthSignUpVerifyResendMail = (props) => {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
() => apiRequest.post('auth/register/verify/resend'),
|
||||
props,
|
||||
);
|
||||
};
|
||||
|
||||
interface AuthSignUpVerifyValues {
|
||||
token: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export const useAuthSignUpVerify = (props) => {
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
(values: AuthSignUpVerifyValues) =>
|
||||
apiRequest.post('auth/register/verify', values),
|
||||
props,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useQueryTenant, useRequestQuery } from '../useQueryRequest';
|
||||
import useApiRequest from '../useRequest';
|
||||
import { useSetFeatureDashboardMeta } from '../state/feature';
|
||||
import t from './types';
|
||||
import { useSetAuthEmailConfirmed } from '../state';
|
||||
|
||||
// Common invalidate queries.
|
||||
const commonInvalidateQueries = (queryClient) => {
|
||||
@@ -130,6 +131,8 @@ export function useUser(id, props) {
|
||||
}
|
||||
|
||||
export function useAuthenticatedAccount(props) {
|
||||
const setEmailConfirmed = useSetAuthEmailConfirmed();
|
||||
|
||||
return useRequestQuery(
|
||||
['AuthenticatedAccount'],
|
||||
{
|
||||
@@ -139,6 +142,9 @@ export function useAuthenticatedAccount(props) {
|
||||
{
|
||||
select: (response) => response.data.data,
|
||||
defaultData: {},
|
||||
onSuccess: (data) => {
|
||||
setEmailConfirmed(data.is_verified);
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
@@ -166,4 +172,3 @@ export const useDashboardMeta = (props) => {
|
||||
}, [state.isSuccess, state.data, setFeatureDashboardMeta]);
|
||||
return state;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useCallback } from 'react';
|
||||
import { isAuthenticated } from '@/store/authentication/authentication.reducer';
|
||||
import { setLogin } from '@/store/authentication/authentication.actions';
|
||||
import {
|
||||
setEmailConfirmed,
|
||||
setLogin,
|
||||
} from '@/store/authentication/authentication.actions';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { removeCookie } from '@/utils';
|
||||
|
||||
@@ -64,3 +67,22 @@ export const useAuthUser = () => {
|
||||
export const useAuthOrganizationId = () => {
|
||||
return useSelector((state) => state.authentication.organizationId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the user's email verification status.
|
||||
*/
|
||||
export const useAuthUserVerified = () => {
|
||||
return useSelector((state) => state.authentication.verified);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the user's email verification status.
|
||||
*/
|
||||
export const useSetAuthEmailConfirmed = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return useCallback(
|
||||
(verified?: boolean = true) => dispatch(setEmailConfirmed(verified)),
|
||||
[dispatch],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,10 +28,16 @@ export default [
|
||||
loader: () => import('@/containers/Authentication/InviteAccept'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/register/email_confirmation`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/EmailConfirmation'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/register`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/Register'),
|
||||
}),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,4 +3,8 @@ import t from '@/store/types';
|
||||
|
||||
export const setLogin = () => ({ type: t.LOGIN_SUCCESS });
|
||||
export const setLogout = () => ({ type: t.LOGOUT });
|
||||
export const setStoreReset = () => ({ type: t.RESET });
|
||||
export const setStoreReset = () => ({ type: t.RESET });
|
||||
export const setEmailConfirmed = (verified?: boolean) => ({
|
||||
type: t.SET_EMAIL_VERIFIED,
|
||||
action: { verified },
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { PayloadAction, createReducer } from '@reduxjs/toolkit';
|
||||
import { persistReducer } from 'redux-persist';
|
||||
import purgeStoredState from 'redux-persist/es/purgeStoredState';
|
||||
import storage from 'redux-persist/lib/storage';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { getCookie } from '@/utils';
|
||||
import t from '@/store/types';
|
||||
|
||||
@@ -13,6 +14,7 @@ const initialState = {
|
||||
tenantId: getCookie('tenant_id'),
|
||||
userId: getCookie('authenticated_user_id'),
|
||||
locale: getCookie('locale'),
|
||||
verified: true, // Let's be optimistic and assume the user's email is confirmed.
|
||||
errors: [],
|
||||
};
|
||||
|
||||
@@ -32,6 +34,15 @@ const reducerInstance = createReducer(initialState, {
|
||||
state.errors = [];
|
||||
},
|
||||
|
||||
[t.SET_EMAIL_VERIFIED]: (
|
||||
state,
|
||||
payload: PayloadAction<{ verified?: boolean }>,
|
||||
) => {
|
||||
state.verified = !isUndefined(payload.action.verified)
|
||||
? payload.action.verified
|
||||
: true;
|
||||
},
|
||||
|
||||
[t.RESET]: (state) => {
|
||||
purgeStoredState(CONFIG);
|
||||
},
|
||||
|
||||
@@ -7,4 +7,5 @@ export default {
|
||||
LOGOUT: 'LOGOUT',
|
||||
LOGIN_CLEAR_ERRORS: 'LOGIN_CLEAR_ERRORS',
|
||||
RESET: 'RESET',
|
||||
SET_EMAIL_VERIFIED: 'SET_EMAIL_VERIFIED'
|
||||
};
|
||||
Reference in New Issue
Block a user