feat: sync the isVerified state of authed user

This commit is contained in:
Ahmed Bouhuolia
2024-05-03 16:00:31 +02:00
parent b9fc0cdd9e
commit cb88c234d1
15 changed files with 133 additions and 52 deletions

View File

@@ -9,7 +9,7 @@ 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';
@@ -20,6 +20,9 @@ import { queryConfig } from '../hooks/query/base';
import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified';
import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated';
const EmailConfirmation = LazyLoader({
loader: () => import('@/containers/Authentication/EmailConfirmation'),
});
const RegisterVerify = LazyLoader({
loader: () => import('@/containers/Authentication/RegisterVerify'),
});
@@ -33,24 +36,28 @@ function AppInsider({ history }) {
<DashboardThemeProvider>
<Router history={history}>
<Switch>
<Route path={'/auth/register/verify'}>
<EnsureAuthenticated>
<RegisterVerify />
</EnsureAuthenticated>
</Route>
<Route path={'/auth/email_confirmation'}>
<EmailConfirmation />
</Route>
<Route path={'/auth'}>
<EnsureAuthNotAuthenticated>
<Authentication />
</EnsureAuthNotAuthenticated>
</Route>
<Route path={'/register/verify'}>
<PrivateRoute>
<RegisterVerify />
</PrivateRoute>
</Route>
<Route path={'/'}>
<PrivateRoute>
<EnsureAuthenticated>
<EnsureUserEmailVerified>
<DashboardPrivatePages />
</EnsureUserEmailVerified>
</PrivateRoute>
</EnsureAuthenticated>
</Route>
</Switch>
</Router>

View File

@@ -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,

View File

@@ -3,12 +3,20 @@ import React from 'react';
import { Redirect } from 'react-router-dom';
import { useIsAuthenticated } from '@/hooks/state';
interface PrivateRouteProps {
interface EnsureAuthNotAuthenticatedProps {
children: React.ReactNode;
redirectTo?: string;
}
export function EnsureAuthNotAuthenticated({ children }: PrivateRouteProps) {
export function EnsureAuthNotAuthenticated({
children,
redirectTo = '/',
}: EnsureAuthNotAuthenticatedProps) {
const isAuthenticated = useIsAuthenticated();
return !isAuthenticated ? children : <Redirect to={{ pathname: '/' }} />;
return !isAuthenticated ? (
<>{children}</>
) : (
<Redirect to={{ pathname: redirectTo }} />
);
}

View File

@@ -1,19 +1,22 @@
// @ts-nocheck
import React from 'react';
import BodyClassName from 'react-body-classname';
import { Redirect } from 'react-router-dom';
import { useIsAuthenticated } from '@/hooks/state';
interface PrivateRouteProps {
interface EnsureAuthenticatedProps {
children: React.ReactNode;
redirectTo?: string;
}
export default function PrivateRoute({ children }: PrivateRouteProps) {
export function EnsureAuthenticated({
children,
redirectTo = '/auth/login',
}: EnsureAuthenticatedProps) {
const isAuthenticated = useIsAuthenticated();
return isAuthenticated ? (
children
<>{children}</>
) : (
<Redirect to={{ pathname: '/auth/login' }} />
<Redirect to={{ pathname: redirectTo }} />
);
}

View File

@@ -4,6 +4,7 @@ import { useAuthUserVerified } from '@/hooks/state';
interface EnsureUserEmailVerifiedProps {
children: React.ReactNode;
redirectTo?: string;
}
/**
@@ -12,11 +13,12 @@ interface EnsureUserEmailVerifiedProps {
*/
export function EnsureUserEmailVerified({
children,
redirectTo = '/auth/register/verify',
}: EnsureUserEmailVerifiedProps) {
const isAuthVerified = useAuthUserVerified();
if (!isAuthVerified) {
return <Redirect to={{ pathname: '/register/verify' }} />;
return <Redirect to={{ pathname: redirectTo }} />;
}
return <>{children}</>;
}

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import { Route, Switch, useLocation } from 'react-router-dom';
import BodyClassName from 'react-body-classname';
import styled from 'styled-components';

View File

@@ -1,27 +1,46 @@
// @ts-nocheck
import { useEffect } from 'react';
import { useHistory, useParams } from 'react-router-dom';
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 params = useParams();
const history = useHistory();
const query = useQuery();
const token = params.token;
const email = params.email;
const token = query.get('token');
const email = query.get('email');
useEffect(() => {
if (!token || !email) {
history.push('register/email_confirmation');
history.push('/auth/login');
}
}, [history, token, email]);
useEffect(() => {
authSignupVerify(token, email)
.then(() => {})
.catch((error) => {});
}, [token, email, authSignupVerify]);
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;
}

View File

@@ -7,10 +7,8 @@ import { AppToaster, Stack } from '@/components';
import { useAuthActions } from '@/hooks/state';
import { useAuthSignUpVerifyResendMail } from '@/hooks/query';
import { AuthContainer } from './AuthContainer';
import { useHistory } from 'react-router-dom';
export default function RegisterVerify() {
const history = useHistory();
const { setLogout } = useAuthActions();
const { mutateAsync: resendSignUpVerifyMail, isLoading } =
useAuthSignUpVerifyResendMail();
@@ -30,8 +28,6 @@ export default function RegisterVerify() {
});
});
};
// Handle logout link click.
const handleSignOutBtnClick = () => {
setLogout();
};
@@ -60,11 +56,11 @@ export default function RegisterVerify() {
<Button
large
fill
intent={Intent.DANGER}
minimal
intent={Intent.DANGER}
onClick={handleSignOutBtnClick}
>
Signout
Not my email
</Button>
</Stack>
</AuthInsiderCard>

View File

@@ -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,12 +88,11 @@ export const useAuthMetadata = (props) => {
defaultData: {},
...props,
},
);
}
);
};
/**
*
*
*/
export const useAuthSignUpVerifyResendMail = (props) => {
const apiRequest = useApiRequest();
@@ -104,16 +103,20 @@ export const useAuthSignUpVerifyResendMail = (props) => {
);
};
interface AuthSignUpVerifyValues {
token: string;
email: string;
}
/**
*
*
*/
export const useAuthSignUpVerify = (props) => {
const apiRequest = useApiRequest();
return useMutation(
(token: string, email: string) => apiRequest.post('auth/register/verify'),
(values: AuthSignUpVerifyValues) =>
apiRequest.post('auth/register/verify', values),
props,
);
};
};

View File

@@ -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;
};

View File

@@ -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';
@@ -66,8 +69,20 @@ export const useAuthOrganizationId = () => {
};
/**
*
* Retrieves the user's email verification status.
*/
export const useAuthUserVerified = () => {
return useSelector(() => false);
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],
);
};

View File

@@ -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 },
});

View File

@@ -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);
},

View File

@@ -7,4 +7,5 @@ export default {
LOGOUT: 'LOGOUT',
LOGIN_CLEAR_ERRORS: 'LOGIN_CLEAR_ERRORS',
RESET: 'RESET',
SET_EMAIL_VERIFIED: 'SET_EMAIL_VERIFIED'
};