import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import api from '../services/api';
import toast from 'react-hot-toast';
import { useTranslation } from '../utils/tempTranslations';
import {
CogIcon,
ShieldCheckIcon,
PaintBrushIcon,
UserGroupIcon,
GlobeAltIcon,
KeyIcon,
EyeIcon,
EyeSlashIcon,
TrashIcon,
PencilIcon
} from '@heroicons/react/24/outline';
import { hasPermission, canAccessSettings } from '../utils/rbac';
const Settings = () => {
const { user } = useAuth();
const { t } = useTranslation();
const [tenantConfig, setTenantConfig] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// Define tabs with translations inside component after useTranslation hook
const allTabs = [
{
id: 'general',
name: t('settings.general'),
icon: CogIcon,
permission: 'tenant.view'
},
{
id: 'branding',
name: t('settings.branding'),
icon: PaintBrushIcon,
permission: 'branding.view'
},
{
id: 'security',
name: t('settings.security'),
icon: ShieldCheckIcon,
permission: 'security.view'
},
{
id: 'authentication',
name: t('settings.authentication'),
icon: KeyIcon,
permission: 'auth.view'
},
{
id: 'users',
name: t('settings.users'),
icon: UserGroupIcon,
permission: 'users.view'
}
];
// Calculate available tabs
const availableTabs = user?.role
? allTabs.filter(tab => hasPermission(user.role, tab.permission))
: [];
// Set active tab - default to first available or general
const [activeTab, setActiveTab] = useState(() => {
return availableTabs.length > 0 ? availableTabs[0].id : 'general';
});
// Check if user can access settings based on RBAC permissions
const canAccess = canAccessSettings(user?.role);
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 (!canAccess) {
return (
{t('settings.accessDenied')}
{t('settings.accessDeniedMessage')}
);
}
return (
{t('settings.title')}
{availableTabs.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' && 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') && (
)}
);
};
// General Settings Component
const GeneralSettings = ({ tenantConfig }) => {
const { t } = useTranslation();
return (
{t('settings.generalInformation')}
{t('settings.tenantName')}
{tenantConfig?.name}
{t('settings.tenantId')}
{tenantConfig?.slug}
{t('settings.authenticationProvider')}
{tenantConfig?.auth_provider}
);
};
// Branding Settings Component
const BrandingSettings = ({ tenantConfig, onRefresh }) => {
const { user } = useAuth();
const { t } = useTranslation();
const [branding, setBranding] = useState({
logo_url: '',
primary_color: '#3B82F6',
secondary_color: '#1F2937',
company_name: ''
});
const [saving, setSaving] = useState(false);
const [uploading, setUploading] = useState(false);
const [logoPreview, setLogoPreview] = useState(null);
const canEdit = hasPermission(user?.role, 'branding.edit');
useEffect(() => {
if (tenantConfig?.branding) {
setBranding(tenantConfig.branding);
}
}, [tenantConfig]);
const handleSave = async () => {
setSaving(true);
try {
await api.put('/tenant/branding', branding);
toast.success(t('settings.brandingUpdated'));
if (onRefresh) onRefresh();
} catch (error) {
toast.error(t('settings.brandingUpdateFailed'));
} finally {
setSaving(false);
}
};
const handleLogoUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
toast.error('Please select an image file');
return;
}
// Validate file size (5MB)
if (file.size > 5 * 1024 * 1024) {
toast.error('File size must be less than 5MB');
return;
}
setUploading(true);
try {
const formData = new FormData();
formData.append('logo', file);
const response = await api.post('/tenant/logo-upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
if (response.data.success) {
setBranding(prev => ({ ...prev, logo_url: response.data.data.logo_url }));
setLogoPreview(null);
// Clear the file input to allow selecting the same file again
event.target.value = '';
toast.success('Logo uploaded successfully');
if (onRefresh) onRefresh();
}
} catch (error) {
toast.error('Failed to upload logo');
// Clear the file input on error too
event.target.value = '';
} finally {
setUploading(false);
}
};
const handleFilePreview = (event) => {
const file = event.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => setLogoPreview(e.target.result);
reader.readAsDataURL(file);
}
};
const handleLogoRemove = async () => {
if (!branding.logo_url) return;
// Confirm removal
if (!window.confirm(t('settings.confirmRemoveLogo'))) {
return;
}
setUploading(true);
try {
const response = await api.delete('/tenant/logo');
if (response.data.success) {
setBranding(prev => ({ ...prev, logo_url: null }));
setLogoPreview(null);
toast.success(t('settings.logoRemoved'));
if (onRefresh) onRefresh();
}
} catch (error) {
console.error('Error removing logo:', error);
toast.error(t('settings.logoRemoveFailed'));
} finally {
setUploading(false);
}
};
return (
Branding & Appearance
{t('settings.companyName')}
setBranding(prev => ({ ...prev, company_name: e.target.value }))}
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"
/>
Company Logo
{/* Current logo display */}
{branding.logo_url && (
{
e.target.style.display = 'none';
}}
/>
Current logo
document.getElementById('logo-file-input').click()}
disabled={!canEdit || uploading}
className="px-3 py-1.5 text-xs font-medium text-primary-700 bg-primary-100 rounded hover:bg-primary-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
{t('settings.changeLogo')}
{t('settings.removeLogo')}
)}
{/* Upload interface */}
{/* Preview */}
{logoPreview && (
Preview
)}
{/* Manual URL input as fallback */}
{t('settings.logoUrl')}
setBranding(prev => ({ ...prev, logo_url: e.target.value }))}
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"
/>
{canEdit ? (
{saving ? t('settings.saving') : t('settings.saveBranding')}
) : (
You don't have permission to edit branding settings
)}
);
};
// Placeholder components for other tabs
const SecuritySettings = ({ tenantConfig, onRefresh }) => {
const { user } = useAuth();
const { t } = useTranslation();
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);
const canEdit = hasPermission(user?.role, 'security.edit');
console.log('🔐 SecuritySettings Debug:', {
userRole: user?.role,
canEdit: canEdit,
tenantConfig: tenantConfig,
securitySettings: securitySettings
});
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 {
console.log('🔒 Sending security settings:', securitySettings);
await api.put('/tenant/security', securitySettings);
toast.success(t('settings.securityUpdated'));
if (onRefresh) onRefresh();
} catch (error) {
console.error('Failed to update security settings:', error);
toast.error(t('settings.securityUpdateFailed'));
} finally {
setSaving(false);
}
};
return (
Security Settings
{
console.log('🔘 Save button clicked!');
console.log('🔘 canEdit:', canEdit);
console.log('🔘 saving:', saving);
handleSave();
}}
disabled={saving || !canEdit}
className="bg-primary-600 text-white px-4 py-2 rounded-md hover:bg-primary-700 disabled:opacity-50"
>
{saving ? t('settings.saving') : t('settings.saveChanges')}
{/* IP Restriction Toggle */}
{/* 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 */}
{/* 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 */}
)}
);
};
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 [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState(false);
useEffect(() => {
fetchAuthConfig();
}, []);
const fetchAuthConfig = async () => {
try {
const response = await api.get('/tenant/auth');
setAuthConfig(response.data.data);
} catch (error) {
console.error('Failed to fetch auth config:', error);
toast.error('Failed to load authentication settings');
} finally {
setLoading(false);
}
};
const saveAuthConfig = async (newConfig) => {
setSaving(true);
try {
const response = await api.put('/tenant/auth', newConfig);
setAuthConfig(response.data.data);
setEditing(false);
toast.success('Authentication settings updated successfully');
} catch (error) {
console.error('Failed to save auth config:', error);
toast.error(error.response?.data?.message || 'Failed to save authentication settings');
} finally {
setSaving(false);
}
};
if (loading) {
return (
);
}
const authProvider = tenantConfig?.auth_provider || 'local';
return (
{/* Current Authentication Provider */}
Authentication Provider
Current authentication method for this tenant
{authProvider.toUpperCase()}
setEditing(!editing)}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
{editing ? 'Cancel' : 'Configure'}
{/* Provider Description */}
{authProvider === 'local' && 'Users are managed directly in this system with username/password authentication.'}
{authProvider === 'saml' && 'Users authenticate through SAML Single Sign-On (SSO) provider.'}
{authProvider === 'oauth' && 'Users authenticate through OAuth provider (Google, Microsoft, etc.).'}
{authProvider === 'ldap' && 'Users authenticate through LDAP/Active Directory.'}
{authProvider === 'ad' && 'Users authenticate through Active Directory.'}
{/* Authentication Configuration */}
{editing && (
setEditing(false)}
saving={saving}
/>
)}
{/* User Role Mappings */}
Role Mappings
Configure how external users are assigned roles in your system
{authProvider === 'local' ? (
Role assignments for local users are managed in the Users tab.
) : (
)}
{/* Session Settings */}
Session Settings
Configure session timeout and security settings
);
};
// Auth Provider Configuration Component
const AuthProviderConfig = ({ provider, config, onSave, onCancel, saving }) => {
const [formData, setFormData] = useState(config || {});
const handleSubmit = (e) => {
e.preventDefault();
onSave(formData);
};
return (
{provider.toUpperCase()} Configuration
);
};
// SAML Configuration
const SAMLConfig = ({ formData, setFormData }) => (
);
// OAuth Configuration
const OAuthConfig = ({ formData, setFormData }) => (
);
// LDAP Configuration
const LDAPConfig = ({ formData, setFormData }) => (
);
// Active Directory Configuration
const ADConfig = ({ formData, setFormData }) => (
);
// Role Mapping Configuration
const RoleMappingConfig = ({ provider, config }) => (
Configure how {provider.toUpperCase()} groups/attributes map to system roles:
{['admin', 'user_admin', 'security_admin', 'branding_admin', 'operator', 'viewer'].map(role => (
))}
);
// Session Configuration
const SessionConfig = ({ config, onSave }) => {
const [sessionSettings, setSessionSettings] = useState({
session_timeout: config?.session_timeout || 480, // 8 hours default
require_mfa: config?.require_mfa || false,
allow_concurrent_sessions: config?.allow_concurrent_sessions || true
});
const handleSave = () => {
onSave({ ...config, ...sessionSettings });
};
return (
);
};
const UsersSettings = ({ tenantConfig, onRefresh }) => {
const { user } = useAuth();
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateUser, setShowCreateUser] = useState(false);
const [showEditUser, setShowEditUser] = useState(false);
const [editingUser, setEditingUser] = useState(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [deletingUser, setDeletingUser] = useState(null);
const authProvider = tenantConfig?.auth_provider;
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(() => {
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);
}
};
// Helper functions for local user management
const handleEditUser = (user) => {
setEditingUser(user);
setShowEditUser(true);
};
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');
}
};
const handleDeleteUser = (user) => {
setDeletingUser(user);
setShowDeleteConfirm(true);
};
const confirmDeleteUser = async () => {
if (!deletingUser) return;
try {
await api.delete(`/tenant/users/${deletingUser.id}`);
toast.success('User deleted successfully');
fetchUsers();
setShowDeleteConfirm(false);
setDeletingUser(null);
} catch (error) {
toast.error('Failed to delete user');
}
};
if (loading) {
return (
);
}
return (
User Management
{isLocalAuth
? canManageUsers
? 'Manage local users for this tenant'
: 'View users for this tenant'
: `Users are managed through ${authProvider.toUpperCase()}. Showing read-only information.`
}
{canCreateUsers && (
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
{isLocalAuth
? canCreateUsers
? '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.'
}
) : (
User
Role
Status
Last Login
{(canEditUsers || canDeleteUsers) && (
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'
}
{(canEditUsers || canDeleteUsers) && (
{canEditUsers && (
handleEditUser(user)}
className="inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
title="Edit user"
>
Edit
)}
{canDeleteUsers && (
handleToggleUserStatus(user)}
className={`inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded focus:outline-none focus:ring-2 focus:ring-offset-2 ${
user.is_active
? 'text-orange-700 bg-orange-100 hover:bg-orange-200 focus:ring-orange-500'
: 'text-green-700 bg-green-100 hover:bg-green-200 focus:ring-green-500'
}`}
title={user.is_active ? 'Deactivate user' : 'Activate user'}
>
{user.is_active ? 'Deactivate' : 'Activate'}
)}
{canDeleteUsers && (
handleDeleteUser(user)}
className="inline-flex items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
title="Delete user permanently"
>
Delete
)}
)}
))}
)}
{/* Non-Local Auth Guidance */}
{!isLocalAuth && (
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 && canCreateUsers && (
setShowCreateUser(false)}
onUserCreated={() => {
fetchUsers();
setShowCreateUser(false);
}}
/>
)}
{/* Edit User Modal for Local Auth */}
{showEditUser && canEditUsers && editingUser && (
{
setShowEditUser(false);
setEditingUser(null);
}}
user={editingUser}
onUserUpdated={() => {
fetchUsers();
setShowEditUser(false);
setEditingUser(null);
}}
/>
)}
{/* Delete Confirmation Modal */}
{showDeleteConfirm && deletingUser && (
Delete User
Are you sure you want to delete {deletingUser.username} ?
This action cannot be undone and will permanently remove the user and all associated data.
{
setShowDeleteConfirm(false);
setDeletingUser(null);
}}
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500"
>
Cancel
Delete User
)}
);
};
// 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 (
);
};
// Edit User Modal Component (for local auth only)
const EditUserModal = ({ isOpen, onClose, user, onUserUpdated }) => {
const [formData, setFormData] = useState({
email: '',
first_name: '',
last_name: '',
phone: '',
role: 'viewer',
password: '',
confirmPassword: ''
});
const [saving, setSaving] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
useEffect(() => {
if (user) {
setFormData({
email: user.email || '',
first_name: user.first_name || '',
last_name: user.last_name || '',
phone: user.phone || '',
role: user.role || 'viewer',
password: '',
confirmPassword: ''
});
}
}, [user]);
const handleSubmit = async (e) => {
e.preventDefault();
// Validate passwords if provided
if (formData.password && formData.password !== formData.confirmPassword) {
toast.error('Passwords do not match');
return;
}
setSaving(true);
try {
// Prepare update data (exclude password if empty)
const updateData = {
email: formData.email,
first_name: formData.first_name,
last_name: formData.last_name,
phone: formData.phone,
role: formData.role
};
// Only include password if it's provided
if (formData.password.trim()) {
updateData.password = formData.password;
}
await api.put(`/tenant/users/${user.id}`, updateData);
toast.success('User updated successfully');
onUserUpdated();
} catch (error) {
const message = error.response?.data?.message || 'Failed to update user';
toast.error(message);
} finally {
setSaving(false);
}
};
if (!isOpen) return null;
return (
);
};
export default Settings;