From 78a9fe9dc58ec2a129c0c681bde5cb1a8fe62a8b Mon Sep 17 00:00:00 2001 From: Mohammed Imran Date: Tue, 14 Oct 2025 23:31:41 +0530 Subject: [PATCH 1/5] feat: add a button to copy backup codes to clipboard --- .../dashboard/settings/profile/enable-2fa.tsx | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx index e630ec4f8..293fc62f0 100644 --- a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx @@ -1,5 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { Fingerprint, QrCode } from "lucide-react"; +import { CopyIcon, Fingerprint, QrCode } from "lucide-react"; import QRCode from "qrcode"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -29,6 +29,12 @@ import { InputOTPGroup, InputOTPSlot, } from "@/components/ui/input-otp"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { authClient } from "@/lib/auth-client"; import { api } from "@/utils/api"; @@ -281,7 +287,33 @@ export const Enable2FA = () => { {backupCodes && backupCodes.length > 0 && (
-

Backup Codes

+
+

Backup Codes

+ + + + + + +

Copy

+
+
+
+
{backupCodes.map((code, index) => ( Date: Thu, 16 Oct 2025 10:29:10 +0530 Subject: [PATCH 2/5] fix: ensure button type is correctly set when not explicitly defined --- apps/dokploy/components/ui/button.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/dokploy/components/ui/button.tsx b/apps/dokploy/components/ui/button.tsx index d2db30cd2..f504b4c2d 100644 --- a/apps/dokploy/components/ui/button.tsx +++ b/apps/dokploy/components/ui/button.tsx @@ -55,6 +55,8 @@ const Button = React.forwardRef( ref, ) => { const Comp = asChild ? Slot : "button"; + const type = props.type ?? undefined; + return ( <> ( ref={ref} {...props} disabled={isLoading || props.disabled} + type={type} > {isLoading && } {children} From 8338b27ab8d3a81ab21cbbadc246fed74ef48b65 Mon Sep 17 00:00:00 2001 From: Mohammed Imran Date: Thu, 16 Oct 2025 10:30:01 +0530 Subject: [PATCH 3/5] feat: add functionality to download refactor the copy feature to use `copy-to-clipboard`. --- .../dashboard/settings/profile/enable-2fa.tsx | 92 ++++++++++++++----- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx index 293fc62f0..37edccb64 100644 --- a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx @@ -1,5 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { CopyIcon, Fingerprint, QrCode } from "lucide-react"; +import copy from "copy-to-clipboard"; +import { CopyIcon, DownloadIcon, Fingerprint, QrCode } from "lucide-react"; import QRCode from "qrcode"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -184,6 +185,34 @@ export const Enable2FA = () => { } }; + const handleDownloadBackupCodes = () => { + if (!backupCodes || backupCodes.length === 0) { + toast.error("No backup codes to download."); + return; + } + + const backupCodesText = backupCodes.join("\n"); + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const filename = `dokploy-2fa-backup-codes-${year}${month}${day}.txt`; + const blob = new Blob([backupCodesText], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const handleCopyBackupCodes = () => { + copy(backupCodes.join("\n")); + toast.success("Backup codes copied to clipboard"); + }; + return ( @@ -289,30 +318,43 @@ export const Enable2FA = () => {

Backup Codes

- - - - - - -

Copy

-
-
-
+
+ + + + + + +

Copy

+
+
+
+ + + + + + + +

Download

+
+
+
+
{backupCodes.map((code, index) => ( From d858acbaaadabc0eb0a00265813c47ffdeeb943e Mon Sep 17 00:00:00 2001 From: Mohammed Imran Date: Thu, 16 Oct 2025 21:16:41 +0530 Subject: [PATCH 4/5] chore: add comment to ignore lint warning for img element in 2FA QR code --- .../dokploy/components/dashboard/settings/profile/enable-2fa.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx index 37edccb64..c833b3208 100644 --- a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx @@ -299,6 +299,7 @@ export const Enable2FA = () => { Scan this QR code with your authenticator app + {/** biome-ignore lint/performance/noImgElement: This is a valid use case for an img element */} 2FA QR Code Date: Fri, 17 Oct 2025 19:47:03 +0530 Subject: [PATCH 5/5] refactor: Add a basic template to backup codes copy and download --- .../dashboard/settings/profile/enable-2fa.tsx | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx index c833b3208..22da8ed44 100644 --- a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx @@ -61,6 +61,26 @@ type TwoFactorSetupData = { type PasswordForm = z.infer; type PinForm = z.infer; +const USERNAME_PLACEHOLDER = "%username%"; +const DATE_PLACEHOLDER = "%date%"; +const BACKUP_CODES_PLACEHOLDER = "%backupCodes%"; + +const backupCodeTemplate = `Dokploy - BACKUP VERIFICATION CODES + +Points to note +-------------- +# Each code can be used only once. +# Do not share these codes with anyone. + +Generated codes +--------------- +Username: ${USERNAME_PLACEHOLDER} +Generated on: ${DATE_PLACEHOLDER} + + +${BACKUP_CODES_PLACEHOLDER} +`; + export const Enable2FA = () => { const utils = api.useUtils(); const [data, setData] = useState(null); @@ -69,6 +89,7 @@ export const Enable2FA = () => { const [step, setStep] = useState<"password" | "verify">("password"); const [isPasswordLoading, setIsPasswordLoading] = useState(false); const [otpValue, setOtpValue] = useState(""); + const { data: currentUser } = api.user.get.useQuery(); const handleVerifySubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -191,12 +212,21 @@ export const Enable2FA = () => { return; } - const backupCodesText = backupCodes.join("\n"); + const backupCodesFormatted = backupCodes + .map((code, index) => ` ${index + 1}. ${code}`) + .join("\n"); + const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const filename = `dokploy-2fa-backup-codes-${year}${month}${day}.txt`; + + const backupCodesText = backupCodeTemplate + .replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown") + .replace(DATE_PLACEHOLDER, date.toLocaleString()) + .replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted); + const blob = new Blob([backupCodesText], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -209,7 +239,18 @@ export const Enable2FA = () => { }; const handleCopyBackupCodes = () => { - copy(backupCodes.join("\n")); + const date = new Date(); + + const backupCodesFormatted = backupCodes + .map((code, index) => ` ${index + 1}. ${code}`) + .join("\n"); + + const backupCodesText = backupCodeTemplate + .replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown") + .replace(DATE_PLACEHOLDER, date.toLocaleString()) + .replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted); + + copy(backupCodesText); toast.success("Backup codes copied to clipboard"); };