From 13113b8c4674cd767c94798875471f64b11e656d Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 26 Apr 2026 18:00:57 +0300 Subject: [PATCH] web: AuthGate consolidates three "not authenticated" cards Create AuthGate component as the single source of truth for the unauthenticated UI. Cold renders show a manual sign-in button (IV7); mid-session expiry auto-redirects via useEffect (IV7); auth_error shows the distinct error card with a "Try again" button and never auto-retries (IV6). Swap the inline AuthError cards at index, projects, and SavedSearchesSection call sites. --- internal/server/web/dist/index.html | 2 +- .../settings/SavedSearchesSection.tsx | 54 ++++----- web/src/routes/index.tsx | 37 +++--- web/src/routes/projects.tsx | 33 +++--- web/src/shell/AuthGate.tsx | 110 ++++++++++++++++++ 5 files changed, 161 insertions(+), 75 deletions(-) create mode 100644 web/src/shell/AuthGate.tsx diff --git a/internal/server/web/dist/index.html b/internal/server/web/dist/index.html index 5693357865b9c5482826a8ea3df24b4ef8841d68..5c62577e34fb15ec388e2aefb2c1b0225c33deda 100644 --- a/internal/server/web/dist/index.html +++ b/internal/server/web/dist/index.html @@ -13,7 +13,7 @@ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" /> - + diff --git a/web/src/features/settings/SavedSearchesSection.tsx b/web/src/features/settings/SavedSearchesSection.tsx index f30e2bd62eac4b99b6d70c55f07b82902e237b84..26f4e95d2434fef1e30721577452df996cdf5bb3 100644 --- a/web/src/features/settings/SavedSearchesSection.tsx +++ b/web/src/features/settings/SavedSearchesSection.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import { EmptyState, Sub } from '../../primitives' +import { AuthGate } from '../../shell/AuthGate' import { AuthError, APIError } from '../../api/client' import type { SavedSearch } from '../../api/adapters' import { @@ -175,20 +176,7 @@ export function SavedSearchesSection(): React.JSX.Element { return loading… } - if (error != null) { - if (error instanceof AuthError) { - return ( -
-
-
not authenticated
-
Sign in to manage your saved searches.
-
-
- ) - } + if (error != null && !(error instanceof AuthError)) { const detail = error instanceof APIError ? error.message : String(error) return (
-
Saved searches
- - {data != null && data.length === 0 ? ( - - ) : ( -
-
- Name - Query - Updated - + +
+
Saved searches
+ + {data != null && data.length === 0 ? ( + + ) : ( +
+
+ Name + Query + Updated + +
+ {data?.map((row) => ( + + ))}
- {data?.map((row) => ( - - ))} -
- )} -
+ )} +
+ ) } diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index b5d75d19b4e255f633fd4c1ac7ee35e158623436..19a0d1fd12b3149020a0bb19863c2ad86a136164 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useCallback } from 'react' import { createFileRoute, useNavigate } from '@tanstack/react-router' import { SubBar } from '../shell/SubBar' import { Sub } from '../primitives' +import { AuthGate } from '../shell/AuthGate' import { FilterChips } from '../features/home/FilterChips' import { SessionsTable } from '../features/home/SessionsTable' import { useSessions } from '../features/home/useSessions' @@ -86,17 +87,7 @@ function HomeRoute(): React.JSX.Element { ) } - if (error != null) { - if (error instanceof AuthError) { - return ( -
-
-
not authenticated
-
Sign in to view your sessions.
-
-
- ) - } + if (error != null && !(error instanceof AuthError)) { const detail = error instanceof APIError ? error.message : String(error) return (
@@ -109,16 +100,18 @@ function HomeRoute(): React.JSX.Element { } return ( - <> - - - - - + + <> + + + + + + ) } diff --git a/web/src/routes/projects.tsx b/web/src/routes/projects.tsx index 091624c16a478e730228196c62424e30b07bcd13..5f13f42111cba1638a60fcd5f25267c446c4e181 100644 --- a/web/src/routes/projects.tsx +++ b/web/src/routes/projects.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useCallback } from 'react' import { createFileRoute, useNavigate } from '@tanstack/react-router' import { SubBar } from '../shell/SubBar' import { Sub } from '../primitives' +import { AuthGate } from '../shell/AuthGate' import { ProjectsTable } from '../features/projects/ProjectsTable' import { useProjects } from '../features/projects/useProjects' import { useProjectsCursor } from '../features/projects/useProjectsCursor' @@ -115,17 +116,7 @@ function ProjectsRoute(): React.JSX.Element { ) } - if (error != null) { - if (error instanceof AuthError) { - return ( -
-
-
not authenticated
-
Sign in to view your projects.
-
-
- ) - } + if (error != null && !(error instanceof AuthError)) { const detail = error instanceof APIError ? error.message : String(error) return (
@@ -138,14 +129,16 @@ function ProjectsRoute(): React.JSX.Element { } return ( - <> - {subBar} - - + + <> + {subBar} + + + ) } diff --git a/web/src/shell/AuthGate.tsx b/web/src/shell/AuthGate.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d1945b13916f7d0ce730677349a52078b4672735 --- /dev/null +++ b/web/src/shell/AuthGate.tsx @@ -0,0 +1,110 @@ +import React, { useEffect } from 'react' +import { useAuth } from '../lib/authContext' + +// ── AuthGate ────────────────────────────────────────────────────────────────── +// +// Single source of truth for the "not authenticated" UI (IV6, IV7). +// +// - authenticated → renders children normally. +// - unauthenticated (cold, hasBeenAuthenticated === false) +// → shows a manual "Sign in with OIDC" card (IV7). +// - unauthenticated (mid-session, hasBeenAuthenticated === true) +// → auto-redirects via useEffect, shows a placeholder (IV7). +// - auth_error → shows the distinct error card with a manual "Try again" +// button (IV6). Never auto-retries. + +export function AuthGate({ children }: { children: React.ReactNode }): React.JSX.Element { + const { state, signIn } = useAuth() + const { status, hasBeenAuthenticated, error } = state + + // Mid-session expiry: auto-redirect on mount only (IV7). + useEffect(() => { + if (status === 'unauthenticated' && hasBeenAuthenticated) { + signIn(window.location.pathname) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [status, hasBeenAuthenticated]) // intentionally omits signIn (stable callback) + + if (status === 'authenticated') { + return <>{children} + } + + if (status === 'auth_error') { + return ( +
+
+
couldn't sign you in
+
+ {error ?? 'An unknown error occurred.'} +
+ +
+
+ ) + } + + // status === 'unauthenticated' + if (hasBeenAuthenticated) { + // Mid-session expiry — auto-redirect is in progress (useEffect above). + return ( +
+
+
session expired
+
Redirecting to sign in…
+
+
+ ) + } + + // Cold first render — show manual sign-in button (IV7). + return ( +
+
+
not authenticated
+
+ Sign in to continue. +
+ +
+
+ ) +}