Actually usable admin panel
This commit is contained in:
parent
4db7f2e9a6
commit
f3e3679b6d
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
|
||||||
import CredentialList from '@/components/CredentialList.vue'
|
import CredentialList from '@/components/CredentialList.vue'
|
||||||
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
import RegistrationLinkModal from '@/components/RegistrationLinkModal.vue'
|
||||||
import StatusMessage from '@/components/StatusMessage.vue'
|
import StatusMessage from '@/components/StatusMessage.vue'
|
||||||
@ -16,6 +16,85 @@ const userDetail = ref(null) // cached user detail object
|
|||||||
const userLink = ref(null) // latest generated registration link
|
const userLink = ref(null) // latest generated registration link
|
||||||
const userLinkExpires = ref(null)
|
const userLinkExpires = ref(null)
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const addingOrgForPermission = ref(null)
|
||||||
|
|
||||||
|
function handleGlobalClick(e) {
|
||||||
|
if (!addingOrgForPermission.value) return
|
||||||
|
const menu = e.target.closest('.org-add-menu')
|
||||||
|
const trigger = e.target.closest('.add-org-btn')
|
||||||
|
if (!menu && !trigger) {
|
||||||
|
addingOrgForPermission.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleGlobalClick)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', handleGlobalClick)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build a summary: for each permission id -> { orgs: Set(org_display_name), userCount }
|
||||||
|
const permissionSummary = computed(() => {
|
||||||
|
const summary = {}
|
||||||
|
for (const o of orgs.value) {
|
||||||
|
const orgBase = { uuid: o.uuid, display_name: o.display_name }
|
||||||
|
// Org-level permissions (direct)
|
||||||
|
for (const pid of 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)
|
||||||
|
summary[pid].orgSet.add(o.uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Role-based permissions (inheritance)
|
||||||
|
for (const r of o.roles) {
|
||||||
|
for (const pid of r.permissions) {
|
||||||
|
if (!summary[pid]) summary[pid] = { orgs: [], orgSet: new Set(), userCount: 0 }
|
||||||
|
if (!summary[pid].orgSet.has(o.uuid)) {
|
||||||
|
summary[pid].orgs.push(orgBase)
|
||||||
|
summary[pid].orgSet.add(o.uuid)
|
||||||
|
}
|
||||||
|
summary[pid].userCount += r.users.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const display = {}
|
||||||
|
for (const [pid, v] of Object.entries(summary)) {
|
||||||
|
display[pid] = { orgs: v.orgs.sort((a,b)=>a.display_name.localeCompare(b.display_name)), userCount: v.userCount }
|
||||||
|
}
|
||||||
|
return display
|
||||||
|
})
|
||||||
|
|
||||||
|
function availableOrgsForPermission(pid) {
|
||||||
|
return orgs.value.filter(o => !o.permissions.includes(pid))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function attachPermissionToOrg(pid, orgUuid) {
|
||||||
|
if (!orgUuid) return
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ permission_id: pid })
|
||||||
|
const res = await fetch(`/auth/admin/orgs/${orgUuid}/permission?${params.toString()}`, { method: 'POST' })
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.detail) throw new Error(data.detail)
|
||||||
|
await loadOrgs()
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || 'Failed to add permission to org')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detachPermissionFromOrg(pid, orgUuid) {
|
||||||
|
if (!confirm('Remove permission from this org?')) return
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ permission_id: pid })
|
||||||
|
const res = await fetch(`/auth/admin/orgs/${orgUuid}/permission?${params.toString()}`, { method: 'DELETE' })
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.detail) throw new Error(data.detail)
|
||||||
|
await loadOrgs()
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || 'Failed to remove permission from org')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseHash() {
|
function parseHash() {
|
||||||
const h = window.location.hash || ''
|
const h = window.location.hash || ''
|
||||||
@ -88,7 +167,7 @@ async function createOrg() {
|
|||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.detail) return alert(data.detail)
|
if (data.detail) return alert(data.detail)
|
||||||
await loadOrgs()
|
await Promise.all([loadOrgs(), loadPermissions()])
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateOrg(org) {
|
async function updateOrg(org) {
|
||||||
@ -229,11 +308,8 @@ async function createPermission() {
|
|||||||
async function updatePermission(p) {
|
async function updatePermission(p) {
|
||||||
const name = prompt('Permission display name:', p.display_name)
|
const name = prompt('Permission display name:', p.display_name)
|
||||||
if (!name) return
|
if (!name) return
|
||||||
const res = await fetch(`/auth/admin/permissions/${encodeURIComponent(p.id)}`, {
|
const params = new URLSearchParams({ permission_id: p.id, display_name: name })
|
||||||
method: 'PUT',
|
const res = await fetch(`/auth/admin/permission?${params.toString()}`, { method: 'PUT' })
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
body: JSON.stringify({ display_name: name })
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.detail) return alert(data.detail)
|
if (data.detail) return alert(data.detail)
|
||||||
await loadPermissions()
|
await loadPermissions()
|
||||||
@ -241,7 +317,8 @@ async function updatePermission(p) {
|
|||||||
|
|
||||||
async function deletePermission(p) {
|
async function deletePermission(p) {
|
||||||
if (!confirm(`Delete permission ${p.id}?`)) return
|
if (!confirm(`Delete permission ${p.id}?`)) return
|
||||||
const res = await fetch(`/auth/admin/permissions/${encodeURIComponent(p.id)}`, { method: 'DELETE' })
|
const params = new URLSearchParams({ permission_id: p.id })
|
||||||
|
const res = await fetch(`/auth/admin/permission?${params.toString()}`, { method: 'DELETE' })
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.detail) return alert(data.detail)
|
if (data.detail) return alert(data.detail)
|
||||||
await loadPermissions()
|
await loadPermissions()
|
||||||
@ -340,8 +417,6 @@ async function toggleRolePermission(role, permId, checked) {
|
|||||||
<a href="/auth/" class="back-link" title="Back to User App">User</a>
|
<a href="/auth/" class="back-link" title="Back to User App">User</a>
|
||||||
<a v-if="selectedOrg && info?.is_global_admin" @click.prevent="goOverview" href="#overview" class="nav-link" title="Back to overview">Overview</a>
|
<a v-if="selectedOrg && info?.is_global_admin" @click.prevent="goOverview" href="#overview" class="nav-link" title="Back to overview">Overview</a>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="subtitle" v-if="!selectedUser">Manage organizations, roles, and permissions</p>
|
|
||||||
|
|
||||||
<div v-if="loading">Loading…</div>
|
<div v-if="loading">Loading…</div>
|
||||||
<div v-else-if="error" class="error">{{ error }}</div>
|
<div v-else-if="error" class="error">{{ error }}</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@ -418,18 +493,13 @@ async function toggleRolePermission(role, permId, checked) {
|
|||||||
<span class="org-name">{{ selectedOrg.display_name }}</span>
|
<span class="org-name">{{ selectedOrg.display_name }}</span>
|
||||||
<button @click="updateOrg(selectedOrg)" class="icon-btn" aria-label="Rename organization" title="Rename organization">✏️</button>
|
<button @click="updateOrg(selectedOrg)" class="icon-btn" aria-label="Rename organization" title="Rename organization">✏️</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="org-actions">
|
<div class="org-actions"></div>
|
||||||
<button @click="deleteOrg(selectedOrg)" v-if="info.is_global_admin" class="icon-btn delete-icon" aria-label="Delete organization" title="Delete organization">❌</button>
|
|
||||||
<button @click="createRole(selectedOrg)">+ Role</button>
|
|
||||||
<button @click="goOverview" v-if="info.is_global_admin">Back</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="matrix-wrapper">
|
<div class="matrix-wrapper">
|
||||||
<h3>Permissions Matrix</h3>
|
|
||||||
<div class="matrix-scroll">
|
<div class="matrix-scroll">
|
||||||
<div
|
<div
|
||||||
class="perm-matrix-grid"
|
class="perm-matrix-grid"
|
||||||
:style="{ gridTemplateColumns: 'minmax(180px, 1fr) ' + selectedOrg.roles.map(()=> '2.2rem').join(' ') }"
|
:style="{ gridTemplateColumns: 'minmax(180px, 1fr) ' + selectedOrg.roles.map(()=> '2.2rem').join(' ') + ' 2.2rem' }"
|
||||||
>
|
>
|
||||||
<!-- Headers -->
|
<!-- Headers -->
|
||||||
<div class="grid-head perm-head">Permission</div>
|
<div class="grid-head perm-head">Permission</div>
|
||||||
@ -441,6 +511,7 @@ async function toggleRolePermission(role, permId, checked) {
|
|||||||
>
|
>
|
||||||
<span>{{ r.display_name }}</span>
|
<span>{{ r.display_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid-head role-head add-role-head" title="Add role" @click="createRole(selectedOrg)" role="button">➕</div>
|
||||||
|
|
||||||
<!-- Data Rows -->
|
<!-- Data Rows -->
|
||||||
<template v-for="pid in selectedOrg.permissions" :key="pid">
|
<template v-for="pid in selectedOrg.permissions" :key="pid">
|
||||||
@ -456,6 +527,7 @@ async function toggleRolePermission(role, permId, checked) {
|
|||||||
@change="e => toggleRolePermission(r, pid, e.target.checked)"
|
@change="e => toggleRolePermission(r, pid, e.target.checked)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="matrix-cell add-role-cell" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -507,14 +579,57 @@ async function toggleRolePermission(role, permId, checked) {
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button @click="createPermission">+ Create Permission</button>
|
<button @click="createPermission">+ Create Permission</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="p in permissions" :key="p.id" class="perm" :title="p.id">
|
<div class="permission-grid">
|
||||||
<div class="perm-name-line">
|
<div class="perm-grid-head">Permission</div>
|
||||||
<span>{{ p.display_name }}</span>
|
<div class="perm-grid-head">Orgs</div>
|
||||||
<button @click="updatePermission(p)" class="icon-btn" aria-label="Rename permission" title="Rename permission">✏️</button>
|
<div class="perm-grid-head center">Members</div>
|
||||||
|
<div class="perm-grid-head center">Actions</div>
|
||||||
|
<template v-for="p in [...permissions].sort((a,b)=> a.id.localeCompare(b.id))" :key="p.id">
|
||||||
|
<div class="perm-cell perm-name" :title="p.id">
|
||||||
|
<span class="perm-title">{{ p.display_name }}</span>
|
||||||
|
<span class="perm-id muted">({{ p.id }})</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="perm-actions">
|
<div class="perm-cell perm-orgs" :title="permissionSummary[p.id]?.orgs?.map(o=>o.display_name).join(', ') || ''">
|
||||||
|
<template v-if="permissionSummary[p.id]">
|
||||||
|
<span class="org-pill" v-for="o in permissionSummary[p.id].orgs" :key="o.uuid">
|
||||||
|
{{ o.display_name }}
|
||||||
|
<button class="pill-x" @click.stop="detachPermissionFromOrg(p.id, o.uuid)" aria-label="Remove">×</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span class="org-add-wrapper">
|
||||||
|
<button
|
||||||
|
v-if="availableOrgsForPermission(p.id).length && addingOrgForPermission !== p.id"
|
||||||
|
class="add-org-btn"
|
||||||
|
@click.stop="addingOrgForPermission = p.id"
|
||||||
|
aria-label="Add organization"
|
||||||
|
title="Add organization"
|
||||||
|
>➕</button>
|
||||||
|
<div
|
||||||
|
v-if="addingOrgForPermission === p.id"
|
||||||
|
class="org-add-menu"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown.escape.stop.prevent="addingOrgForPermission = null"
|
||||||
|
>
|
||||||
|
<div class="org-add-list">
|
||||||
|
<button
|
||||||
|
v-for="o in availableOrgsForPermission(p.id)"
|
||||||
|
:key="o.uuid"
|
||||||
|
class="org-add-item"
|
||||||
|
@click.stop="attachPermissionToOrg(p.id, o.uuid); addingOrgForPermission = null"
|
||||||
|
>{{ o.display_name }}</button>
|
||||||
|
</div>
|
||||||
|
<div class="org-add-footer">
|
||||||
|
<button class="org-add-cancel" @click.stop="addingOrgForPermission = null" aria-label="Cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="perm-cell perm-users center">{{ permissionSummary[p.id]?.userCount || 0 }}</div>
|
||||||
|
<div class="perm-cell perm-actions center">
|
||||||
|
<button @click="updatePermission(p)" class="icon-btn" aria-label="Rename permission" title="Rename permission">✏️</button>
|
||||||
<button @click="deletePermission(p)" class="icon-btn delete-icon" aria-label="Delete permission" title="Delete permission">❌</button>
|
<button @click="deletePermission(p)" class="icon-btn delete-icon" aria-label="Delete permission" title="Delete permission">❌</button>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -544,8 +659,10 @@ async function toggleRolePermission(role, permId, checked) {
|
|||||||
.pill-x { background: transparent; border: none; color: #900; cursor: pointer }
|
.pill-x { background: transparent; border: none; color: #900; cursor: pointer }
|
||||||
button { padding: .25rem .5rem; border-radius: 6px; border: 1px solid #ddd; background: #fff; cursor: pointer }
|
button { padding: .25rem .5rem; border-radius: 6px; border: 1px solid #ddd; background: #fff; cursor: pointer }
|
||||||
button:hover { background: #f7f7f7 }
|
button:hover { background: #f7f7f7 }
|
||||||
.roles-grid { display: flex; gap: 1rem; align-items: stretch; overflow-x: auto; padding: .5rem 0 }
|
/* Avoid global button 100% width from frontend main styles */
|
||||||
.role-column { background: #fafafa; border: 1px solid #eee; border-radius: 8px; padding: .5rem; min-width: 200px; flex: 0 0 240px; display: flex; flex-direction: column; }
|
button, .perm-actions button, .org-actions button, .role-actions button { width: auto; }
|
||||||
|
.roles-grid { display: flex; flex-wrap: wrap; gap: 1rem; align-items: stretch; padding: .5rem 0; }
|
||||||
|
.role-column { background: #fafafa; border: 1px solid #eee; border-radius: 8px; padding: .5rem; min-width: 200px; flex: 1 1 240px; display: flex; flex-direction: column; max-width: 300px; }
|
||||||
.role-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .25rem }
|
.role-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .25rem }
|
||||||
.user-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .25rem; flex: 1 1 auto; }
|
.user-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .25rem; flex: 1 1 auto; }
|
||||||
.user-chip { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: .25rem .4rem; display: flex; justify-content: space-between; gap: .5rem; cursor: grab; }
|
.user-chip { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: .25rem .4rem; display: flex; justify-content: space-between; gap: .5rem; cursor: grab; }
|
||||||
@ -573,6 +690,10 @@ button:hover { background: #f7f7f7 }
|
|||||||
.perm-matrix-grid .matrix-cell { display: flex; justify-content: center; align-items: center; }
|
.perm-matrix-grid .matrix-cell { display: flex; justify-content: center; align-items: center; }
|
||||||
.perm-matrix-grid .matrix-cell input { cursor: pointer; }
|
.perm-matrix-grid .matrix-cell input { cursor: pointer; }
|
||||||
.matrix-hint { font-size: .7rem; margin-top: .25rem; }
|
.matrix-hint { font-size: .7rem; margin-top: .25rem; }
|
||||||
|
/* Add role column styles */
|
||||||
|
.add-role-head { cursor: pointer; color: #2a6; font-size: 1rem; display:flex; justify-content:center; align-items:flex-end; }
|
||||||
|
.add-role-head:hover { color:#1c4; }
|
||||||
|
/* Removed add-role placeholder styles */
|
||||||
/* Inline organization title with icon */
|
/* Inline organization title with icon */
|
||||||
.org-title { display: flex; align-items: center; gap: .4rem; }
|
.org-title { display: flex; align-items: center; gap: .4rem; }
|
||||||
.org-title .org-name { flex: 0 1 auto; }
|
.org-title .org-name { flex: 0 1 auto; }
|
||||||
@ -580,7 +701,7 @@ button:hover { background: #f7f7f7 }
|
|||||||
.plus-btn { background: none; border: none; font-size: 1.15rem; line-height: 1; padding: 0 .1rem; cursor: pointer; opacity: .6; }
|
.plus-btn { background: none; border: none; font-size: 1.15rem; line-height: 1; padding: 0 .1rem; cursor: pointer; opacity: .6; }
|
||||||
.plus-btn:hover, .plus-btn:focus { opacity: 1; outline: none; }
|
.plus-btn:hover, .plus-btn:focus { opacity: 1; outline: none; }
|
||||||
.plus-btn:focus-visible { outline: 2px solid #555; outline-offset: 2px; }
|
.plus-btn:focus-visible { outline: 2px solid #555; outline-offset: 2px; }
|
||||||
.empty-role { display: flex; flex-direction: column; gap: .4rem; align-items: flex-start; padding: .35rem .25rem; flex: 1 1 auto; width: 100%; }
|
.empty-role { display: flex; flex-direction: column; gap: .4rem; align-items: flex-start; padding: .35rem .25rem; /* removed flex grow & width for natural size */ }
|
||||||
.empty-role .empty-text { font-size: .7rem; margin: 0; }
|
.empty-role .empty-text { font-size: .7rem; margin: 0; }
|
||||||
.delete-icon { color: #c00; }
|
.delete-icon { color: #c00; }
|
||||||
.delete-icon:hover, .delete-icon:focus { color: #ff0000; }
|
.delete-icon:hover, .delete-icon:focus { color: #ff0000; }
|
||||||
@ -601,4 +722,33 @@ button:hover { background: #f7f7f7 }
|
|||||||
.cred-item { background: #fff; border: 1px solid #eee; border-radius: 6px; padding: .35rem .5rem; font-size: .65rem; }
|
.cred-item { background: #fff; border: 1px solid #eee; border-radius: 6px; padding: .35rem .5rem; font-size: .65rem; }
|
||||||
.cred-line { display: flex; flex-direction: column; gap: .15rem; }
|
.cred-line { display: flex; flex-direction: column; gap: .15rem; }
|
||||||
.cred-line .dates { color: #555; font-size: .6rem; }
|
.cred-line .dates { color: #555; font-size: .6rem; }
|
||||||
|
/* Permission grid */
|
||||||
|
.permission-grid { display: grid; grid-template-columns: minmax(220px,2fr) minmax(160px,3fr) 70px 90px; gap: 2px; margin-top: .5rem; }
|
||||||
|
.permission-grid .perm-grid-head { font-size: .6rem; text-transform: uppercase; letter-spacing: .05em; font-weight: 600; padding: .35rem .4rem; background: #f3f3f3; border: 1px solid #e1e1e1; }
|
||||||
|
.permission-grid .perm-cell { background: #fff; border: 1px solid #eee; padding: .35rem .4rem; font-size: .7rem; display: flex; align-items: center; gap: .4rem; }
|
||||||
|
.permission-grid .perm-name { flex-direction: row; flex-wrap: wrap; }
|
||||||
|
.permission-grid .perm-title { font-weight: 600; }
|
||||||
|
.permission-grid .perm-id { font-size: .55rem; }
|
||||||
|
.permission-grid .center { justify-content: center; }
|
||||||
|
.permission-grid .perm-actions { gap: .25rem; }
|
||||||
|
.permission-grid .perm-actions .icon-btn { font-size: .9rem; }
|
||||||
|
/* Org pill editing */
|
||||||
|
.perm-orgs { flex-wrap: wrap; gap: .25rem; }
|
||||||
|
.perm-orgs .org-pill { background:#eef4ff; border:1px solid #d0dcf0; padding:2px 6px; border-radius:999px; font-size:.55rem; display:inline-flex; align-items:center; gap:4px; }
|
||||||
|
.perm-orgs .org-pill .pill-x { background:none; border:none; cursor:pointer; font-size:.7rem; line-height:1; padding:0; margin:0; color:#555; }
|
||||||
|
.perm-orgs .org-pill .pill-x:hover { color:#c00; }
|
||||||
|
.add-org-btn { background:none; border:none; cursor:pointer; font-size:.7rem; padding:0 2px; line-height:1; opacity:.55; display:inline; }
|
||||||
|
.add-org-btn:hover, .add-org-btn:focus { opacity:1; }
|
||||||
|
.add-org-btn:focus-visible { outline:2px solid #555; outline-offset:2px; }
|
||||||
|
.org-add-wrapper { position:relative; display:inline-block; }
|
||||||
|
.org-add-menu { position:absolute; top:100%; left:0; z-index:20; margin-top:4px; min-width:160px; background:#fff; border:1px solid #e2e6ea; border-radius:6px; padding:.3rem .35rem; box-shadow:0 4px 10px rgba(0,0,0,.08); display:flex; flex-direction:column; gap:.25rem; font-size:.6rem; }
|
||||||
|
.org-add-menu:before { content:""; position:absolute; top:-5px; left:10px; width:8px; height:8px; background:#fff; border-left:1px solid #e2e6ea; border-top:1px solid #e2e6ea; transform:rotate(45deg); }
|
||||||
|
.org-add-list { display:flex; flex-direction:column; gap:0; max-height:180px; overflow-y:auto; scrollbar-width:thin; }
|
||||||
|
.org-add-item { background:transparent; border:none; padding:.25rem .4rem; font-size:.6rem; border-radius:4px; cursor:pointer; line-height:1.1; text-align:left; width:100%; color:#222; }
|
||||||
|
.org-add-item:hover, .org-add-item:focus { background:#f2f5f9; }
|
||||||
|
.org-add-item:active { background:#e6ebf0; }
|
||||||
|
.org-add-footer { margin-top:.25rem; display:flex; justify-content:flex-end; }
|
||||||
|
.org-add-cancel { background:transparent; border:none; font-size:.55rem; padding:.15rem .35rem; cursor:pointer; color:#666; border-radius:4px; }
|
||||||
|
.org-add-cancel:hover, .org-add-cancel:focus { background:#f2f5f9; color:#222; }
|
||||||
|
.org-add-cancel:active { background:#e6ebf0; }
|
||||||
</style>
|
</style>
|
||||||
|
@ -241,27 +241,21 @@ def register_api_routes(app: FastAPI):
|
|||||||
await db.instance.delete_organization(org_uuid)
|
await db.instance.delete_organization(org_uuid)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
# Manage an org's grantable permissions
|
# Manage an org's grantable permissions (query param for permission_id)
|
||||||
@app.post("/auth/admin/orgs/{org_uuid}/permissions/{permission_id}")
|
@app.post("/auth/admin/orgs/{org_uuid}/permission")
|
||||||
async def admin_add_org_permission(
|
async def admin_add_org_permission(org_uuid: UUID, permission_id: str, auth=Cookie(None)):
|
||||||
org_uuid: UUID, permission_id: str, auth=Cookie(None)
|
|
||||||
):
|
|
||||||
ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth)
|
ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth)
|
||||||
if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)):
|
if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)):
|
||||||
raise ValueError("Insufficient permissions")
|
raise ValueError("Insufficient permissions")
|
||||||
await db.instance.add_permission_to_organization(str(org_uuid), permission_id)
|
await db.instance.add_permission_to_organization(str(org_uuid), permission_id)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@app.delete("/auth/admin/orgs/{org_uuid}/permissions/{permission_id}")
|
@app.delete("/auth/admin/orgs/{org_uuid}/permission")
|
||||||
async def admin_remove_org_permission(
|
async def admin_remove_org_permission(org_uuid: UUID, permission_id: str, auth=Cookie(None)):
|
||||||
org_uuid: UUID, permission_id: str, auth=Cookie(None)
|
|
||||||
):
|
|
||||||
ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth)
|
ctx, is_global_admin, is_org_admin = await _get_ctx_and_admin_flags(auth)
|
||||||
if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)):
|
if not (is_global_admin or (is_org_admin and ctx.org.uuid == org_uuid)):
|
||||||
raise ValueError("Insufficient permissions")
|
raise ValueError("Insufficient permissions")
|
||||||
await db.instance.remove_permission_from_organization(
|
await db.instance.remove_permission_from_organization(str(org_uuid), permission_id)
|
||||||
str(org_uuid), permission_id
|
|
||||||
)
|
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
# -------------------- Admin API: Roles --------------------
|
# -------------------- Admin API: Roles --------------------
|
||||||
@ -471,7 +465,7 @@ def register_api_routes(app: FastAPI):
|
|||||||
"aaguid_info": aaguid_info,
|
"aaguid_info": aaguid_info,
|
||||||
}
|
}
|
||||||
|
|
||||||
# -------------------- Admin API: Permissions (global) --------------------
|
# Admin API: Permissions (global)
|
||||||
|
|
||||||
@app.get("/auth/admin/permissions")
|
@app.get("/auth/admin/permissions")
|
||||||
async def admin_list_permissions(auth=Cookie(None)):
|
async def admin_list_permissions(auth=Cookie(None)):
|
||||||
@ -497,24 +491,18 @@ def register_api_routes(app: FastAPI):
|
|||||||
)
|
)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@app.put("/auth/admin/permissions/{permission_id}")
|
@app.put("/auth/admin/permission")
|
||||||
async def admin_update_permission(
|
async def admin_update_permission(permission_id: str, display_name: str, auth=Cookie(None)):
|
||||||
permission_id: str, payload: dict = Body(...), auth=Cookie(None)
|
|
||||||
):
|
|
||||||
_, is_global_admin, _ = await _get_ctx_and_admin_flags(auth)
|
_, is_global_admin, _ = await _get_ctx_and_admin_flags(auth)
|
||||||
if not is_global_admin:
|
if not is_global_admin:
|
||||||
raise ValueError("Global admin required")
|
raise ValueError("Global admin required")
|
||||||
from ..db import Permission as PermDC
|
from ..db import Permission as PermDC
|
||||||
|
|
||||||
display_name = payload.get("display_name")
|
|
||||||
if not display_name:
|
if not display_name:
|
||||||
raise ValueError("display_name is required")
|
raise ValueError("display_name is required")
|
||||||
await db.instance.update_permission(
|
await db.instance.update_permission(PermDC(id=permission_id, display_name=display_name))
|
||||||
PermDC(id=permission_id, display_name=display_name)
|
|
||||||
)
|
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
@app.delete("/auth/admin/permissions/{permission_id}")
|
@app.delete("/auth/admin/permission")
|
||||||
async def admin_delete_permission(permission_id: str, auth=Cookie(None)):
|
async def admin_delete_permission(permission_id: str, auth=Cookie(None)):
|
||||||
_, is_global_admin, _ = await _get_ctx_and_admin_flags(auth)
|
_, is_global_admin, _ = await _get_ctx_and_admin_flags(auth)
|
||||||
if not is_global_admin:
|
if not is_global_admin:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user