Fix jwt-token
This commit is contained in:
@@ -3,6 +3,7 @@ import { Outlet, Link, useLocation } from 'react-router-dom';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useSocket } from '../contexts/SocketContext';
|
import { useSocket } from '../contexts/SocketContext';
|
||||||
import DebugToggle from './DebugToggle';
|
import DebugToggle from './DebugToggle';
|
||||||
|
import { canAccessSettings, hasPermission } from '../utils/rbac';
|
||||||
import {
|
import {
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
@@ -27,25 +28,31 @@ const baseNavigation = [
|
|||||||
{ name: 'Alerts', href: '/alerts', icon: BellIcon },
|
{ name: 'Alerts', href: '/alerts', icon: BellIcon },
|
||||||
];
|
];
|
||||||
|
|
||||||
const adminNavigation = [
|
|
||||||
{ name: 'Settings', href: '/settings', icon: CogIcon },
|
|
||||||
{ name: 'Debug', href: '/debug', icon: BugAntIcon },
|
|
||||||
];
|
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const { connected, recentDetections } = useSocket();
|
const { connected, recentDetections } = useSocket();
|
||||||
const location = useLocation();
|
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(() => {
|
const navigation = React.useMemo(() => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return baseNavigation; // Return base navigation if user not loaded yet
|
return baseNavigation; // Return base navigation if user not loaded yet
|
||||||
}
|
}
|
||||||
return user.role === 'admin'
|
|
||||||
? [...baseNavigation, ...adminNavigation]
|
const nav = [...baseNavigation];
|
||||||
: 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]);
|
}, [user?.role]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
EyeIcon,
|
EyeIcon,
|
||||||
EyeSlashIcon
|
EyeSlashIcon
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
|
import { hasPermission, canAccessSettings } from '../utils/rbac';
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -20,8 +21,8 @@ const Settings = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
// Check if user has admin role
|
// Check if user can access settings based on RBAC permissions
|
||||||
const isAdmin = user?.role === 'admin';
|
const canAccess = canAccessSettings(user?.role);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTenantConfig();
|
fetchTenantConfig();
|
||||||
@@ -48,27 +49,60 @@ const Settings = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!canAccess) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<ShieldCheckIcon className="mx-auto h-12 w-12 text-gray-400" />
|
<ShieldCheckIcon className="mx-auto h-12 w-12 text-gray-400" />
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">Access Denied</h3>
|
<h3 className="mt-2 text-sm font-medium text-gray-900">Access Denied</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
You need admin privileges to access tenant settings.
|
You don't have permission to access tenant settings.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = [
|
// Filter tabs based on user permissions
|
||||||
{ id: 'general', name: 'General', icon: CogIcon },
|
const availableTabs = [
|
||||||
{ id: 'branding', name: 'Branding', icon: PaintBrushIcon },
|
{
|
||||||
{ id: 'security', name: 'Security', icon: ShieldCheckIcon },
|
id: 'general',
|
||||||
{ id: 'authentication', name: 'Authentication', icon: KeyIcon },
|
name: 'General',
|
||||||
{ id: 'users', name: 'Users', icon: UserGroupIcon },
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
@@ -81,7 +115,7 @@ const Settings = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="mt-4 sm:mt-0 sm:ml-10">
|
<div className="mt-4 sm:mt-0 sm:ml-10">
|
||||||
<nav className="-mb-px flex space-x-8">
|
<nav className="-mb-px flex space-x-8">
|
||||||
{tabs.map((tab) => {
|
{availableTabs.map((tab) => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -104,11 +138,21 @@ const Settings = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{activeTab === 'general' && <GeneralSettings tenantConfig={tenantConfig} />}
|
{activeTab === 'general' && hasPermission(user?.role, 'tenant.view') && (
|
||||||
{activeTab === 'branding' && <BrandingSettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />}
|
<GeneralSettings tenantConfig={tenantConfig} />
|
||||||
{activeTab === 'security' && <SecuritySettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />}
|
)}
|
||||||
{activeTab === 'authentication' && <AuthenticationSettings tenantConfig={tenantConfig} />}
|
{activeTab === 'branding' && hasPermission(user?.role, 'branding.view') && (
|
||||||
{activeTab === 'users' && <UsersSettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />}
|
<BrandingSettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />
|
||||||
|
)}
|
||||||
|
{activeTab === 'security' && hasPermission(user?.role, 'security.view') && (
|
||||||
|
<SecuritySettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />
|
||||||
|
)}
|
||||||
|
{activeTab === 'authentication' && hasPermission(user?.role, 'auth.view') && (
|
||||||
|
<AuthenticationSettings tenantConfig={tenantConfig} />
|
||||||
|
)}
|
||||||
|
{activeTab === 'users' && hasPermission(user?.role, 'users.view') && (
|
||||||
|
<UsersSettings tenantConfig={tenantConfig} onRefresh={fetchTenantConfig} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,6 +185,7 @@ const GeneralSettings = ({ tenantConfig }) => (
|
|||||||
|
|
||||||
// Branding Settings Component
|
// Branding Settings Component
|
||||||
const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
||||||
|
const { user } = useAuth();
|
||||||
const [branding, setBranding] = useState({
|
const [branding, setBranding] = useState({
|
||||||
logo_url: '',
|
logo_url: '',
|
||||||
primary_color: '#3B82F6',
|
primary_color: '#3B82F6',
|
||||||
@@ -151,6 +196,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [logoPreview, setLogoPreview] = useState(null);
|
const [logoPreview, setLogoPreview] = useState(null);
|
||||||
|
|
||||||
|
const canEdit = hasPermission(user?.role, 'branding.edit');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tenantConfig?.branding) {
|
if (tenantConfig?.branding) {
|
||||||
setBranding(tenantConfig.branding);
|
setBranding(tenantConfig.branding);
|
||||||
@@ -231,7 +278,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
value={branding.company_name}
|
value={branding.company_name}
|
||||||
onChange={(e) => setBranding(prev => ({ ...prev, company_name: e.target.value }))}
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -259,12 +307,12 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
|
disabled={!canEdit || uploading}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
handleFilePreview(e);
|
handleFilePreview(e);
|
||||||
handleLogoUpload(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"
|
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"
|
||||||
disabled={uploading}
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">PNG, JPG up to 5MB</p>
|
<p className="text-xs text-gray-500 mt-1">PNG, JPG up to 5MB</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,7 +344,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
type="url"
|
type="url"
|
||||||
value={branding.logo_url}
|
value={branding.logo_url}
|
||||||
onChange={(e) => setBranding(prev => ({ ...prev, logo_url: e.target.value }))}
|
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"
|
placeholder="https://example.com/logo.png"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -310,13 +359,15 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
type="color"
|
type="color"
|
||||||
value={branding.primary_color}
|
value={branding.primary_color}
|
||||||
onChange={(e) => setBranding(prev => ({ ...prev, primary_color: e.target.value }))}
|
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"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={branding.primary_color}
|
value={branding.primary_color}
|
||||||
onChange={(e) => setBranding(prev => ({ ...prev, primary_color: e.target.value }))}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,19 +379,22 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
type="color"
|
type="color"
|
||||||
value={branding.secondary_color}
|
value={branding.secondary_color}
|
||||||
onChange={(e) => setBranding(prev => ({ ...prev, secondary_color: e.target.value }))}
|
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"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={branding.secondary_color}
|
value={branding.secondary_color}
|
||||||
onChange={(e) => setBranding(prev => ({ ...prev, secondary_color: e.target.value }))}
|
onChange={(e) => 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
{canEdit ? (
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
@@ -348,6 +402,11 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
>
|
>
|
||||||
{saving ? 'Saving...' : 'Save Branding'}
|
{saving ? 'Saving...' : 'Save Branding'}
|
||||||
</button>
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-gray-500 py-2">
|
||||||
|
You don't have permission to edit branding settings
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -357,6 +416,7 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
|
|
||||||
// Placeholder components for other tabs
|
// Placeholder components for other tabs
|
||||||
const SecuritySettings = ({ tenantConfig, onRefresh }) => {
|
const SecuritySettings = ({ tenantConfig, onRefresh }) => {
|
||||||
|
const { user } = useAuth();
|
||||||
const [securitySettings, setSecuritySettings] = useState({
|
const [securitySettings, setSecuritySettings] = useState({
|
||||||
ip_restriction_enabled: false,
|
ip_restriction_enabled: false,
|
||||||
ip_whitelist: [],
|
ip_whitelist: [],
|
||||||
@@ -365,6 +425,8 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
const [newIP, setNewIP] = useState('');
|
const [newIP, setNewIP] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const canEdit = hasPermission(user?.role, 'security.edit');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tenantConfig) {
|
if (tenantConfig) {
|
||||||
setSecuritySettings({
|
setSecuritySettings({
|
||||||
@@ -432,7 +494,7 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Security Settings</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Security Settings</h3>
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={saving || !canEdit}
|
||||||
className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700 disabled:opacity-50"
|
className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
{saving ? 'Saving...' : 'Save Changes'}
|
||||||
@@ -453,11 +515,12 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="ip_restriction_enabled"
|
id="ip_restriction_enabled"
|
||||||
checked={securitySettings.ip_restriction_enabled}
|
checked={securitySettings.ip_restriction_enabled}
|
||||||
|
disabled={!canEdit}
|
||||||
onChange={(e) => setSecuritySettings(prev => ({
|
onChange={(e) => setSecuritySettings(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
ip_restriction_enabled: e.target.checked
|
ip_restriction_enabled: e.target.checked
|
||||||
}))}
|
}))}
|
||||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded disabled:opacity-50"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="ip_restriction_enabled" className="ml-2 text-sm font-medium text-gray-700">
|
<label htmlFor="ip_restriction_enabled" className="ml-2 text-sm font-medium text-gray-700">
|
||||||
Enable IP Restrictions
|
Enable IP Restrictions
|
||||||
@@ -566,6 +629,9 @@ const SecuritySettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AuthenticationSettings = ({ tenantConfig }) => {
|
const AuthenticationSettings = ({ tenantConfig }) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const canEdit = hasPermission(user?.role, 'auth.edit');
|
||||||
|
const canView = hasPermission(user?.role, 'auth.view');
|
||||||
const [authConfig, setAuthConfig] = useState(null);
|
const [authConfig, setAuthConfig] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -1025,6 +1091,7 @@ const SessionConfig = ({ config, onSave }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
||||||
|
const { user } = useAuth();
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showCreateUser, setShowCreateUser] = useState(false);
|
const [showCreateUser, setShowCreateUser] = useState(false);
|
||||||
@@ -1032,7 +1099,14 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
const [editingUser, setEditingUser] = useState(null);
|
const [editingUser, setEditingUser] = useState(null);
|
||||||
|
|
||||||
const authProvider = tenantConfig?.auth_provider;
|
const authProvider = tenantConfig?.auth_provider;
|
||||||
const canManageUsers = authProvider === 'local'; // Only local auth allows user management
|
const isLocalAuth = authProvider === 'local';
|
||||||
|
|
||||||
|
// Check RBAC permissions for user management
|
||||||
|
const canViewUsers = hasPermission(user?.role, 'users.view');
|
||||||
|
const canCreateUsers = hasPermission(user?.role, 'users.create') && isLocalAuth;
|
||||||
|
const canEditUsers = hasPermission(user?.role, 'users.edit') && isLocalAuth;
|
||||||
|
const canDeleteUsers = hasPermission(user?.role, 'users.delete') && isLocalAuth;
|
||||||
|
const canManageUsers = canCreateUsers || canEditUsers; // Show management UI if can create or edit
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
@@ -1073,13 +1147,15 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">User Management</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900">User Management</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{authProvider === 'local'
|
{isLocalAuth
|
||||||
|
? canManageUsers
|
||||||
? 'Manage local users for this tenant'
|
? 'Manage local users for this tenant'
|
||||||
|
: 'View users for this tenant'
|
||||||
: `Users are managed through ${authProvider.toUpperCase()}. Showing read-only information.`
|
: `Users are managed through ${authProvider.toUpperCase()}. Showing read-only information.`
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{canManageUsers && (
|
{canCreateUsers && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateUser(true)}
|
onClick={() => setShowCreateUser(true)}
|
||||||
className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700"
|
className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700"
|
||||||
@@ -1113,8 +1189,10 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
<UserGroupIcon className="mx-auto h-12 w-12 text-gray-400" />
|
<UserGroupIcon className="mx-auto h-12 w-12 text-gray-400" />
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">No users found</h3>
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No users found</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
{canManageUsers
|
{isLocalAuth
|
||||||
|
? canCreateUsers
|
||||||
? 'Get started by creating a new user.'
|
? 'Get started by creating a new user.'
|
||||||
|
: 'No users have been created yet.'
|
||||||
: 'Users will appear here when they log in through your authentication provider.'
|
: 'Users will appear here when they log in through your authentication provider.'
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
@@ -1136,7 +1214,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Last Login
|
Last Login
|
||||||
</th>
|
</th>
|
||||||
{canManageUsers && (
|
{(canEditUsers || canDeleteUsers) && (
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Actions
|
Actions
|
||||||
</th>
|
</th>
|
||||||
@@ -1178,14 +1256,17 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
: 'Never'
|
: 'Never'
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
{canManageUsers && (
|
{(canEditUsers || canDeleteUsers) && (
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
{canEditUsers && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEditUser(user)}
|
onClick={() => handleEditUser(user)}
|
||||||
className="text-primary-600 hover:text-primary-900 mr-4"
|
className="text-primary-600 hover:text-primary-900 mr-4"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
{canDeleteUsers && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggleUserStatus(user)}
|
onClick={() => handleToggleUserStatus(user)}
|
||||||
className={user.is_active
|
className={user.is_active
|
||||||
@@ -1195,6 +1276,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
>
|
>
|
||||||
{user.is_active ? 'Deactivate' : 'Activate'}
|
{user.is_active ? 'Deactivate' : 'Activate'}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1205,7 +1287,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Non-Local Auth Guidance */}
|
{/* Non-Local Auth Guidance */}
|
||||||
{!canManageUsers && (
|
{!isLocalAuth && (
|
||||||
<div className="mt-6 p-4 bg-gray-50 border border-gray-200 rounded-md">
|
<div className="mt-6 p-4 bg-gray-50 border border-gray-200 rounded-md">
|
||||||
<h4 className="text-sm font-medium text-gray-900 mb-2">
|
<h4 className="text-sm font-medium text-gray-900 mb-2">
|
||||||
User Management for {authProvider?.toUpperCase()}
|
User Management for {authProvider?.toUpperCase()}
|
||||||
@@ -1238,7 +1320,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create User Modal for Local Auth */}
|
{/* Create User Modal for Local Auth */}
|
||||||
{showCreateUser && canManageUsers && (
|
{showCreateUser && canCreateUsers && (
|
||||||
<CreateUserModal
|
<CreateUserModal
|
||||||
isOpen={showCreateUser}
|
isOpen={showCreateUser}
|
||||||
onClose={() => setShowCreateUser(false)}
|
onClose={() => setShowCreateUser(false)}
|
||||||
@@ -1250,7 +1332,7 @@ const UsersSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit User Modal for Local Auth */}
|
{/* Edit User Modal for Local Auth */}
|
||||||
{showEditUser && canManageUsers && editingUser && (
|
{showEditUser && canEditUsers && editingUser && (
|
||||||
<EditUserModal
|
<EditUserModal
|
||||||
isOpen={showEditUser}
|
isOpen={showEditUser}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|||||||
170
client/src/utils/rbac.js
Normal file
170
client/src/utils/rbac.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* Frontend RBAC Utility
|
||||||
|
* Client-side permission checking to match server-side RBAC system
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Define the same permissions as the server
|
||||||
|
export const PERMISSIONS = {
|
||||||
|
// General tenant management
|
||||||
|
'tenant.view': 'View tenant information',
|
||||||
|
'tenant.edit': 'Edit basic tenant settings',
|
||||||
|
|
||||||
|
// Branding permissions
|
||||||
|
'branding.view': 'View branding settings',
|
||||||
|
'branding.edit': 'Edit branding and appearance',
|
||||||
|
|
||||||
|
// Security permissions
|
||||||
|
'security.view': 'View security settings',
|
||||||
|
'security.edit': 'Edit security settings and IP restrictions',
|
||||||
|
|
||||||
|
// User management permissions
|
||||||
|
'users.view': 'View user list',
|
||||||
|
'users.create': 'Create new users',
|
||||||
|
'users.edit': 'Edit user details',
|
||||||
|
'users.delete': 'Delete or deactivate users',
|
||||||
|
'users.manage_roles': 'Change user roles',
|
||||||
|
|
||||||
|
// Authentication permissions
|
||||||
|
'auth.view': 'View authentication settings',
|
||||||
|
'auth.edit': 'Edit authentication provider settings',
|
||||||
|
|
||||||
|
// Operational permissions
|
||||||
|
'dashboard.view': 'View dashboard',
|
||||||
|
'devices.view': 'View devices',
|
||||||
|
'devices.manage': 'Add, edit, delete devices',
|
||||||
|
'detections.view': 'View detections',
|
||||||
|
'alerts.view': 'View alerts',
|
||||||
|
'alerts.manage': 'Manage alert configurations',
|
||||||
|
'debug.access': 'Access debug information'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define roles and their permissions (must match server-side)
|
||||||
|
export const ROLES = {
|
||||||
|
// Full tenant administrator
|
||||||
|
'admin': [
|
||||||
|
'tenant.view', 'tenant.edit',
|
||||||
|
'branding.view', 'branding.edit',
|
||||||
|
'security.view', 'security.edit',
|
||||||
|
'users.view', 'users.create', 'users.edit', 'users.delete', 'users.manage_roles',
|
||||||
|
'auth.view', 'auth.edit',
|
||||||
|
'dashboard.view',
|
||||||
|
'devices.view', 'devices.manage',
|
||||||
|
'detections.view',
|
||||||
|
'alerts.view', 'alerts.manage',
|
||||||
|
'debug.access'
|
||||||
|
],
|
||||||
|
|
||||||
|
// User management specialist
|
||||||
|
'user_admin': [
|
||||||
|
'tenant.view',
|
||||||
|
'users.view', 'users.create', 'users.edit', 'users.delete', 'users.manage_roles',
|
||||||
|
'dashboard.view',
|
||||||
|
'devices.view',
|
||||||
|
'detections.view',
|
||||||
|
'alerts.view'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Security specialist
|
||||||
|
'security_admin': [
|
||||||
|
'tenant.view',
|
||||||
|
'security.view', 'security.edit',
|
||||||
|
'auth.view', 'auth.edit',
|
||||||
|
'users.view',
|
||||||
|
'dashboard.view',
|
||||||
|
'devices.view',
|
||||||
|
'detections.view',
|
||||||
|
'alerts.view'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Branding/marketing specialist
|
||||||
|
'branding_admin': [
|
||||||
|
'tenant.view',
|
||||||
|
'branding.view', 'branding.edit',
|
||||||
|
'dashboard.view',
|
||||||
|
'devices.view',
|
||||||
|
'detections.view',
|
||||||
|
'alerts.view'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Operations manager
|
||||||
|
'operator': [
|
||||||
|
'tenant.view',
|
||||||
|
'dashboard.view',
|
||||||
|
'devices.view', 'devices.manage',
|
||||||
|
'detections.view',
|
||||||
|
'alerts.view', 'alerts.manage'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Read-only user
|
||||||
|
'viewer': [
|
||||||
|
'dashboard.view',
|
||||||
|
'devices.view',
|
||||||
|
'detections.view',
|
||||||
|
'alerts.view'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user has a specific permission
|
||||||
|
* @param {string} userRole - The user's role
|
||||||
|
* @param {string} permission - The permission to check
|
||||||
|
* @returns {boolean} True if the user has the permission
|
||||||
|
*/
|
||||||
|
export function hasPermission(userRole, permission) {
|
||||||
|
if (!userRole || !permission) return false;
|
||||||
|
|
||||||
|
const rolePermissions = ROLES[userRole];
|
||||||
|
if (!rolePermissions) return false;
|
||||||
|
|
||||||
|
return rolePermissions.includes(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user has any of the provided permissions
|
||||||
|
* @param {string} userRole - The user's role
|
||||||
|
* @param {string[]} permissions - Array of permissions to check
|
||||||
|
* @returns {boolean} True if the user has at least one permission
|
||||||
|
*/
|
||||||
|
export function hasAnyPermission(userRole, permissions) {
|
||||||
|
if (!userRole || !permissions || !Array.isArray(permissions)) return false;
|
||||||
|
|
||||||
|
return permissions.some(permission => hasPermission(userRole, permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user has all of the provided permissions
|
||||||
|
* @param {string} userRole - The user's role
|
||||||
|
* @param {string[]} permissions - Array of permissions to check
|
||||||
|
* @returns {boolean} True if the user has all permissions
|
||||||
|
*/
|
||||||
|
export function hasAllPermissions(userRole, permissions) {
|
||||||
|
if (!userRole || !permissions || !Array.isArray(permissions)) return false;
|
||||||
|
|
||||||
|
return permissions.every(permission => hasPermission(userRole, permission));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all permissions for a role
|
||||||
|
* @param {string} userRole - The user's role
|
||||||
|
* @returns {string[]} Array of permissions for the role
|
||||||
|
*/
|
||||||
|
export function getRolePermissions(userRole) {
|
||||||
|
if (!userRole) return [];
|
||||||
|
|
||||||
|
return ROLES[userRole] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user can access settings at all
|
||||||
|
* @param {string} userRole - The user's role
|
||||||
|
* @returns {boolean} True if user can access any settings
|
||||||
|
*/
|
||||||
|
export function canAccessSettings(userRole) {
|
||||||
|
return hasAnyPermission(userRole, [
|
||||||
|
'tenant.view',
|
||||||
|
'branding.view',
|
||||||
|
'security.view',
|
||||||
|
'auth.view',
|
||||||
|
'users.view'
|
||||||
|
]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user