From b0ab41bb4e18128d531db14ced5d746dabf06e45 Mon Sep 17 00:00:00 2001 From: Barkley Trimsworth Date: Mon, 30 Mar 2026 19:12:38 +0000 Subject: [PATCH] fix(portal): redirect unauthenticated users to /login CustomerPortal now redirects to /login after session init completes with no valid session, preventing portal chrome from rendering for unauthenticated users. Dashboard !sessionId branch uses Navigate redirect instead of dead-end UI. Staff redirect in App.tsx verified. Co-Authored-By: Paperclip --- apps/web/src/App.tsx | 5 ++++ apps/web/src/portal/CustomerPortal.tsx | 31 +++++++++++++++++++--- apps/web/src/portal/sections/Dashboard.tsx | 9 ++----- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index a8f7b2a..c8301c1 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -249,6 +249,11 @@ export function App() { return ; } + // Dev mode: staff users should not land on the customer portal — redirect to admin + if (authDisabled && getDevUser()?.type === "staff") { + return ; + } + // Show login BEFORE checking needsSetup (needsSetup is never set for unauthenticated users) if (!authDisabled && !session) { return ; diff --git a/apps/web/src/portal/CustomerPortal.tsx b/apps/web/src/portal/CustomerPortal.tsx index 80d4e5b..f3d567a 100644 --- a/apps/web/src/portal/CustomerPortal.tsx +++ b/apps/web/src/portal/CustomerPortal.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect, useRef } from "react"; -import { useSearchParams } from "react-router-dom"; +import { Navigate, useSearchParams } from "react-router-dom"; import { Home, Calendar, PawPrint, FileText, CreditCard, MessageSquare, Settings, LogOut, Shield, @@ -37,6 +37,8 @@ export function CustomerPortal() { const [rescheduleAppointment, setRescheduleAppointment] = useState | null>(null); const [session, setSession] = useState(null); const [sessionExtended, setSessionExtended] = useState(false); + const [sessionError, setSessionError] = useState(null); + const [isInitializing, setIsInitializing] = useState(true); const [clientName, setClientName] = useState(""); const { branding } = useBranding(); const [searchParams, setSearchParams] = useSearchParams(); @@ -68,7 +70,8 @@ export function CustomerPortal() { }) .catch(() => { setSearchParams({}, { replace: true }); - }); + }) + .finally(() => setIsInitializing(false)); return; } @@ -81,16 +84,26 @@ export function CustomerPortal() { body: JSON.stringify({ clientId: devUser.id }), }) .then((r) => { - if (!r.ok) return null; + if (!r.ok) { + setSessionError("Failed to create portal session. Please try again."); + return null; + } return r.json() as Promise; }) .then((s) => { if (s && s.id) { setSession(s); setClientName(devUser.name); + setSessionError(null); } }) - .catch(() => {}); + .catch(() => { + setSessionError("Failed to connect. Please check your connection and try again."); + }) + .finally(() => setIsInitializing(false)); + } else { + // No sessionId param and no dev user — init is complete with no session + setIsInitializing(false); } }, []); @@ -168,6 +181,11 @@ export function CustomerPortal() { const avatarInitials = (clientName.split(" ")[0] || "G").charAt(0).toUpperCase(); + // Redirect to login if init is complete and no valid session exists + if (!isInitializing && !session) { + return ; + } + return (
+ {sessionError && ( +
+

{sessionError}

+
+ )} {renderSection()}
diff --git a/apps/web/src/portal/sections/Dashboard.tsx b/apps/web/src/portal/sections/Dashboard.tsx index 43abe5c..f252e8a 100644 --- a/apps/web/src/portal/sections/Dashboard.tsx +++ b/apps/web/src/portal/sections/Dashboard.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { Navigate } from "react-router-dom"; import { Calendar, Clock, PawPrint, CreditCard, Star, ChevronRight, AlertTriangle } from "lucide-react"; interface DashboardProps { @@ -183,13 +184,7 @@ export function Dashboard({ } if (!sessionId) { - return ( -
-
-

Please sign in to view your dashboard.

-
-
- ); + return ; } const upcomingAppointments = getUpcomingAppointments();