Fix jwt-token
This commit is contained in:
614
management/src/components/TenantModal.jsx
Normal file
614
management/src/components/TenantModal.jsx
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
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: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const [showSecrets, setShowSecrets] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||||
|
<div className="relative top-8 mx-auto p-5 border w-full max-w-4xl shadow-lg rounded-md bg-white mb-8">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
|
{tenant ? 'Edit Tenant' : 'Create New Tenant'}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<XMarkIcon className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{/* Basic Information */}
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<h4 className="text-md font-medium text-gray-900 mb-4">Basic Information</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Organization Name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
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 Corporation"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Slug (Subdomain) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.slug}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Will be: https://{formData.slug || 'slug'}.dev.uggla.uamils.com
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Custom Domain
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.domain}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Subscription Type
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.subscription_type}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, subscription_type: 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"
|
||||||
|
>
|
||||||
|
<option value="free">Free</option>
|
||||||
|
<option value="basic">Basic</option>
|
||||||
|
<option value="premium">Premium</option>
|
||||||
|
<option value="enterprise">Enterprise</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Admin Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={formData.admin_email}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="is_active"
|
||||||
|
checked={formData.is_active}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, is_active: e.target.checked }))}
|
||||||
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor="is_active" className="ml-2 block text-sm text-gray-900">
|
||||||
|
Active Tenant
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Authentication Configuration */}
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<h4 className="text-md font-medium text-gray-900 mb-4">Authentication</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Authentication Provider
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.auth_provider}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, auth_provider: 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"
|
||||||
|
>
|
||||||
|
<option value="local">Local (Username/Password)</option>
|
||||||
|
<option value="saml">SAML (Single Sign-On)</option>
|
||||||
|
<option value="oauth">OAuth (Azure AD, Google, etc.)</option>
|
||||||
|
<option value="ldap">LDAP (Active Directory)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Local Auth Config */}
|
||||||
|
{formData.auth_provider === 'local' && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="allow_registration"
|
||||||
|
checked={formData.auth_config.allow_registration}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<label htmlFor="allow_registration" className="ml-2 block text-sm text-gray-900">
|
||||||
|
Allow User Registration
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="require_email_verification"
|
||||||
|
checked={formData.auth_config.require_email_verification}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<label htmlFor="require_email_verification" className="ml-2 block text-sm text-gray-900">
|
||||||
|
Require Email Verification
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SAML Config */}
|
||||||
|
{formData.auth_provider === 'saml' && (
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
SSO URL *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={formData.auth_config.sso_url}
|
||||||
|
onChange={(e) => 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/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Issuer *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.auth_config.issuer}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
X.509 Certificate *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.auth_config.certificate}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
auth_config: { ...prev.auth_config, certificate: 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"
|
||||||
|
rows="4"
|
||||||
|
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* OAuth Config */}
|
||||||
|
{formData.auth_provider === 'oauth' && (
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Client ID *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.auth_config.client_id}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
auth_config: { ...prev.auth_config, client_id: 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Client Secret *
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showSecrets ? "text" : "password"}
|
||||||
|
value={formData.auth_config.client_secret}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
auth_config: { ...prev.auth_config, client_secret: e.target.value }
|
||||||
|
}))}
|
||||||
|
className="w-full border border-gray-300 rounded-md px-3 py-2 pr-10 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowSecrets(!showSecrets)}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
{showSecrets ? (
|
||||||
|
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-5 w-5 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Authorization URL *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={formData.auth_config.authorization_url}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
auth_config: { ...prev.auth_config, authorization_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://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Token URL *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={formData.auth_config.token_url}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
auth_config: { ...prev.auth_config, token_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://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* LDAP Config */}
|
||||||
|
{formData.auth_provider === 'ldap' && (
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
LDAP URL *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.auth_config.url}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
auth_config: { ...prev.auth_config, 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="ldaps://dc.company.com:636"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Base DN *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.auth_config.base_dn}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
auth_config: { ...prev.auth_config, base_dn: 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="dc=company,dc=com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Bind DN
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.auth_config.bind_dn}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
auth_config: { ...prev.auth_config, bind_dn: 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="cn=service,ou=accounts,dc=company,dc=com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Bind Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={showSecrets ? "text" : "password"}
|
||||||
|
value={formData.auth_config.bind_password}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
auth_config: { ...prev.auth_config, bind_password: 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features & Limits */}
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<h4 className="text-md font-medium text-gray-900 mb-4">Features & Limits</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Max Devices
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.features.max_devices}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
features: { ...prev.features, max_devices: parseInt(e.target.value) || 0 }
|
||||||
|
}))}
|
||||||
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Max Users
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.features.max_users}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
features: { ...prev.features, max_users: parseInt(e.target.value) || 0 }
|
||||||
|
}))}
|
||||||
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
API Rate Limit
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.features.api_rate_limit}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
features: { ...prev.features, api_rate_limit: parseInt(e.target.value) || 0 }
|
||||||
|
}))}
|
||||||
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Data Retention (Days)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={formData.features.data_retention_days}
|
||||||
|
onChange={(e) => setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
features: { ...prev.features, data_retention_days: parseInt(e.target.value) || 0 }
|
||||||
|
}))}
|
||||||
|
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Actions */}
|
||||||
|
<div className="flex justify-end space-x-3 pt-6 border-t">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Saving...' : (tenant ? 'Update Tenant' : 'Create Tenant')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TenantModal
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import api from '../services/api'
|
import api from '../services/api'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
import TenantModal from '../components/TenantModal'
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
@@ -55,6 +56,28 @@ const Tenants = () => {
|
|||||||
loadTenants(0, term)
|
loadTenants(0, term)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSaveTenant = async (tenantData) => {
|
||||||
|
try {
|
||||||
|
if (editingTenant) {
|
||||||
|
// Update existing tenant
|
||||||
|
await api.put(`/tenants/${editingTenant.id}`, tenantData)
|
||||||
|
toast.success('Tenant updated successfully')
|
||||||
|
} else {
|
||||||
|
// Create new tenant
|
||||||
|
await api.post('/tenants', tenantData)
|
||||||
|
toast.success('Tenant created successfully')
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditingTenant(null)
|
||||||
|
setShowCreateModal(false)
|
||||||
|
loadTenants() // Reload the list
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving tenant:', error)
|
||||||
|
const message = error.response?.data?.message || 'Failed to save tenant'
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deleteTenant = async (tenantId) => {
|
const deleteTenant = async (tenantId) => {
|
||||||
if (!confirm('Are you sure you want to delete this tenant? This action cannot be undone.')) {
|
if (!confirm('Are you sure you want to delete this tenant? This action cannot be undone.')) {
|
||||||
return
|
return
|
||||||
@@ -70,6 +93,18 @@ const Tenants = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEditTenant = async (tenant) => {
|
||||||
|
try {
|
||||||
|
// Fetch full tenant details for editing
|
||||||
|
const response = await api.get(`/tenants/${tenant.id}`)
|
||||||
|
setEditingTenant(response.data.data)
|
||||||
|
setShowCreateModal(true)
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to load tenant details')
|
||||||
|
console.error('Error loading tenant:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getAuthProviderBadge = (provider) => {
|
const getAuthProviderBadge = (provider) => {
|
||||||
const colors = {
|
const colors = {
|
||||||
local: 'bg-gray-100 text-gray-800',
|
local: 'bg-gray-100 text-gray-800',
|
||||||
@@ -204,7 +239,7 @@ const Tenants = () => {
|
|||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<div className="flex items-center justify-end space-x-2">
|
<div className="flex items-center justify-end space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingTenant(tenant)}
|
onClick={() => handleEditTenant(tenant)}
|
||||||
className="text-blue-600 hover:text-blue-900 p-1 rounded"
|
className="text-blue-600 hover:text-blue-900 p-1 rounded"
|
||||||
title="Edit"
|
title="Edit"
|
||||||
>
|
>
|
||||||
@@ -275,36 +310,16 @@ const Tenants = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modals would go here */}
|
{/* Tenant Creation/Edit Modal */}
|
||||||
{showCreateModal && (
|
<TenantModal
|
||||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
isOpen={showCreateModal}
|
||||||
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
onClose={() => {
|
||||||
<div className="mt-3">
|
setShowCreateModal(false)
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Create New Tenant</h3>
|
setEditingTenant(null)
|
||||||
<p className="text-sm text-gray-500 mb-4">
|
}}
|
||||||
Tenant creation modal would go here with form fields for name, slug, domain, auth provider, etc.
|
tenant={editingTenant}
|
||||||
</p>
|
onSave={handleSaveTenant}
|
||||||
<div className="flex justify-end space-x-3">
|
/>
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowCreateModal(false)
|
|
||||||
toast.success('Tenant creation modal - implement form handling')
|
|
||||||
}}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user