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}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if data.user}
|
{#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'}
|
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
|
||||||
<a
|
<a
|
||||||
href="/admin"
|
href="/admin"
|
||||||
@@ -143,6 +151,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1">
|
<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>
|
<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'}
|
{#if data.user.tenantRole === 'ADMIN' || data.user.globalRole === 'SITE_ADMIN'}
|
||||||
<a
|
<a
|
||||||
href="/admin"
|
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 withTenant(tenant.id, async (tx) => {
|
||||||
const [users, boards, tags, categories] = await Promise.all([
|
const [users, boards, tags, categories] = await Promise.all([
|
||||||
tx.user.findMany({
|
tx.user.findMany({
|
||||||
where: { tenantId: tenant.id },
|
where: { tenantId: tenant.id, globalRole: { not: 'SITE_ADMIN' } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
email: true,
|
email: true,
|
||||||
tenantRole: true,
|
tenantRole: true,
|
||||||
globalRole: true,
|
|
||||||
createdAt: true
|
createdAt: true
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'asc' }
|
orderBy: { createdAt: 'asc' }
|
||||||
|
|||||||
@@ -141,12 +141,12 @@
|
|||||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
<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">
|
<td class="px-4 py-3 font-medium text-gray-900 dark:text-gray-100">
|
||||||
{user.name}
|
{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>
|
||||||
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">{user.email}</td>
|
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">{user.email}</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
|
{#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
|
<select
|
||||||
value={user.tenantRole}
|
value={user.tenantRole}
|
||||||
onchange={(e) => changeRole(user.id, e.currentTarget.value)}
|
onchange={(e) => changeRole(user.id, e.currentTarget.value)}
|
||||||
@@ -155,6 +155,7 @@
|
|||||||
<option value="ADMIN">Admin</option>
|
<option value="ADMIN">Admin</option>
|
||||||
<option value="MEMBER">Member</option>
|
<option value="MEMBER">Member</option>
|
||||||
</select>
|
</select>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{formatDate(user.createdAt)}</td>
|
<td class="px-4 py-3 text-gray-500 dark:text-gray-400">{formatDate(user.createdAt)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export const PATCH: RequestHandler = async ({ params, request, locals }) => {
|
|||||||
});
|
});
|
||||||
if (!targetUser) throw error(404, 'User not found');
|
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
|
// Prevent demoting the last tenant admin
|
||||||
if (targetUser.tenantRole === 'ADMIN' && result.data.tenantRole === 'MEMBER') {
|
if (targetUser.tenantRole === 'ADMIN' && result.data.tenantRole === 'MEMBER') {
|
||||||
const adminCount = await tx.user.count({
|
const adminCount = await tx.user.count({
|
||||||
|
|||||||
Reference in New Issue
Block a user