feat(frontend): OIDC login + runtime env (FABRIC_OIDC_ONLY/FIX_TO_CENTER)
- 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>
This commit is contained in:
66
src/pages/OidcCallback.tsx
Normal file
66
src/pages/OidcCallback.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user