diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx
index 69b1a3323..ccc16f3eb 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([], {
+ 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([], {
+ 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)
+
+ )}
+
-
- ))}
+ );
+ })}
diff --git a/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx
index 468db8510..4bb197b29 100644
--- a/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx
+++ b/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx
@@ -11,7 +11,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
-import { TrashIcon } from "lucide-react";
+import { Trash2 } from "lucide-react";
import React from "react";
import { toast } from "sonner";
@@ -24,8 +24,13 @@ export const DeleteNotification = ({ notificationId }: Props) => {
return (
-
diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx
index c22f7b720..10ea7304e 100644
--- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx
+++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx
@@ -40,48 +40,58 @@ export const ShowNotifications = () => {
) : (
-
- {data?.map((notification, index) => (
-
-
- {notification.notificationType === "slack" && (
-
- )}
- {notification.notificationType === "telegram" && (
-
- )}
- {notification.notificationType === "discord" && (
-
- )}
- {notification.notificationType === "email" && (
-
- )}
-
- {notification.name}
-
-
-
-
-
-
-
-
- ))}
-
-
-
+
+ {data?.map((notification, index) => (
+
+
+ {notification.notificationType === "slack" && (
+
+
+
+ )}
+ {notification.notificationType === "telegram" && (
+
+
+
+ )}
+ {notification.notificationType === "discord" && (
+
+
+
+ )}
+ {notification.notificationType === "email" && (
+
+
+
+ )}
+
+
+ {notification.name}
+
+
+ {notification.notificationType?.[0]?.toUpperCase() + notification.notificationType?.slice(1)} notification
+
+
+
+
+
+
+
+ ))}
+
+
+
)}
diff --git a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx
index 9bdf35f14..cfa2e0bab 100644
--- a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx
+++ b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx
@@ -26,7 +26,7 @@ import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
-import { Mail, PenBoxIcon } from "lucide-react";
+import { Mail, Pen } from "lucide-react";
import { useEffect, useState } from "react";
import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -218,8 +218,10 @@ export const UpdateNotification = ({ notificationId }: Props) => {
return (