fix(ci): Job names + test(e2e): add E2E test suite for portal auth regressions #189

Closed
groombook-engineer[bot] wants to merge 6 commits from fix/gro-300-dev-client-portal-auth into main
3 changed files with 34 additions and 11 deletions
Showing only changes of commit b0ab41bb4e - Show all commits
+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();