feat: Enhance ProxyHost configuration with application presets and internal IP support

This commit is contained in:
Wikid82
2025-11-27 03:54:41 +00:00
parent 09231ed6da
commit 51664416b6
13 changed files with 615 additions and 29 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { CircleHelp, AlertCircle, Check, X, Loader2 } from 'lucide-react'
import type { ProxyHost } from '../api/proxyHosts'
import { CircleHelp, AlertCircle, Check, X, Loader2, Copy, Info } from 'lucide-react'
import type { ProxyHost, ApplicationPreset } from '../api/proxyHosts'
import { testProxyHostConnection } from '../api/proxyHosts'
import { useRemoteServers } from '../hooks/useRemoteServers'
import { useDomains } from '../hooks/useDomains'
@@ -8,6 +8,32 @@ import { useCertificates } from '../hooks/useCertificates'
import { useDocker } from '../hooks/useDocker'
import { parse } from 'tldts'
// Application preset configurations
const APPLICATION_PRESETS: { value: ApplicationPreset; label: string; description: string }[] = [
{ value: 'none', label: 'None', description: 'Standard reverse proxy' },
{ value: 'plex', label: 'Plex', description: 'Media server with remote access' },
{ value: 'jellyfin', label: 'Jellyfin', description: 'Open source media server' },
{ value: 'emby', label: 'Emby', description: 'Media server' },
{ value: 'homeassistant', label: 'Home Assistant', description: 'Home automation' },
{ value: 'nextcloud', label: 'Nextcloud', description: 'File sync and share' },
{ value: 'vaultwarden', label: 'Vaultwarden', description: 'Password manager' },
]
// Docker image to preset mapping for auto-detection
const IMAGE_TO_PRESET: Record<string, ApplicationPreset> = {
'plexinc/pms-docker': 'plex',
'linuxserver/plex': 'plex',
'jellyfin/jellyfin': 'jellyfin',
'linuxserver/jellyfin': 'jellyfin',
'emby/embyserver': 'emby',
'linuxserver/emby': 'emby',
'homeassistant/home-assistant': 'homeassistant',
'ghcr.io/home-assistant/home-assistant': 'homeassistant',
'nextcloud': 'nextcloud',
'linuxserver/nextcloud': 'nextcloud',
'vaultwarden/server': 'vaultwarden',
}
interface ProxyHostFormProps {
host?: ProxyHost
onSubmit: (data: Partial<ProxyHost>) => Promise<void>
@@ -27,11 +53,57 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
hsts_subdomains: host?.hsts_subdomains ?? true,
block_exploits: host?.block_exploits ?? true,
websocket_support: host?.websocket_support ?? true,
application: (host?.application || 'none') as ApplicationPreset,
advanced_config: host?.advanced_config || '',
enabled: host?.enabled ?? true,
certificate_id: host?.certificate_id,
})
// CPMP internal IP for config helpers
const [cpmpInternalIP, setCpmpInternalIP] = useState<string>('')
const [copiedField, setCopiedField] = useState<string | null>(null)
// Fetch CPMP internal IP on mount
useEffect(() => {
fetch('/api/v1/health')
.then(res => res.json())
.then(data => {
if (data.internal_ip) {
setCpmpInternalIP(data.internal_ip)
}
})
.catch(() => {})
}, [])
// Auto-detect application preset from Docker image
const detectApplicationPreset = (imageName: string): ApplicationPreset => {
const lowerImage = imageName.toLowerCase()
for (const [pattern, preset] of Object.entries(IMAGE_TO_PRESET)) {
if (lowerImage.includes(pattern.toLowerCase())) {
return preset
}
}
return 'none'
}
// Copy to clipboard helper
const copyToClipboard = async (text: string, field: string) => {
try {
await navigator.clipboard.writeText(text)
setCopiedField(field)
setTimeout(() => setCopiedField(null), 2000)
} catch {
console.error('Failed to copy to clipboard')
}
}
// Get the external URL for this proxy host
const getExternalUrl = () => {
const domain = formData.domain_names.split(',')[0]?.trim()
if (!domain) return ''
return `https://${domain}:443`
}
const { servers: remoteServers } = useRemoteServers()
const { domains, createDomain } = useDomains()
const { certificates } = useCertificates()
@@ -184,12 +256,19 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
newDomainNames = `${subdomain}.${selectedDomain}`
}
// Auto-detect application preset from image name
const detectedPreset = detectApplicationPreset(container.image)
// Auto-enable websockets for apps that need it
const needsWebsockets = ['plex', 'jellyfin', 'emby', 'homeassistant', 'vaultwarden'].includes(detectedPreset)
setFormData({
...formData,
forward_host: host,
forward_port: port,
forward_scheme: 'http',
domain_names: newDomainNames,
application: detectedPreset,
websocket_support: needsWebsockets || formData.websocket_support,
})
}
}
@@ -411,6 +490,156 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
</p>
</div>
{/* Application Preset */}
<div>
<label htmlFor="application-preset" className="block text-sm font-medium text-gray-300 mb-2">
Application Preset
<span className="text-gray-500 font-normal ml-2">(Optional)</span>
</label>
<select
id="application-preset"
value={formData.application}
onChange={e => {
const preset = e.target.value as ApplicationPreset
// Auto-enable websockets for apps that need it
const needsWebsockets = ['plex', 'jellyfin', 'emby', 'homeassistant', 'vaultwarden'].includes(preset)
setFormData({
...formData,
application: preset,
websocket_support: needsWebsockets || formData.websocket_support,
})
}}
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"
>
{APPLICATION_PRESETS.map(preset => (
<option key={preset.value} value={preset.value}>
{preset.label} - {preset.description}
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">
Presets automatically configure headers for remote access behind tunnels/CGNAT.
</p>
</div>
{/* Application Config Helper */}
{formData.application !== 'none' && formData.domain_names && (
<div className="bg-blue-900/20 border border-blue-700 rounded-lg p-4">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 space-y-3">
<h4 className="text-sm font-semibold text-blue-300">
{formData.application === 'plex' && 'Plex Remote Access Setup'}
{formData.application === 'jellyfin' && 'Jellyfin Proxy Setup'}
{formData.application === 'emby' && 'Emby Proxy Setup'}
{formData.application === 'homeassistant' && 'Home Assistant Proxy Setup'}
{formData.application === 'nextcloud' && 'Nextcloud Proxy Setup'}
{formData.application === 'vaultwarden' && 'Vaultwarden Setup'}
</h4>
{/* Plex Helper */}
{formData.application === 'plex' && (
<>
<p className="text-xs text-gray-300">
Copy this URL and paste it into <strong>Plex Settings Network Custom server access URLs</strong>
</p>
<div className="flex items-center gap-2">
<code className="flex-1 bg-gray-900 px-3 py-2 rounded text-sm text-green-400 font-mono">
{getExternalUrl()}
</code>
<button
type="button"
onClick={() => copyToClipboard(getExternalUrl(), 'plex-url')}
className="px-3 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded text-sm flex items-center gap-1"
>
{copiedField === 'plex-url' ? <Check size={14} /> : <Copy size={14} />}
{copiedField === 'plex-url' ? 'Copied!' : 'Copy'}
</button>
</div>
</>
)}
{/* Jellyfin/Emby Helper */}
{(formData.application === 'jellyfin' || formData.application === 'emby') && cpmpInternalIP && (
<>
<p className="text-xs text-gray-300">
Add this IP to <strong>{formData.application === 'jellyfin' ? 'Jellyfin' : 'Emby'} Dashboard Networking Known Proxies</strong>
</p>
<div className="flex items-center gap-2">
<code className="flex-1 bg-gray-900 px-3 py-2 rounded text-sm text-green-400 font-mono">
{cpmpInternalIP}
</code>
<button
type="button"
onClick={() => copyToClipboard(cpmpInternalIP, 'proxy-ip')}
className="px-3 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded text-sm flex items-center gap-1"
>
{copiedField === 'proxy-ip' ? <Check size={14} /> : <Copy size={14} />}
{copiedField === 'proxy-ip' ? 'Copied!' : 'Copy'}
</button>
</div>
</>
)}
{/* Home Assistant Helper */}
{formData.application === 'homeassistant' && cpmpInternalIP && (
<>
<p className="text-xs text-gray-300">
Add this to your <strong>configuration.yaml</strong> under <code>http:</code>
</p>
<div className="relative">
<pre className="bg-gray-900 px-3 py-2 rounded text-sm text-green-400 font-mono overflow-x-auto">
{`http:
use_x_forwarded_for: true
trusted_proxies:
- ${cpmpInternalIP}`}
</pre>
<button
type="button"
onClick={() => copyToClipboard(`http:\n use_x_forwarded_for: true\n trusted_proxies:\n - ${cpmpInternalIP}`, 'ha-yaml')}
className="absolute top-2 right-2 px-2 py-1 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs flex items-center gap-1"
>
{copiedField === 'ha-yaml' ? <Check size={12} /> : <Copy size={12} />}
{copiedField === 'ha-yaml' ? 'Copied!' : 'Copy'}
</button>
</div>
</>
)}
{/* Nextcloud Helper */}
{formData.application === 'nextcloud' && cpmpInternalIP && (
<>
<p className="text-xs text-gray-300">
Add this to your <strong>config/config.php</strong>
</p>
<div className="relative">
<pre className="bg-gray-900 px-3 py-2 rounded text-sm text-green-400 font-mono overflow-x-auto">
{`'trusted_proxies' => ['${cpmpInternalIP}'],
'overwriteprotocol' => 'https',`}
</pre>
<button
type="button"
onClick={() => copyToClipboard(`'trusted_proxies' => ['${cpmpInternalIP}'],\n'overwriteprotocol' => 'https',`, 'nc-php')}
className="absolute top-2 right-2 px-2 py-1 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs flex items-center gap-1"
>
{copiedField === 'nc-php' ? <Check size={12} /> : <Copy size={12} />}
{copiedField === 'nc-php' ? 'Copied!' : 'Copy'}
</button>
</div>
</>
)}
{/* Vaultwarden Helper */}
{formData.application === 'vaultwarden' && (
<p className="text-xs text-gray-300">
WebSocket support is enabled automatically for live sync. Ensure your Bitwarden clients use this domain: <code className="text-green-400">{formData.domain_names.split(',')[0]?.trim()}</code>
</p>
)}
</div>
</div>
</div>
)}
{/* SSL & Security Options */}
<div className="space-y-3">
<label className="flex items-center gap-3">