feat(web): gate settings nav by workspace role (MS21-RBAC-001) #579

Merged
jason.woltje merged 1 commits from feat/ms21-rbac into main 2026-02-28 23:06:23 +00:00
2 changed files with 49 additions and 3 deletions

View File

@@ -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<boolean>(false);
const checkRole = useCallback(async (): Promise<void> => {
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 (
<div className="max-w-6xl mx-auto p-6">
{/* Page header */}
@@ -305,7 +333,7 @@ export default function SettingsPage(): ReactElement {
{/* Category grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{categories.map((category) => (
{visibleCategories.map((category) => (
<SettingsCategoryCard key={category.href} category={category} />
))}
</div>

View File

@@ -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<string>("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 (
<footer