import type { ClientCertificate } from '@yaakapp-internal/models'; import { patchModel, settingsAtom } from '@yaakapp-internal/models'; import { useAtomValue } from 'jotai'; import { useRef } from 'react'; import { showConfirmDelete } from '../../lib/confirm'; import { Button } from '../core/Button'; import { Checkbox } from '../core/Checkbox'; import { DetailsBanner } from '../core/DetailsBanner'; import { Heading } from '../core/Heading'; import { IconButton } from '../core/IconButton'; import { InlineCode } from '../core/InlineCode'; import { PlainInput } from '../core/PlainInput'; import { Separator } from '../core/Separator'; import { HStack, VStack } from '../core/Stacks'; import { SelectFile } from '../SelectFile'; function createEmptyCertificate(): ClientCertificate { return { host: '', port: null, crtFile: null, keyFile: null, pfxFile: null, passphrase: null, enabled: true, }; } interface CertificateEditorProps { certificate: ClientCertificate; index: number; onUpdate: (index: number, cert: ClientCertificate) => void; onRemove: (index: number) => void; } function CertificateEditor({ certificate, index, onUpdate, onRemove }: CertificateEditorProps) { const updateField = ( field: K, value: ClientCertificate[K], ) => { onUpdate(index, { ...certificate, [field]: value }); }; const hasPfx = Boolean(certificate.pfxFile && certificate.pfxFile.length > 0); const hasCrtKey = Boolean( (certificate.crtFile && certificate.crtFile.length > 0) || (certificate.keyFile && certificate.keyFile.length > 0), ); // Determine certificate type for display const certType = hasPfx ? 'PFX' : hasCrtKey ? 'CERT' : null; const defaultOpen = useRef(!certificate.host); return ( updateField('enabled', enabled)} /> {certificate.host ? ( {certificate.host || <> } {certificate.port != null && `:${certificate.port}`} ) : ( Configure Certificate )} {certType && {certType}} onRemove(index)} /> } > https:// } validate={(value) => { if (!value) return false; if (!/^[a-zA-Z0-9_.-]+$/.test(value)) return false; return true; }} label="Host" placeholder="example.com" size="sm" required defaultValue={certificate.host} onChange={(host) => updateField('host', host)} /> { if (!value) return true; if (Number.isNaN(parseInt(value, 10))) return false; return true; }} placeholder="443" leftSlot={
:
} size="sm" className="w-24" defaultValue={certificate.port?.toString() ?? ''} onChange={(port) => updateField('port', port ? parseInt(port, 10) : null)} />
updateField('crtFile', filePath)} /> updateField('keyFile', filePath)} /> updateField('pfxFile', filePath)} /> updateField('passphrase', passphrase || null)} />
); } export function SettingsCertificates() { const settings = useAtomValue(settingsAtom); const certificates = settings.clientCertificates ?? []; const updateCertificates = async (newCertificates: ClientCertificate[]) => { await patchModel(settings, { clientCertificates: newCertificates }); }; const handleAdd = async () => { const newCert = createEmptyCertificate(); await updateCertificates([...certificates, newCert]); }; const handleUpdate = async (index: number, cert: ClientCertificate) => { const newCertificates = [...certificates]; newCertificates[index] = cert; await updateCertificates(newCertificates); }; const handleRemove = async (index: number) => { const cert = certificates[index]; if (cert == null) return; const host = cert.host || 'this certificate'; const port = cert.port != null ? `:${cert.port}` : ''; const confirmed = await showConfirmDelete({ id: 'confirm-remove-certificate', title: 'Delete Certificate', description: ( <> Permanently delete certificate for{' '} {host} {port} ? ), }); if (!confirmed) return; const newCertificates = certificates.filter((_, i) => i !== index); await updateCertificates(newCertificates); }; return (
Client Certificates

Add and manage TLS certificates on a per domain basis

{certificates.length > 0 && ( {certificates.map((cert, index) => ( ))} )}
); }