feat: enhance auth guard

This commit is contained in:
Nathnael
2026-06-11 08:22:39 +00:00
parent 2cd585a3ac
commit 16f506062a
2 changed files with 140 additions and 91 deletions

View File

@@ -8,8 +8,9 @@ import {
Settings,
User,
} from "lucide-react";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import {
Navigate,
Outlet,
Route,
Routes,
@@ -18,17 +19,6 @@ import {
} from "react-router-dom";
import useAuth from "./hooks/useAuth";
function LogoutHandler() {
const { logout } = useAuth();
const navigate = useNavigate();
useEffect(() => {
logout().then(() => navigate("/login", { replace: true }));
}, []);
return null;
}
import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
import MyPortalPage from "./pages/MyPortalPage";
import ProfilePage from "./pages/ProfilePage";
@@ -47,8 +37,82 @@ import NewBookingPage from "./pages/bookings/NewBookingPage";
import CheckPaymentPage from "./pages/payments/CheckPaymentPage";
import TrackingPage from "./pages/tracking/TrackingPage";
function FullScreenSpinner() {
return (
<div className="flex items-center justify-center h-screen">
<Loader2 className="animate-spin text-primary" />
</div>
);
}
function LogoutHandler() {
const { logout } = useAuth();
const navigate = useNavigate();
const hasRun = useRef(false);
useEffect(() => {
if (hasRun.current) return;
hasRun.current = true;
logout().then(() => navigate("/login", { replace: true }));
}, []);
return <FullScreenSpinner />;
}
/** Blocks unauthenticated users; renders children only with a valid session. */
function RequireAuth() {
const { isPending, isAuthenticated } = useAuth();
const location = useLocation();
if (isPending) return <FullScreenSpinner />;
if (!isAuthenticated)
return <Navigate to="/login" replace state={{ from: location }} />;
return <Outlet />;
}
/**
* Sends authenticated users without a company to onboarding.
* Only redirects on a confirmed "no company" response — never on a
* transient query error.
*/
function RequireCompany() {
const { customerQuery } = useAuth();
if (customerQuery.isPending) return <FullScreenSpinner />;
if (customerQuery.isSuccess && !customerQuery.data)
return <Navigate to="/onboarding" replace />;
return <Outlet />;
}
/** Keeps already-onboarded users out of the onboarding flow. */
function RequireNoCompany() {
const { customerQuery } = useAuth();
if (customerQuery.isPending) return <FullScreenSpinner />;
if (customerQuery.data) return <Navigate to="/portal" replace />;
return <Outlet />;
}
/** Keeps authenticated users off the login/signup pages. */
function RedirectIfAuthed() {
const { isPending, isAuthenticated } = useAuth();
if (isPending) return <FullScreenSpinner />;
if (isAuthenticated) return <Navigate to="/portal" replace />;
return <Outlet />;
}
/** Landing page for visitors; authenticated users go straight to the portal. */
function LandingRoute() {
const { isPending, isAuthenticated } = useAuth();
if (isPending) return <FullScreenSpinner />;
if (isAuthenticated) return <Navigate to="/portal" replace />;
return <EDRFreightLandingPage />;
}
const sidebarItems: SidebarItem[] = [
{ label: "Home", href: "/portal", icon: <Home size={18} /> },
{ label: "Home", href: "/portal", icon: <Home size={18} /> },
{
label: "My Bookings",
href: "/bookings",
@@ -81,79 +145,70 @@ const sidebarItems: SidebarItem[] = [
const App = () => {
const navigate = useNavigate();
const location = useLocation();
const { user, isPending, customer, customerQuery } = useAuth();
useEffect(() => {
if (isPending || customerQuery.isPending) return;
const isInProtectedRoutes = sidebarItems.find((item) =>
location.pathname.startsWith(item.href),
);
console.log({ isInProtectedRoutes, location });
if (!user) {
if (isInProtectedRoutes) return navigate("/login");
return;
}
if (user && location.pathname === "/") navigate("/portal");
else if (!customer && !!isInProtectedRoutes) navigate("/onboarding");
}, [user, location, customer, customerQuery.isPending]);
if (isPending) {
return (
<div className="flex items-center justify-center h-screen">
<Loader2 className="animate-spin text-primary" />
</div>
);
}
const { user } = useAuth();
const displayName = user?.name?.en || user?.username || user?.email || "User";
const userEmail = user?.email;
return (
<Routes>
<Route>
<Route index element={<EDRFreightLandingPage />} />
<Route path="/logout" element={<LogoutHandler />} />
{/* Public routes */}
<Route index element={<LandingRoute />} />
<Route path="/logout" element={<LogoutHandler />} />
<Route
path="/booking/check-status/:orderId"
element={<CheckPaymentPage />}
/>
{/* Auth pages — inaccessible once logged in */}
<Route element={<RedirectIfAuthed />}>
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
<Route path="/otp" element={<VerificationOtpPage />} />
<Route path="/set-password" element={<SetPasswordPage />} />
<Route path="/onboarding" element={<OnboardingPage />} />
<Route
path="/booking/check-status/:orderId"
element={<CheckPaymentPage />}
/>
</Route>
<Route
element={
<AppLayout
title="EDR Freight"
sidebarItems={sidebarItems}
activeHref={location.pathname}
onNavigate={navigate}
enableThemeToggle
userName={displayName}
userEmail={userEmail}
{/* Signup-flow pages; reached while a session already exists */}
<Route path="/otp" element={<VerificationOtpPage />} />
<Route path="/set-password" element={<SetPasswordPage />} />
<Route element={<RequireAuth />}>
<Route element={<RequireNoCompany />}>
<Route path="/onboarding" element={<OnboardingPage />} />
</Route>
<Route element={<RequireCompany />}>
<Route
element={
<AppLayout
title="EDR Freight"
sidebarItems={sidebarItems}
activeHref={location.pathname}
onNavigate={navigate}
enableThemeToggle
userName={displayName}
userEmail={userEmail}
>
<Outlet />
</AppLayout>
}
>
<Outlet />
</AppLayout>
}
>
<Route path="/portal" element={<MyPortalPage />} />
<Route path="/bookings" element={<MyBookings />} />
<Route path="/bookings/new" element={<NewBookingPage />} />
<Route path="/bookings/:id/edit" element={<EditBookingPage />} />
<Route path="/bookings/:id" element={<BookingDetailPage />} />
<Route
path="/bookings/:id/contract"
element={<BookingContractPage />}
/>
<Route path="/tracking" element={<TrackingPage />} />
<Route path="/billing" element={<BillingPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/portal" element={<MyPortalPage />} />
<Route path="/bookings" element={<MyBookings />} />
<Route path="/bookings/new" element={<NewBookingPage />} />
<Route path="/bookings/:id/edit" element={<EditBookingPage />} />
<Route path="/bookings/:id" element={<BookingDetailPage />} />
<Route
path="/bookings/:id/contract"
element={<BookingContractPage />}
/>
<Route path="/tracking" element={<TrackingPage />} />
<Route path="/billing" element={<BillingPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/settings" element={<SettingsPage />} />
</Route>
</Route>
</Route>
{/* <Route path="*" element={<Navigate to="/" replace />} /> */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
};

View File

@@ -47,21 +47,14 @@ const useAuth = () => {
);
useEffect(() => {
console.log({
user: authQuery.data,
company: companyQuery.data,
isCompany: !!companyQuery.data,
isUserPending: authQuery.isPending,
isCompanyPending: companyQuery.isPending,
});
}, [
authQuery.data,
companyQuery.data,
authQuery.isPending,
companyQuery.isPending,
]);
if (authQuery.isError) {
queryClient.clear();
localStorage.clear();
}
}, [authQuery.isError, queryClient]);
const isPending = authQuery.isPending && hasToken;
const isAuthenticated = hasToken && !!authQuery.data && !authQuery.isError;
const login = async (
payload: LoginPayload,
@@ -183,9 +176,10 @@ const useAuth = () => {
return {
isPending,
user: authQuery.data ?? null,
company: companyQuery.data ?? null,
customer: companyQuery.data ?? null,
isAuthenticated,
user: isAuthenticated ? (authQuery.data ?? null) : null,
company: isAuthenticated ? (companyQuery.data ?? null) : null,
customer: isAuthenticated ? (companyQuery.data ?? null) : null,
login,
signup,
setPassword,