From c939a541a7ad9af164d430a5ebceff7e55826896 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sat, 28 Feb 2026 23:06:23 +0000 Subject: [PATCH] feat(web): gate settings nav by workspace role (MS21-RBAC-001) (#579) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .../src/app/(authenticated)/settings/page.tsx | 32 +++++++++++++++++-- apps/web/src/components/layout/AppSidebar.tsx | 20 +++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(authenticated)/settings/page.tsx b/apps/web/src/app/(authenticated)/settings/page.tsx index 403a2ed..8048a24 100644 --- a/apps/web/src/app/(authenticated)/settings/page.tsx +++ b/apps/web/src/app/(authenticated)/settings/page.tsx @@ -1,7 +1,10 @@ "use client"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import type { ReactElement, ReactNode } from "react"; +import { WorkspaceMemberRole } from "@mosaic/shared"; +import { useAuth } from "@/lib/auth/auth-context"; +import { fetchUserWorkspaces } from "@/lib/api/workspaces"; import Link from "next/link"; interface CategoryConfig { @@ -11,6 +14,7 @@ interface CategoryConfig { accent: string; iconBg: string; icon: ReactNode; + adminOnly?: boolean; } interface SettingsCategoryCardProps { @@ -200,6 +204,7 @@ const categories: CategoryConfig[] = [ title: "Users", description: "Invite, manage roles, and deactivate users across your workspaces.", href: "/settings/users", + adminOnly: true, accent: "var(--ms-green-400)", iconBg: "rgba(34, 197, 94, 0.12)", icon: ( @@ -277,7 +282,30 @@ const categories: CategoryConfig[] = [ }, ]; +const ADMIN_ROLES: WorkspaceMemberRole[] = [WorkspaceMemberRole.OWNER, WorkspaceMemberRole.ADMIN]; + export default function SettingsPage(): ReactElement { + const { user } = useAuth(); + const [isAdmin, setIsAdmin] = useState(false); + + const checkRole = useCallback(async (): Promise => { + if (user === null) return; + try { + const workspaces = await fetchUserWorkspaces(); + const hasAdminRole = workspaces.some((ws) => ADMIN_ROLES.includes(ws.role)); + setIsAdmin(hasAdminRole); + } catch { + // Fail open — show all items if we can't determine role + setIsAdmin(true); + } + }, [user]); + + useEffect(() => { + void checkRole(); + }, [checkRole]); + + const visibleCategories = categories.filter((c) => c.adminOnly !== true || isAdmin); + return (
{/* Page header */} @@ -305,7 +333,7 @@ export default function SettingsPage(): ReactElement { {/* Category grid */}
- {categories.map((category) => ( + {visibleCategories.map((category) => ( ))}
diff --git a/apps/web/src/components/layout/AppSidebar.tsx b/apps/web/src/components/layout/AppSidebar.tsx index b678795..d0cbd97 100644 --- a/apps/web/src/components/layout/AppSidebar.tsx +++ b/apps/web/src/components/layout/AppSidebar.tsx @@ -1,9 +1,11 @@ "use client"; +import { useEffect, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import Image from "next/image"; import { useAuth } from "@/lib/auth/auth-context"; +import { fetchUserWorkspaces } from "@/lib/api/workspaces"; import { useSidebar } from "./SidebarContext"; // --------------------------------------------------------------------------- @@ -461,10 +463,26 @@ interface UserCardProps { function UserCard({ collapsed }: UserCardProps): React.JSX.Element { const { user } = useAuth(); + const [roleLabel, setRoleLabel] = useState("Member"); + + useEffect(() => { + if (user === null) return; + fetchUserWorkspaces() + .then((workspaces) => { + if (workspaces.length === 0) return; + const first = workspaces[0]; + if (!first) return; + const role = first.role; + setRoleLabel(role.charAt(0) + role.slice(1).toLowerCase()); + }) + .catch(() => { + // keep default + }); + }, [user]); const displayName = user?.name ?? "User"; const initials = getInitials(displayName); - const role = "Member"; + const role = roleLabel; return (