 c177363a19
			
		
	
	c177363a19
	
	
	
		
			
			🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			683 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			683 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 'use client'
 | ||
| 
 | ||
| import { useState, useEffect } from 'react'
 | ||
| import { 
 | ||
|   ShieldCheckIcon,
 | ||
|   KeyIcon,
 | ||
|   LockClosedIcon,
 | ||
|   ServerIcon,
 | ||
|   EyeIcon,
 | ||
|   EyeSlashIcon,
 | ||
|   DocumentDuplicateIcon,
 | ||
|   CheckCircleIcon,
 | ||
|   XCircleIcon,
 | ||
|   ExclamationTriangleIcon
 | ||
| } from '@heroicons/react/24/outline'
 | ||
| 
 | ||
| interface SecuritySetupProps {
 | ||
|   systemInfo: any
 | ||
|   configData: any
 | ||
|   onComplete: (data: any) => void
 | ||
|   onBack?: () => void
 | ||
|   isCompleted: boolean
 | ||
| }
 | ||
| 
 | ||
| interface SecurityConfig {
 | ||
|   sshKeyType: 'generate' | 'existing' | 'manual'
 | ||
|   sshPublicKey: string
 | ||
|   sshPrivateKey: string
 | ||
|   sshUsername: string
 | ||
|   sshPassword: string
 | ||
|   sshPort: number
 | ||
|   enableTLS: boolean
 | ||
|   tlsCertType: 'self-signed' | 'letsencrypt' | 'existing'
 | ||
|   tlsCertPath: string
 | ||
|   tlsKeyPath: string
 | ||
|   authMethod: 'token' | 'certificate' | 'hybrid'
 | ||
|   clusterSecret: string
 | ||
|   accessPolicy: 'open' | 'restricted' | 'invite-only'
 | ||
|   enableFirewall: boolean
 | ||
|   allowedPorts: string[]
 | ||
|   trustedIPs: string[]
 | ||
| }
 | ||
| 
 | ||
