OAuth2 OpenID Connect provider support, API and DB refactoring (#3)
Allows Paskia to authenticate the user to a client site. - User friendly client registration flow on the admin app - Redirect-based authentication flow (per spec) - Backchannel logout both ways to keep sessions synchronized - Groups integrated with Paskia's permission system - Adds email, preferred username and telephone fields on user profile - All new user basic info layout to show the new information, better looks - API and DB structures redesigned - Various unrelated fixes to theming and layout
This commit was merged in pull request #3.
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'
|
||||
|
||||
@@ -24,9 +26,12 @@ 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 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:._~-]+$'
|
||||
@@ -43,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)
|
||||
@@ -80,10 +86,11 @@ const permissionSummary = computed(() => {
|
||||
const summary = {}
|
||||
for (const o of orgs.value) {
|
||||
const orgBase = { uuid: o.uuid, display_name: o.org.display_name }
|
||||
const orgPerms = new Set(Object.keys(o.permissions))
|
||||
// o.permissions is a dict[UUID, Permission]
|
||||
const orgPermUuids = new Set(Object.keys(o.permissions || {}))
|
||||
|
||||
// Org-level permissions (direct) - only count if org can grant them
|
||||
for (const pid of Object.keys(o.permissions)) {
|
||||
for (const pid of Object.keys(o.permissions || {})) {
|
||||
if (!summary[pid]) summary[pid] = { orgs: [], orgSet: new Set(), userCount: 0 }
|
||||
if (!summary[pid].orgSet.has(o.uuid)) {
|
||||
summary[pid].orgs.push(orgBase)
|
||||
@@ -92,10 +99,11 @@ const permissionSummary = computed(() => {
|
||||
}
|
||||
|
||||
// Role-based permissions (inheritance) - only count if org can grant them
|
||||
for (const [roleUuid, r] of Object.entries(o.roles)) {
|
||||
for (const [roleUuid, r] of Object.entries(o.roles || {})) {
|
||||
// r.permissions is dict[UUID, bool]
|
||||
for (const pid of Object.keys(r.permissions || {})) {
|
||||
// Only count if the org can grant this permission
|
||||
if (!orgPerms.has(pid)) continue
|
||||
if (!orgPermUuids.has(pid)) continue
|
||||
|
||||
if (!summary[pid]) summary[pid] = { orgs: [], orgSet: new Set(), userCount: 0 }
|
||||
if (!summary[pid].orgSet.has(o.uuid)) {
|
||||
@@ -120,16 +128,49 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrgs() {
|
||||
const data = await apiJson('/auth/api/admin/orgs')
|
||||
orgs.value = Object.entries(data).map(([uuid, o]) => ({ uuid, ...o }))
|
||||
async function loadAdminData() {
|
||||
const data = await apiJson('/auth/api/admin/info')
|
||||
// Convert dicts to arrays with uuid added
|
||||
orgs.value = Object.entries(data.orgs).map(([uuid, o]) => ({ uuid, ...o }))
|
||||
permissions.value = Object.entries(data.permissions).map(([uuid, p]) => ({ uuid, ...p }))
|
||||
oidcClients.value = Object.entries(data.oidc_clients).map(([uuid, c]) => ({ uuid, ...c }))
|
||||
}
|
||||
|
||||
// Helper to get users for a role as sorted array of [uuid, user]
|
||||
@@ -153,10 +194,6 @@ function orgUserCount(org) {
|
||||
return Object.keys(org.users).length
|
||||
}
|
||||
|
||||
async function loadPermissions() {
|
||||
permissions.value = Object.values(await apiJson('/auth/api/admin/permissions'))
|
||||
}
|
||||
|
||||
async function loadUserInfo() {
|
||||
const data = await apiJson('/auth/api/validate', { method: 'POST' })
|
||||
info.value = data
|
||||
@@ -167,7 +204,9 @@ function clearSensitiveState() {
|
||||
info.value = null
|
||||
orgs.value = []
|
||||
permissions.value = []
|
||||
oidcClients.value = []
|
||||
userDetail.value = null
|
||||
editingOidcClient.value = null
|
||||
authenticated.value = false
|
||||
}
|
||||
|
||||
@@ -192,7 +231,7 @@ async function load() {
|
||||
error.value = null
|
||||
try {
|
||||
// Load admin data first - apiJson will handle 401/403 with iframe authentication
|
||||
await Promise.all([loadOrgs(), loadPermissions()])
|
||||
await loadAdminData()
|
||||
// If we get here, user has admin access - now fetch user info for display
|
||||
await loadUserInfo()
|
||||
|
||||
@@ -214,13 +253,13 @@ async function load() {
|
||||
// Org actions
|
||||
function createOrg() { openDialog('org-create', {}) }
|
||||
|
||||
function updateOrg(org) { openDialog('org-update', { org, name: org.org.display_name }) }
|
||||
function updateOrg(org) { openDialog('org-update', { org, name: org.display_name }) }
|
||||
|
||||
function editUserName(user) { openDialog('user-update-name', { user, name: user.display_name }) }
|
||||
|
||||
async function performOrgDeletion(orgUuid) {
|
||||
await apiJson(`/auth/api/admin/orgs/${orgUuid}`, { method: 'DELETE' })
|
||||
await Promise.all([loadOrgs(), loadPermissions()])
|
||||
await Promise.all([loadAdminData()])
|
||||
}
|
||||
|
||||
function deleteOrg(org) {
|
||||
@@ -240,9 +279,9 @@ function deleteOrg(org) {
|
||||
|
||||
// Build detailed breakdown of users by role
|
||||
const roleParts = Object.entries(org.roles)
|
||||
.map(([uuid, r]) => [roleUserCount(org, uuid), r.display_name])
|
||||
.filter(([count]) => count > 0)
|
||||
.map(([count, name]) => `${count} ${name}`)
|
||||
.map(([uuid, r]) => ({ role: r, count: roleUserCount(org, uuid) }))
|
||||
.filter(x => x.count > 0)
|
||||
.map(x => `${x.count} ${x.role.display_name}`)
|
||||
|
||||
const affects = roleParts.join(', ')
|
||||
|
||||
@@ -254,7 +293,7 @@ function deleteOrg(org) {
|
||||
function createUserInRole(org, role) { openDialog('user-create', { org, role }) }
|
||||
|
||||
function deleteUser(user, userDetail) {
|
||||
const credentialCount = Object.keys(userDetail?.credentials || {}).length
|
||||
const credentialCount = userDetail?.credentials ? Object.keys(userDetail.credentials).length : 0
|
||||
const userUuid = user.uuid
|
||||
const userName = user.display_name
|
||||
const orgUuid = user.org // org UUID is stored in selectedUser
|
||||
@@ -278,7 +317,7 @@ async function performUserDeletion(userUuid, userName, orgUuid) {
|
||||
try {
|
||||
await apiJson(`/auth/api/admin/users/${userUuid}`, { method: 'DELETE' })
|
||||
authStore.showMessage(`User "${userName}" deleted.`, 'success', 2500)
|
||||
await loadOrgs()
|
||||
await loadAdminData()
|
||||
window.location.hash = `#org/${orgUuid}`
|
||||
} catch (e) {
|
||||
authStore.showMessage(e.message || 'Failed to delete user', 'error')
|
||||
@@ -292,7 +331,7 @@ async function moveUserToRole(userUuid, user, targetRoleUuid) {
|
||||
method: 'PATCH',
|
||||
body: { role_uuid: targetRoleUuid }
|
||||
})
|
||||
await loadOrgs()
|
||||
await loadAdminData()
|
||||
} catch (e) {
|
||||
authStore.showMessage(e.message || 'Failed to update user role')
|
||||
}
|
||||
@@ -308,13 +347,13 @@ function onRoleDragOver(e) {
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
|
||||
function onRoleDrop(e, org, roleUuid) {
|
||||
function onRoleDrop(e, org, role) {
|
||||
e.preventDefault()
|
||||
try {
|
||||
const data = JSON.parse(e.dataTransfer.getData('text/plain'))
|
||||
if (data.org !== org.uuid) return // only within same org
|
||||
const user = org.users[data.user_uuid]
|
||||
if (user) moveUserToRole(data.user_uuid, user, roleUuid)
|
||||
if (user) moveUserToRole(data.user_uuid, user, role.uuid)
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
@@ -323,22 +362,22 @@ function createRole(org) { openDialog('role-create', { org }) }
|
||||
|
||||
function updateRole(role) { openDialog('role-update', { role, name: role.display_name }) }
|
||||
|
||||
function deleteRole(roleUuid, role) {
|
||||
function deleteRole(role) {
|
||||
// UI only allows deleting empty roles, so no confirmation needed
|
||||
apiJson(`/auth/api/admin/roles/${roleUuid}`, { method: 'DELETE' })
|
||||
apiJson(`/auth/api/admin/roles/${role.uuid}`, { method: 'DELETE' })
|
||||
.then(() => {
|
||||
authStore.showMessage(`Role "${role.display_name}" deleted.`, 'success', 2500)
|
||||
loadOrgs()
|
||||
loadAdminData()
|
||||
})
|
||||
.catch(e => {
|
||||
authStore.showMessage(e.message || 'Failed to delete role', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
async function toggleRolePermission(roleUuid, role, pid, checked) {
|
||||
// Optimistic update
|
||||
const prevPermissions = { ...(role.permissions || {}) }
|
||||
const newPermissions = { ...(role.permissions || {}) }
|
||||
async function toggleRolePermission(role, pid, checked) {
|
||||
// Optimistic update - role.permissions is dict[UUID, bool]
|
||||
const prevPermissions = { ...role.permissions }
|
||||
const newPermissions = { ...role.permissions }
|
||||
if (checked) {
|
||||
newPermissions[pid] = true
|
||||
} else {
|
||||
@@ -348,10 +387,10 @@ async function toggleRolePermission(roleUuid, role, pid, checked) {
|
||||
|
||||
try {
|
||||
const method = checked ? 'POST' : 'DELETE'
|
||||
await apiJson(`/auth/api/admin/roles/${roleUuid}/permissions/${pid}`, {
|
||||
await apiJson(`/auth/api/admin/roles/${role.uuid}/permissions/${pid}`, {
|
||||
method
|
||||
})
|
||||
await loadOrgs()
|
||||
await loadAdminData()
|
||||
} catch (e) {
|
||||
authStore.showMessage(e.message || 'Failed to update role permission')
|
||||
role.permissions = prevPermissions // revert
|
||||
@@ -362,7 +401,7 @@ async function toggleRolePermission(roleUuid, role, pid, checked) {
|
||||
async function performPermissionDeletion(permissionUuid) {
|
||||
const params = new URLSearchParams({ permission_uuid: permissionUuid })
|
||||
await apiJson(`/auth/api/admin/permission?${params.toString()}`, { method: 'DELETE' })
|
||||
await loadPermissions()
|
||||
await loadAdminData()
|
||||
}
|
||||
|
||||
function deletePermission(p) {
|
||||
@@ -400,6 +439,87 @@ function deletePermission(p) {
|
||||
} })
|
||||
}
|
||||
|
||||
// OIDC Client actions
|
||||
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 createPermissionForClient(clientId) {
|
||||
openDialog('perm-create', { display_name: '', scope: '', domain: clientId })
|
||||
}
|
||||
|
||||
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)
|
||||
// Navigate back to overview if we were on the detail page
|
||||
if (currentOidcId.value === client.uuid) {
|
||||
window.location.hash = '#overview'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function performOidcClientDeletion(clientUuid, clientName) {
|
||||
await apiJson(`/auth/api/admin/oidc-clients/${clientUuid}`, { method: 'DELETE' })
|
||||
authStore.showMessage(`OIDC client "${clientName}" deleted.`, 'success', 2500)
|
||||
await loadAdminData()
|
||||
}
|
||||
|
||||
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 loadAdminData()
|
||||
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) {
|
||||
@@ -431,21 +551,27 @@ const breadcrumbEntries = computed(() => {
|
||||
const entries = [
|
||||
{ label: 'My Profile', href: makeUiHref() }
|
||||
]
|
||||
// For org admins, combine Admin and their org
|
||||
if (isOrgAdmin.value && !isMasterAdmin.value && orgs.value.length > 0) {
|
||||
const org = orgs.value[0]
|
||||
entries.push({ label: `Admin: ${org.org.display_name}`, href: `#org/${org.uuid}` })
|
||||
} else {
|
||||
entries.push({ label: 'Admin', href: adminUiPath() })
|
||||
}
|
||||
// Determine organization for user view if selectedOrg not explicitly chosen.
|
||||
let orgForUser = null
|
||||
if (selectedUser.value) {
|
||||
orgForUser = orgs.value.find(o => o.uuid === selectedUser.value.org) || null
|
||||
}
|
||||
const orgToShow = selectedOrg.value || orgForUser
|
||||
if (orgToShow && isOrgAdmin.value && !isMasterAdmin.value) {
|
||||
// For org admins, combine Admin and org name into one link
|
||||
entries.push({ label: `Admin: ${orgToShow.org.display_name}`, href: `#org/${orgToShow.uuid}` })
|
||||
} else {
|
||||
// For master admins or when not showing an org, separate Admin link
|
||||
entries.push({ label: 'Admin', href: isMasterAdmin.value ? adminUiPath() : (orgs.value.length === 1 ? `#org/${orgs.value[0].uuid}` : adminUiPath()) })
|
||||
if (orgToShow) {
|
||||
entries.push({ label: orgToShow.org.display_name, href: `#org/${orgToShow.uuid}` })
|
||||
}
|
||||
// Add org breadcrumb only if it's not already included in the Admin entry
|
||||
const adminOrg = (isOrgAdmin.value && !isMasterAdmin.value && orgs.value.length > 0) ? orgs.value[0] : null
|
||||
if (orgToShow && (!adminOrg || orgToShow.uuid !== adminOrg.uuid)) {
|
||||
entries.push({ label: orgToShow.org.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}` })
|
||||
@@ -468,23 +594,27 @@ function generateUserRegistrationLink(u) {
|
||||
}
|
||||
|
||||
async function toggleOrgPermission(org, permId, checked) {
|
||||
// Build next permission dict
|
||||
// org.permissions is dict[UUID, Permission]
|
||||
const has = permId in org.permissions
|
||||
if (checked && has) return
|
||||
if (!checked && !has) return
|
||||
const next = { ...org.permissions }
|
||||
if (checked) {
|
||||
next[permId] = true // Placeholder, real data comes from loadOrgs
|
||||
} else {
|
||||
delete next[permId]
|
||||
}
|
||||
// Optimistic update
|
||||
const prev = { ...org.permissions }
|
||||
org.permissions = next
|
||||
if (checked) {
|
||||
// Need to fetch the permission object to add it
|
||||
const perm = permissions.value.find(p => p.uuid === permId)
|
||||
if (perm) {
|
||||
org.permissions = { ...org.permissions, [permId]: perm }
|
||||
}
|
||||
} else {
|
||||
const next = { ...org.permissions }
|
||||
delete next[permId]
|
||||
org.permissions = next
|
||||
}
|
||||
try {
|
||||
const params = new URLSearchParams({ permission_uuid: permId })
|
||||
await apiJson(`/auth/api/admin/orgs/${org.uuid}/permission?${params.toString()}`, { method: checked ? 'POST' : 'DELETE' })
|
||||
await loadOrgs()
|
||||
await loadAdminData()
|
||||
} catch (e) {
|
||||
authStore.showMessage(e.message || 'Failed to update organization permission', 'error')
|
||||
org.permissions = prev // revert
|
||||
@@ -594,7 +724,7 @@ function handlePanelNavigateOut(direction) {
|
||||
}
|
||||
|
||||
async function refreshUserDetail() {
|
||||
await loadOrgs()
|
||||
await loadAdminData()
|
||||
if (selectedUser.value) {
|
||||
try {
|
||||
userDetail.value = await apiJson(`/auth/api/admin/users/${selectedUser.value.uuid}`)
|
||||
@@ -620,7 +750,7 @@ async function submitDialog() {
|
||||
apiJson('/auth/api/admin/orgs', { method: 'POST', body: { display_name: name, permissions: [] } })
|
||||
.then(() => {
|
||||
authStore.showMessage(`Organization "${name}" created.`, 'success', 2500)
|
||||
Promise.all([loadOrgs(), loadPermissions()])
|
||||
loadAdminData()
|
||||
})
|
||||
.catch(e => {
|
||||
authStore.showMessage(e.message || 'Failed to create organization', 'error')
|
||||
@@ -634,7 +764,7 @@ async function submitDialog() {
|
||||
apiJson(`/auth/api/admin/orgs/${org.uuid}`, { method: 'PATCH', body: { display_name: name } })
|
||||
.then(() => {
|
||||
authStore.showMessage(`Organization renamed to "${name}".`, 'success', 2500)
|
||||
loadOrgs()
|
||||
loadAdminData()
|
||||
})
|
||||
.catch(e => {
|
||||
authStore.showMessage(e.message || 'Failed to update organization', 'error')
|
||||
@@ -648,7 +778,7 @@ async function submitDialog() {
|
||||
apiJson(`/auth/api/admin/orgs/${org.uuid}/roles`, { method: 'POST', body: { display_name: name, permissions: [] } })
|
||||
.then(() => {
|
||||
authStore.showMessage(`Role "${name}" created.`, 'success', 2500)
|
||||
loadOrgs()
|
||||
loadAdminData()
|
||||
})
|
||||
.catch(e => {
|
||||
authStore.showMessage(e.message || 'Failed to create role', 'error')
|
||||
@@ -676,7 +806,7 @@ async function submitDialog() {
|
||||
apiJson(`/auth/api/admin/orgs/${org.uuid}/users`, { method: 'POST', body: { display_name: name, role: role.display_name } })
|
||||
.then(() => {
|
||||
authStore.showMessage(`User "${name}" added to ${role.display_name} role.`, 'success', 2500)
|
||||
loadOrgs()
|
||||
loadAdminData()
|
||||
})
|
||||
.catch(e => {
|
||||
authStore.showMessage(e.message || 'Failed to add user', 'error')
|
||||
@@ -687,7 +817,7 @@ async function submitDialog() {
|
||||
|
||||
// Close dialog immediately, then perform async operation
|
||||
closeDialog()
|
||||
apiJson(`/auth/api/admin/users/${user.uuid}/display-name`, { method: 'PATCH', body: { display_name: name } })
|
||||
apiJson(`/auth/api/admin/users/${user.uuid}/info`, { method: 'PATCH', body: { display_name: name } })
|
||||
.then(() => {
|
||||
authStore.showMessage(`User renamed to "${name}".`, 'success', 2500)
|
||||
onUserNameSaved()
|
||||
@@ -722,7 +852,7 @@ async function submitDialog() {
|
||||
apiJson(`/auth/api/admin/permission?${params.toString()}`, { method: 'PATCH' })
|
||||
.then(() => {
|
||||
authStore.showMessage(`Permission "${newDisplay}" updated.`, 'success', 2500)
|
||||
loadPermissions()
|
||||
loadAdminData()
|
||||
})
|
||||
.catch(e => {
|
||||
authStore.showMessage(e.message || 'Failed to update permission', 'error')
|
||||
@@ -738,12 +868,37 @@ async function submitDialog() {
|
||||
apiJson('/auth/api/admin/permissions', { method: 'POST', body: { scope, display_name, domain: domain || undefined } })
|
||||
.then(() => {
|
||||
authStore.showMessage(`Permission "${display_name}" created.`, 'success', 2500)
|
||||
loadPermissions()
|
||||
loadAdminData()
|
||||
})
|
||||
.catch(e => {
|
||||
authStore.showMessage(e.message || 'Failed to create permission', 'error')
|
||||
})
|
||||
return // Don't call closeDialog() again
|
||||
} else if (t === 'oidc-edit') {
|
||||
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')
|
||||
|
||||
const redirect_uris = uris ? uris.split('\n').map(u => u.trim()).filter(u => u) : []
|
||||
|
||||
// Close dialog immediately, then perform async operation
|
||||
closeDialog()
|
||||
|
||||
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}" ${isNew ? 'created' : 'updated'}.`, 'success', 2500)
|
||||
loadAdminData()
|
||||
})
|
||||
.catch(e => {
|
||||
authStore.showMessage(e.message || `Failed to ${isNew ? 'create' : '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)
|
||||
@@ -790,11 +945,12 @@ 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"
|
||||
:permissions="permissions"
|
||||
:oidc-clients="oidcClients"
|
||||
:navigation-disabled="hasActiveModal"
|
||||
:permission-summary="permissionSummary"
|
||||
@create-org="createOrg"
|
||||
@@ -805,6 +961,9 @@ async function submitDialog() {
|
||||
@open-dialog="openDialog"
|
||||
@delete-permission="deletePermission"
|
||||
@rename-permission-display="renamePermissionDisplay"
|
||||
@create-oidc-client="createOidcClient"
|
||||
@open-oidc-client="openOidcClient"
|
||||
@delete-oidc-client="deleteOidcClient"
|
||||
@navigate-out="handlePanelNavigateOut"
|
||||
/>
|
||||
|
||||
@@ -846,6 +1005,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>
|
||||
@@ -857,13 +1031,14 @@ async function submitDialog() {
|
||||
:settings="authStore.settings"
|
||||
@submit-dialog="submitDialog"
|
||||
@close-dialog="closeDialog"
|
||||
@reset-oidc-secret="resetOidcSecret"
|
||||
@create-permission-for-client="createPermissionForClient"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.view-admin { padding-bottom: var(--space-3xl); }
|
||||
.admin-section { margin-top: var(--space-xl); }
|
||||
.admin-section-body { display: flex; flex-direction: column; gap: var(--space-xl); }
|
||||
.admin-panels { display: flex; flex-direction: column; gap: var(--space-xl); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user