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 />;
}
// 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)
if (!authDisabled && !session) {
return <LoginPage />;
+27 -4
View File
@@ -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<Record<string, unknown> | null>(null);
const [session, setSession] = useState<ImpersonationSession | null>(null);
const [sessionExtended, setSessionExtended] = useState(false);
const [sessionError, setSessionError] = useState<string | null>(null);
const [isInitializing, setIsInitializing] = useState(true);
const [clientName, setClientName] = useState<string>("");
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<ImpersonationSession>;
})
.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 <Navigate to="/login" replace />;
}
return (
<div
className="min-h-screen bg-[#faf8f5] font-sans"
@@ -314,6 +332,11 @@ export function CustomerPortal() {
</div>
</div>
<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()}
</div>
</main>
+2 -7
View File
@@ -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 (
<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>
);
return <Navigate to="/login" replace />;
}
const upcomingAppointments = getUpcomingAppointments();