| export default function SecuritySetup({ 
 | ||
|   systemInfo, 
 | ||
|   configData, 
 | ||
|   onComplete, 
 | ||
|   onBack, 
 | ||
|   isCompleted 
 | ||
| }: SecuritySetupProps) {
 | ||
|   console.log('SecuritySetup: Component rendered with configData:', configData)
 | ||
|   
 | ||
|   const [config, setConfig] = useState<SecurityConfig>({
 | ||
|     sshKeyType: 'generate',
 | ||
|     sshPublicKey: '',
 | ||
|     sshPrivateKey: '',
 | ||
|     sshUsername: 'ubuntu',
 | ||
|     sshPassword: '',
 | ||
|     sshPort: 22,
 | ||
|     enableTLS: true,
 | ||
|     tlsCertType: 'self-signed',
 | ||
|     tlsCertPath: '',
 | ||
|     tlsKeyPath: '',
 | ||
|     authMethod: 'token',
 | ||
|     clusterSecret: '',
 | ||
|     accessPolicy: 'restricted',
 | ||
|     enableFirewall: true,
 | ||
|     allowedPorts: ['22', '8080', '8090', '9100', '3000'],
 | ||
|     trustedIPs: [],
 | ||
|     ...configData?.security // Load saved security config if exists
 | ||
|   })
 | ||
| 
 | ||
|   const [showPrivateKey, setShowPrivateKey] = useState(false)
 | ||
|   const [showClusterSecret, setShowClusterSecret] = useState(false)
 | ||
|   const [showSSHPassword, setShowSSHPassword] = useState(false)
 | ||
|   const [generating, setGenerating] = useState(false)
 | ||
|   const [validation, setValidation] = useState<{[key: string]: boolean}>({})
 | ||
|   const [portsInitialized, setPortsInitialized] = useState(false)
 | ||
| 
 | ||
|   // Generate cluster secret on mount if not exists
 | ||
|   useEffect(() => {
 | ||
|     if (!config.clusterSecret) {
 | ||
|       generateClusterSecret()
 | ||
|     }
 | ||
|   }, [])
 | ||
| 
 | ||
|   // Update firewall ports based on network configuration from previous step
 | ||
|   useEffect(() => {
 | ||
|     console.log('SecuritySetup: configData changed', {
 | ||
|       hasNetwork: !!configData?.network,
 | ||
|       portsInitialized,
 | ||
|       hasSavedSecurity: !!configData?.security?.allowedPorts,
 | ||
|       networkConfig: configData?.network
 | ||
|     })
 | ||
|     
 | ||
|     // If we have network config and haven't initialized ports yet, AND we don't have saved security config
 | ||
|     if (configData?.network && !portsInitialized && !configData?.security?.allowedPorts) {
 | ||
|       const networkConfig = configData.network
 | ||
|       const networkPorts = [
 | ||
|         networkConfig.bzzzPort?.toString(),
 | ||
|         networkConfig.mcpPort?.toString(), 
 | ||
|         networkConfig.webUIPort?.toString(),
 | ||
|         networkConfig.p2pPort?.toString()
 | ||
|       ].filter(port => port && port !== 'undefined')
 | ||
|       
 | ||
|       console.log('SecuritySetup: Auto-populating ports', { networkPorts, networkConfig })
 | ||
|       
 | ||
|       // Include standard ports plus network configuration ports
 | ||
|       const standardPorts = ['22', '8090'] // SSH and setup interface
 | ||
|       const allPorts = [...new Set([...standardPorts, ...networkPorts])]
 | ||
|       
 | ||
|       console.log('SecuritySetup: Setting allowed ports to', allPorts)
 | ||
|       setConfig(prev => ({ ...prev, allowedPorts: allPorts }))
 | ||
|       setPortsInitialized(true)
 | ||
|     }
 | ||
|   }, [configData, portsInitialized])
 | ||
| 
 | ||
|   const generateClusterSecret = () => {
 | ||
|     const secret = Array.from(crypto.getRandomValues(new Uint8Array(32)))
 | ||
|       .map(b => b.toString(16).padStart(2, '0'))
 | ||
|       .join('')
 | ||
|     setConfig(prev => ({ ...prev, clusterSecret: secret }))
 | ||
|   }
 | ||
| 
 | ||
|   const generateSSHKeys = async () => {
 | ||
|     setGenerating(true)
 | ||
|     try {
 | ||
|       // In a real implementation, this would call the backend to generate SSH keys
 | ||
|       // For now, simulate the process
 | ||
|       await new Promise(resolve => setTimeout(resolve, 2000))
 | ||
|       
 | ||
|       // Mock generated keys (in real implementation, these would come from backend)
 | ||
|       const mockPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC... chorus@${systemInfo?.network?.hostname || 'localhost'}`
 | ||
|       const mockPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
 | ||
| b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAFwwAAAAd...
 | ||
| -----END OPENSSH PRIVATE KEY-----`
 | ||
| 
 | ||
|       setConfig(prev => ({
 | ||
|         ...prev,
 | ||
|         sshPublicKey: mockPublicKey,
 | ||
|         sshPrivateKey: mockPrivateKey
 | ||
|       }))
 | ||
| 
 | ||
|       setValidation(prev => ({ ...prev, sshKeys: true }))
 | ||
|     } catch (error) {
 | ||
|       console.error('Failed to generate SSH keys:', error)
 | ||
|       setValidation(prev => ({ ...prev, sshKeys: false }))
 | ||
|     } finally {
 | ||
|       setGenerating(false)
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   const copyToClipboard = async (text: string) => {
 | ||
|     try {
 | ||
|       await navigator.clipboard.writeText(text)
 | ||
|     } catch (error) {
 | ||
|       console.error('Failed to copy to clipboard:', error)
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   const handleSubmit = (e: React.FormEvent) => {
 | ||
|     e.preventDefault()
 | ||
|     
 | ||
|     // Validate required fields
 | ||
|     const newValidation: {[key: string]: boolean} = {}
 | ||
|     
 | ||
|     if (config.sshKeyType === 'generate' && !config.sshPublicKey) {
 | ||
|       newValidation.sshKeys = false
 | ||
|     } else if (config.sshKeyType === 'existing' && !config.sshPublicKey) {
 | ||
|       newValidation.sshKeys = false
 | ||
|     } else {
 | ||
|       newValidation.sshKeys = true
 | ||
|     }
 | ||
| 
 | ||
|     if (config.enableTLS && config.tlsCertType === 'existing' && (!config.tlsCertPath || !config.tlsKeyPath)) {
 | ||
|       newValidation.tlsCert = false
 | ||
|     } else {
 | ||
|       newValidation.tlsCert = true
 | ||
|     }
 | ||
| 
 | ||
|     if (!config.clusterSecret) {
 | ||
|       newValidation.clusterSecret = false
 | ||
|     } else {
 | ||
|       newValidation.clusterSecret = true
 | ||
|     }
 | ||
| 
 | ||
|     if (config.sshKeyType === 'manual' && (!config.sshUsername || !config.sshPassword)) {
 | ||
|       newValidation.sshCredentials = false
 | ||
|     } else {
 | ||
|       newValidation.sshCredentials = true
 | ||
|     }
 | ||
| 
 | ||
|     setValidation(newValidation)
 | ||
| 
 | ||
|     // Check if all validations pass
 | ||
|     const isValid = Object.values(newValidation).every(v => v)
 | ||
|     
 | ||
|     if (isValid) {
 | ||
|       onComplete({ security: config })
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   return (
 | ||
|     <form onSubmit={handleSubmit} className="space-y-8">
 | ||
| 
 | ||
|       {/* SSH Key Configuration */}
 | ||
|       <div className="card">
 | ||
|         <div className="flex items-center mb-4">
 | ||
|           <KeyIcon className="h-6 w-6 text-bzzz-primary mr-2" />
 | ||
|           <h3 className="text-lg font-medium text-gray-900">SSH Key Management</h3>
 | ||
|           {validation.sshKeys === true && <CheckCircleIcon className="h-5 w-5 text-green-500 ml-2" />}
 | ||
|           {validation.sshKeys === false && <XCircleIcon className="h-5 w-5 text-red-500 ml-2" />}
 | ||
|         </div>
 | ||
| 
 | ||
|         <div className="space-y-4">
 | ||
|           <div>
 | ||
|             <label className="block text-sm font-medium text-gray-700 mb-2">SSH Key Type</label>
 | ||
|             <div className="space-y-2">
 | ||
|               <label className="flex items-center">
 | ||
|                 <input
 | ||
|                   type="radio"
 | ||
|                   value="generate"
 | ||
|                   checked={config.sshKeyType === 'generate'}
 | ||
|                   onChange={(e) => setConfig(prev => ({ ...prev, sshKeyType: e.target.value as any }))}
 | ||
|                   className="mr-2"
 | ||
|                 />
 | ||
|                 Generate new SSH key pair
 | ||
|               </label>
 | ||
|               <label className="flex items-center">
 | ||
|                 <input
 | ||
|                   type="radio"
 | ||
|                   value="existing"
 | ||
|                   checked={config.sshKeyType === 'existing'}
 | ||
|                   onChange={(e) => setConfig(prev => ({ ...prev, sshKeyType: e.target.value as any }))}
 | ||
|                   className="mr-2"
 | ||
|                 />
 | ||
|                 Use existing SSH key
 | ||
|               </label>
 | ||
|               <label className="flex items-center">
 | ||
|                 <input
 | ||
|                   type="radio"
 | ||
|                   value="manual"
 | ||
|                   checked={config.sshKeyType === 'manual'}
 | ||
|                   onChange={(e) => setConfig(prev => ({ ...prev, sshKeyType: e.target.value as any }))}
 | ||
|                   className="mr-2"
 | ||
|                 />
 | ||
|                 Configure manually with SSH username/password
 | ||
|               </label>
 | ||
|             </div>
 | ||
|           </div>
 | ||
| 
 | ||
|           {config.sshKeyType === 'generate' && (
 | ||
|             <div className="space-y-4">
 | ||
|               {!config.sshPublicKey ? (
 | ||
|                 <button
 | ||
|                   type="button"
 | ||
|                   onClick={generateSSHKeys}
 | ||
|                   disabled={generating}
 | ||
|                   className="btn-primary"
 | ||
|                 >
 | ||
|                   {generating ? 'Generating Keys...' : 'Generate SSH Key Pair'}
 | ||
|                 </button>
 | ||
|               ) : (
 | ||
|                 <div className="space-y-4">
 | ||
|                   <div>
 | ||
|                     <label className="block text-sm font-medium text-gray-700 mb-2">Public Key</label>
 | ||
|                     <div className="relative">
 | ||
|                       <textarea
 | ||
|                         value={config.sshPublicKey}
 | ||
|                         readOnly
 | ||
|                         className="w-full p-3 border border-gray-300 rounded-lg bg-gray-50 font-mono text-sm"
 | ||
|                         rows={3}
 | ||
|                       />
 | ||
|                       <button
 | ||
|                         type="button"
 | ||
|                         onClick={() => copyToClipboard(config.sshPublicKey)}
 | ||
|                         className="absolute top-2 right-2 p-1 text-gray-500 hover:text-gray-700"
 | ||
|                       >
 | ||
|                         <DocumentDuplicateIcon className="h-4 w-4" />
 | ||
|                       </button>
 | ||
|                     </div>
 | ||
|                   </div>
 | ||
| 
 | ||
|                   <div>
 | ||
|                     <label className="block text-sm font-medium text-gray-700 mb-2">Private Key</label>
 | ||
|                     <div className="relative">
 | ||
|                       <textarea
 | ||
|                         value={showPrivateKey ? config.sshPrivateKey : '••••••••••••••••••••••••••••••••'}
 | ||
|                         readOnly
 | ||
|                         className="w-full p-3 border border-gray-300 rounded-lg bg-gray-50 font-mono text-sm"
 | ||
|                         rows={6}
 | ||
|                       />
 | ||
|                       <div className="absolute top-2 right-2 flex space-x-1">
 | ||
|                         <button
 | ||
|                           type="button"
 | ||
|                           onClick={() => setShowPrivateKey(!showPrivateKey)}
 | ||
|                           className="p-1 text-gray-500 hover:text-gray-700"
 | ||
|                         >
 | ||
|                           {showPrivateKey ? <EyeSlashIcon className="h-4 w-4" /> : <EyeIcon className="h-4 w-4" />}
 | ||
|                         </button>
 | ||
|                         <button
 | ||
|                           type="button"
 | ||
|                           onClick={() => copyToClipboard(config.sshPrivateKey)}
 | ||
|                           className="p-1 text-gray-500 hover:text-gray-700"
 | ||
|                         >
 | ||
|                           <DocumentDuplicateIcon className="h-4 w-4" />
 | ||
|                         </button>
 | ||
|                       </div>
 | ||
|                     </div>
 | ||
|                     <p className="text-sm text-yellow-600 mt-1">⚠️ Store this private key securely. It cannot be recovered.</p>
 | ||
|                   </div>
 | ||
|                 </div>
 | ||
|               )}
 | ||
|             </div>
 | ||
|           )}
 | ||
| 
 | ||
|           {config.sshKeyType === 'existing' && (
 | ||
|             <div>
 | ||
|               <label className="block text-sm font-medium text-gray-700 mb-2">SSH Public Key</label>
 | ||
|               <textarea
 | ||
|                 value={config.sshPublicKey}
 | ||
|                 onChange={(e) => setConfig(prev => ({ ...prev, sshPublicKey: e.target.value }))}
 | ||
|                 placeholder="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC..."
 | ||
|                 className="w-full p-3 border border-gray-300 rounded-lg font-mono text-sm"
 | ||
|                 rows={3}
 | ||
|               />
 | ||
|             </div>
 | ||
|           )}
 | ||
| 
 | ||
|           {config.sshKeyType === 'manual' && (
 | ||
|             <div className="space-y-4">
 | ||
|               <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
 | ||
|                 <div className="flex items-start">
 | ||
|                   <div className="flex-shrink-0">
 | ||
|                     <ExclamationTriangleIcon className="h-5 w-5 text-yellow-600 mt-0.5" />
 | ||
|                   </div>
 | ||
|                   <div className="ml-3">
 | ||
|                     <h4 className="text-sm font-medium text-yellow-800">Manual SSH Configuration</h4>
 | ||
|                     <p className="text-sm text-yellow-700 mt-1">
 | ||
|                       Provide SSH credentials for cluster machines. SSH keys will be automatically generated and deployed using these credentials. 
 | ||
|                       <strong> Passwords are only used during setup and are not stored.</strong>
 | ||
|                     </p>
 | ||
|                   </div>
 | ||
|                 </div>
 | ||
|               </div>
 | ||
| 
 | ||
|               <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
 | ||
|                 <div>
 | ||
|                   <label className="block text-sm font-medium text-gray-700 mb-2">
 | ||
|                     SSH Username <span className="text-red-500">*</span>
 | ||
|                   </label>
 | ||
|                   <input
 | ||
|                     type="text"
 | ||
|                     value={config.sshUsername}
 | ||
|                     onChange={(e) => setConfig(prev => ({ ...prev, sshUsername: e.target.value }))}
 | ||
|                     placeholder="ubuntu"
 | ||
|                     className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary"
 | ||
|                     required
 | ||
|                   />
 | ||
|                   <p className="text-sm text-gray-500 mt-1">
 | ||
|                     Exact SSH username for cluster machines
 | ||
|                   </p>
 | ||
|                 </div>
 | ||
| 
 | ||
|                 <div>
 | ||
|                   <label className="block text-sm font-medium text-gray-700 mb-2">
 | ||
|                     SSH Port
 | ||
|                   </label>
 | ||
|                   <input
 | ||
|                     type="number"
 | ||
|                     value={config.sshPort}
 | ||
|                     onChange={(e) => setConfig(prev => ({ ...prev, sshPort: parseInt(e.target.value) || 22 }))}
 | ||
|                     min="1"
 | ||
|                     max="65535"
 | ||
|                     className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary"
 | ||
|                   />
 | ||
|                   <p className="text-sm text-gray-500 mt-1">
 | ||
|                     SSH port number (default: 22)
 | ||
|                   </p>
 | ||
|                 </div>
 | ||
|               </div>
 | ||
| 
 | ||
|               <div>
 | ||
|                 <label className="block text-sm font-medium text-gray-700 mb-2">
 | ||
|                   SSH Password <span className="text-red-500">*</span>
 | ||
|                 </label>
 | ||
|                 <div className="relative">
 | ||
|                   <input
 | ||
|                     type={showSSHPassword ? 'text' : 'password'}
 | ||
|                     value={config.sshPassword}
 | ||
|                     onChange={(e) => setConfig(prev => ({ ...prev, sshPassword: e.target.value }))}
 | ||
|                     placeholder="Enter SSH password for cluster machines"
 | ||
|                     className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary"
 | ||
|                     required
 | ||
|                   />
 | ||
|                   <button
 | ||
|                     type="button"
 | ||
|                     onClick={() => setShowSSHPassword(!showSSHPassword)}
 | ||
|                     className="absolute inset-y-0 right-0 pr-3 flex items-center"
 | ||
|                   >
 | ||
|                     {showSSHPassword ? (
 | ||
|                       <EyeSlashIcon className="h-4 w-4 text-gray-400" />
 | ||
|                     ) : (
 | ||
|                       <EyeIcon className="h-4 w-4 text-gray-400" />
 | ||
|                     )}
 | ||
|                   </button>
 | ||
|                 </div>
 | ||
|                 <p className="text-sm text-gray-500 mt-1">
 | ||
|                   SSH password for the specified username (used only during setup)
 | ||
|                 </p>
 | ||
|               </div>
 | ||
|             </div>
 | ||
|           )}
 | ||
|         </div>
 | ||
|       </div>
 | ||
| 
 | ||
| 
 | ||
|       {/* TLS/SSL Configuration */}
 | ||
|       <div className="card">
 | ||
|         <div className="flex items-center mb-4">
 | ||
|           <LockClosedIcon className="h-6 w-6 text-bzzz-primary mr-2" />
 | ||
|           <h3 className="text-lg font-medium text-gray-900">TLS/SSL Configuration</h3>
 | ||
|           {validation.tlsCert === true && <CheckCircleIcon className="h-5 w-5 text-green-500 ml-2" />}
 | ||
|           {validation.tlsCert === false && <XCircleIcon className="h-5 w-5 text-red-500 ml-2" />}
 | ||
|         </div>
 | ||
| 
 | ||
|         <div className="space-y-4">
 | ||
|           <label className="flex items-center">
 | ||
|             <input
 | ||
|               type="checkbox"
 | ||
|               checked={config.enableTLS}
 | ||
|               onChange={(e) => setConfig(prev => ({ ...prev, enableTLS: e.target.checked }))}
 | ||
|               className="mr-2"
 | ||
|             />
 | ||
|             Enable TLS encryption for cluster communication
 | ||
|           </label>
 | ||
| 
 | ||
|           {config.enableTLS && (
 | ||
|             <div className="space-y-4 ml-6">
 | ||
|               <div>
 | ||
|                 <label className="block text-sm font-medium text-gray-700 mb-2">Certificate Type</label>
 | ||
|                 <div className="space-y-2">
 | ||
|                   <label className="flex items-center">
 | ||
|                     <input
 | ||
|                       type="radio"
 | ||
|                       value="self-signed"
 | ||
|                       checked={config.tlsCertType === 'self-signed'}
 | ||
|                       onChange={(e) => setConfig(prev => ({ ...prev, tlsCertType: e.target.value as any }))}
 | ||
|                       className="mr-2"
 | ||
|                     />
 | ||
|                     Generate self-signed certificate
 | ||
|                   </label>
 | ||
|                   <label className="flex items-center">
 | ||
|                     <input
 | ||
|                       type="radio"
 | ||
|                       value="letsencrypt"
 | ||
|                       checked={config.tlsCertType === 'letsencrypt'}
 | ||
|                       onChange={(e) => setConfig(prev => ({ ...prev, tlsCertType: e.target.value as any }))}
 | ||
|                       className="mr-2"
 | ||
|                     />
 | ||
|                     Use Let's Encrypt (requires domain)
 | ||
|                   </label>
 | ||
|                   <label className="flex items-center">
 | ||
|                     <input
 | ||
|                       type="radio"
 | ||
|                       value="existing"
 | ||
|                       checked={config.tlsCertType === 'existing'}
 | ||
|                       onChange={(e) => setConfig(prev => ({ ...prev, tlsCertType: e.target.value as any }))}
 | ||
|                       className="mr-2"
 | ||
|                     />
 | ||
|                     Use existing certificate
 | ||
|                   </label>
 | ||
|                 </div>
 | ||
|               </div>
 | ||
| 
 | ||
|               {config.tlsCertType === 'existing' && (
 | ||
|                 <div className="space-y-4">
 | ||
|                   <div>
 | ||
|                     <label className="block text-sm font-medium text-gray-700 mb-2">Certificate Path</label>
 | ||
|                     <input
 | ||
|                       type="text"
 | ||
|                       value={config.tlsCertPath}
 | ||
|                       onChange={(e) => setConfig(prev => ({ ...prev, tlsCertPath: e.target.value }))}
 | ||
|                       placeholder="/path/to/certificate.crt"
 | ||
|                       className="w-full p-3 border border-gray-300 rounded-lg"
 | ||
|                     />
 | ||
|                   </div>
 | ||
|                   <div>
 | ||
|                     <label className="block text-sm font-medium text-gray-700 mb-2">Private Key Path</label>
 | ||
|                     <input
 | ||
|                       type="text"
 | ||
|                       value={config.tlsKeyPath}
 | ||
|                       onChange={(e) => setConfig(prev => ({ ...prev, tlsKeyPath: e.target.value }))}
 | ||
|                       placeholder="/path/to/private.key"
 | ||
|                       className="w-full p-3 border border-gray-300 rounded-lg"
 | ||
|                     />
 | ||
|                   </div>
 | ||
|                 </div>
 | ||
|               )}
 | ||
|             </div>
 | ||
|           )}
 | ||
|         </div>
 | ||
|       </div>
 | ||
| 
 | ||
|       {/* Authentication Method */}
 | ||
|       <div className="card">
 | ||
|         <div className="flex items-center mb-4">
 | ||
|           <ShieldCheckIcon className="h-6 w-6 text-bzzz-primary mr-2" />
 | ||
|           <h3 className="text-lg font-medium text-gray-900">Authentication Method</h3>
 | ||
|         </div>
 | ||
| 
 | ||
|         <div className="space-y-4">
 | ||
|           <div>
 | ||
|             <label className="block text-sm font-medium text-gray-700 mb-2">Authentication Type</label>
 | ||
|             <div className="space-y-2">
 | ||
|               <label className="flex items-center">
 | ||
|                 <input
 | ||
|                   type="radio"
 | ||
|                   value="token"
 | ||
|                   checked={config.authMethod === 'token'}
 | ||
|                   onChange={(e) => setConfig(prev => ({ ...prev, authMethod: e.target.value as any }))}
 | ||
|                   className="mr-2"
 | ||
|                 />
 | ||
|                 API Token-based authentication
 | ||
|               </label>
 | ||
|               <label className="flex items-center">
 | ||
|                 <input
 | ||
|                   type="radio"
 | ||
|                   value="certificate"
 | ||
|                   checked={config.authMethod === 'certificate'}
 | ||
|                   onChange={(e) => setConfig(prev => ({ ...prev, authMethod: e.target.value as any }))}
 | ||
|                   className="mr-2"
 | ||
|                 />
 | ||
|                 Certificate-based authentication
 | ||
|               </label>
 | ||
|               <label className="flex items-center">
 | ||
|                 <input
 | ||
|                   type="radio"
 | ||
|                   value="hybrid"
 | ||
|                   checked={config.authMethod === 'hybrid'}
 | ||
|                   onChange={(e) => setConfig(prev => ({ ...prev, authMethod: e.target.value as any }))}
 | ||
|                   className="mr-2"
 | ||
|                 />
 | ||
|                 Hybrid (Token + Certificate)
 | ||
|               </label>
 | ||
|             </div>
 | ||
|           </div>
 | ||
| 
 | ||
|           <div>
 | ||
|             <label className="block text-sm font-medium text-gray-700 mb-2">Cluster Secret</label>
 | ||
|             <div className="relative">
 | ||
|               <input
 | ||
|                 type={showClusterSecret ? "text" : "password"}
 | ||
|                 value={config.clusterSecret}
 | ||
|                 onChange={(e) => setConfig(prev => ({ ...prev, clusterSecret: e.target.value }))}
 | ||
|                 className="w-full p-3 border border-gray-300 rounded-lg font-mono"
 | ||
|                 placeholder="Cluster authentication secret"
 | ||
|               />
 | ||
|               <div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex space-x-1">
 | ||
|                 <button
 | ||
|                   type="button"
 | ||
|                   onClick={() => setShowClusterSecret(!showClusterSecret)}
 | ||
|                   className="p-1 text-gray-500 hover:text-gray-700"
 | ||
|                 >
 | ||
|                   {showClusterSecret ? <EyeSlashIcon className="h-4 w-4" /> : <EyeIcon className="h-4 w-4" />}
 | ||
|                 </button>
 | ||
|                 <button
 | ||
|                   type="button"
 | ||
|                   onClick={generateClusterSecret}
 | ||
|                   className="p-1 text-gray-500 hover:text-gray-700"
 | ||
|                 >
 | ||
|                   <KeyIcon className="h-4 w-4" />
 | ||
|                 </button>
 | ||
|               </div>
 | ||
|             </div>
 | ||
|             {validation.clusterSecret === false && (
 | ||
|               <p className="text-sm text-red-600 mt-1">Cluster secret is required</p>
 | ||
|             )}
 | ||
|           </div>
 | ||
|         </div>
 | ||
|       </div>
 | ||
| 
 | ||
|       {/* Access Control */}
 | ||
|       <div className="card">
 | ||
|         <div className="flex items-center mb-4">
 | ||
|           <ServerIcon className="h-6 w-6 text-bzzz-primary mr-2" />
 | ||
|           <h3 className="text-lg font-medium text-gray-900">Access Control</h3>
 | ||
|         </div>
 | ||
| 
 | ||
|         <div className="space-y-4">
 | ||
|           <div>
 | ||
|             <label className="block text-sm font-medium text-gray-700 mb-2">Access Policy</label>
 | ||
|             <select
 | ||
|               value={config.accessPolicy}
 | ||
|               onChange={(e) => setConfig(prev => ({ ...prev, accessPolicy: e.target.value as any }))}
 | ||
|               className="w-full p-3 border border-gray-300 rounded-lg"
 | ||
|             >
 | ||
|               <option value="open">Open (Anyone can join cluster)</option>
 | ||
|               <option value="restricted">Restricted (Require authentication)</option>
 | ||
|               <option value="invite-only">Invite Only (Manual approval required)</option>
 | ||
|             </select>
 | ||
|           </div>
 | ||
| 
 | ||
|           <label className="flex items-center">
 | ||
|             <input
 | ||
|               type="checkbox"
 | ||
|               checked={config.enableFirewall}
 | ||
|               onChange={(e) => setConfig(prev => ({ ...prev, enableFirewall: e.target.checked }))}
 | ||
|               className="mr-2"
 | ||
|             />
 | ||
|             Enable firewall configuration
 | ||
|           </label>
 | ||
| 
 | ||
|           {config.enableFirewall && (
 | ||
|             <div className="ml-6 space-y-4">
 | ||
|               <div>
 | ||
|                 <label className="block text-sm font-medium text-gray-700 mb-2">Allowed Ports</label>
 | ||
|                 <input
 | ||
|                   type="text"
 | ||
|                   value={config.allowedPorts.join(', ')}
 | ||
|                   onChange={(e) => setConfig(prev => ({ 
 | ||
|                     ...prev, 
 | ||
|                     allowedPorts: e.target.value.split(',').map(p => p.trim()).filter(p => p)
 | ||
|                   }))}
 | ||
|                   placeholder="22, 8080, 8090, 9100, 3000"
 | ||
|                   className="w-full p-3 border border-gray-300 rounded-lg"
 | ||
|                 />
 | ||
|                 {configData?.network && (
 | ||
|                   <p className="text-sm text-green-600 mt-1 flex items-center">
 | ||
|                     <CheckCircleIcon className="h-4 w-4 mr-1" />
 | ||
|                     Ports automatically configured from Network Settings: {[
 | ||
|                       configData.network.bzzzPort,
 | ||
|                       configData.network.mcpPort, 
 | ||
|                       configData.network.webUIPort,
 | ||
|                       configData.network.p2pPort
 | ||
|                     ].filter(p => p).join(', ')}
 | ||
|                   </p>
 | ||
|                 )}
 | ||
|                 <p className="text-sm text-gray-500 mt-1">
 | ||
|                   Comma-separated list of ports to allow through the firewall
 | ||
|                 </p>
 | ||
|               </div>
 | ||
|             </div>
 | ||
|           )}
 | ||
|         </div>
 | ||
|       </div>
 | ||
| 
 | ||
|       {/* Security Summary */}
 | ||
|       <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
 | ||
|         <div className="flex items-start">
 | ||
|           <ExclamationTriangleIcon className="h-5 w-5 text-blue-500 mt-0.5 mr-2" />
 | ||
|           <div>
 | ||
|             <h4 className="text-sm font-medium text-blue-800">Security Summary</h4>
 | ||
|             <ul className="text-sm text-blue-700 mt-1 space-y-1">
 | ||
|               <li>• SSH access: {config.sshKeyType === 'generate' ? 'New key pair will be generated' : config.sshKeyType === 'existing' ? 'Using provided key' : 'Manual configuration'}</li>
 | ||
|               <li>• TLS encryption: {config.enableTLS ? 'Enabled' : 'Disabled'}</li>
 | ||
|               <li>• Authentication: {config.authMethod}</li>
 | ||
|               <li>• Access policy: {config.accessPolicy}</li>
 | ||
|               <li>• Firewall: {config.enableFirewall ? 'Enabled' : 'Disabled'}</li>
 | ||
|             </ul>
 | ||
|           </div>
 | ||
|         </div>
 | ||
|       </div>
 | ||
| 
 | ||
|       <div className="flex justify-between pt-6 border-t border-gray-200">
 | ||
|         <div>
 | ||
|           {onBack && (
 | ||
|             <button type="button" onClick={onBack} className="btn-outline">
 | ||
|               Back
 | ||
|             </button>
 | ||
|           )}
 | ||
|         </div>
 | ||
|         <button 
 | ||
|           type="submit" 
 | ||
|           disabled={config.sshKeyType === 'generate' && !config.sshPublicKey}
 | ||
|           className="btn-primary"
 | ||
|         >
 | ||
|           {isCompleted ? 'Continue' : 'Next: AI Integration'}
 | ||
|         </button>
 | ||
|       </div>
 | ||
|     </form>
 | ||
|   )
 | ||
| } |