Fix jwt-token

This commit is contained in:
2025-09-13 12:04:02 +02:00
parent 30e64a590e
commit 65b7e0d965
2 changed files with 660 additions and 31 deletions

View 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-----&#10;...&#10;-----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

View File

@@ -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={() => {
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>
)}
{/* Tenant Creation/Edit Modal */}
<TenantModal
isOpen={showCreateModal}
onClose={() => {
setShowCreateModal(false)
setEditingTenant(null)
}}
tenant={editingTenant}
onSave={handleSaveTenant}
/>
</div>
)
}