From af5a48f565cce159aa2136f41a3f6e3105f0d118 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 5 Feb 2026 13:57:07 +0000 Subject: [PATCH] API/DB cleanup for flat URLs that don't include org where users etc. are referred to. Implement user deletion in admin app and API, UI improvement. Reset token DB factory function revised to create passphrase and key internally. Removed unneeded functions and args, using update_user_role instead of a separate deleted _in_organization function. --- frontend/auth/admin/AdminApp.vue | 55 ++++- frontend/src/admin/AdminUserDetail.vue | 45 ++-- paskia/bootstrap.py | 6 +- paskia/db/__init__.py | 2 - paskia/db/operations.py | 65 ++--- paskia/db/structs.py | 36 +++ paskia/fastapi/admin.py | 173 +++++++------- paskia/fastapi/remote.py | 7 +- paskia/fastapi/reset.py | 6 +- paskia/fastapi/user.py | 6 +- paskia/migrate/__init__.py | 25 +- tests/test_admin.py | 314 +++++++++++++++++-------- 12 files changed, 448 insertions(+), 292 deletions(-) diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index 0479481..37d06a6 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -239,12 +239,44 @@ function deleteOrg(org) { function createUserInRole(org, role) { openDialog('user-create', { org, role }) } -async function moveUserToRole(org, user, targetRoleDisplayName) { - if (user.role === targetRoleDisplayName) return +function deleteUser(user, userDetail) { + const credentialCount = userDetail?.credentials?.length || 0 + const userUuid = user.uuid + const userName = user.display_name + const orgUuid = user.org // org UUID is stored in selectedUser + + if (credentialCount === 0) { + // No credentials, safe to delete directly + performUserDeletion(userUuid, userName, orgUuid) + return + } + + const passkeys = credentialCount === 1 ? '1 passkey' : `${credentialCount} passkeys` + openDialog('confirm', { + message: `Delete user "${userName}" with ${passkeys}? This action cannot be undone.`, + action: async () => { + await performUserDeletion(userUuid, userName, orgUuid) + } + }) +} + +async function performUserDeletion(userUuid, userName, orgUuid) { try { - await apiJson(`/auth/api/admin/orgs/${org.uuid}/users/${user.uuid}/role`, { + await apiJson(`/auth/api/admin/users/${userUuid}`, { method: 'DELETE' }) + authStore.showMessage(`User "${userName}" deleted.`, 'success', 2500) + await loadOrgs() + window.location.hash = `#org/${orgUuid}` + } catch (e) { + authStore.showMessage(e.message || 'Failed to delete user', 'error') + } +} + +async function moveUserToRole(user, targetRoleUuid) { + if (user.role_uuid === targetRoleUuid) return + try { + await apiJson(`/auth/api/admin/users/${user.uuid}/role`, { method: 'PATCH', - body: { role: targetRoleDisplayName } + body: { role_uuid: targetRoleUuid } }) await loadOrgs() } catch (e) { @@ -268,7 +300,7 @@ function onRoleDrop(e, org, role) { const data = JSON.parse(e.dataTransfer.getData('text/plain')) if (data.org !== org.uuid) return // only within same org const user = org.roles.flatMap(r => r.users).find(u => u.uuid === data.user_uuid) - if (user) moveUserToRole(org, user, role.display_name) + if (user) moveUserToRole(user, role.uuid) } catch (_) { /* ignore */ } } @@ -279,7 +311,7 @@ function updateRole(role) { openDialog('role-update', { role, name: role.display function deleteRole(role) { // UI only allows deleting empty roles, so no confirmation needed - apiJson(`/auth/api/admin/orgs/${role.org}/roles/${role.uuid}`, { method: 'DELETE' }) + apiJson(`/auth/api/admin/roles/${role.uuid}`, { method: 'DELETE' }) .then(() => { authStore.showMessage(`Role "${role.display_name}" deleted.`, 'success', 2500) loadOrgs() @@ -299,7 +331,7 @@ async function toggleRolePermission(role, pid, checked) { try { const method = checked ? 'POST' : 'DELETE' - await apiJson(`/auth/api/admin/orgs/${role.org}/roles/${role.uuid}/permissions/${pid}`, { + await apiJson(`/auth/api/admin/roles/${role.uuid}/permissions/${pid}`, { method }) await loadOrgs() @@ -406,7 +438,7 @@ const breadcrumbEntries = computed(() => { watch(selectedUser, async (u) => { if (!u) { userDetail.value = null; return } try { - userDetail.value = await apiJson(`/auth/api/admin/orgs/${u.org}/users/${u.uuid}`) + userDetail.value = await apiJson(`/auth/api/admin/users/${u.uuid}`) } catch (e) { userDetail.value = { error: e.message } } @@ -542,7 +574,7 @@ async function refreshUserDetail() { await loadOrgs() if (selectedUser.value) { try { - userDetail.value = await apiJson(`/auth/api/admin/orgs/${selectedUser.value.org}/users/${selectedUser.value.uuid}`) + userDetail.value = await apiJson(`/auth/api/admin/users/${selectedUser.value.uuid}`) } catch (e) { authStore.showMessage(e.message || 'Failed to reload user', 'error') } } } @@ -604,7 +636,7 @@ async function submitDialog() { // Close dialog immediately, then perform async operation closeDialog() - apiJson(`/auth/api/admin/orgs/${role.org}/roles/${role.uuid}`, { method: 'PATCH', body: { display_name: name } }) + apiJson(`/auth/api/admin/roles/${role.uuid}`, { method: 'PATCH', body: { display_name: name } }) .then(() => { authStore.showMessage(`Role renamed to "${name}".`, 'success', 2500) loadOrgs() @@ -632,7 +664,7 @@ async function submitDialog() { // Close dialog immediately, then perform async operation closeDialog() - apiJson(`/auth/api/admin/orgs/${user.org}/users/${user.uuid}/display-name`, { method: 'PATCH', body: { display_name: name } }) + apiJson(`/auth/api/admin/users/${user.uuid}/display-name`, { method: 'PATCH', body: { display_name: name } }) .then(() => { authStore.showMessage(`User renamed to "${name}".`, 'success', 2500) onUserNameSaved() @@ -771,6 +803,7 @@ async function submitDialog() { @edit-user-name="editUserName" @close-reg-modal="showRegModal = false" @navigate-out="handlePanelNavigateOut" + @delete-user="deleteUser(selectedUser, userDetail)" /> + > +
+ + +
+
{{ userDetail.error }}