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:
@@ -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"
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user