import { Routes, Route, Link, useLocation, Navigate, useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; import { AppointmentsPage } from "./pages/Appointments.js"; import { ClientsPage } from "./pages/Clients.js"; import { ClientDetailPage } from "./pages/ClientDetailPage.js"; import { ServicesPage } from "./pages/Services.js"; import { MessagesPage } from "./pages/Messages.js"; import { StaffPage } from "./pages/Staff.js"; import { InvoicesPage } from "./pages/Invoices.js"; import { BookPage } from "./pages/Book.js"; import { ReportsPage } from "./pages/Reports.js"; import { GroupBookingPage } from "./pages/GroupBooking.js"; import { SettingsPage } from "./pages/Settings.js"; import { BookingConfirmedPage } from "./pages/BookingConfirmed.js"; import { BookingCancelledPage } from "./pages/BookingCancelled.js"; import { BookingErrorPage } from "./pages/BookingError.js"; import { SetupWizard } from "./pages/SetupWizard.tsx"; import { CustomerPortal } from "./portal/CustomerPortal.js"; import { DevLoginSelector, getDevUser } from "./pages/DevLoginSelector.js"; import { DevSessionIndicator } from "./components/DevSessionIndicator.js"; import { BrandingProvider, useBranding } from "./BrandingContext.js"; import { GlobalSearch } from "./components/GlobalSearch.js"; import { useSession, signIn, signOut } from "./lib/auth-client.js"; function LoginPage() { const [isLoading, setIsLoading] = useState(false); const [providers, setProviders] = useState([]); const [error, setError] = useState(null); useEffect(() => { fetch("/api/auth/providers") .then((r) => r.json()) .then((data) => setProviders(data.providers ?? [])) .catch(() => setProviders([])); const params = new URLSearchParams(window.location.search); const authError = params.get("error"); if (authError) setError(authError.replace(/_/g, " ")); }, []); const handleSocialLogin = async (provider: string) => { setIsLoading(true); setError(null); // Use /admin as callback URL so Better-Auth redirects to the app's dashboard // after the OAuth callback completes, rather than back to /login const callbackURL = `${window.location.origin}/admin`; const result = await signIn.social({ provider, callbackURL }); if (result?.error) { setError(result.error.message ?? "Sign-in failed"); setIsLoading(false); } }; const isGoogle = providers.includes("google"); const isGitHub = providers.includes("github"); const isAuthentik = providers.includes("authentik"); return (

GroomBook

Sign in to continue

{error && (
{error}
)} {isGoogle && ( )} {isGitHub && ( )} {isAuthentik && ( )}
); } const NAV_LINKS = [ { to: "/admin", label: "Appointments" }, { to: "/admin/messages", label: "Messages" }, { to: "/admin/clients", label: "Clients" }, { to: "/admin/services", label: "Services" }, { to: "/admin/staff", label: "Staff" }, { to: "/admin/invoices", label: "Invoices" }, { to: "/admin/group-bookings", label: "Group Bookings" }, { to: "/admin/reports", label: "Reports" }, { to: "/admin/settings", label: "Settings" }, { to: "/", label: "Customer Portal" }, ]; function AdminLayout() { const location = useLocation(); const navigate = useNavigate(); const { branding } = useBranding(); const logoSrc = branding.logoBase64 && branding.logoMimeType ? `data:${branding.logoMimeType};base64,${branding.logoBase64}` : null; return (
} /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } />
); } export function App() { const location = useLocation(); const [authDisabled, setAuthDisabled] = useState(null); const [needsSetup, setNeedsSetup] = useState(null); const { data: rawSession, isPending: rawSessionLoading } = useSession(); // In dev mode (authDisabled=true), session state is irrelevant - skip useSession result const session = authDisabled ? null : rawSession; const sessionLoading = authDisabled ? false : rawSessionLoading; useEffect(() => { fetch("/api/dev/config") .then((r) => r.json()) .then((data) => setAuthDisabled(data.authDisabled === true)) .catch(() => setAuthDisabled(false)); }, []); // After session is confirmed, check if setup is needed useEffect(() => { if (authDisabled === null || sessionLoading) return; // Skip if no authenticated session (will redirect to login or dev selector) if (!authDisabled && !session) return; if (authDisabled && !getDevUser()) return; fetch("/api/setup/status") .then((r) => r.json()) .then((data) => setNeedsSetup(data.needsSetup === true)) .catch(() => setNeedsSetup(false)); }, [authDisabled, session, sessionLoading]); // Public booking redirect pages — no auth or portal chrome needed if (location.pathname === "/booking/confirmed") { return ; } if (location.pathname === "/booking/cancelled") { return ; } if (location.pathname === "/booking/error") { return ; } // Setup wizard — standalone, no admin chrome if (location.pathname === "/setup") { return ( setNeedsSetup(false)} /> ); } // Still loading auth state or setup check (skip setup check in dev mode) if (authDisabled === null || sessionLoading) return null; // Dev mode: show login selector (no setup check needed in dev mode) if (authDisabled && location.pathname === "/login") { return ; } // Dev mode: use dev login selector (no setup check needed in dev mode) if (authDisabled && !getDevUser()) { return ; } // Show login BEFORE checking needsSetup (needsSetup is never set for unauthenticated users) if (!authDisabled && !session) { return ; } // Production: need setup check if (needsSetup === null) return null; // Redirect to setup wizard if needed if (needsSetup) { return ; } // Redirect authenticated users to /admin (but preserve impersonation flow via ?sessionId=) const searchParams = new URLSearchParams(location.search); if (!authDisabled && session && !location.pathname.startsWith("/admin") && !searchParams.has("sessionId")) { return ; } // Don't render portal chrome at /login — DevLoginSelector is shown instead const showCustomerPortal = !location.pathname.startsWith("/admin") && location.pathname !== "/login"; return ( {location.pathname.startsWith("/admin") ? ( <> } /> {authDisabled && } ) : showCustomerPortal ? ( <> {authDisabled && } ) : null} ); }