feat(auth): add combined AuthAndSetupGuard for route protection
- Introduced AuthAndSetupGuard to enforce both authentication and setup completion for protected routes. - Updated main routes to utilize the new guard, ensuring users are redirected appropriately based on their authentication and setup status. - Adjusted setup route to only require authentication, allowing access without completed setup. - Refactored account setup component to use custom dispatch hook for improved state management and added session refresh after setup completion.
This commit is contained in:
@@ -90,6 +90,23 @@ export const SetupGuard = memo(({ children }: GuardProps) => {
|
|||||||
|
|
||||||
SetupGuard.displayName = 'SetupGuard';
|
SetupGuard.displayName = 'SetupGuard';
|
||||||
|
|
||||||
|
// Combined guard for routes that require both authentication and setup completion
|
||||||
|
export const AuthAndSetupGuard = memo(({ children }: GuardProps) => {
|
||||||
|
const { isAuthenticated, isSetupComplete, location } = useAuthStatus();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSetupComplete) {
|
||||||
|
return <Navigate to="/worklenz/setup" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
});
|
||||||
|
|
||||||
|
AuthAndSetupGuard.displayName = 'AuthAndSetupGuard';
|
||||||
|
|
||||||
// Optimized route wrapping function with Suspense boundaries
|
// Optimized route wrapping function with Suspense boundaries
|
||||||
const wrapRoutes = (
|
const wrapRoutes = (
|
||||||
routes: RouteObject[],
|
routes: RouteObject[],
|
||||||
@@ -171,9 +188,11 @@ StaticLicenseExpired.displayName = 'StaticLicenseExpired';
|
|||||||
// Create route arrays (moved outside of useMemo to avoid hook violations)
|
// Create route arrays (moved outside of useMemo to avoid hook violations)
|
||||||
const publicRoutes = [...rootRoutes, ...authRoutes, notFoundRoute];
|
const publicRoutes = [...rootRoutes, ...authRoutes, notFoundRoute];
|
||||||
|
|
||||||
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
|
// Apply combined guard to main routes that require both auth and setup completion
|
||||||
|
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthAndSetupGuard);
|
||||||
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
|
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
|
||||||
const setupRoutes = wrapRoutes([accountSetupRoute], SetupGuard);
|
// Setup route should be accessible without setup completion, only requires authentication
|
||||||
|
const setupRoutes = wrapRoutes([accountSetupRoute], AuthGuard);
|
||||||
|
|
||||||
// License expiry check function
|
// License expiry check function
|
||||||
const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Space, Steps, Button, Typography } from 'antd/es';
|
import { Space, Steps, Button, Typography } from 'antd/es';
|
||||||
@@ -26,6 +26,7 @@ import { validateEmail } from '@/utils/validateEmail';
|
|||||||
import { sanitizeInput } from '@/utils/sanitizeInput';
|
import { sanitizeInput } from '@/utils/sanitizeInput';
|
||||||
import logo from '@/assets/images/worklenz-light-mode.png';
|
import logo from '@/assets/images/worklenz-light-mode.png';
|
||||||
import logoDark from '@/assets/images/worklenz-dark-mode.png';
|
import logoDark from '@/assets/images/worklenz-dark-mode.png';
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
|
||||||
import './account-setup.css';
|
import './account-setup.css';
|
||||||
import { IAccountSetupRequest } from '@/types/project-templates/project-templates.types';
|
import { IAccountSetupRequest } from '@/types/project-templates/project-templates.types';
|
||||||
@@ -34,7 +35,7 @@ import { profileSettingsApiService } from '@/api/settings/profile/profile-settin
|
|||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
const AccountSetup: React.FC = () => {
|
const AccountSetup: React.FC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation('account-setup');
|
const { t } = useTranslation('account-setup');
|
||||||
useDocumentTitle(t('setupYourAccount', 'Account Setup'));
|
useDocumentTitle(t('setupYourAccount', 'Account Setup'));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -52,8 +53,7 @@ const AccountSetup: React.FC = () => {
|
|||||||
trackMixpanelEvent(evt_account_setup_visit);
|
trackMixpanelEvent(evt_account_setup_visit);
|
||||||
const verifyAuthStatus = async () => {
|
const verifyAuthStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const response = (await dispatch(verifyAuthentication()).unwrap())
|
const response = await dispatch(verifyAuthentication()).unwrap() as IAuthorizeResponse;
|
||||||
.payload as IAuthorizeResponse;
|
|
||||||
if (response?.authenticated) {
|
if (response?.authenticated) {
|
||||||
setSession(response.user);
|
setSession(response.user);
|
||||||
dispatch(setUser(response.user));
|
dispatch(setUser(response.user));
|
||||||
@@ -163,6 +163,18 @@ const AccountSetup: React.FC = () => {
|
|||||||
const res = await profileSettingsApiService.setupAccount(model);
|
const res = await profileSettingsApiService.setupAccount(model);
|
||||||
if (res.done && res.body.id) {
|
if (res.done && res.body.id) {
|
||||||
trackMixpanelEvent(skip ? evt_account_setup_skip_invite : evt_account_setup_complete);
|
trackMixpanelEvent(skip ? evt_account_setup_skip_invite : evt_account_setup_complete);
|
||||||
|
|
||||||
|
// Refresh user session to update setup_completed status
|
||||||
|
try {
|
||||||
|
const authResponse = await dispatch(verifyAuthentication()).unwrap() as IAuthorizeResponse;
|
||||||
|
if (authResponse?.authenticated && authResponse?.user) {
|
||||||
|
setSession(authResponse.user);
|
||||||
|
dispatch(setUser(authResponse.user));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to refresh user session after setup completion', error);
|
||||||
|
}
|
||||||
|
|
||||||
navigate(`/worklenz/projects/${res.body.id}?tab=tasks-list&pinned_tab=tasks-list`);
|
navigate(`/worklenz/projects/${res.body.id}?tab=tasks-list&pinned_tab=tasks-list`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user