import { useEffect, useState } from 'react'; import { Button, Input, Toggle, FormField, SectionHeader, Tag, ConfirmDialog, } from '@cameleer/design-system'; import { useToast } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; import { adminFetch } from '../../api/queries/admin/admin-api'; import styles from './OidcConfigPage.module.css'; import sectionStyles from '../../styles/section-card.module.css'; interface OidcFormData { enabled: boolean; autoSignup: boolean; issuerUri: string; clientId: string; clientSecret: string; rolesClaim: string; displayNameClaim: string; userIdClaim: string; defaultRoles: string[]; audience: string; additionalScopes: string[]; } const EMPTY_CONFIG: OidcFormData = { enabled: false, autoSignup: true, issuerUri: '', clientId: '', clientSecret: '', rolesClaim: 'roles', displayNameClaim: 'name', userIdClaim: 'sub', defaultRoles: ['VIEWER'], audience: '', additionalScopes: [], }; export default function OidcConfigPage() { const [form, setForm] = useState(null); const [editing, setEditing] = useState(false); const [formDraft, setFormDraft] = useState(null); const [newRole, setNewRole] = useState(''); const [deleteOpen, setDeleteOpen] = useState(false); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const { toast } = useToast(); useEffect(() => { adminFetch & { configured?: boolean }>('/oidc') .then((data) => setForm({ enabled: data.enabled ?? false, autoSignup: data.autoSignup ?? true, issuerUri: data.issuerUri ?? '', clientId: data.clientId ?? '', clientSecret: data.clientSecret ?? '', rolesClaim: data.rolesClaim ?? 'roles', displayNameClaim: data.displayNameClaim ?? 'name', userIdClaim: data.userIdClaim ?? 'sub', defaultRoles: data.defaultRoles ?? ['VIEWER'], audience: (data as any).audience ?? '', additionalScopes: (data as any).additionalScopes ?? [], })) .catch(() => setForm(EMPTY_CONFIG)); }, []); // The display values come from formDraft when editing, form otherwise const current = editing ? formDraft : form; function startEditing() { setFormDraft(form ? { ...form } : null); setEditing(true); } function cancelEditing() { setFormDraft(null); setEditing(false); setNewRole(''); } function updateDraft(key: K, value: OidcFormData[K]) { setFormDraft((prev) => prev ? { ...prev, [key]: value } : prev); } function addRole() { if (!current) return; const role = newRole.trim().toUpperCase(); if (role && !(current.defaultRoles || []).includes(role)) { updateDraft('defaultRoles', [...(current.defaultRoles || []), role]); setNewRole(''); } } function removeRole(role: string) { if (!current) return; updateDraft('defaultRoles', (current.defaultRoles || []).filter((r) => r !== role)); } async function handleSave() { if (!formDraft) return; setSaving(true); try { await adminFetch('/oidc', { method: 'PUT', body: JSON.stringify(formDraft) }); setForm({ ...formDraft }); setFormDraft(null); setEditing(false); toast({ title: 'Settings saved', description: 'OIDC configuration updated successfully.', variant: 'success' }); } catch (e: any) { toast({ title: 'Failed to save OIDC configuration', description: e.message, variant: 'error', duration: 86_400_000 }); } finally { setSaving(false); } } async function handleTest() { if (!form) return; setTesting(true); try { const result = await adminFetch<{ status: string; authorizationEndpoint?: string }>('/oidc/test', { method: 'POST' }); toast({ title: 'Connection test', description: `OIDC provider responded: ${result.status}`, variant: 'success' }); } catch (e: any) { toast({ title: 'Connection test failed', description: e.message, variant: 'error', duration: 86_400_000 }); } finally { setTesting(false); } } async function handleDelete() { setDeleteOpen(false); try { await adminFetch('/oidc', { method: 'DELETE' }); setForm(EMPTY_CONFIG); setFormDraft(null); setEditing(false); toast({ title: 'Configuration deleted', description: 'OIDC configuration has been removed.', variant: 'warning' }); } catch (e: any) { toast({ title: 'Failed to delete OIDC configuration', description: e.message, variant: 'error', duration: 86_400_000 }); } } if (!form) return ; return (
{editing ? ( <> ) : ( <> )}
Behavior
updateDraft('enabled', e.target.checked)} disabled={!editing} />
updateDraft('autoSignup', e.target.checked)} disabled={!editing} /> Automatically create accounts for new OIDC users
Provider Settings updateDraft('issuerUri', e.target.value)} disabled={!editing} /> updateDraft('clientId', e.target.value)} disabled={!editing} /> updateDraft('clientSecret', e.target.value)} disabled={!editing} /> updateDraft('audience', e.target.value)} disabled={!editing} /> updateDraft('additionalScopes', e.target.value.split(',').map(s => s.trim()).filter(Boolean))} disabled={!editing} />
Claim Mapping updateDraft('rolesClaim', e.target.value)} disabled={!editing} /> updateDraft('userIdClaim', e.target.value)} disabled={!editing} /> updateDraft('displayNameClaim', e.target.value)} disabled={!editing} />
Default Roles
{(current?.defaultRoles || []).map((role) => ( removeRole(role) : undefined} /> ))} {(current?.defaultRoles || []).length === 0 && ( No default roles configured )}
{editing && (
setNewRole(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addRole(); } }} className={styles.roleInput} />
)}
Danger Zone setDeleteOpen(false)} onConfirm={handleDelete} message="Delete OIDC configuration? All users signed in via OIDC will lose access." confirmText="delete oidc" loading={saving} />
); }