+
)
}
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 @@ >
)
}
- if (error != null) {
- if (error instanceof AuthError) {
- return (
-
- )
- }
+ if (error != null && !(error instanceof AuthError)) {
const detail = error instanceof APIError ? error.message : String(error)
return (
@@ -109,16 +100,18 @@ )
}
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 @@ >
)
}
- 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 @@ )
}
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.
+
+
+
+
+ )
+}