feat(web): gate settings nav by workspace role (MS21-RBAC-001) #579
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user