diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index 069ac18..ea53db5 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -24,6 +24,7 @@ const showBackMessage = ref(false) const error = ref(null) const orgs = ref([]) const permissions = ref([]) +const oidcClients = ref([]) const currentOrgId = ref(null) // UUID of selected org for detail view const currentUserId = ref(null) // UUID for user detail view const userDetail = ref(null) // cached user detail object @@ -143,6 +144,24 @@ async function loadPermissions() { permissions.value = await apiJson('/auth/api/admin/permissions') } +async function loadOidcClients() { + // Only master admins can view OIDC clients + if (!isMasterAdmin.value) { + oidcClients.value = [] + return + } + try { + oidcClients.value = await apiJson('/auth/api/admin/oidc-clients') + } catch (e) { + // If 403, user is not master admin - silently skip + if (e.message?.includes('403') || e.message?.includes('Forbidden')) { + oidcClients.value = [] + } else { + throw e + } + } +} + async function loadUserInfo() { const data = await apiJson('/auth/api/validate', { method: 'POST' }) info.value = data @@ -153,6 +172,7 @@ function clearSensitiveState() { info.value = null orgs.value = [] permissions.value = [] + oidcClients.value = [] userDetail.value = null authenticated.value = false } @@ -181,6 +201,8 @@ async function load() { await Promise.all([loadOrgs(), loadPermissions()]) // If we get here, user has admin access - now fetch user info for display await loadUserInfo() + // Load OIDC clients after authentication (master admin only) + await loadOidcClients() if (!isMasterAdmin.value && isOrgAdmin.value && orgs.value.length === 1) { if (!window.location.hash || window.location.hash === '#overview') { @@ -383,6 +405,34 @@ function deletePermission(p) { } }) } +// OIDC Client actions +function createOidcClient() { + openDialog('oidc-create', { name: '', redirect_uris: '' }) +} + +function editOidcClient(client) { + openDialog('oidc-edit', { + client, + name: client.name, + redirect_uris: client.redirect_uris.join('\n') + }) +} + +function deleteOidcClient(client) { + openDialog('confirm', { + message: `Delete OIDC client "${client.name}"? This will break any applications using this client.`, + action: async () => { + await performOidcClientDeletion(client.uuid, client.name) + } + }) +} + +async function performOidcClientDeletion(clientUuid, clientName) { + await apiJson(`/auth/api/admin/oidc-clients/${clientUuid}`, { method: 'DELETE' }) + authStore.showMessage(`OIDC client "${clientName}" deleted.`, 'success', 2500) + await loadOidcClients() +} + const selectedOrg = computed(() => orgs.value.find(o => o.uuid === currentOrgId.value) || null) function openOrg(o) { @@ -721,6 +771,59 @@ async function submitDialog() { authStore.showMessage(e.message || 'Failed to create permission', 'error') }) return // Don't call closeDialog() again + } else if (t === 'oidc-create') { + const name = dialog.value.data.name?.trim() + const uris = dialog.value.data.redirect_uris?.trim() + if (!name) throw new Error('Client name required') + if (!uris) throw new Error('Redirect URIs required') + + const redirect_uris = uris.split('\n').map(u => u.trim()).filter(u => u) + if (redirect_uris.length === 0) throw new Error('At least one redirect URI required') + + // Close dialog immediately, then perform async operation + closeDialog() + apiJson('/auth/api/admin/oidc-clients', { method: 'POST', body: { name, redirect_uris } }) + .then((result) => { + // Show success dialog with client credentials + openDialog('oidc-created', { + client_id: result.client_id, + client_secret: result.client_secret + }) + loadOidcClients() + }) + .catch(e => { + authStore.showMessage(e.message || 'Failed to create OIDC client', 'error') + }) + return // Don't call closeDialog() again + } else if (t === 'oidc-edit') { + const { client } = dialog.value.data + const name = dialog.value.data.name?.trim() + const uris = dialog.value.data.redirect_uris?.trim() + if (!name) throw new Error('Client name required') + if (!uris) throw new Error('Redirect URIs required') + + const redirect_uris = uris.split('\n').map(u => u.trim()).filter(u => u) + if (redirect_uris.length === 0) throw new Error('At least one redirect URI required') + + // Close dialog immediately, then perform async operation + closeDialog() + + // Check if anything changed + const oldUris = [...client.redirect_uris].sort().join('\n') + const newUris = [...redirect_uris].sort().join('\n') + if (name === client.name && oldUris === newUris) { + return // No changes + } + + apiJson(`/auth/api/admin/oidc-clients/${client.uuid}`, { method: 'PATCH', body: { name, redirect_uris } }) + .then(() => { + authStore.showMessage(`OIDC client "${name}" updated.`, 'success', 2500) + loadOidcClients() + }) + .catch(e => { + authStore.showMessage(e.message || 'Failed to update OIDC client', 'error') + }) + return // Don't call closeDialog() again } else if (t === 'confirm') { const action = dialog.value.data.action // Close dialog first, then perform action (errors shown via showMessage) @@ -773,6 +876,7 @@ async function submitDialog() { :info="info" :orgs="orgs" :permissions="permissions" + :oidc-clients="oidcClients" :navigation-disabled="hasActiveModal" :permission-summary="permissionSummary" @create-org="createOrg" @@ -783,6 +887,9 @@ async function submitDialog() { @open-dialog="openDialog" @delete-permission="deletePermission" @rename-permission-display="renamePermissionDisplay" + @create-oidc-client="createOidcClient" + @edit-oidc-client="editOidcClient" + @delete-oidc-client="deleteOidcClient" @navigate-out="handlePanelNavigateOut" /> diff --git a/frontend/src/admin/AdminDialogs.vue b/frontend/src/admin/AdminDialogs.vue index 92faaf9..438ebc9 100644 --- a/frontend/src/admin/AdminDialogs.vue +++ b/frontend/src/admin/AdminDialogs.vue @@ -12,6 +12,7 @@ const props = defineProps({ const emit = defineEmits(['submitDialog', 'closeDialog']) const NAME_EDIT_TYPES = new Set(['org-update', 'role-update', 'user-update-name']) +const NO_SUBMIT_TYPES = new Set(['oidc-created']) const rpId = computed(() => props.settings?.rp_id || 'the configured domain') @@ -25,6 +26,9 @@ const rpId = computed(() => props.settings?.rp_id || 'the configured domain') Add User To Role Edit User Name {{ dialog.type === 'perm-create' ? 'Create Permission' : 'Edit Permission' }} + Create OIDC Client + Edit OIDC Client + OIDC Client Created Confirm