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) => ( ))} )}
); }