- Updated configuration and deployment files - Improved system architecture and components - Enhanced documentation and testing - Fixed various issues and added new features 🤖 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-eucalyptus-600 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-eucalyptus-600 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-eucalyptus-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>
|
||
)
|
||
} |