"use client" import { useMemo, useState } from "react" import { PageHeader } from "@/components/page-header" import { routerCertificates, servers } from "@/lib/data" import type { RouterCertificate, CertStatus } from "@/lib/data" import { Flag } from "@/components/flag" import { Card, CardContent } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { SearchIcon, ShieldCheckIcon, ShieldAlertIcon, ShieldOffIcon, BadgeCheckIcon, AlertTriangleIcon, AlertCircleIcon, CalendarIcon, KeyRoundIcon, ServerIcon, PlusIcon, ChevronDownIcon, ChevronRightIcon, } from "lucide-react" // ─── helpers ────────────────────────────────────────────────────────────────── const STATUS_CONFIG: Record = { valid: { label: "Действителен", icon: , badge: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", row: "", }, expired: { label: "Истёк", icon: , badge: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", row: "bg-red-500/5", }, revoked: { label: "Отозван", icon: , badge: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20", row: "bg-amber-500/5", }, } function daysLeftColor(days: number): string { if (days < 0) return "text-red-500" if (days <= 7) return "text-red-500" if (days <= 30) return "text-amber-500" return "text-emerald-600 dark:text-emerald-400" } function daysLeftBar(days: number, total = 365): number { if (days <= 0) return 0 return Math.min(100, Math.round((days / total) * 100)) } function serverForCert(cert: RouterCertificate) { return servers.find((s) => s.id === cert.serverId) } // ─── Certificate row ────────────────────────────────────────────────────────── function CertRow({ cert, expanded, onToggle, }: { cert: RouterCertificate expanded: boolean onToggle: () => void }) { const srv = serverForCert(cert) const cfg = STATUS_CONFIG[cert.status] const pct = daysLeftBar(cert.daysLeft) return (
{/* expand */} {/* name */}
{cfg.icon} {cert.name}

{cert.commonName}

{/* server */}
{srv ? <>{srv.name} : }
{/* issued by */}

{cert.issuedBy}

{/* days left */}
{cert.daysLeft < 0 ? `Истёк ${-cert.daysLeft}д назад` : `${cert.daysLeft}д осталось`} {cert.validUntil}
{/* usage badges */}
{cert.usage.map((u) => ( {u} ))}
{/* status */} {cfg.label}
{/* expanded detail */} {expanded && (

Key size

{cert.keySize} bit

Действителен с

{cert.validFrom}

SAN / Alt Names

{cert.sans.length > 0 ? cert.sans.map((s) => {s}) : }

Trusted

{cert.trusted ? "Да (доверенный)" : "Нет (не доверенный)"}

)}
) } // ════════════════════════════════════════════════════════════════════════════ export default function CertificatesPage() { const [search, setSearch] = useState("") const [statusFilter, setStatusFilter] = useState("all") const [expandedIds, setExpandedIds] = useState>(new Set()) const expiring = useMemo( () => routerCertificates.filter((c) => c.status === "valid" && c.daysLeft >= 0 && c.daysLeft <= 30), [], ) const expired = useMemo(() => routerCertificates.filter((c) => c.status === "expired"), []) const filtered = useMemo(() => { return routerCertificates.filter((c) => { if (statusFilter !== "all" && c.status !== statusFilter) return false if (!search) return true const q = search.toLowerCase() return ( c.name.toLowerCase().includes(q) || c.commonName.toLowerCase().includes(q) || c.issuedBy.toLowerCase().includes(q) || c.sans.some((s) => s.includes(q)) ) }) }, [search, statusFilter]) function toggleExpand(id: string) { setExpandedIds((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id); else next.add(id) return next }) } return (
Выпустить сертификат } />
{/* Alerts */} {(expiring.length > 0 || expired.length > 0) && (
{expired.length > 0 && (

{expired.length} {expired.length === 1 ? "истёкший сертификат" : "истёкших сертификата"}

{expired.map((c) => c.name).join(", ")} — требуют обновления

)} {expiring.length > 0 && (

{expiring.length} {expiring.length === 1 ? "сертификат истекает" : "сертификата истекают"} в течение 30 дней

{expiring.map((c) => `${c.name} (${c.daysLeft}д)`).join(", ")}

)}
)} {/* KPI */}
{[ { label: "Всего", value: routerCertificates.length, icon: }, { label: "Действующих", value: routerCertificates.filter((c) => c.status === "valid").length, icon: }, { label: "Истекают", value: expiring.length, icon: }, { label: "Истёкших", value: expired.length, icon: }, ].map((s) => (

{s.label}

{s.value}

{s.icon}
))}
{/* Table */}
{/* search */}
setSearch(e.target.value)} />
{/* status filter */}
{(["all", "valid", "expired", "revoked"] as const).map((s) => ( ))}
{filtered.length} сертификатов
{/* table header */}
Имя / CN Сервер Выпущен
Срок
Использование
Статус
{filtered.length === 0 ? (

Сертификаты не найдены

) : ( filtered.map((cert) => ( toggleExpand(cert.id)} /> )) )}
{/* RouterOS reference */}

RouterOS 7 · /certificate — команды управления

{[ { title: "Создать CA", lines: [ "/certificate add \\", " name=my-ca \\", " common-name=MyCA \\", " key-size=4096 \\", " days-valid=3650 \\", " key-usage=key-cert-sign,crl-sign", "/certificate sign my-ca", ], }, { title: "Выпустить сертификат", lines: [ "/certificate add \\", " name=router-cert \\", " common-name=router.example.com \\", " subject-alt-name=\\", " IP:10.0.0.1 \\", " key-size=2048 days-valid=365", "/certificate sign router-cert \\", " ca=my-ca", ], }, { title: "Статус и экспорт", lines: [ "# Список:", "/certificate print", "", "# Экспорт (PKCS12):", "/certificate export-certificate \\", " router-cert \\", " export-passphrase=secret", "", "# Импорт:", "/certificate import \\", " file-name=cert.crt", ], }, ].map((b) => (

{b.title}

                      {b.lines.join("\n")}
                    
))}
) }