diff --git a/client/src/App.jsx b/client/src/App.jsx
index ce7b55e..eaaa792 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -11,6 +11,7 @@ import Devices from './pages/Devices';
import Detections from './pages/Detections';
import Alerts from './pages/Alerts';
import Debug from './pages/Debug';
+import Settings from './pages/Settings';
import Login from './pages/Login';
import ProtectedRoute from './components/ProtectedRoute';
@@ -70,6 +71,7 @@ function App() {
} />
} />
} />
+ } />
} />
diff --git a/client/src/components/Layout.jsx b/client/src/components/Layout.jsx
index 210de40..fcf73c8 100644
--- a/client/src/components/Layout.jsx
+++ b/client/src/components/Layout.jsx
@@ -14,7 +14,8 @@ import {
XMarkIcon,
SignalIcon,
WifiIcon,
- BugAntIcon
+ BugAntIcon,
+ CogIcon
} from '@heroicons/react/24/outline';
import classNames from 'classnames';
@@ -27,6 +28,7 @@ const baseNavigation = [
];
const adminNavigation = [
+ { name: 'Settings', href: '/settings', icon: CogIcon },
{ name: 'Debug', href: '/debug', icon: BugAntIcon },
];
diff --git a/client/src/pages/Settings.jsx b/client/src/pages/Settings.jsx
new file mode 100644
index 0000000..a9526fd
--- /dev/null
+++ b/client/src/pages/Settings.jsx
@@ -0,0 +1,865 @@
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../contexts/AuthContext';
+import api from '../services/api';
+import toast from 'react-hot-toast';
+import {
+ CogIcon,
+ ShieldCheckIcon,
+ PaintBrushIcon,
+ UserGroupIcon,
+ GlobeAltIcon,
+ KeyIcon,
+ EyeIcon,
+ EyeSlashIcon
+} from '@heroicons/react/24/outline';
+
+const Settings = () => {
+ const { user } = useAuth();
+ const [activeTab, setActiveTab] = useState('general');
+ const [tenantConfig, setTenantConfig] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+
+ // Check if user has admin role
+ const isAdmin = user?.role === 'admin';
+
+ useEffect(() => {
+ fetchTenantConfig();
+ }, []);
+
+ const fetchTenantConfig = async () => {
+ try {
+ // Get current tenant configuration
+ const response = await api.get('/tenant/info');
+ setTenantConfig(response.data.data);
+ } catch (error) {
+ console.error('Failed to fetch tenant config:', error);
+ toast.error('Failed to load tenant settings');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!isAdmin) {
+ return (
+
+
+
+
Access Denied
+
+ You need admin privileges 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 },
+ ];
+
+ return (
+
+
+
+
+
+
+ Tenant Settings
+
+
+
+ {tabs.map((tab) => {
+ const Icon = tab.icon;
+ return (
+ setActiveTab(tab.id)}
+ className={`${
+ activeTab === tab.id
+ ? 'border-primary-500 text-primary-600'
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
+ } whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm flex items-center`}
+ >
+
+ {tab.name}
+
+ );
+ })}
+
+
+
+
+
+
+ {activeTab === 'general' &&
}
+ {activeTab === 'branding' &&
}
+ {activeTab === 'security' &&
}
+ {activeTab === 'authentication' &&
}
+ {activeTab === 'users' &&
}
+
+
+
+
+ );
+};
+
+// General Settings Component
+const GeneralSettings = ({ tenantConfig }) => (
+
+
+
General Information
+
+
+
Tenant Name
+
{tenantConfig?.name}
+
+
+
Tenant ID
+
{tenantConfig?.slug}
+
+
+
Authentication Provider
+
{tenantConfig?.auth_provider}
+
+
+
+
+);
+
+// Branding Settings Component
+const BrandingSettings = ({ tenantConfig, onRefresh }) => {
+ const [branding, setBranding] = useState({
+ logo_url: '',
+ primary_color: '#3B82F6',
+ secondary_color: '#1F2937',
+ company_name: ''
+ });
+ const [saving, setSaving] = useState(false);
+
+ useEffect(() => {
+ if (tenantConfig?.branding) {
+ setBranding(tenantConfig.branding);
+ }
+ }, [tenantConfig]);
+
+ const handleSave = async () => {
+ setSaving(true);
+ try {
+ await api.put('/tenant/branding', branding);
+ toast.success('Branding updated successfully');
+ if (onRefresh) onRefresh();
+ } catch (error) {
+ toast.error('Failed to update branding');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+
+
Branding & Appearance
+
+
+ Company Name
+ 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"
+ />
+
+
+
+ Logo URL
+ 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"
+ placeholder="https://example.com/logo.png"
+ />
+
+
+
+
+
+
+ {saving ? 'Saving...' : 'Save Branding'}
+
+
+
+
+
+ );
+};
+
+// Placeholder components for other tabs
+const SecuritySettings = ({ tenantConfig, onRefresh }) => {
+ const [securitySettings, setSecuritySettings] = useState({
+ ip_restriction_enabled: false,
+ ip_whitelist: [],
+ ip_restriction_message: 'Access denied. Your IP address is not authorized to access this tenant.'
+ });
+ const [newIP, setNewIP] = useState('');
+ const [saving, setSaving] = useState(false);
+
+ useEffect(() => {
+ if (tenantConfig) {
+ setSecuritySettings({
+ ip_restriction_enabled: tenantConfig.ip_restriction_enabled || false,
+ ip_whitelist: tenantConfig.ip_whitelist || [],
+ ip_restriction_message: tenantConfig.ip_restriction_message || 'Access denied. Your IP address is not authorized to access this tenant.'
+ });
+ }
+ }, [tenantConfig]);
+
+ const addIPToWhitelist = () => {
+ if (!newIP.trim()) {
+ toast.error('Please enter an IP address or CIDR block');
+ return;
+ }
+
+ // Basic validation for IP format
+ const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/(?:[0-9]|[1-2][0-9]|3[0-2]))?$|^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^\d{1,3}\.\d{1,3}\.\d{1,3}\.\*$/;
+
+ if (!ipPattern.test(newIP.trim())) {
+ toast.error('Please enter a valid IP address, CIDR block (e.g., 192.168.1.0/24), or wildcard (e.g., 192.168.1.*)');
+ return;
+ }
+
+ const ip = newIP.trim();
+ if (securitySettings.ip_whitelist.includes(ip)) {
+ toast.error('This IP is already in the whitelist');
+ return;
+ }
+
+ setSecuritySettings(prev => ({
+ ...prev,
+ ip_whitelist: [...prev.ip_whitelist, ip]
+ }));
+ setNewIP('');
+ toast.success('IP added to whitelist');
+ };
+
+ const removeIPFromWhitelist = (ipToRemove) => {
+ setSecuritySettings(prev => ({
+ ...prev,
+ ip_whitelist: prev.ip_whitelist.filter(ip => ip !== ipToRemove)
+ }));
+ toast.success('IP removed from whitelist');
+ };
+
+ const handleSave = async () => {
+ setSaving(true);
+ try {
+ await api.put('/tenant/security', securitySettings);
+ toast.success('Security settings updated successfully');
+ if (onRefresh) onRefresh();
+ } catch (error) {
+ console.error('Failed to update security settings:', error);
+ toast.error('Failed to update security settings');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+
+
+
Security Settings
+
+ {saving ? 'Saving...' : 'Save Changes'}
+
+
+
+
+ {/* IP Restriction Toggle */}
+
+
+
IP Access Control
+
+ Restrict access to this tenant to specific IP addresses
+
+
+
+ setSecuritySettings(prev => ({
+ ...prev,
+ ip_restriction_enabled: e.target.checked
+ }))}
+ className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
+ />
+
+ Enable IP Restrictions
+
+
+
+
+ {/* IP Restriction Configuration */}
+ {securitySettings.ip_restriction_enabled && (
+
+
+
+
+
+ ⚠️ Important Security Notes
+
+
+
+ Make sure to include your current IP to avoid being locked out
+ IP restrictions apply to all tenant access including login and API calls
+ Use CIDR notation for IP ranges (e.g., 192.168.1.0/24)
+ Use wildcards for partial matching (e.g., 192.168.1.*)
+
+
+
+
+
+
+ {/* Add IP Input */}
+
+
+ Add IP Address or Range
+
+
+ setNewIP(e.target.value)}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ addIPToWhitelist();
+ }
+ }}
+ className="flex-1 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500"
+ />
+
+ Add IP
+
+
+
+
+ {/* IP Whitelist */}
+ {securitySettings.ip_whitelist.length > 0 && (
+
+
+ Allowed IP Addresses ({securitySettings.ip_whitelist.length})
+
+
+ {securitySettings.ip_whitelist.map((ip, index) => (
+
+ {ip}
+ removeIPFromWhitelist(ip)}
+ className="text-red-600 hover:text-red-800 text-sm"
+ >
+ Remove
+
+
+ ))}
+
+
+ )}
+
+ {/* Custom restriction message */}
+
+
+ Access Denied Message
+
+
+
+ )}
+
+
+
+ );
+};
+
+const AuthenticationSettings = () => (
+
+
+
Authentication Settings
+
+ Authentication provider configuration will be available here.
+
+
+
+);
+
+const UsersSettings = ({ tenantConfig, onRefresh }) => {
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showCreateUser, setShowCreateUser] = useState(false);
+
+ const authProvider = tenantConfig?.auth_provider;
+ const canManageUsers = authProvider === 'local'; // Only local auth allows user management
+
+ useEffect(() => {
+ fetchUsers();
+ }, []);
+
+ const fetchUsers = async () => {
+ try {
+ const response = await api.get('/tenant/users');
+ setUsers(response.data.data || []);
+ } catch (error) {
+ console.error('Failed to fetch users:', error);
+ toast.error('Failed to load users');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
User Management
+
+ {authProvider === 'local'
+ ? 'Manage local users for this tenant'
+ : `Users are managed through ${authProvider.toUpperCase()}. Showing read-only information.`
+ }
+
+
+ {canManageUsers && (
+
setShowCreateUser(true)}
+ className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700"
+ >
+ Add User
+
+ )}
+
+
+ {/* Authentication Provider Info */}
+
+
+
+
+
+ Authentication Provider: {authProvider?.toUpperCase()}
+
+
+ {authProvider === 'local' && 'Local authentication - Users are managed directly in this system.'}
+ {authProvider === 'saml' && 'SAML SSO - Users authenticate through your SAML identity provider.'}
+ {authProvider === 'oauth' && 'OAuth - Users authenticate through your OAuth provider.'}
+ {authProvider === 'ldap' && 'LDAP/Active Directory - Users authenticate through your directory service.'}
+
+
+
+
+
+ {/* Users List */}
+ {users.length === 0 ? (
+
+
+
No users found
+
+ {canManageUsers
+ ? 'Get started by creating a new user.'
+ : 'Users will appear here when they log in through your authentication provider.'
+ }
+
+
+ ) : (
+
+
+
+
+
+ User
+
+
+ Role
+
+
+ Status
+
+
+ Last Login
+
+ {canManageUsers && (
+
+ Actions
+
+ )}
+
+
+
+ {users.map((user) => (
+
+
+
+
{user.username}
+
{user.email}
+
+
+
+
+ {user.role}
+
+
+
+
+ {user.is_active ? 'Active' : 'Inactive'}
+
+
+
+ {user.last_login
+ ? new Date(user.last_login).toLocaleDateString()
+ : 'Never'
+ }
+
+ {canManageUsers && (
+
+ handleEditUser(user)}
+ className="text-primary-600 hover:text-primary-900 mr-4"
+ >
+ Edit
+
+ handleToggleUserStatus(user)}
+ className={user.is_active
+ ? 'text-red-600 hover:text-red-900'
+ : 'text-green-600 hover:text-green-900'
+ }
+ >
+ {user.is_active ? 'Deactivate' : 'Activate'}
+
+
+ )}
+
+ ))}
+
+
+
+ )}
+
+ {/* Non-Local Auth Guidance */}
+ {!canManageUsers && (
+
+
+ User Management for {authProvider?.toUpperCase()}
+
+
+ {authProvider === 'saml' && (
+
+
• Users are managed in your SAML identity provider
+
• Role assignments can be configured in the Authentication tab
+
• User information is synced automatically during login
+
+ )}
+ {authProvider === 'oauth' && (
+
+
• Users authenticate through your OAuth provider
+
• Role mappings can be configured based on OAuth claims
+
• User information is retrieved from the OAuth provider
+
+ )}
+ {authProvider === 'ldap' && (
+
+
• Users are managed in your LDAP/Active Directory
+
• Group-to-role mappings can be configured in Authentication settings
+
• User information is synced from the directory during login
+
+ )}
+
+
+ )}
+
+
+ {/* Create User Modal for Local Auth */}
+ {showCreateUser && canManageUsers && (
+
setShowCreateUser(false)}
+ onUserCreated={() => {
+ fetchUsers();
+ setShowCreateUser(false);
+ }}
+ />
+ )}
+
+ );
+
+ // Helper functions for local user management
+ const handleEditUser = (user) => {
+ // TODO: Implement edit user modal
+ toast.info('Edit user functionality coming soon');
+ };
+
+ const handleToggleUserStatus = async (user) => {
+ try {
+ await api.put(`/tenant/users/${user.id}/status`, {
+ is_active: !user.is_active
+ });
+ toast.success(`User ${user.is_active ? 'deactivated' : 'activated'} successfully`);
+ fetchUsers();
+ } catch (error) {
+ toast.error('Failed to update user status');
+ }
+ };
+};
+
+// Create User Modal Component (for local auth only)
+const CreateUserModal = ({ isOpen, onClose, onUserCreated }) => {
+ const [formData, setFormData] = useState({
+ username: '',
+ email: '',
+ password: '',
+ role: 'viewer',
+ is_active: true
+ });
+ const [saving, setSaving] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setSaving(true);
+
+ try {
+ await api.post('/tenant/users', formData);
+ toast.success('User created successfully');
+ onUserCreated();
+ setFormData({
+ username: '',
+ email: '',
+ password: '',
+ role: 'viewer',
+ is_active: true
+ });
+ } catch (error) {
+ toast.error('Failed to create user');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+ );
+};
+
+export default Settings;
diff --git a/server/middleware/rbac.js b/server/middleware/rbac.js
new file mode 100644
index 0000000..a49e0de
--- /dev/null
+++ b/server/middleware/rbac.js
@@ -0,0 +1,231 @@
+/**
+ * Role-Based Access Control (RBAC) System
+ * Defines granular permissions for different roles
+ */
+
+// Define specific permissions
+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'
+};
+
+// Role definitions with their permissions
+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 user has permission
+ */
+const hasPermission = (userRole, permission) => {
+ if (!userRole || !ROLES[userRole]) {
+ return false;
+ }
+ return ROLES[userRole].includes(permission);
+};
+
+/**
+ * Check if a user has any of the specified permissions
+ * @param {string} userRole - The user's role
+ * @param {Array} permissions - Array of permissions to check
+ * @returns {boolean} - True if user has at least one permission
+ */
+const hasAnyPermission = (userRole, permissions) => {
+ return permissions.some(permission => hasPermission(userRole, permission));
+};
+
+/**
+ * Check if a user has all of the specified permissions
+ * @param {string} userRole - The user's role
+ * @param {Array} permissions - Array of permissions to check
+ * @returns {boolean} - True if user has all permissions
+ */
+const hasAllPermissions = (userRole, permissions) => {
+ return permissions.every(permission => hasPermission(userRole, permission));
+};
+
+/**
+ * Get all permissions for a role
+ * @param {string} userRole - The user's role
+ * @returns {Array} - Array of permissions
+ */
+const getPermissions = (userRole) => {
+ return ROLES[userRole] || [];
+};
+
+/**
+ * Get all available roles
+ * @returns {Array} - Array of role names
+ */
+const getRoles = () => {
+ return Object.keys(ROLES);
+};
+
+/**
+ * Express middleware to check permissions
+ * @param {Array} requiredPermissions - Required permissions
+ * @returns {Function} - Express middleware function
+ */
+const requirePermissions = (requiredPermissions) => {
+ return (req, res, next) => {
+ if (!req.user || !req.user.role) {
+ return res.status(401).json({
+ success: false,
+ message: 'Authentication required'
+ });
+ }
+
+ const userRole = req.user.role;
+ const hasRequiredPermissions = requiredPermissions.every(permission =>
+ hasPermission(userRole, permission)
+ );
+
+ if (!hasRequiredPermissions) {
+ return res.status(403).json({
+ success: false,
+ message: 'Insufficient permissions',
+ required_permissions: requiredPermissions,
+ user_role: userRole
+ });
+ }
+
+ next();
+ };
+};
+
+/**
+ * Express middleware to check if user has any of the specified permissions
+ * @param {Array} permissions - Array of permissions
+ * @returns {Function} - Express middleware function
+ */
+const requireAnyPermission = (permissions) => {
+ return (req, res, next) => {
+ if (!req.user || !req.user.role) {
+ return res.status(401).json({
+ success: false,
+ message: 'Authentication required'
+ });
+ }
+
+ const userRole = req.user.role;
+ const hasRequiredPermission = permissions.some(permission =>
+ hasPermission(userRole, permission)
+ );
+
+ if (!hasRequiredPermission) {
+ return res.status(403).json({
+ success: false,
+ message: 'Insufficient permissions',
+ required_permissions: permissions,
+ user_role: userRole
+ });
+ }
+
+ next();
+ };
+};
+
+module.exports = {
+ PERMISSIONS,
+ ROLES,
+ hasPermission,
+ hasAnyPermission,
+ hasAllPermissions,
+ getPermissions,
+ getRoles,
+ requirePermissions,
+ requireAnyPermission
+};
diff --git a/server/routes/index.js b/server/routes/index.js
index c25d911..d68dac8 100644
--- a/server/routes/index.js
+++ b/server/routes/index.js
@@ -4,6 +4,7 @@ const router = express.Router();
// Import route modules
const managementRoutes = require('./management');
const authRoutes = require('./auth');
+const tenantRoutes = require('./tenant');
const deviceRoutes = require('./device');
const userRoutes = require('./user');
const alertRoutes = require('./alert');
@@ -21,6 +22,9 @@ router.use('/management', managementRoutes);
// Authentication routes (multi-tenant)
router.use('/auth', authRoutes);
+// Tenant self-management routes
+router.use('/tenant', tenantRoutes);
+
// API versioning
router.use('/v1/devices', deviceRoutes);
router.use('/v1/users', userRoutes);
diff --git a/server/routes/tenant.js b/server/routes/tenant.js
new file mode 100644
index 0000000..6e9543e
--- /dev/null
+++ b/server/routes/tenant.js
@@ -0,0 +1,453 @@
+/**
+ * Tenant Self-Management Routes
+ * Allows tenant admins to manage their own tenant settings
+ */
+
+const express = require('express');
+const router = express.Router();
+const { Tenant, User } = require('../models');
+const { authenticateToken } = require('../middleware/auth');
+const { requirePermissions, requireAnyPermission, hasPermission } = require('../middleware/rbac');
+const MultiTenantAuth = require('../middleware/multi-tenant-auth');
+
+// Initialize multi-tenant auth
+const multiAuth = new MultiTenantAuth();
+
+/**
+ * GET /tenant/info
+ * Get current tenant information
+ */
+router.get('/info', authenticateToken, requirePermissions(['tenant.view']), async (req, res) => {
+ try {
+ // Determine tenant from request
+ const tenantId = await multiAuth.determineTenant(req);
+ if (!tenantId) {
+ return res.status(400).json({
+ success: false,
+ message: 'Unable to determine tenant'
+ });
+ }
+
+ const tenant = await Tenant.findOne({ where: { slug: tenantId } });
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ // Return tenant info (excluding sensitive data)
+ const tenantInfo = {
+ id: tenant.id,
+ name: tenant.name,
+ slug: tenant.slug,
+ domain: tenant.domain,
+ subscription_type: tenant.subscription_type,
+ is_active: tenant.is_active,
+ auth_provider: tenant.auth_provider,
+ branding: tenant.branding,
+ features: tenant.features,
+ admin_email: tenant.admin_email,
+ admin_phone: tenant.admin_phone,
+ billing_email: tenant.billing_email,
+ created_at: tenant.created_at,
+ // IP restriction info (for security admins only)
+ ip_restriction_enabled: hasPermission(req.user.role, 'security.view') ? tenant.ip_restriction_enabled : undefined,
+ ip_whitelist: hasPermission(req.user.role, 'security.view') ? tenant.ip_whitelist : undefined,
+ ip_restriction_message: hasPermission(req.user.role, 'security.view') ? tenant.ip_restriction_message : undefined
+ };
+
+ res.json({
+ success: true,
+ data: tenantInfo
+ });
+
+ } catch (error) {
+ console.error('Error fetching tenant info:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to fetch tenant information'
+ });
+ }
+});
+
+/**
+ * PUT /tenant/branding
+ * Update tenant branding (branding admin or higher)
+ */
+router.put('/branding', authenticateToken, requirePermissions(['branding.edit']), async (req, res) => {
+ try {
+ // Determine tenant from request
+ const tenantId = await multiAuth.determineTenant(req);
+ if (!tenantId) {
+ return res.status(400).json({
+ success: false,
+ message: 'Unable to determine tenant'
+ });
+ }
+
+ const tenant = await Tenant.findOne({ where: { slug: tenantId } });
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ const { logo_url, primary_color, secondary_color, company_name } = req.body;
+
+ // Validate colors
+ const colorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
+ if (primary_color && !colorRegex.test(primary_color)) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid primary color format'
+ });
+ }
+ if (secondary_color && !colorRegex.test(secondary_color)) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid secondary color format'
+ });
+ }
+
+ // Update branding
+ const updatedBranding = {
+ ...tenant.branding,
+ logo_url: logo_url || tenant.branding?.logo_url || '',
+ primary_color: primary_color || tenant.branding?.primary_color || '#3B82F6',
+ secondary_color: secondary_color || tenant.branding?.secondary_color || '#1F2937',
+ company_name: company_name || tenant.branding?.company_name || ''
+ };
+
+ await tenant.update({ branding: updatedBranding });
+
+ console.log(`✅ Tenant "${tenantId}" branding updated by user "${req.user.username}"`);
+
+ res.json({
+ success: true,
+ message: 'Branding updated successfully',
+ data: { branding: updatedBranding }
+ });
+
+ } catch (error) {
+ console.error('Error updating tenant branding:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to update branding'
+ });
+ }
+});
+
+/**
+ * PUT /tenant/security
+ * Update tenant security settings (security admin or higher)
+ */
+router.put('/security', authenticateToken, requirePermissions(['security.edit']), async (req, res) => {
+ try {
+ // Determine tenant from request
+ const tenantId = await multiAuth.determineTenant(req);
+ if (!tenantId) {
+ return res.status(400).json({
+ success: false,
+ message: 'Unable to determine tenant'
+ });
+ }
+
+ const tenant = await Tenant.findOne({ where: { slug: tenantId } });
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ const { ip_restriction_enabled, ip_whitelist, ip_restriction_message } = req.body;
+
+ // Validate IP whitelist if provided
+ if (ip_whitelist && Array.isArray(ip_whitelist)) {
+ for (const ip of ip_whitelist) {
+ // Basic IP validation (could be enhanced)
+ if (typeof ip !== 'string' || ip.trim().length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid IP address in whitelist'
+ });
+ }
+ }
+ }
+
+ // Update security settings
+ const updates = {};
+ if (typeof ip_restriction_enabled === 'boolean') {
+ updates.ip_restriction_enabled = ip_restriction_enabled;
+ }
+ if (ip_whitelist) {
+ updates.ip_whitelist = ip_whitelist;
+ }
+ if (ip_restriction_message) {
+ updates.ip_restriction_message = ip_restriction_message;
+ }
+
+ await tenant.update(updates);
+
+ console.log(`✅ Tenant "${tenantId}" security settings updated by user "${req.user.username}"`);
+
+ res.json({
+ success: true,
+ message: 'Security settings updated successfully',
+ data: {
+ ip_restriction_enabled: tenant.ip_restriction_enabled,
+ ip_whitelist: tenant.ip_whitelist,
+ ip_restriction_message: tenant.ip_restriction_message
+ }
+ });
+
+ } catch (error) {
+ console.error('Error updating tenant security:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to update security settings'
+ });
+ }
+});
+
+/**
+ * GET /tenant/users
+ * Get users in current tenant (user admin or higher)
+ */
+router.get('/users', authenticateToken, requirePermissions(['users.view']), async (req, res) => {
+ try {
+ // Determine tenant from request
+ const tenantId = await multiAuth.determineTenant(req);
+ if (!tenantId) {
+ return res.status(400).json({
+ success: false,
+ message: 'Unable to determine tenant'
+ });
+ }
+
+ const tenant = await Tenant.findOne({ where: { slug: tenantId } });
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ // Get users in this tenant
+ const users = await User.findAll({
+ where: { tenant_id: tenant.id },
+ attributes: ['id', 'username', 'email', 'role', 'is_active', 'last_login', 'created_at'],
+ order: [['created_at', 'DESC']]
+ });
+
+ res.json({
+ success: true,
+ data: users
+ });
+
+ } catch (error) {
+ console.error('Error fetching tenant users:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to fetch users'
+ });
+ }
+});
+
+/**
+ * POST /tenant/users
+ * Create a new user in current tenant (user admin or higher, local auth only)
+ */
+router.post('/users', authenticateToken, requirePermissions(['users.create']), async (req, res) => {
+ try {
+ // Determine tenant from request
+ const tenantId = await multiAuth.determineTenant(req);
+ if (!tenantId) {
+ return res.status(400).json({
+ success: false,
+ message: 'Unable to determine tenant'
+ });
+ }
+
+ const tenant = await Tenant.findOne({ where: { slug: tenantId } });
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ // Check if tenant uses local authentication
+ if (tenant.auth_provider !== 'local') {
+ return res.status(400).json({
+ success: false,
+ message: `User creation is only available for local authentication. This tenant uses ${tenant.auth_provider}.`
+ });
+ }
+
+ const { username, email, password, role = 'viewer', is_active = true } = req.body;
+
+ // Validate required fields
+ if (!username || !email || !password) {
+ return res.status(400).json({
+ success: false,
+ message: 'Username, email, and password are required'
+ });
+ }
+
+ // Validate role
+ const validRoles = ['admin', 'operator', 'viewer'];
+ if (!validRoles.includes(role)) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid role. Must be admin, operator, or viewer'
+ });
+ }
+
+ // Check if username or email already exists in this tenant
+ const existingUser = await User.findOne({
+ where: {
+ tenant_id: tenant.id,
+ [require('sequelize').Op.or]: [
+ { username },
+ { email }
+ ]
+ }
+ });
+
+ if (existingUser) {
+ return res.status(400).json({
+ success: false,
+ message: 'Username or email already exists in this tenant'
+ });
+ }
+
+ // Create user
+ const user = await User.create({
+ username,
+ email,
+ password_hash: password, // Will be hashed by the model
+ role,
+ is_active,
+ tenant_id: tenant.id,
+ created_by: req.user.id
+ });
+
+ console.log(`✅ User "${username}" created in tenant "${tenantId}" by admin "${req.user.username}"`);
+
+ // Return user data (without password)
+ const userData = {
+ id: user.id,
+ username: user.username,
+ email: user.email,
+ role: user.role,
+ is_active: user.is_active,
+ created_at: user.created_at
+ };
+
+ res.status(201).json({
+ success: true,
+ message: 'User created successfully',
+ data: userData
+ });
+
+ } catch (error) {
+ console.error('Error creating tenant user:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to create user'
+ });
+ }
+});
+
+/**
+ * PUT /tenant/users/:userId/status
+ * Update user status (activate/deactivate) (user admin or higher, local auth only)
+ */
+router.put('/users/:userId/status', authenticateToken, requirePermissions(['users.edit']), async (req, res) => {
+ try {
+ // Determine tenant from request
+ const tenantId = await multiAuth.determineTenant(req);
+ if (!tenantId) {
+ return res.status(400).json({
+ success: false,
+ message: 'Unable to determine tenant'
+ });
+ }
+
+ const tenant = await Tenant.findOne({ where: { slug: tenantId } });
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ // Check if tenant uses local authentication
+ if (tenant.auth_provider !== 'local') {
+ return res.status(400).json({
+ success: false,
+ message: `User management is only available for local authentication. This tenant uses ${tenant.auth_provider}.`
+ });
+ }
+
+ const { userId } = req.params;
+ const { is_active } = req.body;
+
+ if (typeof is_active !== 'boolean') {
+ return res.status(400).json({
+ success: false,
+ message: 'is_active must be a boolean value'
+ });
+ }
+
+ // Find user in this tenant
+ const user = await User.findOne({
+ where: {
+ id: userId,
+ tenant_id: tenant.id
+ }
+ });
+
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: 'User not found in this tenant'
+ });
+ }
+
+ // Prevent self-deactivation
+ if (user.id === req.user.id && !is_active) {
+ return res.status(400).json({
+ success: false,
+ message: 'You cannot deactivate your own account'
+ });
+ }
+
+ // Update user status
+ await user.update({ is_active });
+
+ console.log(`✅ User "${user.username}" ${is_active ? 'activated' : 'deactivated'} in tenant "${tenantId}" by admin "${req.user.username}"`);
+
+ res.json({
+ success: true,
+ message: `User ${is_active ? 'activated' : 'deactivated'} successfully`,
+ data: {
+ id: user.id,
+ username: user.username,
+ is_active: user.is_active
+ }
+ });
+
+ } catch (error) {
+ console.error('Error updating user status:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to update user status'
+ });
+ }
+});
+
+module.exports = router;
diff --git a/server/test-rbac.js b/server/test-rbac.js
new file mode 100644
index 0000000..f2e010b
--- /dev/null
+++ b/server/test-rbac.js
@@ -0,0 +1,131 @@
+/**
+ * Test script to verify RBAC system functionality
+ */
+
+const { hasPermission, ROLES, PERMISSIONS } = require('./middleware/rbac');
+
+// Mock users with different roles
+const users = {
+ admin: {
+ id: 1,
+ username: 'super_admin',
+ role: 'admin'
+ },
+ user_admin: {
+ id: 2,
+ username: 'user_manager',
+ role: 'user_admin'
+ },
+ security_admin: {
+ id: 3,
+ username: 'security_manager',
+ role: 'security_admin'
+ },
+ branding_admin: {
+ id: 4,
+ username: 'branding_manager',
+ role: 'branding_admin'
+ },
+ operator: {
+ id: 5,
+ username: 'basic_operator',
+ role: 'operator'
+ },
+ viewer: {
+ id: 6,
+ username: 'read_only',
+ role: 'viewer'
+ }
+};
+
+// Test scenarios
+const testScenarios = [
+ {
+ name: 'Admin - Full Access',
+ user: users.admin,
+ permissions: ['tenant.view', 'tenant.edit', 'branding.edit', 'security.edit', 'users.create', 'users.edit', 'users.delete'],
+ expectedResults: [true, true, true, true, true, true, true]
+ },
+ {
+ name: 'User Admin - User Management Only',
+ user: users.user_admin,
+ permissions: ['tenant.view', 'tenant.edit', 'branding.edit', 'security.edit', 'users.create', 'users.edit', 'users.delete'],
+ expectedResults: [true, false, false, false, true, true, true]
+ },
+ {
+ name: 'Security Admin - Security Only',
+ user: users.security_admin,
+ permissions: ['tenant.view', 'tenant.edit', 'branding.edit', 'security.edit', 'users.create', 'users.edit', 'users.delete'],
+ expectedResults: [true, false, false, true, false, false, false]
+ },
+ {
+ name: 'Branding Admin - Branding Only',
+ user: users.branding_admin,
+ permissions: ['tenant.view', 'tenant.edit', 'branding.edit', 'security.edit', 'users.create', 'users.edit', 'users.delete'],
+ expectedResults: [true, false, true, false, false, false, false]
+ },
+ {
+ name: 'Operator - Limited Access',
+ user: users.operator,
+ permissions: ['tenant.view', 'tenant.edit', 'branding.edit', 'security.edit', 'users.create', 'users.edit', 'users.delete'],
+ expectedResults: [true, false, false, false, false, false, false]
+ },
+ {
+ name: 'Viewer - Read Only',
+ user: users.viewer,
+ permissions: ['tenant.view', 'tenant.edit', 'branding.edit', 'security.edit', 'users.create', 'users.edit', 'users.delete'],
+ expectedResults: [true, false, false, false, false, false, false]
+ }
+];
+
+console.log('🧪 Testing RBAC System\n');
+
+// Display available roles and permissions
+console.log('📋 Available Roles:');
+Object.keys(ROLES).forEach(role => {
+ console.log(` ${role}: ${ROLES[role].join(', ')}`);
+});
+
+console.log('\n📋 Available Permissions:');
+Object.keys(PERMISSIONS).forEach(category => {
+ console.log(` ${category}:`);
+ PERMISSIONS[category].forEach(permission => {
+ console.log(` - ${permission}`);
+ });
+});
+
+console.log('\n🔍 Running Permission Tests:\n');
+
+// Run tests
+let totalTests = 0;
+let passedTests = 0;
+
+testScenarios.forEach(scenario => {
+ console.log(`\n👤 ${scenario.name} (${scenario.user.username})`);
+ console.log('─'.repeat(50));
+
+ scenario.permissions.forEach((permission, index) => {
+ totalTests++;
+ const result = hasPermission(scenario.user, permission);
+ const expected = scenario.expectedResults[index];
+ const passed = result === expected;
+
+ if (passed) passedTests++;
+
+ const status = passed ? '✅' : '❌';
+ const expectedText = expected ? 'ALLOW' : 'DENY';
+ const actualText = result ? 'ALLOW' : 'DENY';
+
+ console.log(` ${status} ${permission}: Expected ${expectedText}, Got ${actualText}`);
+ });
+});
+
+console.log('\n📊 Test Results:');
+console.log(` Passed: ${passedTests}/${totalTests}`);
+console.log(` Success Rate: ${Math.round((passedTests/totalTests) * 100)}%`);
+
+if (passedTests === totalTests) {
+ console.log('\n🎉 All tests passed! RBAC system is working correctly.');
+} else {
+ console.log('\n⚠️ Some tests failed. Please check the RBAC configuration.');
+}