guest role; guide section ticking

This commit is contained in:
2026-08-27 13:02:35 -04:00
parent d38a599292
commit 4da6c6aa36
45 changed files with 830 additions and 243 deletions

View File

@@ -11,7 +11,9 @@
</div>
</div>
<div class="sidebar-search">
<!-- Global search is authenticated; offering the box to a signed-out
reader gives them a control that can only answer 401. -->
<div v-if="authStore.isAuthenticated" class="sidebar-search">
<input
v-model="searchQuery"
type="text"
@@ -222,8 +224,23 @@ function buildNavItems(items) {
// Drop items the user cannot reach: a nav item may carry a `permission`
// (e.g. Slides -> slides.manage); hide it from anyone who lacks it so the
// link does not dead-end at the router guard. Admins hold every permission.
const visible = items.filter(item =>
!item.permission || authStore.hasPermission(item.permission))
//
// A signed-out visitor sees only what a plugin has declared guest-reachable.
// Without this the sidebar offers a signed-out reader the whole app and every
// link bounces them to the login page, which reads as a broken site rather
// than as a closed door.
// A nav item may name one permission or several; holding ANY of them is
// enough. GE-Enforce and Slides need that: the page is reachable by a
// manager and by a plain viewer, and they hold different strings.
const holds = item => {
if (!item.permission) return true
const names = Array.isArray(item.permission) ? item.permission : [item.permission]
return names.some(name => authStore.hasPermission(name))
}
// A signed-out visitor gets whatever the server sent, which is already
// filtered to what the guest role can reach - hasPermission knows nothing
// about guests, so asking it here would hide everything.
const visible = authStore.isAuthenticated ? items.filter(holds) : items
const known = new Set(NAV_SECTIONS.map(s => s.key).filter(Boolean))
const bucket = item => {

View File

@@ -225,6 +225,17 @@
<p class="text-muted" style="font-size: 0.85rem; margin: 0 0 0.5rem 0">
Select which actions this role can perform
</p>
<!-- The guest role is everyone who is not signed in, so what it may
hold is bounded: reads, plus the few writes a plugin has
declared safe to do anonymously. The rest are disabled here
rather than refused on save, so the limit is visible while the
decision is being made. -->
<p v-if="editingRole?.isguest" class="guest-note">
This role is <strong>everyone who is not signed in</strong>.
Anything ticked here is readable by anybody who can reach this
site, with no account and no name in the audit log. Greyed-out
permissions cannot be given to guests at all.
</p>
<div class="permissions-grid">
<div v-for="(perms, category) in permissionsGrouped" :key="category" class="permission-category">
<div class="category-header">
@@ -239,10 +250,14 @@
</label>
</div>
<div class="category-perms">
<label v-for="p in perms" :key="p.name" class="checkbox-label perm-item">
<label v-for="p in perms" :key="p.name" class="checkbox-label perm-item"
:class="{ 'perm-blocked': !mayGrant(p.name) }"
:title="mayGrant(p.name) ? null
: 'A guest cannot be given this. Reading is grantable; a write has to be declared safe to do without an account.'">
<input
type="checkbox"
:value="p.name"
:disabled="!mayGrant(p.name)"
v-model="roleForm.permissions"
>
{{ p.description }}
@@ -382,19 +397,33 @@ function formatCategory(cat) {
return cat.charAt(0).toUpperCase() + cat.slice(1)
}
// Which permissions this role is allowed to hold at all. Only the guest role
// is bounded; the API sends the list it will accept, so the grid and the server
// cannot disagree about it.
function mayGrant(name) {
if (!editingRole.value?.isguest) return true
const grantable = editingRole.value?.grantable
return Array.isArray(grantable) ? grantable.includes(name) : false
}
function isCategoryFullySelected(category) {
const catPerms = permissionsGrouped.value[category] || []
const catPerms = (permissionsGrouped.value[category] || [])
.filter(p => mayGrant(p.name))
return catPerms.length > 0 && catPerms.every(p => roleForm.permissions.includes(p.name))
}
function isCategoryPartiallySelected(category) {
const catPerms = permissionsGrouped.value[category] || []
const catPerms = (permissionsGrouped.value[category] || [])
.filter(p => mayGrant(p.name))
const selected = catPerms.filter(p => roleForm.permissions.includes(p.name))
return selected.length > 0 && selected.length < catPerms.length
}
function toggleCategory(category, checked) {
const catPerms = permissionsGrouped.value[category] || []
// Ticking a whole category must not try to grant a guest something the API
// will refuse - the header checkbox would then look broken.
const catPerms = (permissionsGrouped.value[category] || [])
.filter(p => mayGrant(p.name))
const permNames = catPerms.map(p => p.name)
if (checked) {
@@ -780,4 +809,13 @@ onMounted(loadData)
font-size: 0.85rem;
padding: 0.25rem 0;
}
.perm-blocked { opacity: 0.45; cursor: not-allowed; }
.guest-note {
font-size: 0.85rem;
color: var(--warning);
border-left: 3px solid var(--warning);
padding-left: 0.6rem;
margin: 0 0 0.75rem;
}
</style>