Added tenant management

Applied basic access control
This commit is contained in:
2026-10-04 09:13:55 +02:00
parent 8100518549
commit 8fed46961a
7 changed files with 1349 additions and 22 deletions
@@ -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.${