Added tenant management
Applied basic access control
This commit is contained in:
@@ -25,7 +25,7 @@ import { BrandLogo } from '../ui/BrandLogo';
|
||||
import { HelpTooltip } from '../ui/HelpTooltip';
|
||||
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUsers } from '../../services/api';
|
||||
import { getUsers, getTenant } from '../../services/api';
|
||||
import { getUserEmail, registerUserEmail } from '../../lib/formatters';
|
||||
|
||||
interface SubNavItem {
|
||||
@@ -94,6 +94,12 @@ const navItems: NavItem[] = [
|
||||
icon: ShieldAlert,
|
||||
help: 'Monitoring & Evaluation (M&E) framework, indicator tracking, ISO 31000 risk registers, mitigation plans, and compliance audits.',
|
||||
},
|
||||
{
|
||||
name: 'Tenant Management',
|
||||
path: '/tenants',
|
||||
icon: Database,
|
||||
help: 'Multi-tenant partition directory, organizational onboarding, status lifecycle controls, and system-wide isolation boundaries.',
|
||||
},
|
||||
{
|
||||
name: 'Settings & Communications',
|
||||
path: '/settings',
|
||||
@@ -103,7 +109,7 @@ const navItems: NavItem[] = [
|
||||
];
|
||||
|
||||
export const AppLayout: React.FC = () => {
|
||||
const { user, claims, sub, tenantId, hashedTenantId, logout } = useAuth();
|
||||
const { user, claims, sub, tenantId, hashedTenantId, tenantName: authTenantName, canManageTenants, logout } = useAuth();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@@ -112,6 +118,14 @@ 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;
|
||||
});
|
||||
|
||||
// Pre-populate user directory cache so all components and modals resolve user emails instantly
|
||||
useQuery({
|
||||
queryKey: ['users-directory'],
|
||||
@@ -119,6 +133,14 @@ export const AppLayout: React.FC = () => {
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Query tenant endpoint to resolve tenant name if not provided in claims or profile
|
||||
const { data: remoteTenant } = useQuery({
|
||||
queryKey: ['active-tenant-header', tenantId],
|
||||
queryFn: () => getTenant(tenantId!),
|
||||
enabled: !!tenantId && !claims?.tenant && !user?.tenant && !authTenantName,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// Ensure current logged-in user is registered in the email cache
|
||||
const effectiveEmail = ((user?.email || claims?.email || claims?.['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] || claims?.upn) as string);
|
||||
if (sub && effectiveEmail) {
|
||||
@@ -143,14 +165,16 @@ export const AppLayout: React.FC = () => {
|
||||
(claims.upn as string) ||
|
||||
(sub ? getUserEmail(sub) : '');
|
||||
|
||||
// Extract tenant name directly from the user claim named 'tenant'
|
||||
// Extract tenant name from user claim 'tenant', auth context, remote tenant endpoint, or organization name
|
||||
const tenantName =
|
||||
(claims?.tenant as string) ||
|
||||
user?.tenant ||
|
||||
(claims?.['tenant'] as string) ||
|
||||
authTenantName ||
|
||||
user?.tenant ||
|
||||
remoteTenant?.name ||
|
||||
user?.organisationName ||
|
||||
(claims?.organisation as string) ||
|
||||
'Department of Science, Innovation & Technology (DSI)';
|
||||
(tenantId ? `Tenant #${tenantId}` : 'Department of Science, Innovation & Technology (DSI)');
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-[#090d16] font-sans text-slate-100 selection:bg-blue-600 selection:text-white">
|
||||
@@ -189,7 +213,7 @@ export const AppLayout: React.FC = () => {
|
||||
|
||||
{/* Navigation Items */}
|
||||
<nav className="flex-1 overflow-y-auto px-3 py-4 space-y-1.5">
|
||||
{navItems.map((item) => {
|
||||
{visibleNavItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const hasSub = item.subItems && item.subItems.length > 0;
|
||||
|
||||
@@ -289,21 +313,40 @@ export const AppLayout: React.FC = () => {
|
||||
|
||||
<div className="hidden sm:flex items-center gap-2 text-xs font-medium text-slate-300">
|
||||
<span className="text-slate-400">Tenant:</span>
|
||||
<span
|
||||
className="bg-slate-900/80 text-slate-200 px-2.5 py-1 rounded-lg border border-slate-800 flex items-center gap-1.5 font-medium shadow-xs max-w-[320px] truncate"
|
||||
title={`${tenantName}${hashedTenantId ? ` (Partition: ${hashedTenantId})` : ''}`}
|
||||
>
|
||||
<Database className="w-3.5 h-3.5 text-blue-400 shrink-0" />
|
||||
<span className="truncate">{tenantName}</span>
|
||||
{hashedTenantId && (
|
||||
<span
|
||||
className="text-[10px] font-mono text-blue-400 bg-blue-950/60 px-1.5 py-0.5 rounded border border-blue-800/60 shrink-0"
|
||||
title={`Tenant Partition HashID: ${hashedTenantId}`}
|
||||
>
|
||||
{hashedTenantId}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{canManageTenants ? (
|
||||
<Link
|
||||
to="/tenants"
|
||||
className="bg-slate-900/80 hover:bg-slate-800/90 text-slate-200 hover:text-white px-2.5 py-1 rounded-lg border border-slate-800 hover:border-slate-700 flex items-center gap-1.5 font-medium shadow-xs max-w-[320px] truncate transition group cursor-pointer"
|
||||
title={`Manage Tenants • Current: ${tenantName}${hashedTenantId ? ` (Partition: ${hashedTenantId})` : ''}`}
|
||||
>
|
||||
<Database className="w-3.5 h-3.5 text-blue-400 shrink-0 group-hover:text-blue-300 transition" />
|
||||
<span className="truncate">{tenantName}</span>
|
||||
{hashedTenantId && (
|
||||
<span
|
||||
className="text-[10px] font-mono text-blue-400 bg-blue-950/60 px-1.5 py-0.5 rounded border border-blue-800/60 shrink-0"
|
||||
title={`Tenant Partition HashID: ${hashedTenantId}`}
|
||||
>
|
||||
{hashedTenantId}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
) : (
|
||||
<span
|
||||
className="bg-slate-900/80 text-slate-200 px-2.5 py-1 rounded-lg border border-slate-800 flex items-center gap-1.5 font-medium shadow-xs max-w-[320px] truncate"
|
||||
title={`${tenantName}${hashedTenantId ? ` (Partition: ${hashedTenantId})` : ''}`}
|
||||
>
|
||||
<Database className="w-3.5 h-3.5 text-blue-400 shrink-0" />
|
||||
<span className="truncate">{tenantName}</span>
|
||||
{hashedTenantId && (
|
||||
<span
|
||||
className="text-[10px] font-mono text-blue-400 bg-blue-950/60 px-1.5 py-0.5 rounded border border-blue-800/60 shrink-0"
|
||||
title={`Tenant Partition HashID: ${hashedTenantId}`}
|
||||
>
|
||||
{hashedTenantId}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<HelpTooltip
|
||||
title="Tenant Context"
|
||||
content={`Identifies the multi-tenant organization partition and database boundary within the national PFM environment.${
|
||||
|
||||
Reference in New Issue
Block a user