Fix earlier merges done from main which were incomplete.
This commit is contained in:
+14
-3
@@ -47,14 +47,13 @@ const isHostMode = computed(() => {
|
|||||||
const configuredHost = normalizeHost(authHost)
|
const configuredHost = normalizeHost(authHost)
|
||||||
return currentHost !== configuredHost
|
return currentHost !== configuredHost
|
||||||
})
|
})
|
||||||
const userUuid = computed(() => store.userInfo?.ctx.user.uuid)
|
|
||||||
|
|
||||||
function terminateSession() {
|
function terminateSession() {
|
||||||
store.userInfo = null
|
store.userInfo = null
|
||||||
viewState.value = 'terminal'
|
viewState.value = 'terminal'
|
||||||
}
|
}
|
||||||
|
|
||||||
const userUuidGetter = () => store.userInfo?.ctx.user.uuid
|
const userUuidGetter = () => store.ctx?.user.uuid
|
||||||
const sessionValidator = new SessionValidator(userUuidGetter, terminateSession)
|
const sessionValidator = new SessionValidator(userUuidGetter, terminateSession)
|
||||||
|
|
||||||
onMounted(() => sessionValidator.start())
|
onMounted(() => sessionValidator.start())
|
||||||
@@ -62,11 +61,23 @@ onUnmounted(() => sessionValidator.stop())
|
|||||||
|
|
||||||
async function loadUserInfo() {
|
async function loadUserInfo() {
|
||||||
try {
|
try {
|
||||||
store.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' })
|
const [validateData, userInfoData] = await Promise.all([
|
||||||
|
apiJson('/auth/api/validate', { method: 'POST' }),
|
||||||
|
apiJson('/auth/api/user-info', { method: 'POST' })
|
||||||
|
])
|
||||||
|
store.userInfo = userInfoData
|
||||||
|
store.ctx = validateData.ctx
|
||||||
|
// Verify that the user UUIDs match between user-info and validate responses
|
||||||
|
if (store.userInfo.user.uuid !== store.ctx.user.uuid) {
|
||||||
|
console.error('User UUID mismatch between user-info and validate responses')
|
||||||
|
window.location.reload()
|
||||||
|
return false
|
||||||
|
}
|
||||||
viewState.value = 'profile'
|
viewState.value = 'profile'
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
store.userInfo = null
|
store.userInfo = null
|
||||||
|
store.ctx = null
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,11 +85,12 @@ onUnmounted(() => {
|
|||||||
const permissionSummary = computed(() => {
|
const permissionSummary = computed(() => {
|
||||||
const summary = {}
|
const summary = {}
|
||||||
for (const o of orgs.value) {
|
for (const o of orgs.value) {
|
||||||
const orgBase = { uuid: o.uuid, display_name: o.display_name }
|
const orgBase = { uuid: o.uuid, display_name: o.org.display_name }
|
||||||
const orgPerms = new Set(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
|
// Org-level permissions (direct) - only count if org can grant them
|
||||||
for (const pid of o.permissions || []) {
|
for (const pid of Object.keys(o.permissions || {})) {
|
||||||
if (!summary[pid]) summary[pid] = { orgs: [], orgSet: new Set(), userCount: 0 }
|
if (!summary[pid]) summary[pid] = { orgs: [], orgSet: new Set(), userCount: 0 }
|
||||||
if (!summary[pid].orgSet.has(o.uuid)) {
|
if (!summary[pid].orgSet.has(o.uuid)) {
|
||||||
summary[pid].orgs.push(orgBase)
|
summary[pid].orgs.push(orgBase)
|
||||||
@@ -98,17 +99,18 @@ const permissionSummary = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Role-based permissions (inheritance) - only count if org can grant them
|
// Role-based permissions (inheritance) - only count if org can grant them
|
||||||
for (const r of o.roles) {
|
for (const [roleUuid, r] of Object.entries(o.roles || {})) {
|
||||||
for (const pid of r.permissions) {
|
// r.permissions is dict[UUID, bool]
|
||||||
|
for (const pid of Object.keys(r.permissions || {})) {
|
||||||
// Only count if the org can grant this permission
|
// 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]) summary[pid] = { orgs: [], orgSet: new Set(), userCount: 0 }
|
||||||
if (!summary[pid].orgSet.has(o.uuid)) {
|
if (!summary[pid].orgSet.has(o.uuid)) {
|
||||||
summary[pid].orgs.push(orgBase)
|
summary[pid].orgs.push(orgBase)
|
||||||
summary[pid].orgSet.add(o.uuid)
|
summary[pid].orgSet.add(o.uuid)
|
||||||
}
|
}
|
||||||
summary[pid].userCount += r.users.length
|
summary[pid].userCount += roleUserCount(o, roleUuid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,18 +167,33 @@ function parseHash() {
|
|||||||
|
|
||||||
async function loadOrgs() {
|
async function loadOrgs() {
|
||||||
const data = await apiJson('/auth/api/admin/orgs')
|
const data = await apiJson('/auth/api/admin/orgs')
|
||||||
orgs.value = data.map(o => {
|
orgs.value = Object.entries(data).map(([uuid, o]) => ({ uuid, ...o }))
|
||||||
const roles = o.roles.map(r => ({ ...r, org: o.uuid, users: [] }))
|
|
||||||
const roleMap = Object.fromEntries(roles.map(r => [r.display_name, r]))
|
|
||||||
for (const u of o.users || []) {
|
|
||||||
if (roleMap[u.role]) roleMap[u.role].users.push(u)
|
|
||||||
}
|
}
|
||||||
return { ...o, roles }
|
|
||||||
|
// Helper to get users for a role as sorted array of [uuid, user]
|
||||||
|
function roleUsers(org, roleUuid) {
|
||||||
|
return Object.entries(org.users)
|
||||||
|
.filter(([_, u]) => u.role === roleUuid)
|
||||||
|
.sort(([, a], [, b]) => {
|
||||||
|
const nameA = a.display_name.toLowerCase()
|
||||||
|
const nameB = b.display_name.toLowerCase()
|
||||||
|
return nameA.localeCompare(nameB)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to count users in a role
|
||||||
|
function roleUserCount(org, roleUuid) {
|
||||||
|
return Object.values(org.users).filter(u => u.role === roleUuid).length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to count total users in an org
|
||||||
|
function orgUserCount(org) {
|
||||||
|
return Object.keys(org.users).length
|
||||||
|
}
|
||||||
|
|
||||||
async function loadPermissions() {
|
async function loadPermissions() {
|
||||||
permissions.value = await apiJson('/auth/api/admin/permissions')
|
const data = await apiJson('/auth/api/admin/permissions')
|
||||||
|
permissions.value = Object.entries(data).map(([uuid, p]) => ({ uuid, ...p }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadOidcClients() {
|
async function loadOidcClients() {
|
||||||
@@ -268,13 +285,13 @@ async function performOrgDeletion(orgUuid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteOrg(org) {
|
function deleteOrg(org) {
|
||||||
const userCount = org.roles.reduce((acc, r) => acc + r.users.length, 0)
|
const userCount = orgUserCount(org)
|
||||||
|
|
||||||
if (userCount === 0) {
|
if (userCount === 0) {
|
||||||
// No users in the organization, safe to delete directly
|
// No users in the organization, safe to delete directly
|
||||||
performOrgDeletion(org.uuid)
|
performOrgDeletion(org.uuid)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
authStore.showMessage(`Organization "${org.display_name}" deleted.`, 'success', 2500)
|
authStore.showMessage(`Organization "${org.org.display_name}" deleted.`, 'success', 2500)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
authStore.showMessage(e.message || 'Failed to delete organization', 'error')
|
authStore.showMessage(e.message || 'Failed to delete organization', 'error')
|
||||||
@@ -283,13 +300,14 @@ function deleteOrg(org) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build detailed breakdown of users by role
|
// Build detailed breakdown of users by role
|
||||||
const roleParts = org.roles
|
const roleParts = Object.entries(org.roles)
|
||||||
.filter(r => r.users.length > 0)
|
.map(([uuid, r]) => ({ role: r, count: roleUserCount(org, uuid) }))
|
||||||
.map(r => `${r.users.length} ${r.display_name}`)
|
.filter(x => x.count > 0)
|
||||||
|
.map(x => `${x.count} ${x.role.display_name}`)
|
||||||
|
|
||||||
const affects = roleParts.join(', ')
|
const affects = roleParts.join(', ')
|
||||||
|
|
||||||
openDialog('confirm', { message: `Delete organization "${org.display_name}", including accounts of ${affects})?`, action: async () => {
|
openDialog('confirm', { message: `Delete organization "${org.org.display_name}", including accounts of ${affects})?`, action: async () => {
|
||||||
await performOrgDeletion(org.uuid)
|
await performOrgDeletion(org.uuid)
|
||||||
} })
|
} })
|
||||||
}
|
}
|
||||||
@@ -297,7 +315,7 @@ function deleteOrg(org) {
|
|||||||
function createUserInRole(org, role) { openDialog('user-create', { org, role }) }
|
function createUserInRole(org, role) { openDialog('user-create', { org, role }) }
|
||||||
|
|
||||||
function deleteUser(user, userDetail) {
|
function deleteUser(user, userDetail) {
|
||||||
const credentialCount = userDetail?.credentials?.length || 0
|
const credentialCount = userDetail?.credentials ? Object.keys(userDetail.credentials).length : 0
|
||||||
const userUuid = user.uuid
|
const userUuid = user.uuid
|
||||||
const userName = user.display_name
|
const userName = user.display_name
|
||||||
const orgUuid = user.org // org UUID is stored in selectedUser
|
const orgUuid = user.org // org UUID is stored in selectedUser
|
||||||
@@ -341,9 +359,9 @@ async function moveUserToRole(user, targetRoleUuid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUserDragStart(e, user, org) {
|
function onUserDragStart(e, userUuid, org) {
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
e.dataTransfer.setData('text/plain', JSON.stringify({ user_uuid: user.uuid, org }))
|
e.dataTransfer.setData('text/plain', JSON.stringify({ user_uuid: userUuid, org }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRoleDragOver(e) {
|
function onRoleDragOver(e) {
|
||||||
@@ -356,7 +374,7 @@ function onRoleDrop(e, org, role) {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.dataTransfer.getData('text/plain'))
|
const data = JSON.parse(e.dataTransfer.getData('text/plain'))
|
||||||
if (data.org !== org.uuid) return // only within same org
|
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)
|
const user = org.users[data.user_uuid]
|
||||||
if (user) moveUserToRole(user, role.uuid)
|
if (user) moveUserToRole(user, role.uuid)
|
||||||
} catch (_) { /* ignore */ }
|
} catch (_) { /* ignore */ }
|
||||||
}
|
}
|
||||||
@@ -379,11 +397,14 @@ function deleteRole(role) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleRolePermission(role, pid, checked) {
|
async function toggleRolePermission(role, pid, checked) {
|
||||||
// Optimistic update
|
// Optimistic update - role.permissions is dict[UUID, bool]
|
||||||
const prevPermissions = [...role.permissions]
|
const prevPermissions = { ...role.permissions }
|
||||||
const newPermissions = checked
|
const newPermissions = { ...role.permissions }
|
||||||
? [...role.permissions, pid]
|
if (checked) {
|
||||||
: role.permissions.filter(p => p !== pid)
|
newPermissions[pid] = true
|
||||||
|
} else {
|
||||||
|
delete newPermissions[pid]
|
||||||
|
}
|
||||||
role.permissions = newPermissions
|
role.permissions = newPermissions
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -411,8 +432,8 @@ function deletePermission(p) {
|
|||||||
// Count roles that have this permission
|
// Count roles that have this permission
|
||||||
let roleCount = 0
|
let roleCount = 0
|
||||||
for (const org of orgs.value) {
|
for (const org of orgs.value) {
|
||||||
for (const role of org.roles) {
|
for (const role of Object.values(org.roles)) {
|
||||||
if (role.permissions.includes(p.uuid)) {
|
if (p.uuid in (role.permissions || {})) {
|
||||||
roleCount++
|
roleCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,9 +559,10 @@ function openUser(u) {
|
|||||||
const selectedUser = computed(() => {
|
const selectedUser = computed(() => {
|
||||||
if (!currentUserId.value) return null
|
if (!currentUserId.value) return null
|
||||||
for (const o of orgs.value) {
|
for (const o of orgs.value) {
|
||||||
for (const r of o.roles) {
|
const u = o.users[currentUserId.value]
|
||||||
const u = r.users.find(x => x.uuid === currentUserId.value)
|
if (u) {
|
||||||
if (u) return { ...u, org: o.uuid, role_display_name: r.display_name }
|
const role = o.roles[u.role]
|
||||||
|
return { ...u, uuid: currentUserId.value, org: o.uuid, role_display_name: role?.display_name }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -554,7 +576,7 @@ const breadcrumbEntries = computed(() => {
|
|||||||
// For org admins, combine Admin and their org
|
// For org admins, combine Admin and their org
|
||||||
if (isOrgAdmin.value && !isMasterAdmin.value && orgs.value.length > 0) {
|
if (isOrgAdmin.value && !isMasterAdmin.value && orgs.value.length > 0) {
|
||||||
const org = orgs.value[0]
|
const org = orgs.value[0]
|
||||||
entries.push({ label: `Admin: ${org.display_name}`, href: `#org/${org.uuid}` })
|
entries.push({ label: `Admin: ${org.org.display_name}`, href: `#org/${org.uuid}` })
|
||||||
} else {
|
} else {
|
||||||
entries.push({ label: 'Admin', href: adminUiPath() })
|
entries.push({ label: 'Admin', href: adminUiPath() })
|
||||||
}
|
}
|
||||||
@@ -567,7 +589,7 @@ const breadcrumbEntries = computed(() => {
|
|||||||
// Add org breadcrumb only if it's not already included in the Admin entry
|
// 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
|
const adminOrg = (isOrgAdmin.value && !isMasterAdmin.value && orgs.value.length > 0) ? orgs.value[0] : null
|
||||||
if (orgToShow && (!adminOrg || orgToShow.uuid !== adminOrg.uuid)) {
|
if (orgToShow && (!adminOrg || orgToShow.uuid !== adminOrg.uuid)) {
|
||||||
entries.push({ label: orgToShow.display_name, href: `#org/${orgToShow.uuid}` })
|
entries.push({ label: orgToShow.org.display_name, href: `#org/${orgToShow.uuid}` })
|
||||||
}
|
}
|
||||||
if (currentOidcId.value) {
|
if (currentOidcId.value) {
|
||||||
const label = editingOidcClient.value?.isNew ? 'New Client' : (editingOidcClient.value?.name || 'OIDC Client')
|
const label = editingOidcClient.value?.isNew ? 'New Client' : (editingOidcClient.value?.name || 'OIDC Client')
|
||||||
@@ -594,14 +616,23 @@ function generateUserRegistrationLink(u) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleOrgPermission(org, permId, checked) {
|
async function toggleOrgPermission(org, permId, checked) {
|
||||||
// Build next permission list
|
// org.permissions is dict[UUID, Permission]
|
||||||
const has = org.permissions.includes(permId)
|
const has = permId in org.permissions
|
||||||
if (checked && has) return
|
if (checked && has) return
|
||||||
if (!checked && !has) return
|
if (!checked && !has) return
|
||||||
const next = checked ? [...org.permissions, permId] : org.permissions.filter(p => p !== permId)
|
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
const prev = [...org.permissions]
|
const prev = { ...org.permissions }
|
||||||
|
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
|
org.permissions = next
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ permission_uuid: permId })
|
const params = new URLSearchParams({ permission_uuid: permId })
|
||||||
await apiJson(`/auth/api/admin/orgs/${org.uuid}/permission?${params.toString()}`, { method: checked ? 'POST' : 'DELETE' })
|
await apiJson(`/auth/api/admin/orgs/${org.uuid}/permission?${params.toString()}`, { method: checked ? 'POST' : 'DELETE' })
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ const permMatrixRef = ref(null)
|
|||||||
const rolesGridRef = ref(null)
|
const rolesGridRef = ref(null)
|
||||||
|
|
||||||
const sortedRoles = computed(() => {
|
const sortedRoles = computed(() => {
|
||||||
return [...props.selectedOrg.roles].sort((a, b) => {
|
// o.roles is dict[UUID, Role], convert to array for sorting with uuid added
|
||||||
|
return Object.entries(props.selectedOrg.roles).map(([uuid, r]) => ({ uuid, ...r })).sort((a, b) => {
|
||||||
const nameA = a.display_name.toLowerCase()
|
const nameA = a.display_name.toLowerCase()
|
||||||
const nameB = b.display_name.toLowerCase()
|
const nameB = b.display_name.toLowerCase()
|
||||||
if (nameA !== nameB) {
|
if (nameA !== nameB) {
|
||||||
@@ -28,10 +29,27 @@ const sortedRoles = computed(() => {
|
|||||||
|
|
||||||
// Get org's grantable permissions as full permission objects (with UUIDs)
|
// Get org's grantable permissions as full permission objects (with UUIDs)
|
||||||
const orgPermissions = computed(() => {
|
const orgPermissions = computed(() => {
|
||||||
const uuidSet = new Set(props.selectedOrg.permissions || [])
|
// props.selectedOrg.permissions is dict[UUID, Permission]
|
||||||
|
const uuidSet = new Set(Object.keys(props.selectedOrg.permissions || {}))
|
||||||
return props.permissions.filter(p => uuidSet.has(p.uuid))
|
return props.permissions.filter(p => uuidSet.has(p.uuid))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Get users for a role as sorted array of { uuid, ...user }
|
||||||
|
function roleUsers(roleUuid) {
|
||||||
|
return Object.entries(props.selectedOrg.users)
|
||||||
|
.filter(([_, u]) => u.role === roleUuid)
|
||||||
|
.map(([uuid, u]) => ({ uuid, ...u }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const nameA = a.display_name.toLowerCase()
|
||||||
|
const nameB = b.display_name.toLowerCase()
|
||||||
|
return nameA.localeCompare(nameB)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function roleUserCount(roleUuid) {
|
||||||
|
return Object.values(props.selectedOrg.users).filter(u => u.role === roleUuid).length
|
||||||
|
}
|
||||||
|
|
||||||
function permissionDisplayName(scope) {
|
function permissionDisplayName(scope) {
|
||||||
return props.permissions.find(p => p.scope === scope)?.display_name || scope
|
return props.permissions.find(p => p.scope === scope)?.display_name || scope
|
||||||
}
|
}
|
||||||
@@ -83,7 +101,7 @@ function handleMatrixKeydown(event) {
|
|||||||
|
|
||||||
// Calculate grid dimensions
|
// Calculate grid dimensions
|
||||||
const cols = sortedRoles.value.length
|
const cols = sortedRoles.value.length
|
||||||
const rows = props.selectedOrg.permissions.length
|
const rows = Object.keys(props.selectedOrg.permissions).length
|
||||||
|
|
||||||
const currentRow = Math.floor(currentIndex / cols)
|
const currentRow = Math.floor(currentIndex / cols)
|
||||||
const currentCol = currentIndex % cols
|
const currentCol = currentIndex % cols
|
||||||
@@ -219,7 +237,7 @@ function handleRoleHeaderKeydown(event, roleIndex) {
|
|||||||
if (checkboxes?.length) {
|
if (checkboxes?.length) {
|
||||||
// Focus the checkbox in the corresponding column
|
// Focus the checkbox in the corresponding column
|
||||||
const cols = sortedRoles.value.length
|
const cols = sortedRoles.value.length
|
||||||
const rows = props.selectedOrg.permissions.length
|
const rows = Object.keys(props.selectedOrg.permissions).length
|
||||||
const targetIndex = (rows - 1) * cols + roleIndex
|
const targetIndex = (rows - 1) * cols + roleIndex
|
||||||
if (checkboxes[targetIndex]) checkboxes[targetIndex].focus()
|
if (checkboxes[targetIndex]) checkboxes[targetIndex].focus()
|
||||||
else checkboxes[checkboxes.length - 1].focus()
|
else checkboxes[checkboxes.length - 1].focus()
|
||||||
@@ -287,7 +305,7 @@ defineExpose({ focusFirstElement })
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h2 class="org-title" ref="orgTitleRef" @keydown="handleTitleKeydown" :title="selectedOrg.uuid">
|
<h2 class="org-title" ref="orgTitleRef" @keydown="handleTitleKeydown" :title="selectedOrg.uuid">
|
||||||
<span class="org-name">{{ selectedOrg.display_name }}</span>
|
<span class="org-name">{{ selectedOrg.org.display_name }}</span>
|
||||||
<button @click="$emit('updateOrg', selectedOrg)" class="icon-btn" aria-label="Rename organization" title="Rename organization">✏️</button>
|
<button @click="$emit('updateOrg', selectedOrg)" class="icon-btn" aria-label="Rename organization" title="Rename organization">✏️</button>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -317,7 +335,7 @@ defineExpose({ focusFirstElement })
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="r.permissions.includes(p.uuid)"
|
:checked="p.uuid in (r.permissions || {})"
|
||||||
@change="e => toggleRolePermission(r, p.uuid, e.target.checked)"
|
@change="e => toggleRolePermission(r, p.uuid, e.target.checked)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,28 +357,21 @@ defineExpose({ focusFirstElement })
|
|||||||
<strong class="role-name" :title="r.uuid">
|
<strong class="role-name" :title="r.uuid">
|
||||||
<span>{{ r.display_name }}</span>
|
<span>{{ r.display_name }}</span>
|
||||||
<button @click="$emit('updateRole', r)" class="icon-btn" aria-label="Edit role" title="Edit role">✏️</button>
|
<button @click="$emit('updateRole', r)" class="icon-btn" aria-label="Edit role" title="Edit role">✏️</button>
|
||||||
<button v-if="r.users.length === 0" @click="$emit('deleteRole', r)" class="icon-btn delete-icon" aria-label="Delete role" title="Delete role">❌</button>
|
<button v-if="roleUserCount(r.uuid) === 0" @click="$emit('deleteRole', r)" class="icon-btn delete-icon" aria-label="Delete role" title="Delete role">❌</button>
|
||||||
</strong>
|
</strong>
|
||||||
<div class="role-actions">
|
<div class="role-actions">
|
||||||
<button @click="$emit('createUserInRole', selectedOrg, r)" class="plus-btn" aria-label="Add user" title="Add user">➕</button>
|
<button @click="$emit('createUserInRole', selectedOrg, r)" class="plus-btn" aria-label="Add user" title="Add user">➕</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="r.users.length > 0">
|
<template v-if="roleUserCount(r.uuid) > 0">
|
||||||
<ul class="user-list" @keydown="handleUserListKeydown">
|
<ul class="user-list" @keydown="handleUserListKeydown">
|
||||||
<li
|
<li
|
||||||
v-for="u in r.users.slice().sort((a, b) => {
|
v-for="u in roleUsers(r.uuid)"
|
||||||
const nameA = a.display_name.toLowerCase()
|
|
||||||
const nameB = b.display_name.toLowerCase()
|
|
||||||
if (nameA !== nameB) {
|
|
||||||
return nameA.localeCompare(nameB)
|
|
||||||
}
|
|
||||||
return a.uuid.localeCompare(b.uuid)
|
|
||||||
})"
|
|
||||||
:key="u.uuid"
|
:key="u.uuid"
|
||||||
class="user-chip"
|
class="user-chip"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="e => $emit('onUserDragStart', e, u, selectedOrg.uuid)"
|
@dragstart="e => $emit('onUserDragStart', e, u.uuid, selectedOrg.uuid)"
|
||||||
@click="$emit('openUser', u)"
|
@click="$emit('openUser', u)"
|
||||||
@keydown.enter="$emit('openUser', u)"
|
@keydown.enter="$emit('openUser', u)"
|
||||||
:title="u.uuid"
|
:title="u.uuid"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const oidcActionsRef = ref(null)
|
|||||||
const oidcTableRef = ref(null)
|
const oidcTableRef = ref(null)
|
||||||
|
|
||||||
const sortedOrgs = computed(() => [...props.orgs].sort((a,b)=> {
|
const sortedOrgs = computed(() => [...props.orgs].sort((a,b)=> {
|
||||||
const nameCompare = a.display_name.localeCompare(b.display_name)
|
const nameCompare = a.org.display_name.localeCompare(b.org.display_name)
|
||||||
return nameCompare !== 0 ? nameCompare : a.uuid.localeCompare(b.uuid)
|
return nameCompare !== 0 ? nameCompare : a.uuid.localeCompare(b.uuid)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -67,13 +67,18 @@ function permissionDisplayName(scope) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRoleNames(org) {
|
function getRoleNames(org) {
|
||||||
return org.roles
|
// org.roles is dict[UUID, Role]
|
||||||
|
return Object.values(org.roles)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.display_name.localeCompare(b.display_name))
|
.sort((a, b) => a.display_name.localeCompare(b.display_name))
|
||||||
.map(r => r.display_name)
|
.map(r => r.display_name)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function orgUserCount(org) {
|
||||||
|
return Object.keys(org.users).length
|
||||||
|
}
|
||||||
|
|
||||||
// Table navigation for both org and permissions tables
|
// Table navigation for both org and permissions tables
|
||||||
function handleTableKeydown(event, tableType) {
|
function handleTableKeydown(event, tableType) {
|
||||||
if (props.navigationDisabled) return
|
if (props.navigationDisabled) return
|
||||||
@@ -297,11 +302,11 @@ defineExpose({ focusFirstElement })
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="o in sortedOrgs" :key="o.uuid">
|
<tr v-for="o in sortedOrgs" :key="o.uuid">
|
||||||
<td>
|
<td>
|
||||||
<a href="#org/{{o.uuid}}" @click.prevent="$emit('openOrg', o)">{{ o.display_name }}</a>
|
<a href="#org/{{o.uuid}}" @click.prevent="$emit('openOrg', o)">{{ o.org.display_name }}</a>
|
||||||
<button v-if="isMasterAdmin || isOrgAdmin" @click="$emit('updateOrg', o)" class="icon-btn edit-org-btn" aria-label="Rename organization" title="Rename organization">✏️</button>
|
<button v-if="isMasterAdmin || isOrgAdmin" @click="$emit('updateOrg', o)" class="icon-btn edit-org-btn" aria-label="Rename organization" title="Rename organization">✏️</button>
|
||||||
</td>
|
</td>
|
||||||
<td class="role-names">{{ getRoleNames(o) }}</td>
|
<td class="role-names">{{ getRoleNames(o) }}</td>
|
||||||
<td class="center">{{ o.roles.reduce((acc,r)=>acc + r.users.length,0) }}</td>
|
<td class="center">{{ orgUserCount(o) }}</td>
|
||||||
<td v-if="isMasterAdmin" class="center">
|
<td v-if="isMasterAdmin" class="center">
|
||||||
<button @click="$emit('deleteOrg', o)" class="icon-btn delete-icon" aria-label="Delete organization" title="Delete organization">❌</button>
|
<button @click="$emit('deleteOrg', o)" class="icon-btn delete-icon" aria-label="Delete organization" title="Delete organization">❌</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -323,9 +328,9 @@ defineExpose({ focusFirstElement })
|
|||||||
v-for="o in sortedOrgs"
|
v-for="o in sortedOrgs"
|
||||||
:key="'head-' + o.uuid"
|
:key="'head-' + o.uuid"
|
||||||
class="grid-head org-head"
|
class="grid-head org-head"
|
||||||
:title="o.display_name"
|
:title="o.org.display_name"
|
||||||
>
|
>
|
||||||
<span>{{ o.display_name }}</span>
|
<span>{{ o.org.display_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-for="p in sortedPermissions" :key="p.uuid">
|
<template v-for="p in sortedPermissions" :key="p.uuid">
|
||||||
@@ -339,7 +344,7 @@ defineExpose({ focusFirstElement })
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="o.permissions.includes(p.uuid)"
|
:checked="p.uuid in o.permissions"
|
||||||
@change="e => $emit('toggleOrgPermission', o, p.uuid, e.target.checked)"
|
@change="e => $emit('toggleOrgPermission', o, p.uuid, e.target.checked)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -180,15 +180,15 @@ defineExpose({ focusFirstElement })
|
|||||||
<div ref="userInfoRef" @keydown="handleUserInfoKeydown">
|
<div ref="userInfoRef" @keydown="handleUserInfoKeydown">
|
||||||
<UserBasicInfo
|
<UserBasicInfo
|
||||||
v-if="userDetail && !userDetail.error"
|
v-if="userDetail && !userDetail.error"
|
||||||
:name="userDetail.display_name || selectedUser.display_name"
|
:name="userDetail.user.display_name || selectedUser.display_name"
|
||||||
:visits="userDetail.visits"
|
:visits="userDetail.user.visits"
|
||||||
:created-at="userDetail.created_at"
|
:created-at="userDetail.user.created_at"
|
||||||
:last-seen="userDetail.last_seen"
|
:last-seen="userDetail.user.last_seen"
|
||||||
:email="userDetail.email"
|
:email="userDetail.user.email"
|
||||||
:telephone="userDetail.telephone"
|
:telephone="userDetail.user.telephone"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:org-display-name="userDetail.org.display_name"
|
:org-display-name="userDetail.org.display_name"
|
||||||
:role-name="userDetail.role"
|
:role-name="userDetail.role.display_name"
|
||||||
:update-endpoint="`/auth/api/admin/users/${selectedUser.uuid}/info`"
|
:update-endpoint="`/auth/api/admin/users/${selectedUser.uuid}/info`"
|
||||||
@saved="$emit('onUserNameSaved')"
|
@saved="$emit('onUserNameSaved')"
|
||||||
@edit="handleEditName"
|
@edit="handleEditName"
|
||||||
@@ -199,7 +199,7 @@ defineExpose({ focusFirstElement })
|
|||||||
@click="$emit('generateUserRegistrationLink', selectedUser)"
|
@click="$emit('generateUserRegistrationLink', selectedUser)"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
title="Generate a one-time link for this user"
|
title="Generate a one-time link for this user"
|
||||||
>{{ userDetail?.credentials?.length ? 'Recovery Link' : 'Registration Link' }}</button>
|
>{{ userDetail?.credentials && Object.keys(userDetail.credentials).length > 0 ? 'Recovery Link' : 'Registration Link' }}</button>
|
||||||
<button
|
<button
|
||||||
class="btn-danger"
|
class="btn-danger"
|
||||||
@click="handleDeleteUser"
|
@click="handleDeleteUser"
|
||||||
@@ -218,7 +218,7 @@ defineExpose({ focusFirstElement })
|
|||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<CredentialList
|
<CredentialList
|
||||||
ref="credentialListRef"
|
ref="credentialListRef"
|
||||||
:credentials="userDetail.credentials"
|
:credentials="userDetail.credentials ? Object.values(userDetail.credentials) : []"
|
||||||
:aaguid-info="userDetail.aaguid_info"
|
:aaguid-info="userDetail.aaguid_info"
|
||||||
:allow-delete="true"
|
:allow-delete="true"
|
||||||
:hovered-credential-uuid="hoveredCredentialUuid"
|
:hovered-credential-uuid="hoveredCredentialUuid"
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ const currentHost = window.location.host
|
|||||||
const userInfoSection = ref(null)
|
const userInfoSection = ref(null)
|
||||||
const buttonRow = ref(null)
|
const buttonRow = ref(null)
|
||||||
|
|
||||||
const ctx = computed(() => authStore.userInfo?.ctx || null)
|
const ctx = computed(() => authStore.userInfo || null)
|
||||||
const orgDisplayName = computed(() => ctx.value?.org.display_name ?? '')
|
const orgDisplayName = computed(() => ctx.value?.org?.display_name ?? '')
|
||||||
const roleDisplayName = computed(() => ctx.value?.role.display_name ?? '')
|
const roleDisplayName = computed(() => ctx.value?.role?.display_name ?? '')
|
||||||
|
|
||||||
const headingTitle = computed(() => {
|
const headingTitle = computed(() => {
|
||||||
const service = authStore.settings?.rp_name
|
const service = authStore.settings?.rp_name
|
||||||
|
|||||||
@@ -12,18 +12,18 @@
|
|||||||
|
|
||||||
<section class="section-block section-block--constrained" ref="userInfoSection">
|
<section class="section-block section-block--constrained" ref="userInfoSection">
|
||||||
<UserBasicInfo
|
<UserBasicInfo
|
||||||
v-if="authStore.userInfo?.ctx"
|
v-if="authStore.userInfo?.user"
|
||||||
ref="userBasicInfo"
|
ref="userBasicInfo"
|
||||||
:name="authStore.userInfo.ctx.user.display_name"
|
:name="authStore.userInfo.user.display_name"
|
||||||
:email="authStore.userInfo.ctx.user.email"
|
:email="authStore.userInfo.user.email"
|
||||||
:preferred_username="authStore.userInfo.ctx.user.preferred_username"
|
:preferred_username="authStore.userInfo.user.preferred_username"
|
||||||
:telephone="authStore.userInfo.ctx.user.telephone"
|
:telephone="authStore.userInfo.user.telephone"
|
||||||
:visits="authStore.userInfo.visits"
|
:visits="authStore.userInfo.user.visits"
|
||||||
:created-at="authStore.userInfo.created_at"
|
:created-at="authStore.userInfo.user.created_at"
|
||||||
:last-seen="authStore.userInfo.last_seen"
|
:last-seen="authStore.userInfo.user.last_seen"
|
||||||
:loading="authStore.isLoading"
|
:loading="authStore.isLoading"
|
||||||
:org-display-name="authStore.userInfo.ctx.org.display_name"
|
:org-display-name="authStore.ctx?.org.display_name"
|
||||||
:role-name="authStore.userInfo.ctx.role.display_name"
|
:role-name="authStore.ctx?.role.display_name"
|
||||||
update-endpoint="/auth/api/user/info"
|
update-endpoint="/auth/api/user/info"
|
||||||
@saved="authStore.loadUserInfo()"
|
@saved="authStore.loadUserInfo()"
|
||||||
@edit="openEditDialog"
|
@edit="openEditDialog"
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<CredentialList
|
<CredentialList
|
||||||
ref="credentialList"
|
ref="credentialList"
|
||||||
:credentials="authStore.userInfo?.credentials || []"
|
:credentials="authStore.userInfo?.credentials ? Object.values(authStore.userInfo.credentials) : []"
|
||||||
:aaguid-info="authStore.userInfo?.aaguid_info || {}"
|
:aaguid-info="authStore.userInfo?.aaguid_info || {}"
|
||||||
:loading="authStore.isLoading"
|
:loading="authStore.isLoading"
|
||||||
:hovered-credential-uuid="hoveredCredentialUuid"
|
:hovered-credential-uuid="hoveredCredentialUuid"
|
||||||
@@ -184,7 +184,7 @@ const hasActiveModal = computed(() => showEditDialog.value || showRegLink.value)
|
|||||||
|
|
||||||
watch(showEditDialog, (open) => {
|
watch(showEditDialog, (open) => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
const user = authStore.userInfo?.ctx?.user
|
const user = authStore.userInfo?.user
|
||||||
editName.value = user?.display_name ?? ''
|
editName.value = user?.display_name ?? ''
|
||||||
editEmail.value = user?.email ?? ''
|
editEmail.value = user?.email ?? ''
|
||||||
editUsername.value = user?.preferred_username ?? ''
|
editUsername.value = user?.preferred_username ?? ''
|
||||||
@@ -365,11 +365,11 @@ const logoutEverywhere = async () => { await authStore.logoutEverywhere() }
|
|||||||
const logout = async () => { await authStore.logout() }
|
const logout = async () => { await authStore.logout() }
|
||||||
const openEditDialog = () => { showEditDialog.value = true }
|
const openEditDialog = () => { showEditDialog.value = true }
|
||||||
const isAdmin = computed(() => {
|
const isAdmin = computed(() => {
|
||||||
const perms = authStore.userInfo?.ctx.permissions
|
const perms = authStore.ctx?.permissions
|
||||||
return perms.includes('auth:admin') || perms.includes('auth:org:admin')
|
return perms?.includes('auth:admin') || perms?.includes('auth:org:admin')
|
||||||
})
|
})
|
||||||
const hasMultipleSessions = computed(() => sessions.value.length > 1)
|
const hasMultipleSessions = computed(() => sessions.value.length > 1)
|
||||||
const credentials = computed(() => authStore.userInfo?.credentials || [])
|
const credentials = computed(() => authStore.userInfo?.credentials ? Object.values(authStore.userInfo.credentials) : [])
|
||||||
const useWideLayout = computed(() => {
|
const useWideLayout = computed(() => {
|
||||||
// Check if any single site has more than 8 sessions
|
// Check if any single site has more than 8 sessions
|
||||||
const groups = {}
|
const groups = {}
|
||||||
@@ -390,7 +390,7 @@ const breadcrumbEntries = computed(() => { const entries = [{ label: 'My Profile
|
|||||||
const saveProfile = async () => {
|
const saveProfile = async () => {
|
||||||
const name = editName.value.trim()
|
const name = editName.value.trim()
|
||||||
if (!name) { editError.value = 'Name cannot be empty'; return }
|
if (!name) { editError.value = 'Name cannot be empty'; return }
|
||||||
const user = authStore.userInfo.ctx.user
|
const user = authStore.userInfo.user
|
||||||
const emailVal = editEmail.value.trim() || null
|
const emailVal = editEmail.value.trim() || null
|
||||||
const usernameVal = editUsername.value.trim() || null
|
const usernameVal = editUsername.value.trim() || null
|
||||||
const telephoneVal = editTelephone.value.trim() || null
|
const telephoneVal = editTelephone.value.trim() || null
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
state: () => ({
|
state: () => ({
|
||||||
// Auth State
|
// Auth State
|
||||||
userInfo: null, // Contains the full user info response: {user, credentials, aaguid_info}
|
userInfo: null, // Contains the full user info response: {user, credentials, aaguid_info}
|
||||||
|
ctx: null, // Session context from validate
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
@@ -41,21 +42,21 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
}, effectiveDuration)
|
}, effectiveDuration)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async exchangeCode(result) {
|
async setSessionCookie(result) {
|
||||||
if (!result?.exchange_code) {
|
if (!result?.session_token) {
|
||||||
console.error('exchangeCode called with missing exchange_code:', result)
|
console.error('setSessionCookie called with missing session_token:', result)
|
||||||
throw new Error('Authentication response missing exchange_code')
|
throw new Error('Authentication response missing session_token')
|
||||||
}
|
}
|
||||||
return await apiJson('/auth/api/set-session', {
|
return await apiJson('/auth/api/set-session', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Authorization': `Bearer ${result.exchange_code}` },
|
headers: {'Authorization': `Bearer ${result.session_token}`},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async register() {
|
async register() {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
try {
|
try {
|
||||||
const result = await register()
|
const result = await register()
|
||||||
await this.exchangeCode(result)
|
await this.setSessionCookie(result)
|
||||||
await this.loadUserInfo()
|
await this.loadUserInfo()
|
||||||
this.selectView()
|
this.selectView()
|
||||||
return result
|
return result
|
||||||
@@ -68,7 +69,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
try {
|
try {
|
||||||
const result = await authenticate()
|
const result = await authenticate()
|
||||||
|
|
||||||
await this.exchangeCode(result)
|
await this.setSessionCookie(result)
|
||||||
await this.loadUserInfo()
|
await this.loadUserInfo()
|
||||||
this.selectView()
|
this.selectView()
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
async loadUserInfo() {
|
async loadUserInfo() {
|
||||||
try {
|
try {
|
||||||
this.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' })
|
this.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' })
|
||||||
updateThemeFromSession(this.userInfo?.ctx)
|
updateThemeFromSession(this.ctx)
|
||||||
console.log('User info loaded:', this.userInfo)
|
console.log('User info loaded:', this.userInfo)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Suppress toast for 401/403 errors - the auth iframe will handle these
|
// Suppress toast for 401/403 errors - the auth iframe will handle these
|
||||||
|
|||||||
@@ -99,14 +99,13 @@ async def admin_list_orgs(request: Request, auth=AUTH_COOKIE):
|
|||||||
def org_to_dict(o):
|
def org_to_dict(o):
|
||||||
roles = o.roles
|
roles = o.roles
|
||||||
return ApiOrgResponse(
|
return ApiOrgResponse(
|
||||||
uuid=o.uuid,
|
org=o,
|
||||||
display_name=o.display_name,
|
|
||||||
permissions={p.uuid: p for p in o.permissions},
|
permissions={p.uuid: p for p in o.permissions},
|
||||||
roles={r.uuid: r for r in roles},
|
roles={r.uuid: r for r in roles},
|
||||||
users={u.uuid: u for r in roles for u in r.users},
|
users={u.uuid: u for r in roles for u in r.users},
|
||||||
)
|
)
|
||||||
|
|
||||||
return MsgspecResponse([org_to_dict(o) for o in orgs])
|
return MsgspecResponse({o.uuid: org_to_dict(o) for o in orgs})
|
||||||
|
|
||||||
|
|
||||||
@app.post("/orgs")
|
@app.post("/orgs")
|
||||||
@@ -837,7 +836,7 @@ async def admin_list_permissions(request: Request, auth=AUTH_COOKIE):
|
|||||||
host=request.headers.get("host"),
|
host=request.headers.get("host"),
|
||||||
)
|
)
|
||||||
perms = db.data().permissions.values() if master_admin(ctx) else ctx.org.permissions
|
perms = db.data().permissions.values() if master_admin(ctx) else ctx.org.permissions
|
||||||
return MsgspecResponse([ApiPermission.from_db(p) for p in perms])
|
return MsgspecResponse({p.uuid: ApiPermission.from_db(p) for p in perms})
|
||||||
|
|
||||||
|
|
||||||
@app.post("/permissions")
|
@app.post("/permissions")
|
||||||
|
|||||||
@@ -123,9 +123,9 @@ class ApiUserDetail(msgspec.Struct, kw_only=True):
|
|||||||
credentials: dict[UUID, Credential]
|
credentials: dict[UUID, Credential]
|
||||||
aaguid_info: dict[str, ApiAaguidInfo]
|
aaguid_info: dict[str, ApiAaguidInfo]
|
||||||
sessions: list[ApiUserSession]
|
sessions: list[ApiUserSession]
|
||||||
org: ApiOrg
|
|
||||||
role: ApiRole
|
|
||||||
permissions: dict[UUID, ApiPermission] = {}
|
permissions: dict[UUID, ApiPermission] = {}
|
||||||
|
org: ApiOrg | None = None
|
||||||
|
role: ApiRole | None = None
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -136,8 +136,7 @@ class ApiUserDetail(msgspec.Struct, kw_only=True):
|
|||||||
class ApiOrgResponse(msgspec.Struct, kw_only=True):
|
class ApiOrgResponse(msgspec.Struct, kw_only=True):
|
||||||
"""Org response containing Org with roles and users as UUID-keyed dicts."""
|
"""Org response containing Org with roles and users as UUID-keyed dicts."""
|
||||||
|
|
||||||
uuid: UUID
|
org: Org
|
||||||
display_name: str
|
|
||||||
permissions: dict[UUID, Permission]
|
permissions: dict[UUID, Permission]
|
||||||
roles: dict[UUID, Role]
|
roles: dict[UUID, Role]
|
||||||
users: dict[UUID, User]
|
users: dict[UUID, User]
|
||||||
@@ -183,9 +182,6 @@ class ApiUserContext(msgspec.Struct, omit_defaults=True):
|
|||||||
uuid: UUID
|
uuid: UUID
|
||||||
display_name: str
|
display_name: str
|
||||||
theme: str = ""
|
theme: str = ""
|
||||||
email: str | None = None
|
|
||||||
preferred_username: str | None = None
|
|
||||||
telephone: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ApiOrgContext(msgspec.Struct):
|
class ApiOrgContext(msgspec.Struct):
|
||||||
|
|||||||
+10
-19
@@ -6,10 +6,8 @@ from paskia.db import SessionContext
|
|||||||
from paskia.util import hostutil
|
from paskia.util import hostutil
|
||||||
from paskia.util.apistructs import (
|
from paskia.util.apistructs import (
|
||||||
ApiAaguidInfo,
|
ApiAaguidInfo,
|
||||||
ApiOrg,
|
|
||||||
ApiOrgContext,
|
ApiOrgContext,
|
||||||
ApiPermission,
|
ApiPermission,
|
||||||
ApiRole,
|
|
||||||
ApiRoleContext,
|
ApiRoleContext,
|
||||||
ApiSessionContext,
|
ApiSessionContext,
|
||||||
ApiUser,
|
ApiUser,
|
||||||
@@ -25,9 +23,6 @@ def build_session_context(ctx: SessionContext) -> ApiSessionContext:
|
|||||||
uuid=ctx.user.uuid,
|
uuid=ctx.user.uuid,
|
||||||
display_name=ctx.user.display_name,
|
display_name=ctx.user.display_name,
|
||||||
theme=ctx.user.theme,
|
theme=ctx.user.theme,
|
||||||
email=ctx.user.email,
|
|
||||||
preferred_username=ctx.user.preferred_username,
|
|
||||||
telephone=ctx.user.telephone,
|
|
||||||
)
|
)
|
||||||
org = ApiOrgContext(uuid=ctx.org.uuid, display_name=ctx.org.display_name)
|
org = ApiOrgContext(uuid=ctx.org.uuid, display_name=ctx.org.display_name)
|
||||||
role = ApiRoleContext(uuid=ctx.role.uuid, display_name=ctx.role.display_name)
|
role = ApiRoleContext(uuid=ctx.role.uuid, display_name=ctx.role.display_name)
|
||||||
@@ -54,26 +49,22 @@ async def build_user_info(
|
|||||||
sessions = [
|
sessions = [
|
||||||
ApiUserSession.from_db(
|
ApiUserSession.from_db(
|
||||||
s,
|
s,
|
||||||
current_key=session_record.key,
|
current_key=auth,
|
||||||
normalized_host=normalized_host,
|
normalized_host=normalized_host,
|
||||||
expires_delta=EXPIRES,
|
expires_delta=EXPIRES,
|
||||||
)
|
)
|
||||||
for s in user.sessions
|
for s in user.sessions
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return ApiUserDetail(
|
||||||
"ctx": {
|
user=ApiUser.from_db(user),
|
||||||
"user": ApiUser.from_db(user),
|
credentials={c.uuid: c for c in user.credentials},
|
||||||
"permissions": {p.uuid: ApiPermission.from_db(p) for p in ctx.permissions}
|
aaguid_info={
|
||||||
if ctx
|
|
||||||
else {},
|
|
||||||
},
|
|
||||||
"credentials": {c.uuid: c for c in user.credentials},
|
|
||||||
"aaguid_info": {
|
|
||||||
k: ApiAaguidInfo(**v)
|
k: ApiAaguidInfo(**v)
|
||||||
for k, v in aaguid.filter(c.aaguid for c in user.credentials).items()
|
for k, v in aaguid.filter(c.aaguid for c in user.credentials).items()
|
||||||
},
|
},
|
||||||
"sessions": sessions,
|
sessions=sessions,
|
||||||
"org": ApiOrg.from_db(user.org),
|
permissions={p.uuid: ApiPermission.from_db(p) for p in ctx.permissions}
|
||||||
"role": ApiRole.from_db(user.role),
|
if ctx
|
||||||
}
|
else {},
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user