fix(portal): redirect unauthenticated users to login — never show portal chrome (GRO-309)

- CustomerPortal.tsx: add initComplete state to track async session
  initialization. After init completes with no valid session, redirect:
  staff dev users → /admin, all others → /login
- Dashboard.tsx: change !sessionId fallback from dead-end UI to
  <Navigate to="/login" replace /> (defense-in-depth)
- All 85 web unit tests pass

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Barkley Trimsworth
2026-03-31 00:53:59 +00:00
parent 026a2c8b0e
commit 5860d822cf
2 changed files with 21 additions and 10 deletions
+19 -3
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 { useSearchParams, Navigate } from "react-router-dom";
import { import {
Home, Calendar, PawPrint, FileText, CreditCard, MessageSquare, Home, Calendar, PawPrint, FileText, CreditCard, MessageSquare,
Settings, LogOut, Shield, Settings, LogOut, Shield,
@@ -38,6 +38,7 @@ export function CustomerPortal() {
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 [clientName, setClientName] = useState<string>(""); const [clientName, setClientName] = useState<string>("");
const [initComplete, setInitComplete] = useState(false);
const { branding } = useBranding(); const { branding } = useBranding();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@@ -68,7 +69,8 @@ export function CustomerPortal() {
}) })
.catch(() => { .catch(() => {
setSearchParams({}, { replace: true }); setSearchParams({}, { replace: true });
}); })
.finally(() => setInitComplete(true));
return; return;
} }
@@ -90,7 +92,11 @@ export function CustomerPortal() {
setClientName(devUser.name); setClientName(devUser.name);
} }
}) })
.catch(() => {}); .catch(() => {})
.finally(() => setInitComplete(true));
} else {
// No valid session: staff dev users and unauthenticated users fall through here
setInitComplete(true);
} }
}, []); }, []);
@@ -168,6 +174,16 @@ export function CustomerPortal() {
const avatarInitials = (clientName.split(" ")[0] || "G").charAt(0).toUpperCase(); const avatarInitials = (clientName.split(" ")[0] || "G").charAt(0).toUpperCase();
// After init completes, redirect unauthenticated users to /login and staff to /admin
// The portal chrome must NEVER be visible to users without a valid client session
if (initComplete && !session) {
const devUser = getDevUser();
if (devUser && devUser.type === "staff") {
return <Navigate to="/admin" replace />;
}
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"
+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();