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 ( - diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 65ccff0e3..1141397f9 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -104,6 +104,7 @@ export const ProfileForm = () => { .then(async () => { await refetch(); toast.success("Profile Updated"); + form.reset(); }) .catch(() => { toast.error("Error to Update the profile"); diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index 1a7da0eaf..59d8d8c7b 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -18,7 +18,7 @@ import { Logo } from "../shared/logo"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { buttonVariants } from "../ui/button"; -const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 15; +const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 5; export const Navbar = () => { const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); diff --git a/apps/dokploy/server/api/routers/auth.ts b/apps/dokploy/server/api/routers/auth.ts index ef9db4da2..a1345cca3 100644 --- a/apps/dokploy/server/api/routers/auth.ts +++ b/apps/dokploy/server/api/routers/auth.ts @@ -188,9 +188,9 @@ export const authRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const currentAuth = await findAuthByEmail(ctx.user.email); - if (input.password) { + if (input.currentPassword || input.password) { const correctPassword = bcrypt.compareSync( - input.password, + input.currentPassword || "", currentAuth?.password || "", ); if (!correctPassword) { @@ -268,7 +268,9 @@ export const authRouter = createTRPCRouter({ return auth; }), - + verifyToken: protectedProcedure.mutation(async () => { + return true; + }), one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => { const auth = await findAuthById(input.id); return auth; diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index e30cee4a9..1e5b58b5d 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -269,11 +269,11 @@ export const settingsRouter = createTRPCRouter({ message: "You are not authorized to access this admin", }); } - await updateAdmin(ctx.user.authId, { + const adminUpdated = await updateAdmin(ctx.user.authId, { enableDockerCleanup: input.enableDockerCleanup, }); - if (admin.enableDockerCleanup) { + if (adminUpdated?.enableDockerCleanup) { scheduleJob("docker-cleanup", "0 0 * * *", async () => { console.log( `Docker Cleanup ${new Date().toLocaleString()}] Running...`, diff --git a/apps/dokploy/templates/plausible/docker-compose.yml b/apps/dokploy/templates/plausible/docker-compose.yml index 62ce5ece4..bb267f65c 100644 --- a/apps/dokploy/templates/plausible/docker-compose.yml +++ b/apps/dokploy/templates/plausible/docker-compose.yml @@ -26,7 +26,7 @@ services: hard: 262144 plausible: - image: ghcr.io/plausible/community-edition:v2.1.0 + image: ghcr.io/plausible/community-edition:v2.1.4 restart: always command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run" depends_on: diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index f0d926d8f..917184c5d 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -34,7 +34,7 @@ export const templates: TemplateData[] = [ { id: "plausible", name: "Plausible", - version: "v2.1.0", + version: "v2.1.4", description: "Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.", logo: "plausible.svg", diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index b16192538..797feb383 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -11,6 +11,8 @@ import { runMariadbBackup } from "./mariadb"; import { runMongoBackup } from "./mongo"; import { runMySqlBackup } from "./mysql"; import { runPostgresBackup } from "./postgres"; +import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"; +import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; export const initCronJobs = async () => { console.log("Setting up cron jobs...."); @@ -25,14 +27,15 @@ export const initCronJobs = async () => { await cleanUpUnusedImages(); await cleanUpDockerBuilder(); await cleanUpSystemPrune(); + await sendDockerCleanupNotifications(admin.adminId); }); } const servers = await getAllServers(); for (const server of servers) { - const { appName, serverId } = server; - if (serverId) { + const { appName, serverId, enableDockerCleanup } = server; + if (enableDockerCleanup) { scheduleJob(serverId, "0 0 * * *", async () => { console.log( `SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${appName}`, @@ -40,12 +43,17 @@ export const initCronJobs = async () => { await cleanUpUnusedImages(serverId); await cleanUpDockerBuilder(serverId); await cleanUpSystemPrune(serverId); + await sendDockerCleanupNotifications( + admin.adminId, + `Docker cleanup for Server ${appName}`, + ); }); } } const pgs = await db.query.postgres.findMany({ with: { + project: true, backups: { with: { destination: true, @@ -61,18 +69,39 @@ export const initCronJobs = async () => { for (const backup of pg.backups) { const { schedule, backupId, enabled } = backup; if (enabled) { - scheduleJob(backupId, schedule, async () => { - console.log( - `PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, - ); - runPostgresBackup(pg, backup); - }); + try { + scheduleJob(backupId, schedule, async () => { + console.log( + `PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, + ); + runPostgresBackup(pg, backup); + }); + + await sendDatabaseBackupNotifications({ + applicationName: pg.name, + projectName: pg.project.name, + databaseType: "postgres", + type: "success", + adminId: pg.project.adminId, + }); + } catch (error) { + await sendDatabaseBackupNotifications({ + applicationName: pg.name, + projectName: pg.project.name, + databaseType: "postgres", + type: "error", + // @ts-ignore + errorMessage: error?.message || "Error message not provided", + adminId: pg.project.adminId, + }); + } } } } const mariadbs = await db.query.mariadb.findMany({ with: { + project: true, backups: { with: { destination: true, @@ -89,18 +118,38 @@ export const initCronJobs = async () => { for (const backup of maria.backups) { const { schedule, backupId, enabled } = backup; if (enabled) { - scheduleJob(backupId, schedule, async () => { - console.log( - `MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, - ); - await runMariadbBackup(maria, backup); - }); + try { + scheduleJob(backupId, schedule, async () => { + console.log( + `MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, + ); + await runMariadbBackup(maria, backup); + }); + await sendDatabaseBackupNotifications({ + applicationName: maria.name, + projectName: maria.project.name, + databaseType: "mariadb", + type: "success", + adminId: maria.project.adminId, + }); + } catch (error) { + await sendDatabaseBackupNotifications({ + applicationName: maria.name, + projectName: maria.project.name, + databaseType: "mariadb", + type: "error", + // @ts-ignore + errorMessage: error?.message || "Error message not provided", + adminId: maria.project.adminId, + }); + } } } } const mongodbs = await db.query.mongo.findMany({ with: { + project: true, backups: { with: { destination: true, @@ -117,18 +166,38 @@ export const initCronJobs = async () => { for (const backup of mongo.backups) { const { schedule, backupId, enabled } = backup; if (enabled) { - scheduleJob(backupId, schedule, async () => { - console.log( - `MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, - ); - await runMongoBackup(mongo, backup); - }); + try { + scheduleJob(backupId, schedule, async () => { + console.log( + `MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, + ); + await runMongoBackup(mongo, backup); + }); + await sendDatabaseBackupNotifications({ + applicationName: mongo.name, + projectName: mongo.project.name, + databaseType: "mongodb", + type: "success", + adminId: mongo.project.adminId, + }); + } catch (error) { + await sendDatabaseBackupNotifications({ + applicationName: mongo.name, + projectName: mongo.project.name, + databaseType: "mongodb", + type: "error", + // @ts-ignore + errorMessage: error?.message || "Error message not provided", + adminId: mongo.project.adminId, + }); + } } } } const mysqls = await db.query.mysql.findMany({ with: { + project: true, backups: { with: { destination: true, @@ -145,12 +214,31 @@ export const initCronJobs = async () => { for (const backup of mysql.backups) { const { schedule, backupId, enabled } = backup; if (enabled) { - scheduleJob(backupId, schedule, async () => { - console.log( - `MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, - ); - await runMySqlBackup(mysql, backup); - }); + try { + scheduleJob(backupId, schedule, async () => { + console.log( + `MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, + ); + await runMySqlBackup(mysql, backup); + }); + await sendDatabaseBackupNotifications({ + applicationName: mysql.name, + projectName: mysql.project.name, + databaseType: "mysql", + type: "success", + adminId: mysql.project.adminId, + }); + } catch (error) { + await sendDatabaseBackupNotifications({ + applicationName: mysql.name, + projectName: mysql.project.name, + databaseType: "mysql", + type: "error", + // @ts-ignore + errorMessage: error?.message || "Error message not provided", + adminId: mysql.project.adminId, + }); + } } } }