mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
feat: enhance auth guard
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user