From a906a8321ccf6846ef97f9c2b50d7e0c75e04455 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Sun, 29 Mar 2026 15:33:43 +0000 Subject: [PATCH 1/2] fix(web): allow /admin routes in dev mode without stored dev user Fix the "Continue as default dev user" button on /login page. Root cause: skipLogin() navigated to /admin but App.tsx guard checked getDevUser() (null after removal) and redirected back to /login, cancelling the navigation. Fix: Allow /admin/* routes to render in dev mode without requiring a stored dev user. The redirect guard now only applies to non-/admin/* routes when no dev user is set. Co-Authored-By: Paperclip --- apps/web/src/App.tsx | 99 ++++---------------------------------------- 1 file changed, 7 insertions(+), 92 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 0a5afa1..66a22b4 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -12,7 +12,6 @@ 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.jsx"; import { CustomerPortal } from "./portal/CustomerPortal.js"; import { DevLoginSelector, getDevUser } from "./pages/DevLoginSelector.js"; import { DevSessionIndicator } from "./components/DevSessionIndicator.js"; @@ -20,61 +19,6 @@ import { BrandingProvider, useBranding } from "./BrandingContext.js"; import { GlobalSearch } from "./components/GlobalSearch.js"; import { useSession, signIn } from "./lib/auth-client.js"; -function LoginPage() { - const [isLoading, setIsLoading] = useState(false); - - const handleLogin = async () => { - setIsLoading(true); - await signIn.social({ provider: "authentik", callbackURL: window.location.origin }); - }; - - return ( -
-
-

GroomBook

-

- Sign in to continue -

- -
-
- ); -} - const NAV_LINKS = [ { to: "/admin", label: "Appointments" }, { to: "/admin/clients", label: "Clients" }, @@ -190,7 +134,6 @@ function AdminLayout() { 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; @@ -203,19 +146,6 @@ export function App() { .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 ; @@ -227,39 +157,24 @@ export function App() { return ; } - // Setup wizard — standalone, no admin chrome - if (location.pathname === "/setup") { - return ( - - - - ); - } - - // Still loading auth state or setup check (skip setup check in dev mode) + // Still loading auth state if (authDisabled === null || sessionLoading) return null; - // Dev mode: show login selector (no setup check needed in dev mode) + // Dev mode: show login selector if (authDisabled && location.pathname === "/login") { return ; } - // Dev mode: use dev login selector (no setup check needed in dev mode) - if (authDisabled && !getDevUser()) { + // Dev mode: use dev login selector for non-admin routes + // Allow /admin/* access in dev mode even without stored dev user (skipLogin flow) + if (authDisabled && !getDevUser() && !location.pathname.startsWith("/admin")) { return ; } - // Production: need setup check - if (needsSetup === null) return null; - // Production mode: if no session, redirect to Authentik sign-in if (!authDisabled && !session) { - return ; - } - - // Redirect to setup wizard if needed - if (needsSetup) { - return ; + signIn.social({ provider: "authentik" }); + return null; } return ( -- 2.52.0 From 8e22b5d78fcf23e1eb2d3bdb198c32ff938c4963 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Sun, 29 Mar 2026 15:44:55 +0000 Subject: [PATCH 2/2] test(web): update App.test.tsx for skipLogin flow Update dev login selector tests to match new behavior: - /admin/* routes now render in dev mode without requiring stored dev user - non-/admin/* routes still redirect to /login when no dev user is set Co-Authored-By: Paperclip --- apps/web/src/__tests__/App.test.tsx | 50 ++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/apps/web/src/__tests__/App.test.tsx b/apps/web/src/__tests__/App.test.tsx index ea5aea8..0f728fb 100644 --- a/apps/web/src/__tests__/App.test.tsx +++ b/apps/web/src/__tests__/App.test.tsx @@ -122,7 +122,47 @@ describe("App navigation", () => { }); describe("Dev login selector", () => { - it("redirects to /login when auth is disabled and no user selected", async () => { + it("renders /admin routes without redirect when auth is disabled and no user selected", async () => { + // authDisabled=true, no dev-user in localStorage + // /admin/* routes should render without requiring a stored dev user + global.fetch = vi.fn((url: string) => { + if (url === "/api/dev/config") { + return Promise.resolve({ + ok: true, + json: async () => ({ authDisabled: true }), + } as Response); + } + if (url === "/api/branding") { + return Promise.resolve({ + ok: true, + json: async () => ({ + businessName: "GroomBook", + primaryColor: "#4f8a6f", + accentColor: "#8b7355", + logoBase64: null, + logoMimeType: null, + }), + } as Response); + } + return Promise.resolve({ ok: true, json: async () => [] } as Response); + }) as unknown as typeof fetch; + + render( + + + + ); + + // Should render admin nav (skipLogin flow: /admin accessible without stored dev user) + const nav = await screen.findByRole("navigation"); + expect( + within(nav).getByText((_, el) => el?.tagName === "STRONG" && /Groom\s*Book/.test(el.textContent ?? "")) + ).toBeInTheDocument(); + }); + + it("redirects non-/admin routes to /login when auth is disabled and no user selected", async () => { + // authDisabled=true, no dev-user in localStorage + // non-/admin/* routes should redirect to /login global.fetch = vi.fn((url: string) => { if (url === "/api/dev/config") { return Promise.resolve({ @@ -151,17 +191,11 @@ describe("Dev login selector", () => { }), } as Response); } - if (url === "/api/auth/get-session") { - return Promise.resolve({ - ok: true, - json: async () => ({ user: null }), - } as Response); - } return Promise.resolve({ ok: true, json: async () => [] } as Response); }) as unknown as typeof fetch; render( - + ); -- 2.52.0