Implemented rbac

This commit is contained in:
2026-10-11 11:16:08 +02:00
parent 45a6c0aca0
commit 1142cfd94e
24 changed files with 3033 additions and 432 deletions
@@ -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