From 78ea60ec9352a0a099226717837baec010b79106 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Tue, 17 Dec 2024 23:10:19 -0500 Subject: [PATCH] feat(certs): show expiration and chain details --- .../certificates/show-certificates.tsx | 195 ++++++++++++++++-- 1 file changed, 179 insertions(+), 16 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx index 69b1a3323..7a7b83611 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx @@ -6,13 +6,144 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { ShieldCheck } from "lucide-react"; +import { AlertCircle, Link, ShieldCheck } from "lucide-react"; import { AddCertificate } from "./add-certificate"; import { DeleteCertificate } from "./delete-certificate"; export const ShowCertificates = () => { const { data } = api.certificates.all.useQuery(); + const extractExpirationDate = (certData: string): Date | null => { + try { + const match = certData.match( + /-----BEGIN CERTIFICATE-----\s*([^-]+)\s*-----END CERTIFICATE-----/, + ); + if (!match?.[1]) return null; + + const base64Cert = match[1].replace(/\s/g, ""); + const binaryStr = window.atob(base64Cert); + const bytes = new Uint8Array(binaryStr.length); + + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i); + } + + let dateFound = 0; + for (let i = 0; i < bytes.length - 2; i++) { + if (bytes[i] === 0x17 || bytes[i] === 0x18) { + const dateType = bytes[i]; + const dateLength = bytes[i + 1]; + if (typeof dateLength === "undefined") continue; + + if (dateFound === 0) { + dateFound++; + i += dateLength + 1; + continue; + } + + let dateStr = ""; + for (let j = 0; j < dateLength; j++) { + const charCode = bytes[i + 2 + j]; + if (typeof charCode === "undefined") continue; + dateStr += String.fromCharCode(charCode); + } + + if (dateType === 0x17) { + // UTCTime (YYMMDDhhmmssZ) + const year = Number.parseInt(dateStr.slice(0, 2)); + const fullYear = year >= 50 ? 1900 + year : 2000 + year; + return new Date( + Date.UTC( + fullYear, + Number.parseInt(dateStr.slice(2, 4)) - 1, + Number.parseInt(dateStr.slice(4, 6)), + Number.parseInt(dateStr.slice(6, 8)), + Number.parseInt(dateStr.slice(8, 10)), + Number.parseInt(dateStr.slice(10, 12)), + ), + ); + } + + // GeneralizedTime (YYYYMMDDhhmmssZ) + return new Date( + Date.UTC( + Number.parseInt(dateStr.slice(0, 4)), + Number.parseInt(dateStr.slice(4, 6)) - 1, + Number.parseInt(dateStr.slice(6, 8)), + Number.parseInt(dateStr.slice(8, 10)), + Number.parseInt(dateStr.slice(10, 12)), + Number.parseInt(dateStr.slice(12, 14)), + ), + ); + } + } + return null; + } catch (error) { + console.error("Error parsing certificate:", error); + return null; + } + }; + + const getExpirationStatus = (certData: string) => { + const expirationDate = extractExpirationDate(certData); + + if (!expirationDate) + return { + status: "unknown" as const, + className: "text-muted-foreground", + message: "Could not determine expiration", + }; + + const now = new Date(); + const daysUntilExpiration = Math.ceil( + (expirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), + ); + + if (daysUntilExpiration < 0) { + return { + status: "expired" as const, + className: "text-red-500", + message: `Expired on ${expirationDate.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + })}`, + }; + } + + if (daysUntilExpiration <= 30) { + return { + status: "warning" as const, + className: "text-yellow-500", + message: `Expires in ${daysUntilExpiration} days`, + }; + } + + return { + status: "valid" as const, + className: "text-muted-foreground", + message: `Expires ${expirationDate.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + })}`, + }; + }; + + const getCertificateChainInfo = (certData: string) => { + const certCount = (certData.match(/-----BEGIN CERTIFICATE-----/g) || []) + .length; + return certCount > 1 + ? { + isChain: true, + count: certCount, + } + : { + isChain: false, + count: 1, + }; + }; + return (
@@ -23,7 +154,7 @@ export const ShowCertificates = () => { - {data?.length === 0 ? ( + {!data?.length ? (
@@ -35,21 +166,53 @@ export const ShowCertificates = () => { ) : (
- {data?.map((destination, index) => ( -
- - {index + 1}. {destination.name} - -
- + {data.map((certificate, index) => { + const expiration = getExpirationStatus( + certificate.certificateData, + ); + const chainInfo = getCertificateChainInfo( + certificate.certificateData, + ); + return ( +
+
+
+ + {index + 1}. {certificate.name} + + {chainInfo.isChain && ( +
+ + + Chain ({chainInfo.count}) + +
+ )} +
+ +
+
+ {expiration.status !== "valid" && ( + + )} + {expiration.message} + {certificate.autoRenew && + expiration.status !== "valid" && ( + + (Auto-renewal enabled) + + )} +
-
- ))} + ); + })}