import { useState, useEffect } from 'react' import { CircleHelp, AlertCircle, Check, X, Loader2 } from 'lucide-react' import type { ProxyHost } from '../api/proxyHosts' import { testProxyHostConnection } from '../api/proxyHosts' import { useRemoteServers } from '../hooks/useRemoteServers' import { useDomains } from '../hooks/useDomains' import { useCertificates } from '../hooks/useCertificates' import { useDocker } from '../hooks/useDocker' import { parse } from 'tldts' interface ProxyHostFormProps { host?: ProxyHost onSubmit: (data: Partial) => Promise onCancel: () => void } export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFormProps) { const [formData, setFormData] = useState({ name: host?.name || '', domain_names: host?.domain_names || '', forward_scheme: host?.forward_scheme || 'http', forward_host: host?.forward_host || '', forward_port: host?.forward_port || 80, ssl_forced: host?.ssl_forced ?? true, http2_support: host?.http2_support ?? true, hsts_enabled: host?.hsts_enabled ?? true, hsts_subdomains: host?.hsts_subdomains ?? true, block_exploits: host?.block_exploits ?? true, websocket_support: host?.websocket_support ?? true, advanced_config: host?.advanced_config || '', enabled: host?.enabled ?? true, certificate_id: host?.certificate_id, }) const { servers: remoteServers } = useRemoteServers() const { domains, createDomain } = useDomains() const { certificates } = useCertificates() const [connectionSource, setConnectionSource] = useState<'local' | 'custom' | string>('custom') const { containers: dockerContainers, isLoading: dockerLoading, error: dockerError } = useDocker( connectionSource === 'local' ? 'local' : undefined, connectionSource !== 'local' && connectionSource !== 'custom' ? connectionSource : undefined ) const [selectedDomain, setSelectedDomain] = useState('') const [selectedContainerId, setSelectedContainerId] = useState('') // New Domain Popup State const [showDomainPrompt, setShowDomainPrompt] = useState(false) const [pendingDomain, setPendingDomain] = useState('') const [dontAskAgain, setDontAskAgain] = useState(false) useEffect(() => { const stored = localStorage.getItem('cpmp_dont_ask_domain') if (stored === 'true') { setDontAskAgain(true) } }, []) const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle') const checkNewDomains = (input: string) => { if (dontAskAgain) return const domainList = input.split(',').map(d => d.trim()).filter(d => d) for (const domain of domainList) { const parsed = parse(domain) if (parsed.domain && parsed.domain !== domain) { // It's a subdomain, check if the base domain exists const baseDomain = parsed.domain const exists = domains.some(d => d.name === baseDomain) if (!exists) { setPendingDomain(baseDomain) setShowDomainPrompt(true) return // Only prompt for one at a time } } else if (parsed.domain && parsed.domain === domain) { // It is a base domain, check if it exists const exists = domains.some(d => d.name === domain) if (!exists) { setPendingDomain(domain) setShowDomainPrompt(true) return } } } } const handleSaveDomain = async () => { try { await createDomain(pendingDomain) setShowDomainPrompt(false) } catch (err) { console.error("Failed to save domain", err) // Optionally show error } } const handleDontAskToggle = (checked: boolean) => { setDontAskAgain(checked) localStorage.setItem('cpmp_dont_ask_domain', String(checked)) } const handleTestConnection = async () => { if (!formData.forward_host || !formData.forward_port) return setTestStatus('testing') try { await testProxyHostConnection(formData.forward_host, formData.forward_port) setTestStatus('success') // Reset status after 3 seconds setTimeout(() => setTestStatus('idle'), 3000) } catch (err) { console.error("Test connection failed", err) setTestStatus('error') // Reset status after 3 seconds setTimeout(() => setTestStatus('idle'), 3000) } } const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [nameError, setNameError] = useState(null) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) setError(null) setNameError(null) // Validate name is required if (!formData.name.trim()) { setNameError('Name is required') setLoading(false) return } try { await onSubmit(formData) } catch (err: unknown) { console.error("Submit error:", err) // Extract error message from axios response if available const errorObj = err as { response?: { data?: { error?: string } }; message?: string } const message = errorObj.response?.data?.error || errorObj.message || 'Failed to save proxy host' setError(message) } finally { setLoading(false) } } const handleContainerSelect = (containerId: string) => { setSelectedContainerId(containerId) const container = dockerContainers.find(c => c.id === containerId) if (container) { // Default to internal IP and private port let host = container.ip || container.names[0] let port = container.ports && container.ports.length > 0 ? container.ports[0].private_port : 80 // If using a Remote Server, try to use the Host IP and Mapped Public Port if (connectionSource !== 'local' && connectionSource !== 'custom') { const server = remoteServers.find(s => s.uuid === connectionSource) if (server) { // Use the Remote Server's Host IP (e.g. public/tailscale IP) host = server.host // Find a mapped public port // We prefer the first mapped port we find const mappedPort = container.ports?.find(p => p.public_port) if (mappedPort) { port = mappedPort.public_port } else { // If no public port is mapped, we can't reach it from outside // But we'll leave the internal port as a fallback, though it likely won't work console.warn('No public port mapped for container on remote server') } } } let newDomainNames = formData.domain_names if (selectedDomain) { const subdomain = container.names[0].replace(/^\//, '') newDomainNames = `${subdomain}.${selectedDomain}` } setFormData({ ...formData, forward_host: host, forward_port: port, forward_scheme: 'http', domain_names: newDomainNames, }) } } const handleBaseDomainChange = (domain: string) => { setSelectedDomain(domain) if (selectedContainerId && domain) { const container = dockerContainers.find(c => c.id === selectedContainerId) if (container) { const subdomain = container.names[0].replace(/^\//, '') setFormData(prev => ({ ...prev, domain_names: `${subdomain}.${domain}` })) } } } return (

{host ? 'Edit Proxy Host' : 'Add Proxy Host'}

{error && (
{error}
)} {/* Name Field */}
{ setFormData({ ...formData, name: e.target.value }) if (nameError && e.target.value.trim()) { setNameError(null) } }} placeholder="My Service" className={`w-full bg-gray-900 border rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 ${ nameError ? 'border-red-500' : 'border-gray-700' }`} /> {nameError ? (

{nameError}

) : (

A friendly name to identify this proxy host

)}
{/* Docker Container Quick Select */}
{dockerError && connectionSource !== 'custom' && (

Failed to connect: {(dockerError as Error).message}

)}
{/* Domain Names */}
{domains.length > 0 && (
)}
setFormData({ ...formData, domain_names: e.target.value })} onBlur={e => checkNewDomains(e.target.value)} placeholder="example.com, www.example.com" className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" />
{/* Forward Details */}
setFormData({ ...formData, forward_host: e.target.value })} placeholder="192.168.1.100" className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" />
setFormData({ ...formData, forward_port: parseInt(e.target.value) })} className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" />
{/* SSL Certificate Selection */}

Let's Encrypt certificates are managed automatically. Use custom certificates for self-signed or other providers.

{/* SSL & Security Options */}
{/* Advanced Config */}