feat(auth): OIDC login UI + binding management + OIDC-only mode
- useAuthConfig fetches public /auth/config; LoginPage hides the password form when oidc_only and shows an SSO button when enabled. - /oidc/callback route applies the returned JWT (sign-in) or shows the link result; oidc_error surfaced on LoginPage. - UsersPage: hides password fields in OIDC-only mode; admin OIDC bind/unbind UI per user. Sidebar self-service "Link OIDC account" (non-OIDC_ONLY). - Dockerfile ARG/ENV HARBORFORGE_OIDC_ONLY. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,10 +44,16 @@ export function useAuth() {
|
||||
await fetchUser()
|
||||
}
|
||||
|
||||
const loginWithToken = async (token: string) => {
|
||||
localStorage.setItem('token', token)
|
||||
setState((s) => ({ ...s, token }))
|
||||
await fetchUser()
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token')
|
||||
setState({ user: null, token: null, loading: false })
|
||||
}
|
||||
|
||||
return { ...state, login, logout }
|
||||
return { ...state, login, loginWithToken, logout }
|
||||
}
|
||||
|
||||
76
src/hooks/useAuthConfig.ts
Normal file
76
src/hooks/useAuthConfig.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import api from '@/services/api'
|
||||
|
||||
export interface AuthConfig {
|
||||
oidcEnabled: boolean
|
||||
oidcOnly: boolean
|
||||
passwordLogin: boolean
|
||||
oidcLoginUrl: string
|
||||
}
|
||||
|
||||
const DEFAULT: AuthConfig = {
|
||||
oidcEnabled: false,
|
||||
oidcOnly: false,
|
||||
passwordLogin: true,
|
||||
oidcLoginUrl: '/auth/oidc/login',
|
||||
}
|
||||
|
||||
let cache: AuthConfig | null = null
|
||||
let inflight: Promise<AuthConfig> | null = null
|
||||
|
||||
async function load(): Promise<AuthConfig> {
|
||||
if (cache) return cache
|
||||
if (inflight) return inflight
|
||||
inflight = api
|
||||
.get('/auth/config')
|
||||
.then(({ data }) => {
|
||||
cache = {
|
||||
oidcEnabled: !!data.oidc_enabled,
|
||||
oidcOnly: !!data.oidc_only,
|
||||
passwordLogin: data.password_login !== false,
|
||||
oidcLoginUrl: data.oidc_login_url || '/auth/oidc/login',
|
||||
}
|
||||
return cache
|
||||
})
|
||||
.catch(() => {
|
||||
// Backend unreachable / old backend without /auth/config:
|
||||
// fall back to password-only so login is never fully blocked.
|
||||
cache = { ...DEFAULT }
|
||||
return cache
|
||||
})
|
||||
.finally(() => {
|
||||
inflight = null
|
||||
})
|
||||
return inflight
|
||||
}
|
||||
|
||||
/** Absolute backend URL for full-page OIDC redirects. */
|
||||
export function oidcLoginHref(cfg: AuthConfig): string {
|
||||
const base = localStorage.getItem('HF_BACKEND_BASE_URL') ?? ''
|
||||
return `${base}${cfg.oidcLoginUrl}`
|
||||
}
|
||||
|
||||
export function oidcLinkHref(): string {
|
||||
const base = localStorage.getItem('HF_BACKEND_BASE_URL') ?? ''
|
||||
return `${base}/auth/oidc/link`
|
||||
}
|
||||
|
||||
export function useAuthConfig() {
|
||||
const [config, setConfig] = useState<AuthConfig | null>(cache)
|
||||
const [loading, setLoading] = useState(!cache)
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
load().then((c) => {
|
||||
if (alive) {
|
||||
setConfig(c)
|
||||
setLoading(false)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { config: config ?? DEFAULT, loading }
|
||||
}
|
||||
Reference in New Issue
Block a user