Implemented rbac
This commit is contained in:
@@ -29,12 +29,14 @@ import { ErrorBoundary } from '../ui/ErrorBoundary';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUsers, getTenant } from '../../services/api';
|
||||
import { getUserEmail, registerUserEmail } from '../../lib/formatters';
|
||||
import { useRbac, RoleClaim } from '../../auth/rbac';
|
||||
|
||||
interface SubNavItem {
|
||||
name: string;
|
||||
path: string;
|
||||
icon: React.ElementType;
|
||||
help?: string;
|
||||
claim?: RoleClaim;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
@@ -42,6 +44,7 @@ interface NavItem {
|
||||
path: string;
|
||||
icon: React.ElementType;
|
||||
help: string;
|
||||
claim?: RoleClaim;
|
||||
subItems?: SubNavItem[];
|
||||
}
|
||||
|
||||
@@ -51,36 +54,42 @@ const navItems: NavItem[] = [
|
||||
path: '/dashboard',
|
||||
icon: LayoutDashboard,
|
||||
help: 'Strategic macro metrics, portfolio value, disbursed funding, ISO 31000 risk heatmap, and projects requiring immediate intervention.',
|
||||
claim: 'dashboard',
|
||||
},
|
||||
{
|
||||
name: 'Governance & Structure',
|
||||
path: '/governance',
|
||||
icon: Building2,
|
||||
help: 'Master records for legal entities, holding structures, investment portfolios, and thematic programmes funded under statutory mandates.',
|
||||
claim: 'governance',
|
||||
},
|
||||
{
|
||||
name: 'Projects & Milestones',
|
||||
path: '/projects',
|
||||
icon: FolderKanban,
|
||||
help: 'Project Work Breakdown Structure (WBS), contractual deliverables, scheduled milestones, evidence verification, and execution tracking.',
|
||||
claim: 'projects_milestones',
|
||||
subItems: [
|
||||
{
|
||||
name: 'Beneficiaries',
|
||||
path: '/projects/beneficiaries',
|
||||
icon: Users,
|
||||
help: 'Manage target beneficiaries, demographics, and statutory compliance items satisfying beneficiaries.',
|
||||
claim: 'beneficiaries',
|
||||
},
|
||||
{
|
||||
name: 'Change Management',
|
||||
path: '/projects/change-management',
|
||||
icon: GitPullRequest,
|
||||
help: 'Track project issues, contractual change requests, and resolve changes in-place.',
|
||||
claim: 'change_management',
|
||||
},
|
||||
{
|
||||
name: 'Site Visits',
|
||||
path: '/projects/site-visits',
|
||||
icon: MapPin,
|
||||
help: 'Log physical site visits, field observations, inspector findings, and project feedback.',
|
||||
claim: 'site_visits',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -89,18 +98,21 @@ const navItems: NavItem[] = [
|
||||
path: '/grants',
|
||||
icon: Coins,
|
||||
help: 'Financial management including statutory awards, multi-year budget line allocations, tranches, and scheduled payment disbursements.',
|
||||
claim: 'grant_financials',
|
||||
},
|
||||
{
|
||||
name: 'M&E, Evidence & Risks',
|
||||
path: '/monitoring',
|
||||
icon: ShieldAlert,
|
||||
help: 'Monitoring & Evaluation (M&E) framework, indicator tracking, ISO 31000 risk registers, mitigation plans, and compliance audits.',
|
||||
claim: 'evidence_risks',
|
||||
},
|
||||
{
|
||||
name: 'Tenant Management',
|
||||
path: '/tenants',
|
||||
icon: Database,
|
||||
help: 'Multi-tenant partition directory, organizational onboarding, status lifecycle controls, and system-wide isolation boundaries.',
|
||||
claim: 'tenant_management',
|
||||
},
|
||||
{
|
||||
name: 'Settings & Communications',
|
||||
@@ -111,7 +123,9 @@ const navItems: NavItem[] = [
|
||||
];
|
||||
|
||||
export const AppLayout: React.FC = () => {
|
||||
const { user, claims, sub, tenantId, hashedTenantId, tenantName: authTenantName, canManageTenants, logout } = useAuth();
|
||||
const { user, claims, sub, tenantId, hashedTenantId, tenantName: authTenantName, canManageTenants, logout, roles } = useAuth();
|
||||
const userRole = user?.role || (Array.isArray(roles) && roles.length > 0 ? roles[0] : (claims?.role as string) || 'User');
|
||||
const { canView } = useRbac();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@@ -120,13 +134,38 @@ export const AppLayout: React.FC = () => {
|
||||
await logout();
|
||||
};
|
||||
|
||||
// Only show Tenant Management if user's tenant_id == 1 and their role list has Admin in it
|
||||
const visibleNavItems = navItems.filter((item) => {
|
||||
if (item.path === '/tenants') {
|
||||
return canManageTenants;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Filter navigation shelf strictly according to RBAC Role Claims
|
||||
const visibleNavItems = navItems
|
||||
.filter((item) => {
|
||||
if (item.path === '/tenants') {
|
||||
return canManageTenants || canView('tenant_management');
|
||||
}
|
||||
if (item.path === '/settings') {
|
||||
return (
|
||||
canView('settings_users') ||
|
||||
canView('settings_invites') ||
|
||||
canView('settings_notifications') ||
|
||||
canView('settings_auditlog') ||
|
||||
canView('tenant_self_service')
|
||||
);
|
||||
}
|
||||
if (item.claim) {
|
||||
return canView(item.claim);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((item) => {
|
||||
if (!item.subItems) return item;
|
||||
return {
|
||||
...item,
|
||||
subItems: item.subItems.filter((subItem) => {
|
||||
if (subItem.claim) {
|
||||
return canView(subItem.claim);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// Pre-populate user directory cache so all components and modals resolve user emails instantly
|
||||
useQuery({
|
||||
@@ -355,6 +394,11 @@ export const AppLayout: React.FC = () => {
|
||||
<p className="text-[11px] text-slate-400 truncate font-mono" title={tokenUserEmail}>
|
||||
{tokenUserEmail || 'Authenticated User'}
|
||||
</p>
|
||||
<div className="mt-1 flex items-center gap-1.5">
|
||||
<span className="text-[10px] font-semibold text-blue-400 bg-blue-500/10 border border-blue-500/25 px-1.5 py-0.5 rounded truncate" title={`Assigned Role: ${userRole}`}>
|
||||
Role: {userRole}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
@@ -369,13 +413,14 @@ export const AppLayout: React.FC = () => {
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div
|
||||
className="h-10 w-10 shrink-0 rounded-xl bg-blue-600/20 border border-blue-500/40 flex items-center justify-center font-bold text-xs text-blue-400 relative group cursor-pointer shadow-xs"
|
||||
title={`${tokenUserName} (${tokenUserEmail})`}
|
||||
title={`${tokenUserName} (${tokenUserEmail}) • Role: ${userRole}`}
|
||||
>
|
||||
{tokenUserName ? tokenUserName.slice(0, 2).toUpperCase() : 'AU'}
|
||||
<span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-emerald-500 border-2 border-[#0d121f]" />
|
||||
<div className="absolute left-full ml-3 px-3 py-2 bg-[#0d1322] text-white text-xs font-medium rounded-xl shadow-2xl border border-slate-700 whitespace-nowrap z-50 pointer-events-none opacity-0 group-hover:opacity-100 transition-all duration-150">
|
||||
<div className="font-semibold">{tokenUserName}</div>
|
||||
<div className="text-[10px] text-slate-400 font-mono mt-0.5">{tokenUserEmail}</div>
|
||||
<div className="text-[10px] text-blue-400 font-semibold mt-1">Role: {userRole}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -474,9 +519,14 @@ export const AppLayout: React.FC = () => {
|
||||
{/* User Profile and Controls */}
|
||||
<div className="relative flex items-center gap-3">
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-xs font-semibold text-white truncate max-w-[200px]" title={tokenUserName}>
|
||||
{tokenUserName}
|
||||
</p>
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
<p className="text-xs font-semibold text-white truncate max-w-[150px]" title={tokenUserName}>
|
||||
{tokenUserName}
|
||||
</p>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-blue-400 bg-blue-500/10 border border-blue-500/25 px-1.5 py-0.5 rounded truncate" title={`Role: ${userRole}`}>
|
||||
{userRole}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-400 font-mono truncate max-w-[200px]" title={tokenUserEmail}>
|
||||
{tokenUserEmail}
|
||||
</p>
|
||||
@@ -498,8 +548,14 @@ export const AppLayout: React.FC = () => {
|
||||
onMouseLeave={() => setUserMenuOpen(false)}
|
||||
>
|
||||
<div className="p-2.5 border-b border-slate-800">
|
||||
<p className="text-xs font-semibold text-white">{tokenUserName}</p>
|
||||
<p className="text-[11px] text-slate-300 font-medium truncate">{tokenUserEmail || (sub ? getUserEmail(sub) : '')}</p>
|
||||
<div className="flex items-center justify-between gap-1.5">
|
||||
<p className="text-xs font-semibold text-white truncate">{tokenUserName}</p>
|
||||
<span className="text-[10px] font-semibold uppercase text-blue-400 bg-blue-500/10 border border-blue-500/25 px-1.5 py-0.5 rounded shrink-0">
|
||||
{userRole}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-300 font-medium truncate mt-0.5">{tokenUserEmail || (sub ? getUserEmail(sub) : '')}</p>
|
||||
<p className="text-[10px] text-slate-400 mt-1">Role: <strong className="text-blue-300">{userRole}</strong></p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user