Admin OIDC Client editing moved to its own page.
This commit is contained in:
@@ -9,10 +9,12 @@ import AccessDenied from '@/components/AccessDenied.vue'
|
||||
import AdminOverview from '@/admin/AdminOverview.vue'
|
||||
import AdminOrgDetail from '@/admin/AdminOrgDetail.vue'
|
||||
import AdminUserDetail from '@/admin/AdminUserDetail.vue'
|
||||
import AdminOidcDetail from '@/admin/AdminOidcDetail.vue'
|
||||
import AdminDialogs from '@/admin/AdminDialogs.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { adminUiPath, makeUiHref } from '@/utils/settings'
|
||||
import { apiJson, SessionValidator } from 'paskia'
|
||||
import { uuidv7 } from 'uuidv7'
|
||||
import { getDirection } from '@/utils/keynav'
|
||||
import { goBack } from '@/utils/helpers'
|
||||
|
||||
@@ -27,7 +29,9 @@ 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 currentOidcId = ref(null) // UUID for OIDC client detail view
|
||||
const userDetail = ref(null) // cached user detail object
|
||||
const editingOidcClient = ref(null) // OIDC client being edited (with local changes)
|
||||
const authStore = useAuthStore()
|
||||
const addingOrgForPermission = ref(null)
|
||||
const PERMISSION_ID_PATTERN = '^[A-Za-z0-9:._~-]+$'
|
||||
@@ -44,6 +48,7 @@ const breadcrumbsRef = ref(null)
|
||||
const adminOverviewRef = ref(null)
|
||||
const adminOrgDetailRef = ref(null)
|
||||
const adminUserDetailRef = ref(null)
|
||||
const adminOidcDetailRef = ref(null)
|
||||
|
||||
// Check if any modal/dialog is open (blocks arrow key navigation)
|
||||
const hasActiveModal = computed(() => dialog.value.type !== null || showRegModal.value)
|
||||
@@ -121,10 +126,40 @@ function parseHash() {
|
||||
const h = window.location.hash || ''
|
||||
currentOrgId.value = null
|
||||
currentUserId.value = null
|
||||
currentOidcId.value = null
|
||||
editingOidcClient.value = null
|
||||
if (h.startsWith('#org/')) {
|
||||
currentOrgId.value = h.slice(5)
|
||||
} else if (h.startsWith('#user/')) {
|
||||
currentUserId.value = h.slice(6)
|
||||
} else if (h.startsWith('#oidc:')) {
|
||||
const oidcUuid = h.slice(6)
|
||||
currentOidcId.value = oidcUuid
|
||||
// Initialize editing client data
|
||||
if (oidcUuid === 'new') {
|
||||
// Generate client_id and secret for new client
|
||||
const bytes = new Uint8Array(32)
|
||||
crypto.getRandomValues(bytes)
|
||||
const client_secret = btoa(String.fromCharCode(...bytes))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
editingOidcClient.value = {
|
||||
client_id: uuidv7(),
|
||||
client_secret,
|
||||
isNew: true,
|
||||
name: '',
|
||||
redirect_uris: []
|
||||
}
|
||||
} else {
|
||||
const client = oidcClients.value.find(c => c.uuid === oidcUuid)
|
||||
if (client) {
|
||||
editingOidcClient.value = {
|
||||
...client,
|
||||
client_id: client.uuid,
|
||||
client_secret: null,
|
||||
isNew: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +209,7 @@ function clearSensitiveState() {
|
||||
permissions.value = []
|
||||
oidcClients.value = []
|
||||
userDetail.value = null
|
||||
editingOidcClient.value = null
|
||||
authenticated.value = false
|
||||
}
|
||||
|
||||
@@ -406,42 +442,39 @@ function deletePermission(p) {
|
||||
}
|
||||
|
||||
// OIDC Client actions
|
||||
async function createOidcClient() {
|
||||
try {
|
||||
// Create the record on the server immediately, then open edit dialog
|
||||
const result = await apiJson('/auth/api/admin/oidc-clients', { method: 'POST', body: {} })
|
||||
await loadOidcClients()
|
||||
openDialog('oidc-edit', {
|
||||
client_id: result.client_id,
|
||||
client_secret: result.client_secret,
|
||||
isNew: true,
|
||||
name: '',
|
||||
redirect_uris: ''
|
||||
})
|
||||
} catch (e) {
|
||||
authStore.showMessage(e.message || 'Failed to create OIDC client', 'error')
|
||||
async function sha256Hex(text) {
|
||||
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text))
|
||||
return [...new Uint8Array(hash)].map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
}
|
||||
|
||||
function createOidcClient() {
|
||||
// Navigate to new OIDC client page
|
||||
window.location.hash = '#oidc:new'
|
||||
}
|
||||
|
||||
function openOidcClient(client) {
|
||||
// Navigate to OIDC client detail page
|
||||
window.location.hash = `#oidc:${client.uuid}`
|
||||
}
|
||||
|
||||
function resetOidcSecret(clientId) {
|
||||
// Generate new secret locally; it will be sent to server on Save
|
||||
const bytes = new Uint8Array(32)
|
||||
crypto.getRandomValues(bytes)
|
||||
const client_secret = btoa(String.fromCharCode(...bytes))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
// Update editingOidcClient if we're on the detail page
|
||||
if (editingOidcClient.value?.client_id === clientId) {
|
||||
editingOidcClient.value = { ...editingOidcClient.value, client_secret }
|
||||
}
|
||||
// Also update dialog if open (for backwards compatibility)
|
||||
if (dialog.value.type === 'oidc-edit' && dialog.value.data?.client_id === clientId) {
|
||||
dialog.value.data.client_secret = client_secret
|
||||
}
|
||||
}
|
||||
|
||||
function editOidcClient(client) {
|
||||
openDialog('oidc-edit', {
|
||||
client_id: client.uuid,
|
||||
name: client.name,
|
||||
redirect_uris: client.redirect_uris.join('\n')
|
||||
})
|
||||
}
|
||||
|
||||
async function resetOidcSecret(clientId) {
|
||||
try {
|
||||
const result = await apiJson(`/auth/api/admin/oidc-clients/${clientId}/reset-secret`, { method: 'POST' })
|
||||
// Update the dialog data in place so the new secret is shown
|
||||
if (dialog.value.type === 'oidc-edit' && dialog.value.data?.client_id === clientId) {
|
||||
dialog.value.data.client_secret = result.client_secret
|
||||
}
|
||||
authStore.showMessage('Client secret has been reset.', 'success', 2500)
|
||||
} catch (e) {
|
||||
authStore.showMessage(e.message || 'Failed to reset client secret', 'error')
|
||||
}
|
||||
function createPermissionForClient(clientId) {
|
||||
openDialog('perm-create', { display_name: '', scope: '', domain: clientId })
|
||||
}
|
||||
|
||||
function deleteOidcClient(client) {
|
||||
@@ -449,6 +482,10 @@ function deleteOidcClient(client) {
|
||||
message: `Delete OIDC client "${client.name}"? This will break any applications using this client.`,
|
||||
action: async () => {
|
||||
await performOidcClientDeletion(client.uuid, client.name)
|
||||
// Navigate back to overview if we were on the detail page
|
||||
if (currentOidcId.value === client.uuid) {
|
||||
window.location.hash = '#overview'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -459,6 +496,32 @@ async function performOidcClientDeletion(clientUuid, clientName) {
|
||||
await loadOidcClients()
|
||||
}
|
||||
|
||||
async function handleOidcSave(data) {
|
||||
const { client_id, client_secret, name, redirect_uris, isNew } = data
|
||||
|
||||
try {
|
||||
if (client_secret) {
|
||||
const secret_hash = await sha256Hex(client_secret)
|
||||
if (isNew) {
|
||||
await apiJson('/auth/api/admin/oidc-clients', { method: 'POST', body: { client_id, secret_hash, name, redirect_uris } })
|
||||
} else {
|
||||
await apiJson(`/auth/api/admin/oidc-clients/${client_id}`, { method: 'PATCH', body: { name, redirect_uris, secret_hash } })
|
||||
}
|
||||
} else {
|
||||
await apiJson(`/auth/api/admin/oidc-clients/${client_id}`, { method: 'PATCH', body: { name, redirect_uris } })
|
||||
}
|
||||
authStore.showMessage(`OIDC client "${name}" ${isNew ? 'created' : 'updated'}.`, 'success', 2500)
|
||||
await loadOidcClients()
|
||||
window.location.hash = '#overview'
|
||||
} catch (e) {
|
||||
authStore.showMessage(e.message || `Failed to ${isNew ? 'create' : 'update'} OIDC client`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
function handleOidcCancel() {
|
||||
goOverview()
|
||||
}
|
||||
|
||||
const selectedOrg = computed(() => orgs.value.find(o => o.uuid === currentOrgId.value) || null)
|
||||
|
||||
function openOrg(o) {
|
||||
@@ -487,6 +550,7 @@ const selectedUser = computed(() => {
|
||||
const pageHeading = computed(() => {
|
||||
if (selectedUser.value) return 'Admin: User'
|
||||
if (selectedOrg.value) return 'Admin: Org'
|
||||
if (currentOidcId.value) return 'Admin: OIDC Client'
|
||||
return ((authStore.settings?.rp_name) || 'Master') + ' Admin'
|
||||
})
|
||||
|
||||
@@ -505,6 +569,10 @@ const breadcrumbEntries = computed(() => {
|
||||
if (orgToShow) {
|
||||
entries.push({ label: orgToShow.display_name, href: `#org/${orgToShow.uuid}` })
|
||||
}
|
||||
if (currentOidcId.value) {
|
||||
const label = editingOidcClient.value?.isNew ? 'New Client' : (editingOidcClient.value?.name || 'OIDC Client')
|
||||
entries.push({ label, href: `#oidc:${currentOidcId.value}` })
|
||||
}
|
||||
if (selectedUser.value) {
|
||||
entries.push({ label: selectedUser.value.display_name, href: `#user/${selectedUser.value.uuid}` })
|
||||
}
|
||||
@@ -798,7 +866,7 @@ async function submitDialog() {
|
||||
})
|
||||
return // Don't call closeDialog() again
|
||||
} else if (t === 'oidc-edit') {
|
||||
const { client_id } = dialog.value.data
|
||||
const { client_id, client_secret, isNew } = 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')
|
||||
@@ -808,13 +876,18 @@ async function submitDialog() {
|
||||
// Close dialog immediately, then perform async operation
|
||||
closeDialog()
|
||||
|
||||
apiJson(`/auth/api/admin/oidc-clients/${client_id}`, { method: 'PATCH', body: { name, redirect_uris } })
|
||||
const req = client_secret
|
||||
? sha256Hex(client_secret).then(secret_hash => isNew
|
||||
? apiJson('/auth/api/admin/oidc-clients', { method: 'POST', body: { client_id, secret_hash, name, redirect_uris } })
|
||||
: apiJson(`/auth/api/admin/oidc-clients/${client_id}`, { method: 'PATCH', body: { name, redirect_uris, secret_hash } }))
|
||||
: apiJson(`/auth/api/admin/oidc-clients/${client_id}`, { method: 'PATCH', body: { name, redirect_uris } })
|
||||
req
|
||||
.then(() => {
|
||||
authStore.showMessage(`OIDC client "${name}" updated.`, 'success', 2500)
|
||||
authStore.showMessage(`OIDC client "${name}" ${isNew ? 'created' : 'updated'}.`, 'success', 2500)
|
||||
loadOidcClients()
|
||||
})
|
||||
.catch(e => {
|
||||
authStore.showMessage(e.message || 'Failed to update OIDC client', 'error')
|
||||
authStore.showMessage(e.message || `Failed to ${isNew ? 'create' : 'update'} OIDC client`, 'error')
|
||||
})
|
||||
return // Don't call closeDialog() again
|
||||
} else if (t === 'confirm') {
|
||||
@@ -864,7 +937,7 @@ async function submitDialog() {
|
||||
<div class="section-body admin-section-body">
|
||||
<div class="admin-panels">
|
||||
<AdminOverview
|
||||
v-if="!selectedUser && !selectedOrg && (isMasterAdmin || isOrgAdmin)"
|
||||
v-if="!selectedUser && !selectedOrg && !currentOidcId && (isMasterAdmin || isOrgAdmin)"
|
||||
ref="adminOverviewRef"
|
||||
:info="info"
|
||||
:orgs="orgs"
|
||||
@@ -881,7 +954,7 @@ async function submitDialog() {
|
||||
@delete-permission="deletePermission"
|
||||
@rename-permission-display="renamePermissionDisplay"
|
||||
@create-oidc-client="createOidcClient"
|
||||
@edit-oidc-client="editOidcClient"
|
||||
@open-oidc-client="openOidcClient"
|
||||
@delete-oidc-client="deleteOidcClient"
|
||||
@navigate-out="handlePanelNavigateOut"
|
||||
/>
|
||||
@@ -924,6 +997,21 @@ async function submitDialog() {
|
||||
@on-user-drag-start="onUserDragStart"
|
||||
/>
|
||||
|
||||
<AdminOidcDetail
|
||||
v-else-if="currentOidcId && editingOidcClient"
|
||||
ref="adminOidcDetailRef"
|
||||
:client="editingOidcClient"
|
||||
:permissions="permissions"
|
||||
:is-new="editingOidcClient.isNew"
|
||||
:navigation-disabled="hasActiveModal"
|
||||
@save="handleOidcSave"
|
||||
@cancel="handleOidcCancel"
|
||||
@delete="deleteOidcClient"
|
||||
@reset-secret="resetOidcSecret"
|
||||
@create-permission="createPermissionForClient"
|
||||
@navigate-out="handlePanelNavigateOut"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -936,6 +1024,7 @@ async function submitDialog() {
|
||||
@submit-dialog="submitDialog"
|
||||
@close-dialog="closeDialog"
|
||||
@reset-oidc-secret="resetOidcSecret"
|
||||
@create-permission-for-client="createPermissionForClient"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user