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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user