import { useState } from 'react'; import { Link } from 'react-router-dom'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { getTargets, createTarget, deleteTarget } from '../api/client'; import PageHeader from '../components/PageHeader'; import DataTable from '../components/DataTable'; import type { Column } from '../components/DataTable'; import StatusBadge from '../components/StatusBadge'; import ErrorState from '../components/ErrorState'; import { formatDateTime } from '../api/utils'; import type { Target } from '../api/types'; const typeLabels: Record = { NGINX: 'NGINX', Apache: 'Apache', HAProxy: 'HAProxy', Traefik: 'Traefik', Caddy: 'Caddy', Envoy: 'Envoy', Postfix: 'Postfix', Dovecot: 'Dovecot', F5: 'F5 BIG-IP', IIS: 'IIS', SSH: 'SSH', WinCertStore: 'Windows Cert Store', JavaKeystore: 'Java Keystore', }; const TARGET_TYPES = [ { value: 'NGINX', label: 'NGINX', description: 'Deploy to NGINX web server via file write + config validation + reload' }, { value: 'Apache', label: 'Apache httpd', description: 'Separate cert/chain/key files, apachectl configtest, graceful reload' }, { value: 'HAProxy', label: 'HAProxy', description: 'Combined PEM file (cert+chain+key), optional validate, reload' }, { value: 'Traefik', label: 'Traefik', description: 'File provider deployment — writes cert/key to watched directory, auto-reload' }, { value: 'Caddy', label: 'Caddy', description: 'Admin API hot-reload or file-based deployment with configurable mode' }, { value: 'Envoy', label: 'Envoy', description: 'File-based deployment — writes cert/key to watched directory. Optional SDS file generation.' }, { value: 'Postfix', label: 'Postfix', description: 'Postfix MTA — file write + postfix reload' }, { value: 'Dovecot', label: 'Dovecot', description: 'Dovecot IMAP/POP3 — file write + doveadm reload' }, { value: 'F5', label: 'F5 BIG-IP', description: 'iControl REST — cert upload, SSL profile update via proxy agent' }, { value: 'IIS', label: 'IIS', description: 'Windows IIS via agent-local PowerShell or remote WinRM proxy agent' }, { value: 'SSH', label: 'SSH', description: 'Agentless deployment via SSH/SFTP — deploy to any Linux/Unix server without installing an agent' }, { value: 'WinCertStore', label: 'Windows Cert Store', description: 'Import certificates into Windows Certificate Store for Exchange, RDP, SQL Server, ADFS' }, { value: 'JavaKeystore', label: 'Java Keystore', description: 'Deploy to JKS/PKCS#12 keystores for Tomcat, Jetty, Kafka, Elasticsearch, and JVM services' }, ]; const CONFIG_FIELDS: Record = { NGINX: [ { key: 'cert_path', label: 'Certificate Path', placeholder: '/etc/nginx/ssl/cert.pem', required: true }, { key: 'key_path', label: 'Key Path', placeholder: '/etc/nginx/ssl/key.pem', required: true }, { key: 'chain_path', label: 'Chain Path', placeholder: '/etc/nginx/ssl/chain.pem' }, { key: 'reload_command', label: 'Reload Command', placeholder: 'nginx -s reload' }, { key: 'validate_command', label: 'Validate Command', placeholder: 'nginx -t' }, ], Apache: [ { key: 'cert_path', label: 'Certificate Path', placeholder: '/etc/apache2/ssl/cert.pem', required: true }, { key: 'key_path', label: 'Key Path', placeholder: '/etc/apache2/ssl/key.pem', required: true }, { key: 'chain_path', label: 'Chain Path', placeholder: '/etc/apache2/ssl/chain.pem' }, { key: 'reload_command', label: 'Reload Command', placeholder: 'apachectl graceful' }, { key: 'validate_command', label: 'Validate Command', placeholder: 'apachectl configtest' }, ], HAProxy: [ { key: 'pem_path', label: 'Combined PEM Path', placeholder: '/etc/haproxy/certs/combined.pem', required: true }, { key: 'reload_command', label: 'Reload Command', placeholder: 'systemctl reload haproxy' }, { key: 'validate_command', label: 'Validate Command (optional)', placeholder: 'haproxy -c -f /etc/haproxy/haproxy.cfg' }, ], Traefik: [ { key: 'cert_dir', label: 'Certificate Directory', placeholder: '/etc/traefik/certs', required: true }, { key: 'cert_file', label: 'Certificate Filename', placeholder: 'cert.pem (default)' }, { key: 'key_file', label: 'Key Filename', placeholder: 'key.pem (default)' }, ], Caddy: [ { key: 'mode', label: 'Deployment Mode', placeholder: 'api (default) or file', required: true }, { key: 'admin_api', label: 'Admin API URL', placeholder: 'http://localhost:2019 (default)' }, { key: 'cert_dir', label: 'Certificate Directory (file mode)', placeholder: '/etc/caddy/certs' }, { key: 'cert_file', label: 'Certificate Filename', placeholder: 'cert.pem (default)' }, { key: 'key_file', label: 'Key Filename', placeholder: 'key.pem (default)' }, ], Envoy: [ { key: 'cert_dir', label: 'Certificate Directory', placeholder: '/etc/envoy/certs', required: true }, { key: 'cert_filename', label: 'Certificate Filename', placeholder: 'cert.pem (default)' }, { key: 'key_filename', label: 'Key Filename', placeholder: 'key.pem (default)' }, { key: 'chain_filename', label: 'Chain Filename (optional)', placeholder: 'chain.pem (leave empty to append to cert)' }, { key: 'sds_config', label: 'Generate SDS Config', placeholder: 'true or false' }, ], Postfix: [ { key: 'cert_path', label: 'Certificate Path', placeholder: '/etc/postfix/certs/cert.pem' }, { key: 'key_path', label: 'Key Path', placeholder: '/etc/postfix/certs/key.pem' }, { key: 'chain_path', label: 'Chain Path (optional)', placeholder: '/etc/postfix/certs/chain.pem' }, { key: 'reload_command', label: 'Reload Command', placeholder: 'postfix reload' }, { key: 'validate_command', label: 'Validate Command', placeholder: 'postfix check' }, ], Dovecot: [ { key: 'mode', label: 'Mode', placeholder: 'dovecot (auto-set)' }, { key: 'cert_path', label: 'Certificate Path', placeholder: '/etc/dovecot/certs/cert.pem' }, { key: 'key_path', label: 'Key Path', placeholder: '/etc/dovecot/certs/key.pem' }, { key: 'chain_path', label: 'Chain Path (optional)', placeholder: '/etc/dovecot/certs/chain.pem' }, { key: 'reload_command', label: 'Reload Command', placeholder: 'doveadm reload' }, { key: 'validate_command', label: 'Validate Command', placeholder: 'doveconf -n' }, ], F5: [ { key: 'host', label: 'Management Host', placeholder: 'f5.internal.example.com', required: true }, { key: 'port', label: 'Management Port', placeholder: '443' }, { key: 'username', label: 'Username', placeholder: 'admin', required: true }, { key: 'password', label: 'Password', placeholder: 'F5 admin password', required: true }, { key: 'partition', label: 'Partition', placeholder: 'Common' }, { key: 'ssl_profile', label: 'SSL Profile', placeholder: 'clientssl_api', required: true }, { key: 'insecure', label: 'Skip TLS Verify', placeholder: 'true (default)' }, { key: 'timeout', label: 'Timeout (seconds)', placeholder: '30' }, ], IIS: [ { key: 'hostname', label: 'Target Hostname', placeholder: 'iis-server.example.com' }, { key: 'site_name', label: 'IIS Site Name', placeholder: 'Default Web Site', required: true }, { key: 'cert_store', label: 'Certificate Store', placeholder: 'My', required: true }, { key: 'port', label: 'HTTPS Port', placeholder: '443' }, { key: 'ip_address', label: 'Binding IP', placeholder: '*' }, { key: 'binding_info', label: 'Host Header (SNI)', placeholder: 'www.example.com' }, { key: 'sni', label: 'Enable SNI', placeholder: 'true or false' }, { key: 'mode', label: 'Deployment Mode', placeholder: 'local (default) or winrm' }, { key: 'winrm_host', label: 'WinRM Host (remote mode)', placeholder: 'iis-server.example.com' }, { key: 'winrm_port', label: 'WinRM Port', placeholder: '5985 (HTTP) or 5986 (HTTPS)' }, { key: 'winrm_username', label: 'WinRM Username', placeholder: 'Administrator' }, { key: 'winrm_password', label: 'WinRM Password', placeholder: '(sensitive)' }, { key: 'winrm_https', label: 'WinRM Use HTTPS', placeholder: 'true or false' }, { key: 'winrm_insecure', label: 'WinRM Skip TLS Verify', placeholder: 'false' }, { key: 'winrm_timeout', label: 'WinRM Timeout (seconds)', placeholder: '60' }, ], SSH: [ { key: 'host', label: 'SSH Host', placeholder: '192.168.1.100 or server.example.com', required: true }, { key: 'port', label: 'SSH Port', placeholder: '22 (default)' }, { key: 'user', label: 'SSH Username', placeholder: 'root or certctl', required: true }, { key: 'auth_method', label: 'Auth Method', placeholder: 'key (default) or password' }, { key: 'private_key_path', label: 'Private Key Path', placeholder: '/home/certctl/.ssh/id_ed25519' }, { key: 'private_key', label: 'Inline Private Key PEM', placeholder: 'Paste PEM key (alternative to path)' }, { key: 'password', label: 'SSH Password', placeholder: 'Leave empty for key auth' }, { key: 'passphrase', label: 'Key Passphrase', placeholder: 'For encrypted private keys' }, { key: 'cert_path', label: 'Remote Certificate Path', placeholder: '/etc/ssl/certs/cert.pem', required: true }, { key: 'key_path', label: 'Remote Key Path', placeholder: '/etc/ssl/private/key.pem', required: true }, { key: 'chain_path', label: 'Remote Chain Path (optional)', placeholder: '/etc/ssl/certs/chain.pem' }, { key: 'cert_mode', label: 'Cert File Permissions', placeholder: '0644 (default)' }, { key: 'key_mode', label: 'Key File Permissions', placeholder: '0600 (default)' }, { key: 'reload_command', label: 'Reload Command (optional)', placeholder: 'systemctl reload nginx' }, { key: 'timeout', label: 'Connection Timeout (seconds)', placeholder: '30 (default)' }, ], WinCertStore: [ { key: 'store_name', label: 'Certificate Store', placeholder: 'My (default)', required: true }, { key: 'store_location', label: 'Store Location', placeholder: 'LocalMachine (default) or CurrentUser' }, { key: 'friendly_name', label: 'Friendly Name (optional)', placeholder: 'My Production Cert' }, { key: 'remove_expired', label: 'Remove Expired Certs', placeholder: 'false (default)' }, { key: 'mode', label: 'Deployment Mode', placeholder: 'local (default) or winrm' }, { key: 'winrm_host', label: 'WinRM Host (remote mode)', placeholder: 'win-server.example.com' }, { key: 'winrm_port', label: 'WinRM Port', placeholder: '5985 (HTTP) or 5986 (HTTPS)' }, { key: 'winrm_username', label: 'WinRM Username', placeholder: 'Administrator' }, { key: 'winrm_password', label: 'WinRM Password', placeholder: '(sensitive)' }, { key: 'winrm_https', label: 'WinRM Use HTTPS', placeholder: 'true or false' }, { key: 'winrm_insecure', label: 'WinRM Skip TLS Verify', placeholder: 'false' }, ], JavaKeystore: [ { key: 'keystore_path', label: 'Keystore Path', placeholder: '/opt/app/conf/keystore.p12', required: true }, { key: 'keystore_password', label: 'Keystore Password', placeholder: 'changeit', required: true }, { key: 'keystore_type', label: 'Keystore Type', placeholder: 'PKCS12 (default) or JKS' }, { key: 'alias', label: 'Key Alias', placeholder: 'server (default)' }, { key: 'create_keystore', label: 'Create Keystore If Missing', placeholder: 'true (default)' }, { key: 'reload_command', label: 'Reload Command (optional)', placeholder: 'systemctl restart tomcat' }, { key: 'keytool_path', label: 'Keytool Path (optional)', placeholder: 'keytool (default, from PATH)' }, ], }; function CreateTargetWizard({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) { const [step, setStep] = useState<'type' | 'config' | 'review'>('type'); const [targetType, setTargetType] = useState(''); const [name, setName] = useState(''); const [agentId, setAgentId] = useState(''); const [config, setConfig] = useState>({}); const [error, setError] = useState(''); // Fields that backends expect as boolean (Go bool) const BOOL_FIELDS = new Set([ 'sni', 'insecure', 'sds_config', 'remove_expired', 'create_keystore', 'winrm_https', 'winrm_insecure', ]); // Fields that backends expect as integer (Go int) const INT_FIELDS = new Set([ 'port', 'timeout', 'winrm_port', 'winrm_timeout', 'timeout_seconds', ]); // Coerce string form values to their Go types const coerceValue = (key: string, val: string): unknown => { if (BOOL_FIELDS.has(key)) return val === 'true'; if (INT_FIELDS.has(key)) { const n = parseInt(val, 10); return isNaN(n) ? val : n; } return val; }; // Build config payload with type-specific transformations const buildConfigPayload = () => { const flat = Object.fromEntries(Object.entries(config).filter(([, v]) => v)); // Dovecot uses the same Postfix connector with mode="dovecot" if (targetType === 'Dovecot' && !flat['mode']) { flat['mode'] = 'dovecot'; } // IIS backend expects WinRM fields nested under "winrm" key if (targetType === 'IIS') { const iisWinrmKeys = ['winrm_host', 'winrm_port', 'winrm_username', 'winrm_password', 'winrm_https', 'winrm_insecure', 'winrm_timeout']; const winrmObj: Record = {}; const result: Record = {}; for (const [k, v] of Object.entries(flat)) { if (iisWinrmKeys.includes(k)) { winrmObj[k] = coerceValue(k, v); } else { result[k] = coerceValue(k, v); } } if (Object.keys(winrmObj).length > 0) { result['winrm'] = winrmObj; } return result; } // All other target types: coerce values to proper Go types const result: Record = {}; for (const [k, v] of Object.entries(flat)) { result[k] = coerceValue(k, v); } return result; }; const mutation = useMutation({ mutationFn: () => createTarget({ name, type: targetType, agent_id: agentId, config: buildConfigPayload(), }), onSuccess: () => onSuccess(), onError: (err: Error) => setError(err.message), }); const fields = CONFIG_FIELDS[targetType] || []; const canProceedToReview = name && targetType && fields.filter(f => f.required).every(f => config[f.key]); return (
e.stopPropagation()}> {/* Step indicators */}
{['Select Type', 'Configure', 'Review'].map((label, i) => { const stepNames = ['type', 'config', 'review'] as const; const currentIdx = stepNames.indexOf(step); const isActive = i === currentIdx; const isDone = i < currentIdx; return (
{isDone ? '✓' : i + 1}
{label} {i < 2 &&
}
); })}
{error &&
{error}
} {/* Step 1: Select Type */} {step === 'type' && (

Select Target Type

{TARGET_TYPES.map(t => ( ))}
)} {/* Step 2: Configure */} {step === 'config' && (

Configure {typeLabels[targetType] || targetType} Target

setName(e.target.value)} className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400" placeholder="web-server-1" />
setAgentId(e.target.value)} className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400" placeholder="agent-web1" />
{fields.map(f => (
setConfig(c => ({ ...c, [f.key]: e.target.value }))} className="w-full bg-white border border-surface-border rounded px-3 py-2 text-sm text-ink focus:outline-none focus:border-brand-400" placeholder={f.placeholder} />
))}
)} {/* Step 3: Review */} {step === 'review' && (

Review Target

Name {name}
Type {typeLabels[targetType] || targetType}
{agentId && (
Agent {agentId}
)} {Object.entries(config).filter(([, v]) => v).map(([k, v]) => (
{k.replace(/_/g, ' ')} {v}
))}
)}
); } export default function TargetsPage() { const queryClient = useQueryClient(); const [showCreate, setShowCreate] = useState(false); const { data, isLoading, error, refetch } = useQuery({ queryKey: ['targets'], queryFn: () => getTargets(), }); const deleteMutation = useMutation({ mutationFn: deleteTarget, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['targets'] }), }); const columns: Column[] = [ { key: 'name', label: 'Target', render: (t) => (
e.stopPropagation()}> {t.name}
{t.id}
), }, { key: 'type', label: 'Type', render: (t) => ( {typeLabels[t.type] || t.type} ), }, { key: 'agent', label: 'Agent', render: (t) => {t.agent_id || '\u2014'}, }, { key: 'enabled', label: 'Status', render: (t) => , }, { key: 'test_status', label: 'Connection', render: (t) => { if (!t.test_status || t.test_status === 'untested') return ; return ; }, }, { key: 'created', label: 'Created', render: (t) => {formatDateTime(t.created_at)}, }, { key: 'actions', label: '', render: (t) => ( ), }, ]; return ( <> setShowCreate(true)} className="btn btn-primary text-xs"> + New Target } />
{error ? ( refetch()} /> ) : ( )}
{showCreate && ( setShowCreate(false)} onSuccess={() => { setShowCreate(false); queryClient.invalidateQueries({ queryKey: ['targets'] }); }} /> )} ); }