import React, { useState, useEffect } from 'react' import { XMarkIcon, EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline' import toast from 'react-hot-toast' const TenantModal = ({ isOpen, onClose, tenant = null, onSave }) => { const [formData, setFormData] = useState({ name: '', slug: '', domain: '', subscription_type: 'basic', auth_provider: 'local', is_active: true, admin_email: '', admin_phone: '', billing_email: '', auth_config: { // Local auth config allow_registration: true, require_email_verification: false, password_policy: { min_length: 8, require_uppercase: false, require_lowercase: false, require_numbers: false, require_symbols: false }, // SAML config sso_url: '', certificate: '', issuer: '', logout_url: '', // OAuth config client_id: '', client_secret: '', authorization_url: '', token_url: '', userinfo_url: '', scopes: ['openid', 'profile', 'email'], // LDAP config url: '', base_dn: '', bind_dn: '', bind_password: '', user_search_filter: '(sAMAccountName={username})', domain_name: '' }, user_mapping: { username: ['preferred_username', 'username', 'user'], email: ['email', 'mail'], firstName: ['given_name', 'givenName', 'first_name'], lastName: ['family_name', 'surname', 'last_name'], phoneNumber: ['phone_number', 'phone'] }, role_mapping: { admin: ['admin', 'administrator', 'Domain Admins'], operator: ['operator', 'user', 'Users'], viewer: ['viewer', 'guest', 'read-only'], default: 'viewer' }, features: { max_devices: 50, max_users: 10, api_rate_limit: 5000, data_retention_days: 365, features: ['basic_detection', 'alerts', 'dashboard'] }, branding: { logo_url: '', primary_color: '#3B82F6', secondary_color: '#1F2937', company_name: '' }, // IP Restriction settings ip_restriction_enabled: false, ip_whitelist: [], ip_restriction_message: 'Access denied. Your IP address is not authorized to access this tenant.' }) const [showSecrets, setShowSecrets] = useState(false) const [loading, setLoading] = useState(false) const [newIP, setNewIP] = useState('') // Load tenant data when editing useEffect(() => { if (tenant) { setFormData({ ...formData, ...tenant, auth_config: { ...formData.auth_config, ...tenant.auth_config }, user_mapping: { ...formData.user_mapping, ...tenant.user_mapping }, role_mapping: { ...formData.role_mapping, ...tenant.role_mapping }, features: { ...formData.features, ...tenant.features }, branding: { ...formData.branding, ...tenant.branding }, // IP restriction fields with fallbacks ip_restriction_enabled: tenant.ip_restriction_enabled || false, ip_whitelist: tenant.ip_whitelist || [], ip_restriction_message: tenant.ip_restriction_message || 'Access denied. Your IP address is not authorized to access this tenant.' }) } }, [tenant]) // Auto-generate slug from name const handleNameChange = (e) => { const name = e.target.value const slug = name.toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .trim() setFormData(prev => ({ ...prev, name, slug: tenant ? prev.slug : slug // Don't auto-update slug when editing })) } // IP Management functions 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 (formData.ip_whitelist.includes(ip)) { toast.error('This IP is already in the whitelist') return } setFormData(prev => ({ ...prev, ip_whitelist: [...prev.ip_whitelist, ip] })) setNewIP('') toast.success('IP added to whitelist') } const removeIPFromWhitelist = (ipToRemove) => { setFormData(prev => ({ ...prev, ip_whitelist: prev.ip_whitelist.filter(ip => ip !== ipToRemove) })) toast.success('IP removed from whitelist') } const handleSubmit = async (e) => { e.preventDefault() setLoading(true) try { // Validate required fields if (!formData.name || !formData.slug) { toast.error('Name and slug are required') return } // Clean up empty auth config fields const cleanAuthConfig = Object.fromEntries( Object.entries(formData.auth_config).filter(([_, value]) => { if (typeof value === 'string') return value.trim() !== '' if (Array.isArray(value)) return value.length > 0 if (typeof value === 'object') return Object.keys(value).length > 0 return value !== null && value !== undefined }) ) const tenantData = { ...formData, auth_config: cleanAuthConfig, // Convert empty strings to null to avoid validation errors domain: formData.domain?.trim() || null, admin_email: formData.admin_email?.trim() || null, billing_email: formData.billing_email?.trim() || null } await onSave(tenantData) onClose() toast.success(tenant ? 'Tenant updated successfully' : 'Tenant created successfully') } catch (error) { toast.error(error.message || 'Failed to save tenant') } finally { setLoading(false) } } if (!isOpen) return null return (

{tenant ? 'Edit Tenant' : 'Create New Tenant'}

{/* Basic Information */}

Basic Information

setFormData(prev => ({ ...prev, slug: e.target.value.toLowerCase() }))} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="acme" pattern="^[a-z0-9-]+$" required />

Will be: https://{formData.slug || 'slug'}.dev.uggla.uamils.com

setFormData(prev => ({ ...prev, domain: e.target.value }))} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="drones.acme.com" />
setFormData(prev => ({ ...prev, admin_email: e.target.value }))} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="admin@acme.com" />
setFormData(prev => ({ ...prev, billing_email: e.target.value }))} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="billing@acme.com" />
setFormData(prev => ({ ...prev, is_active: e.target.checked }))} className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" />
{/* Authentication Configuration */}

Authentication

{/* Local Auth Config */} {formData.auth_provider === 'local' && (
setFormData(prev => ({ ...prev, auth_config: { ...prev.auth_config, allow_registration: e.target.checked } }))} className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" />
setFormData(prev => ({ ...prev, auth_config: { ...prev.auth_config, require_email_verification: e.target.checked } }))} className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" />
)} {/* SAML Config */} {formData.auth_provider === 'saml' && (
setFormData(prev => ({ ...prev, auth_config: { ...prev.auth_config, sso_url: e.target.value } }))} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://adfs.company.com/adfs/ls/" />
setFormData(prev => ({ ...prev, auth_config: { ...prev.auth_config, issuer: e.target.value } }))} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="urn:company:uav-detection" />