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(t('settings.failedToLoad')); } finally { setLoading(false); } }; if (loading) { return (
{t('settings.loading')}
); } if (!canAccess) { return (

{t('settings.accessDenied')}

{t('settings.noPermission')}

); } return (

{t('settings.title')}

{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')}

{tenantConfig?.name}

{tenantConfig?.slug}

{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

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" />
{/* Current logo display */} {branding.logo_url && (
Current logo { e.target.style.display = 'none'; }} />

Current logo

)} {/* Upload interface */}
{ handleFilePreview(e); handleLogoUpload(e); }} className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 disabled:opacity-50" />

PNG, JPG up to 5MB {branding.logo_url ? '• Click "' + t('settings.changeLogo') + '" to replace current logo' : ''}

{uploading && (
Uploading...
)}
{/* Preview */} {logoPreview && (
Logo preview

Preview

)} {/* Manual URL input as fallback */}
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" />
setBranding(prev => ({ ...prev, primary_color: e.target.value }))} disabled={!canEdit} className="h-10 w-20 border border-gray-300 rounded-md disabled:opacity-50" /> setBranding(prev => ({ ...prev, primary_color: e.target.value }))} 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" />
setBranding(prev => ({ ...prev, secondary_color: e.target.value }))} disabled={!canEdit} className="h-10 w-20 border border-gray-300 rounded-md disabled:opacity-50" /> setBranding(prev => ({ ...prev, secondary_color: e.target.value }))} disabled={!canEdit} className="ml-2 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed" />
{canEdit ? ( ) : (
You don't have permission to edit branding settings
)}
); }; // 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

{/* 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 disabled:opacity-50" />
{/* 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 */}
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" />
{/* IP Whitelist */} {securitySettings.ip_whitelist.length > 0 && (
{securitySettings.ip_whitelist.map((ip, index) => (
{ip}
))}
)} {/* Custom restriction message */}