- 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>
102 lines
3.6 KiB
TypeScript
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>
|
|
}
|