feat(rbac): wire component-level RBAC guards across Sidebar, Categories, Families, Brands, Media Assets, Channels, Users, and Settings
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user