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 <noreply@paperclip.ing>
This commit is contained in:
Barkley Trimsworth
2026-03-30 19:12:38 +00:00
parent 01cb8d87a0
commit b0ab41bb4e
3 changed files with 34 additions and 11 deletions
+5
View File
@@ -249,6 +249,11 @@ export function App() {
return <Navigate to="/login" replace />; return <Navigate to="/login" replace />;
} }
// Dev mode: staff users should not land on the customer portal — redirect to admin
if (authDisabled && getDevUser()?.type === "staff") {
return <Navigate to="/admin" replace />;
}
// Show login BEFORE checking needsSetup (needsSetup is never set for unauthenticated users) // Show login BEFORE checking needsSetup (needsSetup is never set for unauthenticated users)
if (!authDisabled && !session) { if (!authDisabled && !session) {
return <LoginPage />; return <LoginPage />;
+27 -4
View File
@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef } from "react";
import { useSearchParams } from "react-router-dom"; import { Navigate, useSearchParams } from "react-router-dom";
import { import {
Home, Calendar, PawPrint, FileText, CreditCard, MessageSquare, Home, Calendar, PawPrint, FileText, CreditCard, MessageSquare,
Settings, LogOut, Shield, Settings, LogOut, Shield,
@@ -37,6 +37,8 @@ export function CustomerPortal() {
const [rescheduleAppointment, setRescheduleAppointment] = useState<Record<string, unknown> | null>(null); const [rescheduleAppointment, setRescheduleAppointment] = useState<Record<string, unknown> | null>(null);
const [session, setSession] = useState<ImpersonationSession | null>(null); const [session, setSession] = useState<ImpersonationSession | null>(null);
const [sessionExtended, setSessionExtended] = useState(false); const [sessionExtended, setSessionExtended] = useState(false);
const [sessionError, setSessionError] = useState<string | null>(null);
const [isInitializing, setIsInitializing] = useState(true);
const [clientName, setClientName] = useState<string>(""); const [clientName, setClientName] = useState<string>("");
const { branding } = useBranding(); const { branding } = useBranding();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@@ -68,7 +70,8 @@ export function CustomerPortal() {
}) })
.catch(() => { .catch(() => {
setSearchParams({}, { replace: true }); setSearchParams({}, { replace: true });
}); })
.finally(() => setIsInitializing(false));
return; return;
} }
@@ -81,16 +84,26 @@ export function CustomerPortal() {
body: JSON.stringify({ clientId: devUser.id }), body: JSON.stringify({ clientId: devUser.id }),
}) })
.then((r) => { .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<ImpersonationSession>; return r.json() as Promise<ImpersonationSession>;
}) })
.then((s) => { .then((s) => {
if (s && s.id) { if (s && s.id) {
setSession(s); setSession(s);
setClientName(devUser.name); 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(); 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 <Navigate to="/login" replace />;
}
return ( return (
<div <div
className="min-h-screen bg-[#faf8f5] font-sans" className="min-h-screen bg-[#faf8f5] font-sans"
@@ -314,6 +332,11 @@ export function CustomerPortal() {
</div> </div>
</div> </div>
<div className="p-4 md:p-8 max-w-6xl"> <div className="p-4 md:p-8 max-w-6xl">
{sessionError && (
<div className="bg-red-50 border border-red-200 rounded-2xl p-4 mb-6">
<p className="text-red-700 text-sm">{sessionError}</p>
</div>
)}
{renderSection()} {renderSection()}
</div> </div>
</main> </main>
+2 -7
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Navigate } from "react-router-dom";
import { Calendar, Clock, PawPrint, CreditCard, Star, ChevronRight, AlertTriangle } from "lucide-react"; import { Calendar, Clock, PawPrint, CreditCard, Star, ChevronRight, AlertTriangle } from "lucide-react";
interface DashboardProps { interface DashboardProps {
@@ -183,13 +184,7 @@ export function Dashboard({
} }
if (!sessionId) { if (!sessionId) {
return ( return <Navigate to="/login" replace />;
<div className="space-y-6">
<div className="bg-stone-100 rounded-2xl p-5 text-center">
<p className="text-stone-600">Please sign in to view your dashboard.</p>
</div>
</div>
);
} }
const upcomingAppointments = getUpcomingAppointments(); const upcomingAppointments = getUpcomingAppointments();