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

@@ -35,7 +35,7 @@ export default class SystemUser extends SystemModel {
* Virtual attributes. * Virtual attributes.
*/ */
static get virtualAttributes() { static get virtualAttributes() {
return ['fullName', 'isDeleted', 'isInviteAccepted']; return ['fullName', 'isDeleted', 'isInviteAccepted', 'isVerified'];
} }
/** /**

View File

@@ -9,7 +9,7 @@ import 'moment/locale/ar-ly';
import 'moment/locale/es-us'; import 'moment/locale/es-us';
import AppIntlLoader from './AppIntlLoader'; import AppIntlLoader from './AppIntlLoader';
import PrivateRoute from '@/components/Guards/PrivateRoute'; import { EnsureAuthenticated } from '@/components/Guards/EnsureAuthenticated';
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors'; import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages'; import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
import { Authentication } from '@/containers/Authentication/Authentication'; import { Authentication } from '@/containers/Authentication/Authentication';
@@ -20,6 +20,9 @@ import { queryConfig } from '../hooks/query/base';
import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified'; import { EnsureUserEmailVerified } from './Guards/EnsureUserEmailVerified';
import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated'; import { EnsureAuthNotAuthenticated } from './Guards/EnsureAuthNotAuthenticated';
const EmailConfirmation = LazyLoader({
loader: () => import('@/containers/Authentication/EmailConfirmation'),
});
const RegisterVerify = LazyLoader({ const RegisterVerify = LazyLoader({
loader: () => import('@/containers/Authentication/RegisterVerify'), loader: () => import('@/containers/Authentication/RegisterVerify'),
}); });
@@ -33,24 +36,28 @@ function AppInsider({ history }) {
<DashboardThemeProvider> <DashboardThemeProvider>
<Router history={history}> <Router history={history}>
<Switch> <Switch>
<Route path={'/auth/register/verify'}>
<EnsureAuthenticated>
<RegisterVerify />
</EnsureAuthenticated>
</Route>
<Route path={'/auth/email_confirmation'}>
<EmailConfirmation />
</Route>
<Route path={'/auth'}> <Route path={'/auth'}>
<EnsureAuthNotAuthenticated> <EnsureAuthNotAuthenticated>
<Authentication /> <Authentication />
</EnsureAuthNotAuthenticated> </EnsureAuthNotAuthenticated>
</Route> </Route>
<Route path={'/register/verify'}>
<PrivateRoute>
<RegisterVerify />
</PrivateRoute>
</Route>
<Route path={'/'}> <Route path={'/'}>
<PrivateRoute> <EnsureAuthenticated>
<EnsureUserEmailVerified> <EnsureUserEmailVerified>
<DashboardPrivatePages /> <DashboardPrivatePages />
</EnsureUserEmailVerified> </EnsureUserEmailVerified>
</PrivateRoute> </EnsureAuthenticated>
</Route> </Route>
</Switch> </Switch>
</Router> </Router>

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React, { useEffect } from 'react';
import { import {
useAuthenticatedAccount, useAuthenticatedAccount,
useCurrentOrganization, useCurrentOrganization,
@@ -116,6 +116,14 @@ export function useApplicationBoot() {
isBooted.current = true; isBooted.current = true;
}, },
); );
// Reset the loading states once the hook unmount.
useEffect(
() => () => {
isAuthUserLoading && !isBooted.current && stopLoading();
isOrgLoading && !isBooted.current && stopLoading();
},
[isAuthUserLoading, isOrgLoading, stopLoading],
);
return { return {
isLoading: isOrgLoading || isAuthUserLoading, isLoading: isOrgLoading || isAuthUserLoading,

View File

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

View File

@@ -1,19 +1,22 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import BodyClassName from 'react-body-classname';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { useIsAuthenticated } from '@/hooks/state'; import { useIsAuthenticated } from '@/hooks/state';
interface PrivateRouteProps { interface EnsureAuthenticatedProps {
children: React.ReactNode; children: React.ReactNode;
redirectTo?: string;
} }
export default function PrivateRoute({ children }: PrivateRouteProps) { export function EnsureAuthenticated({
children,
redirectTo = '/auth/login',
}: EnsureAuthenticatedProps) {
const isAuthenticated = useIsAuthenticated(); const isAuthenticated = useIsAuthenticated();
return isAuthenticated ? ( 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 { interface EnsureUserEmailVerifiedProps {
children: React.ReactNode; children: React.ReactNode;
redirectTo?: string;
} }
/** /**
@@ -12,11 +13,12 @@ interface EnsureUserEmailVerifiedProps {
*/ */
export function EnsureUserEmailVerified({ export function EnsureUserEmailVerified({
children, children,
redirectTo = '/auth/register/verify',
}: EnsureUserEmailVerifiedProps) { }: EnsureUserEmailVerifiedProps) {
const isAuthVerified = useAuthUserVerified(); const isAuthVerified = useAuthUserVerified();
if (!isAuthVerified) { if (!isAuthVerified) {
return <Redirect to={{ pathname: '/register/verify' }} />; return <Redirect to={{ pathname: redirectTo }} />;
} }
return <>{children}</>; return <>{children}</>;
} }

View File

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

View File

@@ -1,27 +1,46 @@
// @ts-nocheck // @ts-nocheck
import { useEffect } from 'react'; import { useEffect, useMemo } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useLocation, useHistory } from 'react-router-dom';
import { useAuthSignUpVerify } from '@/hooks/query'; 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() { export default function EmailConfirmation() {
const { mutateAsync: authSignupVerify } = useAuthSignUpVerify(); const { mutateAsync: authSignupVerify } = useAuthSignUpVerify();
const params = useParams();
const history = useHistory(); const history = useHistory();
const query = useQuery();
const token = params.token; const token = query.get('token');
const email = params.email; const email = query.get('email');
useEffect(() => { useEffect(() => {
if (!token || !email) { if (!token || !email) {
history.push('register/email_confirmation'); history.push('/auth/login');
} }
}, [history, token, email]); }, [history, token, email]);
useEffect(() => { useEffect(() => {
authSignupVerify(token, email) authSignupVerify({ token, email })
.then(() => {}) .then(() => {
.catch((error) => {}); AppToaster.show({
}, [token, email, authSignupVerify]); 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; return null;
} }

View File

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

View File

@@ -78,7 +78,7 @@ export const useAuthResetPassword = (props) => {
*/ */
export const useAuthMetadata = (props) => { export const useAuthMetadata = (props) => {
return useRequestQuery( return useRequestQuery(
[t.AUTH_METADATA_PAGE,], [t.AUTH_METADATA_PAGE],
{ {
method: 'get', method: 'get',
url: `auth/meta`, url: `auth/meta`,
@@ -88,12 +88,11 @@ export const useAuthMetadata = (props) => {
defaultData: {}, defaultData: {},
...props, ...props,
}, },
); );
} };
/** /**
* *
*/ */
export const useAuthSignUpVerifyResendMail = (props) => { export const useAuthSignUpVerifyResendMail = (props) => {
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
@@ -104,16 +103,20 @@ export const useAuthSignUpVerifyResendMail = (props) => {
); );
}; };
interface AuthSignUpVerifyValues {
token: string;
email: string;
}
/** /**
* *
*/ */
export const useAuthSignUpVerify = (props) => { export const useAuthSignUpVerify = (props) => {
const apiRequest = useApiRequest(); const apiRequest = useApiRequest();
return useMutation( return useMutation(
(token: string, email: string) => apiRequest.post('auth/register/verify'), (values: AuthSignUpVerifyValues) =>
apiRequest.post('auth/register/verify', values),
props, props,
); );
}; };

View File

@@ -5,6 +5,7 @@ import { useQueryTenant, useRequestQuery } from '../useQueryRequest';
import useApiRequest from '../useRequest'; import useApiRequest from '../useRequest';
import { useSetFeatureDashboardMeta } from '../state/feature'; import { useSetFeatureDashboardMeta } from '../state/feature';
import t from './types'; import t from './types';
import { useSetAuthEmailConfirmed } from '../state';
// Common invalidate queries. // Common invalidate queries.
const commonInvalidateQueries = (queryClient) => { const commonInvalidateQueries = (queryClient) => {
@@ -130,6 +131,8 @@ export function useUser(id, props) {
} }
export function useAuthenticatedAccount(props) { export function useAuthenticatedAccount(props) {
const setEmailConfirmed = useSetAuthEmailConfirmed();
return useRequestQuery( return useRequestQuery(
['AuthenticatedAccount'], ['AuthenticatedAccount'],
{ {
@@ -139,6 +142,9 @@ export function useAuthenticatedAccount(props) {
{ {
select: (response) => response.data.data, select: (response) => response.data.data,
defaultData: {}, defaultData: {},
onSuccess: (data) => {
setEmailConfirmed(data.is_verified);
},
...props, ...props,
}, },
); );
@@ -166,4 +172,3 @@ export const useDashboardMeta = (props) => {
}, [state.isSuccess, state.data, setFeatureDashboardMeta]); }, [state.isSuccess, state.data, setFeatureDashboardMeta]);
return state; return state;
}; };

View File

@@ -2,7 +2,10 @@
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { isAuthenticated } from '@/store/authentication/authentication.reducer'; 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 { useQueryClient } from 'react-query';
import { removeCookie } from '@/utils'; import { removeCookie } from '@/utils';
@@ -66,8 +69,20 @@ export const useAuthOrganizationId = () => {
}; };
/** /**
* * Retrieves the user's email verification status.
*/ */
export const useAuthUserVerified = () => { 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 setLogin = () => ({ type: t.LOGIN_SUCCESS });
export const setLogout = () => ({ type: t.LOGOUT }); 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 // @ts-nocheck
import { createReducer } from '@reduxjs/toolkit'; import { PayloadAction, createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist'; import { persistReducer } from 'redux-persist';
import purgeStoredState from 'redux-persist/es/purgeStoredState'; import purgeStoredState from 'redux-persist/es/purgeStoredState';
import storage from 'redux-persist/lib/storage'; import storage from 'redux-persist/lib/storage';
import { isUndefined } from 'lodash';
import { getCookie } from '@/utils'; import { getCookie } from '@/utils';
import t from '@/store/types'; import t from '@/store/types';
@@ -13,6 +14,7 @@ const initialState = {
tenantId: getCookie('tenant_id'), tenantId: getCookie('tenant_id'),
userId: getCookie('authenticated_user_id'), userId: getCookie('authenticated_user_id'),
locale: getCookie('locale'), locale: getCookie('locale'),
verified: true, // Let's be optimistic and assume the user's email is confirmed.
errors: [], errors: [],
}; };
@@ -32,6 +34,15 @@ const reducerInstance = createReducer(initialState, {
state.errors = []; state.errors = [];
}, },
[t.SET_EMAIL_VERIFIED]: (
state,
payload: PayloadAction<{ verified?: boolean }>,
) => {
state.verified = !isUndefined(payload.action.verified)
? payload.action.verified
: true;
},
[t.RESET]: (state) => { [t.RESET]: (state) => {
purgeStoredState(CONFIG); purgeStoredState(CONFIG);
}, },

View File

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