From 2c783498a47883293395b0b04e1f8a3afffa4e5b Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 23 Jan 2026 15:11:01 +0000 Subject: [PATCH] Better handling of Org Admin permission. More guardrails for Master Admin not locking himself out by changes. Admin app UI improvements. --- frontend/auth/admin/AdminApp.vue | 42 +++++++++-- frontend/src/admin/AdminDialogs.vue | 11 +-- frontend/src/stores/auth.js | 8 ++- paskia/bootstrap.py | 13 ++-- paskia/db/json.py | 33 +++------ paskia/fastapi/admin.py | 105 ++++++++++++++++++++++++++-- paskia/migrate/__init__.py | 4 +- tests/test_admin.py | 70 ++++++++++++++++++- 8 files changed, 234 insertions(+), 52 deletions(-) diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index bca60ee..c3cc961 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -11,9 +11,10 @@ import AdminOrgDetail from '@/admin/AdminOrgDetail.vue' import AdminUserDetail from '@/admin/AdminUserDetail.vue' import AdminDialogs from '@/admin/AdminDialogs.vue' import { useAuthStore } from '@/stores/auth' -import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings' +import { adminUiPath, makeUiHref } from '@/utils/settings' import { apiJson } from '@/utils/api' import { getDirection } from '@/utils/keynav' +import { goBack } from '@/utils/helpers' const info = ref(null) const loading = ref(true) @@ -64,8 +65,8 @@ function handleGlobalClick(e) { onMounted(async () => { document.addEventListener('click', handleGlobalClick) window.addEventListener('hashchange', parseHash) - const settings = await getSettings() - if (settings?.rp_name) document.title = settings.rp_name + ' Admin' + await authStore.loadSettings() + if (authStore.settings?.rp_name) document.title = authStore.settings.rp_name + ' Admin' await load() }) @@ -418,7 +419,7 @@ async function toggleOrgPermission(org, permId, checked) { await apiJson(`/auth/api/admin/orgs/${org.uuid}/permission?${params.toString()}`, { method: checked ? 'POST' : 'DELETE' }) await loadOrgs() } catch (e) { - authStore.showMessage(e.message || 'Failed to update organization permission') + authStore.showMessage(e.message || 'Failed to update organization permission', 'error') org.permissions = prev // revert } } @@ -680,7 +681,17 @@ async function submitDialog() { }) return // Don't call closeDialog() again } else if (t === 'confirm') { - const action = dialog.value.data.action; if (action) await action() + const action = dialog.value.data.action + // Close dialog first, then perform action (errors shown via showMessage) + closeDialog() + if (action) { + try { + await action() + } catch (e) { + authStore.showMessage(e.message || 'Action failed', 'error') + } + } + return // Already closed } closeDialog() } catch (e) { @@ -698,6 +709,18 @@ async function submitDialog() { v-else-if="showBackMessage" @reload="reloadPage" /> + +
+
+

⛔ Access Denied

+

{{ error }}

+

You do not have admin permissions for this application.

+
+ + +
+
+

{{ pageHeading }}

@@ -706,8 +729,7 @@ async function submitDialog() {
-
{{ error }}
-
+
@@ -784,4 +807,9 @@ async function submitDialog() { .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); } +.access-denied-container { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; padding: 2rem; } +.access-denied-content { text-align: center; max-width: 480px; } +.access-denied-content h2 { margin: 0 0 1rem; color: var(--color-heading); font-size: 1.5rem; } +.access-denied-content .error-detail { margin: 0 0 1.5rem; color: var(--color-text-muted); } +.access-denied-content .button-row { display: flex; gap: 0.75rem; justify-content: center; } diff --git a/frontend/src/admin/AdminDialogs.vue b/frontend/src/admin/AdminDialogs.vue index 17e7801..a2609b9 100644 --- a/frontend/src/admin/AdminDialogs.vue +++ b/frontend/src/admin/AdminDialogs.vue @@ -1,15 +1,18 @@