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"; "use client";
import { useState } from "react"; import { useCallback, useEffect, useState } from "react";
import type { ReactElement, ReactNode } 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"; import Link from "next/link";
interface CategoryConfig { interface CategoryConfig {
@@ -11,6 +14,7 @@ interface CategoryConfig {
accent: string; accent: string;
iconBg: string; iconBg: string;
icon: ReactNode; icon: ReactNode;
adminOnly?: boolean;
} }
interface SettingsCategoryCardProps { interface SettingsCategoryCardProps {
@@ -200,6 +204,7 @@ const categories: CategoryConfig[] = [
title: "Users", title: "Users",
description: "Invite, manage roles, and deactivate users across your workspaces.", description: "Invite, manage roles, and deactivate users across your workspaces.",
href: "/settings/users", href: "/settings/users",
adminOnly: true,
accent: "var(--ms-green-400)", accent: "var(--ms-green-400)",
iconBg: "rgba(34, 197, 94, 0.12)", iconBg: "rgba(34, 197, 94, 0.12)",
icon: ( icon: (
@@ -277,7 +282,30 @@ const categories: CategoryConfig[] = [
}, },
]; ];
const ADMIN_ROLES: WorkspaceMemberRole[] = [WorkspaceMemberRole.OWNER, WorkspaceMemberRole.ADMIN];
export default function SettingsPage(): ReactElement { 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 ( return (
<div className="max-w-6xl mx-auto p-6"> <div className="max-w-6xl mx-auto p-6">
{/* Page header */} {/* Page header */}
@@ -305,7 +333,7 @@ export default function SettingsPage(): ReactElement {
{/* Category grid */} {/* Category grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{categories.map((category) => ( {visibleCategories.map((category) => (
<SettingsCategoryCard key={category.href} category={category} /> <SettingsCategoryCard key={category.href} category={category} />
))} ))}
</div> </div>

View File

@@ -1,9 +1,11 @@
"use client"; "use client";
import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import Image from "next/image"; import Image from "next/image";
import { useAuth } from "@/lib/auth/auth-context"; import { useAuth } from "@/lib/auth/auth-context";
import { fetchUserWorkspaces } from "@/lib/api/workspaces";
import { useSidebar } from "./SidebarContext"; import { useSidebar } from "./SidebarContext";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -461,10 +463,26 @@ interface UserCardProps {
function UserCard({ collapsed }: UserCardProps): React.JSX.Element { function UserCard({ collapsed }: UserCardProps): React.JSX.Element {
const { user } = useAuth(); 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 displayName = user?.name ?? "User";
const initials = getInitials(displayName); const initials = getInitials(displayName);
const role = "Member"; const role = roleLabel;
return ( return (
<footer <footer