feature/oidc-login #12
@@ -21,6 +21,7 @@ import CalendarPage from '@/pages/CalendarPage'
|
||||
import SupportDetailPage from '@/pages/SupportDetailPage'
|
||||
import MeetingDetailPage from '@/pages/MeetingDetailPage'
|
||||
import OidcCallbackPage from '@/pages/OidcCallbackPage'
|
||||
import OidcSettingsPage from '@/pages/OidcSettingsPage'
|
||||
import axios from 'axios'
|
||||
|
||||
const getStoredWizardPort = (): number | null => {
|
||||
@@ -135,6 +136,7 @@ export default function App() {
|
||||
<Route path="/roles" element={<RoleEditorPage />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/monitor" element={<MonitorPage />} />
|
||||
<Route path="/settings/oidc" element={<OidcSettingsPage />} />
|
||||
<Route path="/oidc/callback" element={<OidcCallbackPage onToken={loginWithToken} />} />
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
|
||||
@@ -41,6 +41,7 @@ export default function Sidebar({ user, onLogout }: Props) {
|
||||
...(user.is_admin ? [
|
||||
{ to: '/users', icon: '👥', label: 'Users' },
|
||||
{ to: '/roles', icon: '🔐', label: 'Roles' },
|
||||
{ to: '/settings/oidc', icon: '🪪', label: 'OIDC' },
|
||||
] : []),
|
||||
] : [
|
||||
{ to: '/monitor', icon: '📡', label: 'Monitor' },
|
||||
|
||||
159
src/pages/OidcSettingsPage.tsx
Normal file
159
src/pages/OidcSettingsPage.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
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
|
||||
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: '',
|
||||
})
|
||||
|
||||
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 || '',
|
||||
})
|
||||
})
|
||||
.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(),
|
||||
}
|
||||
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>
|
||||
<button className="btn-primary" disabled={saving} onClick={save}>
|
||||
{saving ? 'Saving...' : 'Save OIDC Settings'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user