Private
Public Access
1
0

Hide SITE_ADMIN from tenant admin, add Site Admin nav link

- Tenant admin user list filters out SITE_ADMIN users (server-side)
- API guard prevents tenant admins from modifying SITE_ADMIN users
- Tenant admins cannot change their own role (prevents self-demotion)
- Nav bar shows separate "Site Admin" link for SITE_ADMIN users
- Both desktop and mobile nav updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-26 20:00:51 -05:00
parent 8c150e5c24
commit 420ff1e715
4 changed files with 35 additions and 13 deletions
+17
View File
@@ -62,6 +62,14 @@
{/if}
</button>
{#if data.user}
{#if data.user.globalRole === 'SITE_ADMIN'}
<a
href="/admin/site"
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition"
>
Site Admin
</a>
{/if}
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
<a
href="/admin"
@@ -143,6 +151,15 @@
</div>
<div class="flex flex-col gap-1">
<a href="/account" class="text-white/80 text-sm py-1 hover:text-white transition" onclick={() => (mobileMenuOpen = false)}>{data.user.name}</a>
{#if data.user.globalRole === 'SITE_ADMIN'}
<a
href="/admin/site"
class="rounded bg-white/20 px-3 py-1.5 text-sm text-white hover:bg-white/30 transition text-center"
onclick={() => (mobileMenuOpen = false)}
>
Site Admin
</a>
{/if}
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
<a
href="/admin"
+1 -2
View File
@@ -11,13 +11,12 @@ export const load: PageServerLoad = async ({ locals }) => {
const { users, boards, tags, categories } = await withTenant(tenant.id, async (tx) => {
const [users, boards, tags, categories] = await Promise.all([
tx.user.findMany({
where: { tenantId: tenant.id },
where: { tenantId: tenant.id, globalRole: { not: 'SITE_ADMIN' } },
select: {
id: true,
name: true,
email: true,
tenantRole: true,
globalRole: true,
createdAt: true
},
orderBy: { createdAt: 'asc' }
+12 -11
View File
@@ -141,20 +141,21 @@
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3 font-medium text-gray-900 dark:text-gray-100">
{user.name}
{#if user.globalRole === 'SITE_ADMIN'}
<span class="ml-1 rounded bg-purple-100 px-1.5 py-0.5 text-[10px] font-semibold text-purple-700">SITE ADMIN</span>
{/if}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">{user.email}</td>
<td class="px-4 py-3">
<select
value={user.tenantRole}
onchange={(e) => changeRole(user.id, e.currentTarget.value)}
class="rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-2 py-1 text-xs focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
>
<option value="ADMIN">Admin</option>
<option value="MEMBER">Member</option>
</select>
{#if user.id === data.user?.id}
<span class="text-xs text-gray-500 dark:text-gray-400">{user.tenantRole === 'ADMIN' ? 'Admin' : 'Member'}</span>
{:else}
<select
value={user.tenantRole}
onchange={(e) => changeRole(user.id, e.currentTarget.value)}
class="rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-2 py-1 text-xs focus:border-[var(--color-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
>
<option value="ADMIN">Admin</option>
<option value="MEMBER">Member</option>
</select>
{/if}
</td>
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{formatDate(user.createdAt)}</td>
</tr>
@@ -22,6 +22,11 @@ export const PATCH: RequestHandler = async ({ params, request, locals }) => {
});
if (!targetUser) throw error(404, 'User not found');
// Tenant admins cannot modify SITE_ADMIN users
if (targetUser.globalRole === 'SITE_ADMIN') {
throw error(403, 'Cannot modify a site admin');
}
// Prevent demoting the last tenant admin
if (targetUser.tenantRole === 'ADMIN' && result.data.tenantRole === 'MEMBER') {
const adminCount = await tx.user.count({