feat(ipsec): add certificate export functionality by name
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 2m41s
Docker images / frontend-image (push) Successful in 2m57s
Docker images / updater-image (push) Successful in 48s
Docker images / backend-image (push) Successful in 2m48s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 13s
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 2m41s
Docker images / frontend-image (push) Successful in 2m57s
Docker images / updater-image (push) Successful in 48s
Docker images / backend-image (push) Successful in 2m48s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 13s
- Implemented a new backend route for exporting existing client certificates in .p12 format by name. - Enhanced the frontend to support exporting certificates directly from the IPsec server grid. - Updated the IPsec page to manage certificate states and handle exports effectively. - Introduced new utility functions for certificate handling and improved data structures to accommodate the changes. - Added tests to ensure the reliability of the new certificate export feature.
This commit is contained in:
@@ -5,6 +5,7 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
DownloadIcon,
|
||||
GlobeIcon,
|
||||
KeyRoundIcon,
|
||||
NetworkIcon,
|
||||
@@ -91,9 +92,11 @@ function PeerRow({
|
||||
function CertRow({
|
||||
cert,
|
||||
onDelete,
|
||||
onExport,
|
||||
}: {
|
||||
cert: IpsecCertInfoDto
|
||||
onDelete?: (cert: IpsecCertInfoDto) => void
|
||||
onExport?: (cert: IpsecCertInfoDto) => void
|
||||
}) {
|
||||
const roleLabel = cert.role === "ca" ? "CA" : cert.role === "server" ? "сервер" : cert.role === "client" ? "клиент" : "прочий"
|
||||
return (
|
||||
@@ -107,22 +110,41 @@ function CertRow({
|
||||
{cert.trusted === false ? <Badge variant="outline" className="text-[10px]">не trusted</Badge> : null}
|
||||
</div>
|
||||
<p className="truncate font-mono text-[10px] text-muted-foreground">
|
||||
{[cert.commonName, cert.expiresAt ? `до ${cert.expiresAt}` : undefined].filter(Boolean).join(" · ") || "—"}
|
||||
{[
|
||||
cert.commonName,
|
||||
cert.signedBy ? `подписан ${cert.signedBy}` : undefined,
|
||||
cert.expiresAt ? `до ${cert.expiresAt}` : undefined,
|
||||
].filter(Boolean).join(" · ") || "—"}
|
||||
</p>
|
||||
</div>
|
||||
{onDelete ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7 shrink-0 text-destructive"
|
||||
aria-label="Удалить сертификат"
|
||||
title="Удалить сертификат"
|
||||
onClick={() => onDelete(cert)}
|
||||
>
|
||||
<Trash2Icon className="size-3.5" />
|
||||
</Button>
|
||||
) : null}
|
||||
<div className="flex shrink-0 gap-0.5">
|
||||
{onExport && cert.role === "client" ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7"
|
||||
aria-label="Скачать .p12"
|
||||
title="Скачать .p12 (сертификат + ключ)"
|
||||
onClick={() => onExport(cert)}
|
||||
>
|
||||
<DownloadIcon className="size-3.5" />
|
||||
</Button>
|
||||
) : null}
|
||||
{onDelete ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7 shrink-0 text-destructive"
|
||||
aria-label="Удалить сертификат"
|
||||
title="Удалить сертификат"
|
||||
onClick={() => onDelete(cert)}
|
||||
>
|
||||
<Trash2Icon className="size-3.5" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -134,6 +156,7 @@ export interface IpsecServerGridProps {
|
||||
onEditPeer?: (serverId: string, peer: IpsecPeerDto) => void
|
||||
onDeletePeer?: (serverId: string, peer: IpsecPeerDto) => void
|
||||
onDeleteCert?: (serverId: string, cert: IpsecCertInfoDto) => void
|
||||
onExportCert?: (serverId: string, cert: IpsecCertInfoDto) => void
|
||||
}
|
||||
|
||||
function IpsecServerGrid({
|
||||
@@ -143,6 +166,7 @@ function IpsecServerGrid({
|
||||
onEditPeer,
|
||||
onDeletePeer,
|
||||
onDeleteCert,
|
||||
onExportCert,
|
||||
}: IpsecServerGridProps) {
|
||||
return (
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
@@ -157,14 +181,21 @@ function IpsecServerGrid({
|
||||
{s.serverEndpoint ?? "endpoint не определён"}
|
||||
</p>
|
||||
</div>
|
||||
{s.peers.length > 0 ? (
|
||||
<Badge className="gap-1">
|
||||
<ShieldCheckIcon className="size-3" />
|
||||
{s.peers.length} peer
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">нет peer</Badge>
|
||||
)}
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
{s.ike2Ready ? (
|
||||
<Badge className="gap-1">
|
||||
<ShieldCheckIcon className="size-3" />
|
||||
IKEv2 готов
|
||||
</Badge>
|
||||
) : null}
|
||||
{s.peers.length > 0 ? (
|
||||
<Badge variant="outline" className="gap-1">
|
||||
{s.peers.length} peer
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">нет peer</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
@@ -198,6 +229,7 @@ function IpsecServerGrid({
|
||||
key={c.name}
|
||||
cert={c}
|
||||
onDelete={onDeleteCert ? (cert) => onDeleteCert(s.serverId, cert) : undefined}
|
||||
onExport={onExportCert ? (cert) => onExportCert(s.serverId, cert) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -151,7 +151,13 @@ function IpsecUserSheet({
|
||||
/>
|
||||
</FormField>
|
||||
{!editing ? (
|
||||
<FormField label="Аутентификация" required>
|
||||
<FormField
|
||||
label="Аутентификация"
|
||||
required
|
||||
hint={form.authMethod === "certificate"
|
||||
? "CA и серверный серт берутся из уже настроенного IKEv2 (напр. MyCA + vpn-server); новый клиент подписывается этим CA"
|
||||
: undefined}
|
||||
>
|
||||
<div className="flex gap-1.5">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user