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 api from '../services/api'
|
||||
import toast from 'react-hot-toast'
|
||||
import TenantModal from '../components/TenantModal'
|
||||
import {
|
||||
PlusIcon,
|
||||
PencilIcon,
|
||||
@@ -55,6 +56,28 @@ const Tenants = () => {
|
||||
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) => {
|
||||
if (!confirm('Are you sure you want to delete this tenant? This action cannot be undone.')) {
|
||||
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 colors = {
|
||||
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">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => setEditingTenant(tenant)}
|
||||
onClick={() => handleEditTenant(tenant)}
|
||||
className="text-blue-600 hover:text-blue-900 p-1 rounded"
|
||||
title="Edit"
|
||||
>
|
||||
@@ -275,36 +310,16 @@ const Tenants = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modals would go here */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div className="mt-3">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Create New Tenant</h3>
|
||||
<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.
|
||||
</p>
|
||||
<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={() => {
|
||||
{/* Tenant Creation/Edit Modal */}
|
||||
<TenantModal
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => {
|
||||
setShowCreateModal(false)
|
||||
toast.success('Tenant creation modal - implement form handling')
|
||||
setEditingTenant(null)
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
tenant={editingTenant}
|
||||
onSave={handleSaveTenant}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user