Refactor Security Management: Split Security page into Users, Providers, and Policies components; remove deprecated Security component; implement CRUD functionality for users, providers, and policies; enhance Uptime page with monitor editing capabilities.

This commit is contained in:
Wikid82
2025-11-25 14:53:06 +00:00
parent 7a1f577771
commit 07be2155be
37 changed files with 4149 additions and 119 deletions

View File

@@ -1,11 +1,12 @@
import { useState, useEffect } from 'react'
import { CircleHelp, AlertCircle, Check, X, Loader2 } from 'lucide-react'
import { CircleHelp, AlertCircle, Check, X, Loader2, ShieldCheck } 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 { useAuthPolicies } from '../hooks/useSecurity'
import { parse } from 'tldts'
interface ProxyHostFormProps {
@@ -29,6 +30,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
websocket_support: host?.websocket_support ?? true,
forward_auth_enabled: host?.forward_auth_enabled ?? false,
forward_auth_bypass: host?.forward_auth_bypass || '',
auth_policy_id: host?.auth_policy_id || null,
advanced_config: host?.advanced_config || '',
enabled: host?.enabled ?? true,
certificate_id: host?.certificate_id,
@@ -37,6 +39,11 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
const { servers: remoteServers } = useRemoteServers()
const { domains, createDomain } = useDomains()
const { certificates } = useCertificates()
const { policies: authPolicies } = useAuthPolicies()
const { containers: dockerContainers, isLoading: dockerLoading, error: dockerError } = useDocker(
formData.forward_host ? undefined : undefined // Simplified for now, logic below handles it
)
const [connectionSource, setConnectionSource] = useState<'local' | 'custom' | string>('custom')
const [selectedDomain, setSelectedDomain] = useState('')
const [selectedContainerId, setSelectedContainerId] = useState<string>('')
@@ -46,8 +53,6 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
const [pendingDomain, setPendingDomain] = useState('')
const [dontAskAgain, setDontAskAgain] = useState(false)
// Test Connection State
useEffect(() => {
const stored = localStorage.getItem('cpmp_dont_ask_domain')
if (stored === 'true') {
@@ -117,24 +122,6 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
}
}
// Fetch containers based on selected source
// If 'local', host is undefined (which defaults to local socket in backend)
// If remote UUID, we need to find the server and get its host address?
// Actually, the backend ListContainers takes a 'host' query param.
// If it's a remote server, we should probably pass the UUID or the host address.
// Looking at backend/internal/services/docker_service.go, it takes a 'host' string.
// If it's a remote server, we need to pass the TCP address (e.g. tcp://1.2.3.4:2375).
const getDockerHostString = () => {
if (connectionSource === 'local') return undefined;
if (connectionSource === 'custom') return null;
const server = remoteServers.find(s => s.uuid === connectionSource);
if (!server) return null;
// Construct the Docker host string
return `tcp://${server.host}:${server.port}`;
}
const { containers: dockerContainers, isLoading: dockerLoading, error: dockerError } = useDocker(getDockerHostString())
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [nameError, setNameError] = useState<string | null>(null)
@@ -501,39 +488,78 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
</label>
</div>
{/* Forward Auth */}
{/* Access Control (SSO & Forward Auth) */}
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700 space-y-4">
<div className="flex items-center justify-between">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.forward_auth_enabled}
onChange={e => setFormData({ ...formData, forward_auth_enabled: e.target.checked })}
className="w-4 h-4 text-blue-600 bg-gray-900 border-gray-700 rounded focus:ring-blue-500"
/>
<span className="text-sm font-medium text-gray-300">Enable Forward Auth (SSO)</span>
</label>
<div title="Protects this service using your configured global authentication provider (e.g. Authelia, Authentik)." className="text-gray-500 hover:text-gray-300 cursor-help">
<CircleHelp size={14} />
</div>
<div className="flex items-center gap-3 mb-2">
<ShieldCheck className="text-blue-400" size={20} />
<h3 className="text-lg font-medium text-white">Access Control</h3>
</div>
{formData.forward_auth_enabled && (
<div>
<label htmlFor="forward-auth-bypass" className="block text-sm font-medium text-gray-300 mb-2">
Bypass Paths (Optional)
</label>
<textarea
id="forward-auth-bypass"
value={formData.forward_auth_bypass}
onChange={e => setFormData({ ...formData, forward_auth_bypass: e.target.value })}
placeholder="/api/webhook, /public/*"
rows={2}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 mt-1">
Comma-separated list of paths to exclude from authentication.
</p>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Access Policy (Built-in SSO)
</label>
<select
value={formData.auth_policy_id || ''}
onChange={e => {
const val = e.target.value ? parseInt(e.target.value) : null;
setFormData({
...formData,
auth_policy_id: val,
// If a policy is selected, disable legacy forward auth to avoid conflicts
forward_auth_enabled: val ? false : formData.forward_auth_enabled
});
}}
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"
>
<option value="">Public (No Authentication)</option>
{authPolicies.map(policy => (
<option key={policy.id} value={policy.id}>
{policy.name} {policy.description ? `(${policy.description})` : ''}
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">
Select a policy to protect this service with the built-in SSO.
</p>
</div>
{/* Legacy Forward Auth - Only show if no policy is selected */}
{!formData.auth_policy_id && (
<div className="pt-4 border-t border-gray-700">
<div className="flex items-center justify-between">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.forward_auth_enabled}
onChange={e => setFormData({ ...formData, forward_auth_enabled: e.target.checked })}
className="w-4 h-4 text-blue-600 bg-gray-900 border-gray-700 rounded focus:ring-blue-500"
/>
<span className="text-sm font-medium text-gray-300">Enable External Forward Auth</span>
</label>
<div title="Protects this service using your configured global authentication provider (e.g. Authelia, Authentik)." className="text-gray-500 hover:text-gray-300 cursor-help">
<CircleHelp size={14} />
</div>
</div>
{formData.forward_auth_enabled && (
<div className="mt-3">
<label htmlFor="forward-auth-bypass" className="block text-sm font-medium text-gray-300 mb-2">
Bypass Paths (Optional)
</label>
<textarea
id="forward-auth-bypass"
value={formData.forward_auth_bypass}
onChange={e => setFormData({ ...formData, forward_auth_bypass: e.target.value })}
placeholder="/api/webhook, /public/*"
rows={2}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 mt-1">
Comma-separated list of paths to exclude from authentication.
</p>
</div>
)}
</div>
)}
</div>