diff --git a/client/src/components/Layout.jsx b/client/src/components/Layout.jsx
index fcf73c8..b6ce03b 100644
--- a/client/src/components/Layout.jsx
+++ b/client/src/components/Layout.jsx
@@ -3,6 +3,7 @@ import { Outlet, Link, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { useSocket } from '../contexts/SocketContext';
import DebugToggle from './DebugToggle';
+import { canAccessSettings, hasPermission } from '../utils/rbac';
import {
HomeIcon,
MapIcon,
@@ -27,25 +28,31 @@ const baseNavigation = [
{ name: 'Alerts', href: '/alerts', icon: BellIcon },
];
-const adminNavigation = [
- { name: 'Settings', href: '/settings', icon: CogIcon },
- { name: 'Debug', href: '/debug', icon: BugAntIcon },
-];
-
const Layout = () => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const { user, logout } = useAuth();
const { connected, recentDetections } = useSocket();
const location = useLocation();
- // Build navigation based on user role - ensure it's always an array
+ // Build navigation based on user permissions
const navigation = React.useMemo(() => {
if (!user) {
return baseNavigation; // Return base navigation if user not loaded yet
}
- return user.role === 'admin'
- ? [...baseNavigation, ...adminNavigation]
- : baseNavigation;
+
+ const nav = [...baseNavigation];
+
+ // Add Settings if user has any settings permissions
+ if (canAccessSettings(user.role)) {
+ nav.push({ name: 'Settings', href: '/settings', icon: CogIcon });
+ }
+
+ // Add Debug if user has debug permissions
+ if (hasPermission(user.role, 'debug.access')) {
+ nav.push({ name: 'Debug', href: '/debug', icon: BugAntIcon });
+ }
+
+ return nav;
}, [user?.role]);
return (
diff --git a/client/src/pages/Settings.jsx b/client/src/pages/Settings.jsx
index fa29fca..6fa9676 100644
--- a/client/src/pages/Settings.jsx
+++ b/client/src/pages/Settings.jsx
@@ -12,6 +12,7 @@ import {
EyeIcon,
EyeSlashIcon
} from '@heroicons/react/24/outline';
+import { hasPermission, canAccessSettings } from '../utils/rbac';
const Settings = () => {
const { user } = useAuth();
@@ -20,8 +21,8 @@ const Settings = () => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
- // Check if user has admin role
- const isAdmin = user?.role === 'admin';
+ // Check if user can access settings based on RBAC permissions
+ const canAccess = canAccessSettings(user?.role);
useEffect(() => {
fetchTenantConfig();
@@ -48,27 +49,60 @@ const Settings = () => {
);
}
- if (!isAdmin) {
+ if (!canAccess) {
return (
Access Denied
- You need admin privileges to access tenant settings.
+ You don't have permission to access tenant settings.
);
}
- const tabs = [
- { id: 'general', name: 'General', icon: CogIcon },
- { id: 'branding', name: 'Branding', icon: PaintBrushIcon },
- { id: 'security', name: 'Security', icon: ShieldCheckIcon },
- { id: 'authentication', name: 'Authentication', icon: KeyIcon },
- { id: 'users', name: 'Users', icon: UserGroupIcon },
- ];
+ // Filter tabs based on user permissions
+ const availableTabs = [
+ {
+ id: 'general',
+ name: 'General',
+ icon: CogIcon,
+ permission: 'tenant.view'
+ },
+ {
+ id: 'branding',
+ name: 'Branding',
+ icon: PaintBrushIcon,
+ permission: 'branding.view'
+ },
+ {
+ id: 'security',
+ name: 'Security',
+ icon: ShieldCheckIcon,
+ permission: 'security.view'
+ },
+ {
+ id: 'authentication',
+ name: 'Authentication',
+ icon: KeyIcon,
+ permission: 'auth.view'
+ },
+ {
+ id: 'users',
+ name: 'Users',
+ icon: UserGroupIcon,
+ permission: 'users.view'
+ },
+ ].filter(tab => hasPermission(user?.role, tab.permission));
+
+ // Set initial tab to first available tab
+ useEffect(() => {
+ if (availableTabs.length > 0 && !availableTabs.find(tab => tab.id === activeTab)) {
+ setActiveTab(availableTabs[0].id);
+ }
+ }, [availableTabs, activeTab]);
return (
@@ -81,7 +115,7 @@ const Settings = () => {
- {activeTab === 'general' &&
}
- {activeTab === 'branding' &&
}
- {activeTab === 'security' &&
}
- {activeTab === 'authentication' &&
}
- {activeTab === 'users' &&
}
+ {activeTab === 'general' && hasPermission(user?.role, 'tenant.view') && (
+
+ )}
+ {activeTab === 'branding' && hasPermission(user?.role, 'branding.view') && (
+
+ )}
+ {activeTab === 'security' && hasPermission(user?.role, 'security.view') && (
+
+ )}
+ {activeTab === 'authentication' && hasPermission(user?.role, 'auth.view') && (
+
+ )}
+ {activeTab === 'users' && hasPermission(user?.role, 'users.view') && (
+
+ )}
@@ -141,6 +185,7 @@ const GeneralSettings = ({ tenantConfig }) => (
// Branding Settings Component
const BrandingSettings = ({ tenantConfig, onRefresh }) => {
+ const { user } = useAuth();
const [branding, setBranding] = useState({
logo_url: '',
primary_color: '#3B82F6',
@@ -151,6 +196,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
const [uploading, setUploading] = useState(false);
const [logoPreview, setLogoPreview] = useState(null);
+ const canEdit = hasPermission(user?.role, 'branding.edit');
+
useEffect(() => {
if (tenantConfig?.branding) {
setBranding(tenantConfig.branding);
@@ -231,7 +278,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
type="text"
value={branding.company_name}
onChange={(e) => setBranding(prev => ({ ...prev, company_name: e.target.value }))}
- className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
+ disabled={!canEdit}
+ className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
/>
@@ -259,12 +307,12 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
{
handleFilePreview(e);
handleLogoUpload(e);
}}
- className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100"
- disabled={uploading}
+ className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 disabled:opacity-50"
/>
PNG, JPG up to 5MB
@@ -296,7 +344,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
type="url"
value={branding.logo_url}
onChange={(e) => setBranding(prev => ({ ...prev, logo_url: e.target.value }))}
- className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
+ disabled={!canEdit}
+ className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
placeholder="https://example.com/logo.png"
/>
@@ -310,13 +359,15 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
type="color"
value={branding.primary_color}
onChange={(e) => setBranding(prev => ({ ...prev, primary_color: e.target.value }))}
- className="h-10 w-20 border border-gray-300 rounded-md"
+ disabled={!canEdit}
+ className="h-10 w-20 border border-gray-300 rounded-md disabled:opacity-50"
/>
setBranding(prev => ({ ...prev, primary_color: e.target.value }))}
- className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
+ disabled={!canEdit}
+ className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
/>
@@ -328,26 +379,34 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
type="color"
value={branding.secondary_color}
onChange={(e) => setBranding(prev => ({ ...prev, secondary_color: e.target.value }))}
- className="h-10 w-20 border border-gray-300 rounded-md"
+ disabled={!canEdit}
+ className="h-10 w-20 border border-gray-300 rounded-md disabled:opacity-50"
/>
setBranding(prev => ({ ...prev, secondary_color: e.target.value }))}
- className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
+ disabled={!canEdit}
+ className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
/>
-
+ {canEdit ? (
+
+ ) : (
+
+ You don't have permission to edit branding settings
+
+ )}
@@ -357,6 +416,7 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
// Placeholder components for other tabs
const SecuritySettings = ({ tenantConfig, onRefresh }) => {
+ const { user } = useAuth();
const [securitySettings, setSecuritySettings] = useState({
ip_restriction_enabled: false,
ip_whitelist: [],
@@ -365,6 +425,8 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
const [newIP, setNewIP] = useState('');
const [saving, setSaving] = useState(false);
+ const canEdit = hasPermission(user?.role, 'security.edit');
+
useEffect(() => {
if (tenantConfig) {
setSecuritySettings({
@@ -432,7 +494,7 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
Security Settings