mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 13:51:36 +00:00
fix: resolve frontend-to-backend mapping gaps across API types, config fields, and issuer IDs
Full audit of all ~100 backend API endpoints against frontend client functions and TypeScript interfaces. Fixes field name mismatches, missing client functions, phantom interface fields, type coercion for Go bool/int config fields, and issuer type ID alignment with backend domain constants. Backend: - issuer.go/target.go: GUI-created entities default enabled=true (Go bool zero value was overriding DB DEFAULT) Frontend types (types.ts): - Certificate: fingerprint→fingerprint_sha256, phantom fields made optional - CertificateVersion: fingerprint→fingerprint_sha256, chain_pem→pem_chain, removed phantom version/cert_pem fields - Job: error_message→last_error (matches Go json tag) Frontend client (client.ts): - Added getNotification(id) and getAuditEvent(id) for existing backend routes Frontend pages: - CertificateDetailPage: derives serial/fingerprint/issuedAt from latest CertificateVersion instead of empty Certificate fields - JobsPage/JobDetailPage: error_message→last_error - TargetsPage: reload_cmd→reload_command, validate_cmd→validate_command, added missing config fields per backend structs (validate_command for NGINX/Apache, hostname/winrm_timeout for IIS, private_key/passphrase/ cert_mode/key_mode for SSH, winrm_https/winrm_insecure for WinCertStore, create_keystore for JavaKeystore, mode for Dovecot), type coercion via buildConfigPayload() with BOOL_FIELDS/INT_FIELDS sets, IIS WinRM nesting - TargetDetailPage: added passphrase to sensitiveKeys redaction - issuerTypes.ts: type IDs aligned to backend constants (acme→ACME, local→GenericCA, stepca→StepCA, openssl→OpenSSL), backward compat aliases preserved, step-ca config fields updated to match backend struct Utilities (utils.ts): - formatDate/formatDateTime accept string|undefined|null Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,9 @@ export const getNotifications = (params: Record<string, string> = {}) => {
|
||||
return fetchJSON<PaginatedResponse<Notification>>(`${BASE}/notifications?${qs}`);
|
||||
};
|
||||
|
||||
export const getNotification = (id: string) =>
|
||||
fetchJSON<Notification>(`${BASE}/notifications/${id}`);
|
||||
|
||||
export const markNotificationRead = (id: string) =>
|
||||
fetchJSON<{ message: string }>(`${BASE}/notifications/${id}/read`, { method: 'POST' });
|
||||
|
||||
@@ -178,6 +181,9 @@ export const getAuditEvents = (params: Record<string, string> = {}) => {
|
||||
return fetchJSON<PaginatedResponse<AuditEvent>>(`${BASE}/audit?${qs}`);
|
||||
};
|
||||
|
||||
export const getAuditEvent = (id: string) =>
|
||||
fetchJSON<AuditEvent>(`${BASE}/audit/${id}`);
|
||||
|
||||
// Policies
|
||||
export const getPolicies = (params: Record<string, string> = {}) => {
|
||||
const qs = new URLSearchParams({ page: '1', per_page: '50', ...params }).toString();
|
||||
|
||||
+8
-10
@@ -10,11 +10,11 @@ export interface Certificate {
|
||||
team_id: string;
|
||||
renewal_policy_id: string;
|
||||
certificate_profile_id: string;
|
||||
serial_number: string;
|
||||
fingerprint: string;
|
||||
key_algorithm: string;
|
||||
key_size: number;
|
||||
issued_at: string;
|
||||
serial_number?: string;
|
||||
fingerprint_sha256?: string;
|
||||
key_algorithm?: string;
|
||||
key_size?: number;
|
||||
issued_at?: string;
|
||||
expires_at: string;
|
||||
revoked_at?: string;
|
||||
revocation_reason?: string;
|
||||
@@ -40,11 +40,9 @@ export const REVOCATION_REASONS = [
|
||||
export interface CertificateVersion {
|
||||
id: string;
|
||||
certificate_id: string;
|
||||
version: number;
|
||||
serial_number: string;
|
||||
fingerprint: string;
|
||||
cert_pem: string;
|
||||
chain_pem: string;
|
||||
fingerprint_sha256: string;
|
||||
pem_chain: string;
|
||||
csr_pem: string;
|
||||
not_before: string;
|
||||
not_after: string;
|
||||
@@ -80,7 +78,7 @@ export interface Job {
|
||||
status: string;
|
||||
attempts: number;
|
||||
max_attempts: number;
|
||||
error_message: string;
|
||||
last_error?: string;
|
||||
scheduled_at: string;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export function formatDate(iso: string): string {
|
||||
export function formatDate(iso: string | undefined | null): string {
|
||||
if (!iso) return '—';
|
||||
return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
export function formatDateTime(iso: string): string {
|
||||
export function formatDateTime(iso: string | undefined | null): string {
|
||||
if (!iso) return '—';
|
||||
return new Date(iso).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
@@ -29,19 +29,23 @@ export interface IssuerTypeConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical type label map. Keys match what the backend API returns.
|
||||
* DB stores: local, acme, stepca, openssl, VaultPKI, DigiCert
|
||||
* Canonical type label map. Keys MUST match backend IssuerType constants
|
||||
* defined in internal/domain/connector.go (e.g., "ACME", "GenericCA", "StepCA").
|
||||
*/
|
||||
export const typeLabels: Record<string, string> = {
|
||||
local: 'Local CA',
|
||||
GenericCA: 'Local CA',
|
||||
local: 'Local CA', // backward compat for old DB records
|
||||
local_ca: 'Local CA', // backward compat (some frontend references)
|
||||
acme: 'ACME',
|
||||
stepca: 'step-ca',
|
||||
openssl: 'OpenSSL/Custom',
|
||||
ACME: 'ACME',
|
||||
acme: 'ACME', // backward compat for old DB records
|
||||
StepCA: 'step-ca',
|
||||
stepca: 'step-ca', // backward compat for old DB records
|
||||
OpenSSL: 'OpenSSL/Custom',
|
||||
openssl: 'OpenSSL/Custom', // backward compat for old DB records
|
||||
VaultPKI: 'Vault PKI',
|
||||
DigiCert: 'DigiCert',
|
||||
Sectigo: 'Sectigo SCM',
|
||||
manual: 'Manual',
|
||||
GoogleCAS: 'Google CAS',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,7 +54,7 @@ export const typeLabels: Record<string, string> = {
|
||||
*/
|
||||
export const issuerTypes: IssuerTypeConfig[] = [
|
||||
{
|
||||
id: 'acme',
|
||||
id: 'ACME',
|
||||
name: 'ACME',
|
||||
description: "Let's Encrypt, ZeroSSL, or any ACME-compatible CA",
|
||||
icon: '\uD83D\uDD12',
|
||||
@@ -64,7 +68,7 @@ export const issuerTypes: IssuerTypeConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'local',
|
||||
id: 'GenericCA',
|
||||
name: 'Local CA',
|
||||
description: 'Self-signed or subordinate CA for internal certificates',
|
||||
icon: '\uD83C\uDFE0',
|
||||
@@ -74,14 +78,15 @@ export const issuerTypes: IssuerTypeConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'stepca',
|
||||
id: 'StepCA',
|
||||
name: 'step-ca',
|
||||
description: 'Smallstep private CA with JWK provisioner auth',
|
||||
icon: '\uD83D\uDC63',
|
||||
configFields: [
|
||||
{ key: 'ca_url', label: 'CA URL', placeholder: 'https://ca.example.com', required: true },
|
||||
{ key: 'provisioner_name', label: 'Provisioner Name', placeholder: 'my-provisioner', required: true },
|
||||
{ key: 'provisioner_key', label: 'Provisioner Key (JWK)', placeholder: '{...}', type: 'textarea', required: true, sensitive: true },
|
||||
{ key: 'provisioner_key_path', label: 'Provisioner Key Path', placeholder: '/path/to/provisioner.key', required: false, sensitive: true },
|
||||
{ key: 'provisioner_password', label: 'Provisioner Password', placeholder: 'Password for encrypted key', required: false, type: 'password', sensitive: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -110,7 +115,7 @@ export const issuerTypes: IssuerTypeConfig[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'openssl',
|
||||
id: 'OpenSSL',
|
||||
name: 'OpenSSL/Custom',
|
||||
description: 'Script-based signing with your own CA',
|
||||
icon: '\uD83D\uDD27',
|
||||
@@ -188,7 +193,10 @@ export function getIssuerCatalogStatus(
|
||||
}
|
||||
// Match both the canonical id and common aliases
|
||||
const aliases: Record<string, string[]> = {
|
||||
local: ['local', 'local_ca'],
|
||||
GenericCA: ['GenericCA', 'local', 'local_ca'],
|
||||
ACME: ['ACME', 'acme'],
|
||||
StepCA: ['StepCA', 'stepca'],
|
||||
OpenSSL: ['OpenSSL', 'openssl'],
|
||||
};
|
||||
const matchIds = aliases[t.id] || [t.id];
|
||||
const matching = configuredIssuers.filter(i => matchIds.includes(i.type));
|
||||
|
||||
@@ -60,7 +60,7 @@ function TimelineStep({ label, status, time, isLast }: { label: string; status:
|
||||
);
|
||||
}
|
||||
|
||||
function DeploymentTimeline({ certId, certStatus, createdAt, issuedAt }: { certId: string; certStatus: string; createdAt: string; issuedAt: string }) {
|
||||
function DeploymentTimeline({ certId, certStatus, createdAt, issuedAt }: { certId: string; certStatus: string; createdAt: string; issuedAt?: string }) {
|
||||
const { data: jobsData } = useQuery({
|
||||
queryKey: ['jobs', { certificate_id: certId }],
|
||||
queryFn: () => getJobs({ certificate_id: certId }),
|
||||
@@ -372,6 +372,12 @@ export default function CertificateDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Derive certificate metadata from latest version (backend doesn't include these on the cert object)
|
||||
const latestVersion = versions?.data?.[0];
|
||||
const serialNumber = cert.serial_number || latestVersion?.serial_number;
|
||||
const fingerprintSha256 = cert.fingerprint_sha256 || latestVersion?.fingerprint_sha256;
|
||||
const issuedAt = cert.issued_at || latestVersion?.not_before;
|
||||
|
||||
const days = daysUntil(cert.expires_at);
|
||||
const isRevoked = cert.status === 'Revoked';
|
||||
const isArchived = cert.status === 'Archived';
|
||||
@@ -492,7 +498,7 @@ export default function CertificateDetailPage() {
|
||||
)}
|
||||
|
||||
{/* Deployment Status Timeline */}
|
||||
<DeploymentTimeline certId={id!} certStatus={cert.status} createdAt={cert.created_at} issuedAt={cert.issued_at} />
|
||||
<DeploymentTimeline certId={id!} certStatus={cert.status} createdAt={cert.created_at} issuedAt={issuedAt} />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Certificate Info */}
|
||||
@@ -518,9 +524,9 @@ export default function CertificateDetailPage() {
|
||||
})}
|
||||
</span>
|
||||
) : '—'} />
|
||||
<InfoRow label="Serial Number" value={cert.serial_number || '—'} />
|
||||
<InfoRow label="Serial Number" value={serialNumber || '—'} />
|
||||
<InfoRow label="Fingerprint" value={
|
||||
cert.fingerprint ? <span className="font-mono text-xs">{cert.fingerprint.slice(0, 24)}...</span> : '—'
|
||||
fingerprintSha256 ? <span className="font-mono text-xs">{fingerprintSha256.slice(0, 24)}...</span> : '—'
|
||||
} />
|
||||
<InfoRow label="Key Algorithm" value={cert.key_algorithm || '—'} />
|
||||
<InfoRow label="Key Size" value={cert.key_size ? `${cert.key_size} bits` : '—'} />
|
||||
@@ -556,7 +562,7 @@ export default function CertificateDetailPage() {
|
||||
{/* Lifecycle */}
|
||||
<div className="bg-surface border border-surface-border rounded p-5 shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-ink-muted mb-4">Lifecycle</h3>
|
||||
<InfoRow label="Issued" value={formatDate(cert.issued_at)} />
|
||||
<InfoRow label="Issued" value={formatDate(issuedAt)} />
|
||||
<InfoRow label="Expires" value={
|
||||
<span className={isRevoked ? 'text-red-600 line-through' : expiryColor(days)}>
|
||||
{formatDate(cert.expires_at)} ({days <= 0 ? 'expired' : `${days} days`})
|
||||
@@ -615,7 +621,7 @@ export default function CertificateDetailPage() {
|
||||
<div key={v.id} className="flex items-center justify-between py-2 border-b border-surface-border/50 last:border-0">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-ink">Version {v.version}</span>
|
||||
<span className="text-sm text-ink">Version {versions.data.length - idx}</span>
|
||||
{idx === 0 && <span className="text-xs bg-brand-100 text-brand-700 px-1.5 py-0.5 rounded">Current</span>}
|
||||
</div>
|
||||
<div className="text-xs text-ink-faint font-mono">{v.serial_number}</div>
|
||||
|
||||
@@ -114,9 +114,9 @@ export default function JobDetailPage() {
|
||||
} />
|
||||
)}
|
||||
<InfoRow label="Attempts" value={`${job.attempts} / ${job.max_attempts}`} />
|
||||
{job.error_message && (
|
||||
{job.last_error && (
|
||||
<InfoRow label="Error" value={
|
||||
<span className="text-red-600 text-xs">{job.error_message}</span>
|
||||
<span className="text-red-600 text-xs">{job.last_error}</span>
|
||||
} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -139,9 +139,9 @@ export default function JobsPage() {
|
||||
{
|
||||
key: 'error',
|
||||
label: 'Error',
|
||||
render: (j) => j.status === 'Failed' && j.error_message ? (
|
||||
<span className="text-xs text-red-600 truncate max-w-[200px] inline-block" title={j.error_message}>
|
||||
{j.error_message.length > 80 ? j.error_message.substring(0, 80) + '...' : j.error_message}
|
||||
render: (j) => j.status === 'Failed' && j.last_error ? (
|
||||
<span className="text-xs text-red-600 truncate max-w-[200px] inline-block" title={j.last_error}>
|
||||
{j.last_error.length > 80 ? j.last_error.substring(0, 80) + '...' : j.last_error}
|
||||
</span>
|
||||
) : <span className="text-xs text-ink-faint">—</span>,
|
||||
},
|
||||
|
||||
@@ -231,7 +231,7 @@ export default function TargetDetailPage() {
|
||||
{target.config && Object.keys(target.config).length > 0 ? (
|
||||
<div className="space-y-0">
|
||||
{Object.entries(target.config).map(([key, val]) => {
|
||||
const sensitiveKeys = ['password', 'secret', 'token', 'key', 'winrm_password', 'keystore_password'];
|
||||
const sensitiveKeys = ['password', 'secret', 'token', 'key', 'passphrase', 'winrm_password', 'keystore_password'];
|
||||
const isSensitive = sensitiveKeys.some(s => key.toLowerCase().includes(s));
|
||||
const displayVal = isSensitive && val ? '********' : String(val);
|
||||
return (
|
||||
|
||||
@@ -47,18 +47,20 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ 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_cmd', label: 'Reload Command', placeholder: 'nginx -t && systemctl reload nginx' },
|
||||
{ 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_cmd', label: 'Reload Command', placeholder: 'apachectl configtest && apachectl graceful' },
|
||||
{ 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_cmd', label: 'Reload Command', placeholder: 'systemctl reload haproxy' },
|
||||
{ key: 'validate_cmd', label: 'Validate Command (optional)', placeholder: 'haproxy -c -f /etc/haproxy/haproxy.cfg' },
|
||||
{ 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 },
|
||||
@@ -87,6 +89,7 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ 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' },
|
||||
@@ -104,6 +107,7 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ 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' },
|
||||
@@ -111,12 +115,13 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ 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.winrm_host', label: 'WinRM Host (remote mode)', placeholder: 'iis-server.example.com' },
|
||||
{ key: 'winrm.winrm_port', label: 'WinRM Port', placeholder: '5985 (HTTP) or 5986 (HTTPS)' },
|
||||
{ key: 'winrm.winrm_username', label: 'WinRM Username', placeholder: 'Administrator' },
|
||||
{ key: 'winrm.winrm_password', label: 'WinRM Password', placeholder: '(sensitive)' },
|
||||
{ key: 'winrm.winrm_https', label: 'WinRM Use HTTPS', placeholder: 'true or false' },
|
||||
{ key: 'winrm.winrm_insecure', label: 'WinRM Skip TLS Verify', placeholder: 'false' },
|
||||
{ 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 },
|
||||
@@ -124,10 +129,14 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ 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)' },
|
||||
],
|
||||
@@ -141,12 +150,15 @@ const CONFIG_FIELDS: Record<string, { key: string; label: string; placeholder: s
|
||||
{ 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)' },
|
||||
],
|
||||
@@ -160,12 +172,64 @@ function CreateTargetWizard({ onClose, onSuccess }: { onClose: () => void; onSuc
|
||||
const [config, setConfig] = useState<Record<string, string>>({});
|
||||
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<string, unknown> = {};
|
||||
const result: Record<string, unknown> = {};
|
||||
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<string, unknown> = {};
|
||||
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: Object.fromEntries(Object.entries(config).filter(([, v]) => v)),
|
||||
config: buildConfigPayload(),
|
||||
}),
|
||||
onSuccess: () => onSuccess(),
|
||||
onError: (err: Error) => setError(err.message),
|
||||
|
||||
Reference in New Issue
Block a user