Files
Fabric.Frontend/src/auth/AuthContext.tsx
hzhang 92d3b4dc1b 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>
2026-05-18 09:44:49 +01:00

102 lines
3.6 KiB
TypeScript

import { useMemo, useState } from 'react'
import type { PropsWithChildren } from 'react'
import { clearAuthSession, getAuthSession, isAccessTokenStale, setAuthSession } from '../lib/auth-storage'
import type { AuthSession } from '../lib/auth-storage'
import { loginCenter, logoutCenter, meGuildsCenter, refreshCenter, updateMeNameCenter } from '../lib/center-auth-client'
import { AuthContext } from './auth-context'
import type { AuthContextValue } from './auth-context'
export function AuthProvider({ children }: PropsWithChildren) {
const [session, setSession] = useState<AuthSession | null>(getAuthSession())
const value = useMemo<AuthContextValue>(
() => ({
session,
isAuthed: !!session,
login: async (centerApiBase: string, email: string, password: string) => {
const next = await loginCenter(centerApiBase, { email, password })
setAuthSession(next)
setSession(next)
},
// Adopt a session obtained out-of-band (OIDC ticket exchange).
adoptSession: (next: AuthSession) => {
setAuthSession(next)
setSession(next)
},
logout: async () => {
if (session?.refreshToken) {
try {
await logoutCenter(session.centerApiBase, session.refreshToken)
} catch {
// noop
}
}
clearAuthSession()
setSession(null)
},
ensureFreshToken: async () => {
if (!session) return null
if (!isAccessTokenStale(session.accessToken)) return session.accessToken
const refreshed = await refreshCenter(session.centerApiBase, session.refreshToken)
const next: AuthSession = {
...session,
accessToken: refreshed.accessToken,
refreshToken: refreshed.refreshToken,
tokenType: refreshed.tokenType,
expiresIn: refreshed.expiresIn,
}
setAuthSession(next)
setSession(next)
return next.accessToken
},
refreshGuilds: async () => {
if (!session) return
let accessToken = session.accessToken
let refreshToken = session.refreshToken
let tokenType = session.tokenType
let expiresIn = session.expiresIn
if (isAccessTokenStale(session.accessToken)) {
const refreshed = await refreshCenter(session.centerApiBase, session.refreshToken)
accessToken = refreshed.accessToken
refreshToken = refreshed.refreshToken
tokenType = refreshed.tokenType
expiresIn = refreshed.expiresIn
}
const guildData = await meGuildsCenter(session.centerApiBase, accessToken)
const next: AuthSession = {
...session,
accessToken,
refreshToken,
tokenType,
expiresIn,
guilds: guildData.guilds,
guildAccessTokens: guildData.guildAccessTokens,
}
setAuthSession(next)
setSession(next)
},
updateName: async (name: string) => {
if (!session) return
const accessToken = (await (async () => {
if (!isAccessTokenStale(session.accessToken)) return session.accessToken
const refreshed = await refreshCenter(session.centerApiBase, session.refreshToken)
return refreshed.accessToken
})())
const updated = await updateMeNameCenter(session.centerApiBase, accessToken, name)
const next: AuthSession = {
...session,
accessToken,
user: { ...session.user, name: updated.name },
}
setAuthSession(next)
setSession(next)
},
}),
[session],
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}