From 1ec7ace4e31aa2942a384f02af80f566f43e51d0 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:09:16 +0100 Subject: [PATCH] feat: add OIDC Config admin page --- .../Admin/OidcConfig/OidcConfig.module.css | 65 ++++++ src/pages/Admin/OidcConfig/OidcConfig.tsx | 191 ++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 src/pages/Admin/OidcConfig/OidcConfig.module.css create mode 100644 src/pages/Admin/OidcConfig/OidcConfig.tsx diff --git a/src/pages/Admin/OidcConfig/OidcConfig.module.css b/src/pages/Admin/OidcConfig/OidcConfig.module.css new file mode 100644 index 0000000..af3729b --- /dev/null +++ b/src/pages/Admin/OidcConfig/OidcConfig.module.css @@ -0,0 +1,65 @@ +.page { + max-width: 640px; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin: 0; + font-family: var(--font-body); +} + +.headerActions { + display: flex; + gap: 8px; +} + +.section { + margin-bottom: 24px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.toggleRow { + display: flex; + align-items: center; + gap: 12px; +} + +.hint { + font-size: 11px; + color: var(--text-muted); + font-family: var(--font-body); +} + +.tagList { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.noRoles { + font-size: 12px; + color: var(--text-faint); + font-style: italic; + font-family: var(--font-body); +} + +.addRoleRow { + display: flex; + gap: 8px; + align-items: center; +} + +.roleInput { + width: 200px; +} diff --git a/src/pages/Admin/OidcConfig/OidcConfig.tsx b/src/pages/Admin/OidcConfig/OidcConfig.tsx new file mode 100644 index 0000000..be43ed3 --- /dev/null +++ b/src/pages/Admin/OidcConfig/OidcConfig.tsx @@ -0,0 +1,191 @@ +import { useState } from 'react' +import { AdminLayout } from '../Admin' +import { Button } from '../../../design-system/primitives/Button/Button' +import { Input } from '../../../design-system/primitives/Input/Input' +import { Toggle } from '../../../design-system/primitives/Toggle/Toggle' +import { FormField } from '../../../design-system/primitives/FormField/FormField' +import { Tag } from '../../../design-system/primitives/Tag/Tag' +import { SectionHeader } from '../../../design-system/primitives/SectionHeader/SectionHeader' +import { ConfirmDialog } from '../../../design-system/composites/ConfirmDialog/ConfirmDialog' +import { useToast } from '../../../design-system/composites/Toast/Toast' +import styles from './OidcConfig.module.css' + +interface OidcFormData { + enabled: boolean + autoSignup: boolean + issuerUri: string + clientId: string + clientSecret: string + rolesClaim: string + displayNameClaim: string + defaultRoles: string[] +} + +const INITIAL_DATA: OidcFormData = { + enabled: true, + autoSignup: true, + issuerUri: 'https://keycloak.example.com/realms/cameleer', + clientId: 'cameleer-app', + clientSecret: '••••••••••••', + rolesClaim: 'realm_access.roles', + displayNameClaim: 'name', + defaultRoles: ['USER', 'VIEWER'], +} + +export function OidcConfig() { + const [form, setForm] = useState(INITIAL_DATA) + const [newRole, setNewRole] = useState('') + const [deleteOpen, setDeleteOpen] = useState(false) + const { toast } = useToast() + + function update(key: K, value: OidcFormData[K]) { + setForm((prev) => ({ ...prev, [key]: value })) + } + + function addRole() { + const role = newRole.trim().toUpperCase() + if (role && !form.defaultRoles.includes(role)) { + update('defaultRoles', [...form.defaultRoles, role]) + setNewRole('') + } + } + + function removeRole(role: string) { + update('defaultRoles', form.defaultRoles.filter((r) => r !== role)) + } + + function handleSave() { + toast({ title: 'Settings saved', description: 'OIDC configuration updated successfully.', variant: 'success' }) + } + + function handleTest() { + toast({ title: 'Connection test', description: 'OIDC provider responded successfully.', variant: 'info' }) + } + + function handleDelete() { + setDeleteOpen(false) + setForm({ ...INITIAL_DATA, enabled: false, issuerUri: '', clientId: '', clientSecret: '', defaultRoles: [] }) + toast({ title: 'Configuration deleted', description: 'OIDC configuration has been removed.', variant: 'warning' }) + } + + return ( + +
+
+

OIDC Configuration

+
+ + +
+
+ +
+ Behavior +
+ update('enabled', e.target.checked)} + /> +
+
+ update('autoSignup', e.target.checked)} + /> + Automatically create accounts for new OIDC users +
+
+ +
+ Provider Settings + + update('issuerUri', e.target.value)} + /> + + + update('clientId', e.target.value)} + /> + + + update('clientSecret', e.target.value)} + /> + +
+ +
+ Claim Mapping + + update('rolesClaim', e.target.value)} + /> + + + update('displayNameClaim', e.target.value)} + /> + +
+ +
+ Default Roles +
+ {form.defaultRoles.map((role) => ( + removeRole(role)} /> + ))} + {form.defaultRoles.length === 0 && ( + No default roles configured + )} +
+
+ 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" + /> +
+
+
+ ) +}