- Runtime container env injected by docker/entrypoint.sh -> runtime-env.js (loaded before the bundle); src/lib/runtime-env.ts reads it. FABRIC_OIDC_ONLY hides the password form; FIX_TO_CENTER pins the Center base and hides its input. Dockerfile ENTRYPOINT + ENV defaults. - LoginPage: 'Sign in with SSO' when /auth/oidc/status enabled; password form gated by OIDC_ONLY; center input gated by FIX_TO_CENTER. - /oidc route (OidcCallback) redeems the fragment ticket via /auth/oidc/exchange and adopts the session (AuthContext.adoptSession). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { useAuth } from '../auth/auth-context'
|
|
import { APP_NAME } from '../lib/brand'
|
|
import { resolveCenterBase } from '../lib/runtime-env'
|
|
import { oidcExchangeCenter } from '../lib/center-auth-client'
|
|
|
|
// Landing route for the OIDC redirect: Center bounces the browser here
|
|
// with #oidc_ticket=... (or #oidc_error=...). We redeem the one-time
|
|
// ticket for a full session and adopt it.
|
|
export default function OidcCallback() {
|
|
const navigate = useNavigate()
|
|
const { adoptSession } = useAuth()
|
|
const [error, setError] = useState('')
|
|
const ran = useRef(false)
|
|
|
|
useEffect(() => {
|
|
if (ran.current) return
|
|
ran.current = true
|
|
|
|
const hash = window.location.hash.replace(/^#/, '')
|
|
const params = new URLSearchParams(hash)
|
|
const ticket = params.get('oidc_ticket')
|
|
const errParam = params.get('oidc_error')
|
|
// scrub the fragment so the ticket isn't left in the URL/history
|
|
window.history.replaceState(null, '', window.location.pathname)
|
|
|
|
if (errParam) {
|
|
setError(decodeURIComponent(errParam))
|
|
return
|
|
}
|
|
if (!ticket) {
|
|
setError('Missing OIDC ticket.')
|
|
return
|
|
}
|
|
|
|
oidcExchangeCenter(resolveCenterBase(), ticket)
|
|
.then((next) => {
|
|
adoptSession(next)
|
|
navigate('/workspace', { replace: true })
|
|
})
|
|
.catch(() => setError('SSO sign-in failed. The ticket may have expired — please try again.'))
|
|
}, [adoptSession, navigate])
|
|
|
|
return (
|
|
<div className="login-screen">
|
|
<div className="login-card">
|
|
<div className="brand-wordmark">{APP_NAME}</div>
|
|
{error ? (
|
|
<>
|
|
<h1>Sign-in failed</h1>
|
|
<p className="error-text" style={{ marginBottom: 16 }}>{error}</p>
|
|
<button className="btn" type="button" onClick={() => navigate('/login', { replace: true })}>
|
|
Back to sign in
|
|
</button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<h1>Signing you in…</h1>
|
|
<p className="login-sub">Completing SSO authentication.</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|