feat(rbac): wire component-level RBAC guards across Sidebar, Categories, Families, Brands, Media Assets, Channels, Users, and Settings

This commit is contained in:
Inamul-hasan-tec
2026-08-11 11:33:54 +05:30
parent a15e5d9139
commit 27724a2328
9 changed files with 100 additions and 61 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ export function Sidebar() {
return acc;
}
if (item.permission && !hasPermission(permissions, item.permission, "view")) {
if (item.permission && !hasPermission(permissions, item.permission, "view", user)) {
return acc;
}
+19 -15
View File
@@ -21,6 +21,8 @@ import type { Product } from "../../product/types/product.types";
import { notify } from "../../../services/toast";
import apiClient from "../../../api/axiosInstance";
import { getAssetUrl } from "../../../lib/utils";
import { ProtectedRoute } from "../../../components/layouts/ProtectedRoute";
import { Can } from "../../../components/customs/Can";
export default function AssetList() {
const navigate = useNavigate();
@@ -386,21 +388,22 @@ export default function AssetList() {
};
return (
<PageWrapper>
<Breadcrumb
items={[{ label: "Home" }, { label: "Asset Manager" }]}
actions={
<>
<Button
className="bg-primary hover:bg-primary-hover text-white flex items-center shadow-md rounded-lg px-4 py-2"
onClick={() => navigate("/assets/new")}
>
<Upload className="w-4 h-4 mr-2" />
Upload Asset
</Button>
</>
}
/>
<ProtectedRoute node="media.assets">
<PageWrapper>
<Breadcrumb
items={[{ label: "Home" }, { label: "Asset Manager" }]}
actions={
<Can node="media.assets" action="create">
<Button
className="bg-primary hover:bg-primary-hover text-white flex items-center shadow-md rounded-lg px-4 py-2"
onClick={() => navigate("/assets/new")}
>
<Upload className="w-4 h-4 mr-2" />
Upload Asset
</Button>
</Can>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
@@ -1613,5 +1616,6 @@ export default function AssetList() {
onCancel={() => setDeleteModal({ isOpen: false, id: "", name: "" })}
/>
</PageWrapper>
</ProtectedRoute>
);
}
+11 -5
View File
@@ -9,7 +9,11 @@ import { useBrand } from "../hook/useBrand";
import { Breadcrumb } from "../../../components/layouts/Breadcrumb";
import { ConfirmationModal } from "../../../components/modals/ConfirmationModal";
import { Can } from "../../../components/customs/Can";
import { usePermissions } from "../../../hooks/usePermission";
export default function BrandList() {
const { canEdit, canDelete } = usePermissions("masters.brands");
const navigate = useNavigate();
const { brands, fetchBrands, deleteBrand } = useBrand();
const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; id: string; name: string }>({ isOpen: false, id: "", name: "" });
@@ -38,9 +42,11 @@ export default function BrandList() {
<Breadcrumb
items={[{ label: "Home" }, { label: "Brands" }]}
actions={
<Button variant="primary" icon={<Plus className="w-4 h-4" />} onClick={() => navigate("/brands/new")}>
Create Brand
</Button>
<Can node="masters.brands" action="create">
<Button variant="primary" icon={<Plus className="w-4 h-4" />} onClick={() => navigate("/brands/new")}>
Create Brand
</Button>
</Can>
}
/>
@@ -49,8 +55,8 @@ export default function BrandList() {
brands={brands}
onRowClick={(row) => navigate(`/brands/${row.id}/edit`)}
onView={(row) => navigate(`/brands/${row.id}/view`)}
onEdit={(row) => navigate(`/brands/${row.id}/edit`)}
onDelete={(row) => setDeleteModal({ isOpen: true, id: row.id, name: row.name })}
onEdit={canEdit ? ((row) => navigate(`/brands/${row.id}/edit`)) : undefined}
onDelete={canDelete ? ((row) => setDeleteModal({ isOpen: true, id: row.id, name: row.name })) : undefined}
/>
</div>
+11 -7
View File
@@ -11,6 +11,8 @@ import { StatsCard } from "../../../components/customs/StatsCard";
import { ConfirmationModal } from "../../../components/modals/ConfirmationModal";
import { CategoryTaxonomyTree } from "../components/CategoryTaxonomyTree";
import { Can } from "../../../components/customs/Can";
type ViewMode = "table" | "tree";
export default function CategoryList() {
@@ -56,13 +58,15 @@ export default function CategoryList() {
<Breadcrumb
items={[{ label: "Home" }, { label: "Categories" }]}
actions={
<Button
variant="primary"
icon={<Plus className="w-4 h-4" />}
onClick={() => navigate("/categories/new")}
>
Add Root Category
</Button>
<Can node="products.categories" action="create">
<Button
variant="primary"
icon={<Plus className="w-4 h-4" />}
onClick={() => navigate("/categories/new")}
>
Add Root Category
</Button>
</Can>
}
/>
+13 -7
View File
@@ -12,6 +12,9 @@ import { useChannel } from "../hook/useChannel";
import type { Channel } from "../types/channels.types";
import { ConfirmationModal } from "../../../components/modals/ConfirmationModal";
import { Can } from "../../../components/customs/Can";
import { usePermissions } from "../../../hooks/usePermission";
const CHANNEL_TYPES_META: Record<string, { label: string, icon: any, typeColor: string, typeBg: string }> = {
ecommerce: { label: "Ecommerce", icon: ShoppingCart, typeColor: "text-blue-600", typeBg: "bg-blue-50" },
marketplace: { label: "Marketplace", icon: ShoppingBag, typeColor: "text-orange-600", typeBg: "bg-orange-50" },
@@ -24,6 +27,7 @@ const CHANNEL_TYPES_META: Record<string, { label: string, icon: any, typeColor:
};
export default function ChannelList() {
const { canEdit, canDelete } = usePermissions("channels.syndication");
const navigate = useNavigate();
const { items, fetchItems, loading, deleteItem } = useChannel();
const [deleteModal, setDeleteModal] = useState({ isOpen: false, id: "", name: "" });
@@ -109,7 +113,7 @@ export default function ChannelList() {
return (
<ProtectedRoute node="settings.integrations">
<ProtectedRoute node="channels.syndication">
<PageWrapper>
<Breadcrumb
items={[{ label: "Home" }, { label: "Channel Registry" }]}
@@ -119,10 +123,12 @@ export default function ChannelList() {
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
<Button onClick={() => navigate("new")} className="bg-primary hover:bg-primary-hover text-white">
<Plus className="w-4 h-4 mr-2" />
Add Channel
</Button>
<Can node="channels.syndication" action="create">
<Button onClick={() => navigate("new")} className="bg-primary hover:bg-primary-hover text-white">
<Plus className="w-4 h-4 mr-2" />
Add Channel
</Button>
</Can>
</>
}
/>
@@ -167,8 +173,8 @@ export default function ChannelList() {
data={items}
actionConfig={{
onView: (row) => navigate(`${row.id}/view`),
onEdit: (row) => navigate(`${row.id}/edit`),
onDelete: (row) => setDeleteModal({ isOpen: true, id: row.id, name: row.name })
onEdit: canEdit ? ((row) => navigate(`${row.id}/edit`)) : undefined,
onDelete: canDelete ? ((row) => setDeleteModal({ isOpen: true, id: row.id, name: row.name })) : undefined
}}
/>
</div>
+7 -3
View File
@@ -10,6 +10,8 @@ import { Breadcrumb } from "../../../components/layouts/Breadcrumb";
import { StatsCard } from "../../../components/customs/StatsCard";
import { ConfirmationModal } from "../../../components/modals/ConfirmationModal";
import { Can } from "../../../components/customs/Can";
export default function FamilyList() {
const navigate = useNavigate();
const { families, fetchFamilies, deleteFamily } = useFamily();
@@ -49,9 +51,11 @@ export default function FamilyList() {
<Breadcrumb
items={[{ label: "Home" }, { label: "Product Families" }]}
actions={
<Button variant="primary" icon={<Plus className="w-4 h-4" />} onClick={() => navigate("/families/new")}>
Create Family
</Button>
<Can node="products.families" action="create">
<Button variant="primary" icon={<Plus className="w-4 h-4" />} onClick={() => navigate("/families/new")}>
Create Family
</Button>
</Can>
}
/>
+12 -6
View File
@@ -9,6 +9,8 @@ import { Breadcrumb } from "../../../components/layouts/Breadcrumb";
import { Radio } from "../../../components/customs/Radio";
import { settingsService } from "../services/settings.service";
import { notify } from "../../../services/toast";
import { ProtectedRoute } from "../../../components/layouts/ProtectedRoute";
import { Can } from "../../../components/customs/Can";
export default function SettingList() {
const navigate = useNavigate();
@@ -77,8 +79,9 @@ export default function SettingList() {
];
return (
<PageWrapper>
<Breadcrumb items={[{ label: "Home" }, { label: "Settings" }]} />
<ProtectedRoute node="settings.general">
<PageWrapper>
<Breadcrumb items={[{ label: "Home" }, { label: "Settings" }]} />
{/* Horizontal Tabs Header */}
<div className="flex items-center gap-2 border-b border-primary/10 mb-6 overflow-x-auto pb-px">
@@ -191,10 +194,12 @@ export default function SettingList() {
<div className="flex justify-end gap-3 px-6 py-4 border-t border-primary/5 bg-gradient-to-r from-surface to-primary/5/30">
<Button variant="ghost" className="text-muted-foreground hover:text-foreground hover:bg-background" onClick={() => window.location.reload()}>Cancel</Button>
<Button className="bg-primary hover:bg-primary-hover text-white flex items-center gap-2" loading={saving} onClick={handleSaveGeneral}>
<Save className="w-4 h-4" />
Save Changes
</Button>
<Can node="settings.general" action="edit">
<Button className="bg-primary hover:bg-primary-hover text-white flex items-center gap-2" loading={saving} onClick={handleSaveGeneral}>
<Save className="w-4 h-4" />
Save Changes
</Button>
</Can>
</div>
</div>
)}
@@ -397,5 +402,6 @@ export default function SettingList() {
)}
</div>
</PageWrapper>
</ProtectedRoute>
);
}
+17 -11
View File
@@ -14,6 +14,8 @@ import { tenantService } from "../../tenants/services/tenant.service";
import { notify } from "../../../services/toast";
import type { DBRole, PermissionNode } from "../services/roles.service";
import type { Tenant } from "../../tenants/types/tenant.types";
import { ProtectedRoute } from "../../../components/layouts/ProtectedRoute";
import { Can } from "../../../components/customs/Can";
export default function UserList() {
const navigate = useNavigate();
@@ -378,16 +380,19 @@ export default function UserList() {
];
return (
<PageWrapper>
<Breadcrumb
items={[{ label: "Home" }, { label: "Users & Roles" }]}
actions={
<Button onClick={() => navigate("new")} className="bg-primary hover:bg-primary-hover text-white">
<Plus className="w-4 h-4 mr-2" />
Invite Member
</Button>
}
/>
<ProtectedRoute node="settings.users">
<PageWrapper>
<Breadcrumb
items={[{ label: "Home" }, { label: "Users & Roles" }]}
actions={
<Can node="settings.users" action="create">
<Button onClick={() => navigate("new")} className="bg-primary hover:bg-primary-hover text-white">
<Plus className="w-4 h-4 mr-2" />
Invite Member
</Button>
</Can>
}
/>
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
@@ -907,6 +912,7 @@ export default function UserList() {
</div>
</div>
)}
</PageWrapper>
</PageWrapper>
</ProtectedRoute>
);
}
+9 -6
View File
@@ -109,24 +109,27 @@ export const sidebarConfig: SidebarItem[] = [
label: 'Asset Management',
href: '/assets',
icon: Image,
permission: 'media.assets',
children: [
{ label: 'Asset Manager', href: '/assets', icon: Folder },
{ label: 'Asset Types', href: '/asset-types', icon: LayoutGrid },
{ label: 'Asset Families', href: '/asset-families', icon: Layers },
{ label: 'Asset Manager', href: '/assets', icon: Folder, permission: 'media.assets' },
{ label: 'Asset Types', href: '/asset-types', icon: LayoutGrid, permission: 'media.taxonomy' },
{ label: 'Asset Families', href: '/asset-families', icon: Layers, permission: 'media.taxonomy' },
]
},
{
label: 'Workflow & Approvals',
href: '/workflow',
icon: Workflow
icon: Workflow,
permission: 'products.items'
},
{
label: 'Channels & Integration',
href: '/channels',
icon: Radio,
permission: 'channels.syndication',
children: [
{ label: 'Channel Registry', href: '/channels', icon: Radio },
{ label: 'Channel Types', href: '/channel-types', icon: Layers2, permission: 'channels.types' },
{ label: 'Channel Registry', href: '/channels', icon: Radio, permission: 'channels.syndication' },
{ label: 'Channel Types', href: '/channel-types', icon: Layers2, permission: 'channels.syndication' },
{ label: 'Integration Hub', href: '/integrations', icon: Plug, permission: 'settings.integrations' },
]
},