OIDC settings page + setup wizard now configure the bootstrap admin role instead of a hand-typed OIDC subject. The OIDC-only admin link is handled automatically by the backend admin-role auto-connect on first sign-in (explained inline in both the wizard and settings page). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
6.5 KiB
TypeScript
172 lines
6.5 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import api from '@/services/api'
|
|
import { useAuth } from '@/hooks/useAuth'
|
|
|
|
interface Settings {
|
|
enabled: boolean
|
|
issuer: string | null
|
|
client_id: string | null
|
|
has_client_secret: boolean
|
|
redirect_uri: string | null
|
|
scopes: string | null
|
|
post_login_redirect: string | null
|
|
admin_role: string
|
|
oidc_only: boolean
|
|
effective_enabled: boolean
|
|
source: string
|
|
}
|
|
|
|
export default function OidcSettingsPage() {
|
|
const { user } = useAuth()
|
|
const isAdmin = user?.is_admin === true
|
|
|
|
const [loaded, setLoaded] = useState<Settings | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [saving, setSaving] = useState(false)
|
|
const [message, setMessage] = useState('')
|
|
const [form, setForm] = useState({
|
|
enabled: false,
|
|
issuer: '',
|
|
client_id: '',
|
|
client_secret: '',
|
|
redirect_uri: '',
|
|
scopes: 'openid email profile',
|
|
post_login_redirect: '',
|
|
admin_role: 'admin',
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (!isAdmin) { setLoading(false); return }
|
|
api.get<Settings>('/auth/oidc/settings')
|
|
.then(({ data }) => {
|
|
setLoaded(data)
|
|
setForm({
|
|
enabled: data.enabled,
|
|
issuer: data.issuer || '',
|
|
client_id: data.client_id || '',
|
|
client_secret: '',
|
|
redirect_uri: data.redirect_uri || '',
|
|
scopes: data.scopes || 'openid email profile',
|
|
post_login_redirect: data.post_login_redirect || '',
|
|
admin_role: data.admin_role || 'admin',
|
|
})
|
|
})
|
|
.catch((e) => setMessage(e.response?.data?.detail || 'Failed to load OIDC settings'))
|
|
.finally(() => setLoading(false))
|
|
}, [isAdmin])
|
|
|
|
const save = async () => {
|
|
setSaving(true)
|
|
setMessage('')
|
|
try {
|
|
const payload: Record<string, any> = {
|
|
enabled: form.enabled,
|
|
issuer: form.issuer.trim(),
|
|
client_id: form.client_id.trim(),
|
|
redirect_uri: form.redirect_uri.trim(),
|
|
scopes: form.scopes.trim(),
|
|
post_login_redirect: form.post_login_redirect.trim(),
|
|
admin_role: form.admin_role.trim() || 'admin',
|
|
}
|
|
if (form.client_secret) payload.client_secret = form.client_secret
|
|
const { data } = await api.put<Settings>('/auth/oidc/settings', payload)
|
|
setLoaded(data)
|
|
setForm((f) => ({ ...f, client_secret: '' }))
|
|
setMessage('OIDC settings saved successfully')
|
|
} catch (e: any) {
|
|
setMessage(e.response?.data?.detail || 'Failed to save OIDC settings')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
if (loading) return <div className="loading">Loading OIDC settings...</div>
|
|
if (!isAdmin) {
|
|
return (
|
|
<div className="section">
|
|
<h2>🔐 OIDC Settings</h2>
|
|
<p className="empty">Admin access required.</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const callbackHint = form.redirect_uri.trim() || loaded?.redirect_uri || '(set the Redirect / Callback URL below)'
|
|
|
|
return (
|
|
<div className="section">
|
|
<div className="page-header">
|
|
<div>
|
|
<h2>🔐 OIDC Settings</h2>
|
|
<div className="text-dim">Configure the OpenID Connect provider. Saved values override environment defaults.</div>
|
|
</div>
|
|
</div>
|
|
|
|
{message && (
|
|
<div style={{
|
|
padding: '10px 12px', marginBottom: 16, borderRadius: 8,
|
|
background: message.includes('success') ? 'rgba(70,180,135,.14)' : 'rgba(226,85,60,.14)',
|
|
border: `1px solid ${message.includes('success') ? 'rgba(70,180,135,.4)' : 'rgba(226,85,60,.4)'}`,
|
|
}}>{message}</div>
|
|
)}
|
|
|
|
<div className="monitor-card" style={{ marginBottom: 16 }}>
|
|
<div className="monitor-card-header">
|
|
<div style={{ fontWeight: 600 }}>Status</div>
|
|
<span className={'badge ' + (loaded?.effective_enabled ? 'status-online' : 'status-offline')}>
|
|
{loaded?.effective_enabled ? 'OIDC active' : 'OIDC inactive'}
|
|
</span>
|
|
</div>
|
|
<div className="monitor-metrics">
|
|
config source: <b>{loaded?.source}</b> · OIDC-only mode (deploy env): <b>{loaded?.oidc_only ? 'on' : 'off'}</b>
|
|
</div>
|
|
<div style={{ marginTop: 8 }}>
|
|
<div className="text-dim">Register this Redirect / Callback URL at your identity provider:</div>
|
|
<code style={{ display: 'block', marginTop: 6, wordBreak: 'break-all' }}>{callbackHint}</code>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="task-create-form" style={{ maxWidth: 640 }}>
|
|
<label className="filter-check">
|
|
<input type="checkbox" checked={form.enabled} onChange={(e) => setForm({ ...form, enabled: e.target.checked })} />
|
|
Enable OIDC sign-in
|
|
</label>
|
|
<label>
|
|
Issuer (OIDC source)
|
|
<input placeholder="https://idp.example.com" value={form.issuer} onChange={(e) => setForm({ ...form, issuer: e.target.value })} />
|
|
</label>
|
|
<label>
|
|
Client ID
|
|
<input value={form.client_id} onChange={(e) => setForm({ ...form, client_id: e.target.value })} />
|
|
</label>
|
|
<label>
|
|
Client Secret
|
|
<input type="password" placeholder={loaded?.has_client_secret ? '•••••• (leave blank to keep current)' : 'client secret'} value={form.client_secret} onChange={(e) => setForm({ ...form, client_secret: e.target.value })} />
|
|
</label>
|
|
<label>
|
|
Redirect / Callback URL
|
|
<input placeholder="https://hf-api.example.com/auth/oidc/callback" value={form.redirect_uri} onChange={(e) => setForm({ ...form, redirect_uri: e.target.value })} />
|
|
</label>
|
|
<label>
|
|
Scopes
|
|
<input value={form.scopes} onChange={(e) => setForm({ ...form, scopes: e.target.value })} />
|
|
</label>
|
|
<label>
|
|
Post-login redirect (frontend)
|
|
<input placeholder="https://hf.example.com/oidc/callback" value={form.post_login_redirect} onChange={(e) => setForm({ ...form, post_login_redirect: e.target.value })} />
|
|
</label>
|
|
<label>
|
|
Admin role (bootstrap)
|
|
<input placeholder="admin" value={form.admin_role} onChange={(e) => setForm({ ...form, admin_role: e.target.value })} />
|
|
</label>
|
|
<p className="text-dim">
|
|
OIDC-only bootstrap: before any admin is linked, an IdP user whose token carries this role
|
|
auto-connects to the HarborForge admin account on first sign-in. Disables itself once an admin is bound.
|
|
</p>
|
|
<button className="btn-primary" disabled={saving} onClick={save}>
|
|
{saving ? 'Saving...' : 'Save OIDC Settings'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|