From d1246baa7841906a3f78f4106fcc54c525edc564 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Thu, 22 Oct 2026 10:51:07 +0530 Subject: [PATCH 1/3] fix(auth): let password managers fill the 2FA code on login --- apps/dokploy/pages/index.tsx | 42 ++++++++++++++---------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index ef22a378d..4e48cb682 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -5,7 +5,6 @@ import { } from "@dokploy/server"; import { validateRequest } from "@dokploy/server/lib/auth"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; -import { REGEXP_ONLY_DIGITS } from "input-otp"; import { Fingerprint } from "lucide-react"; import type { GetServerSidePropsContext } from "next"; import Link from "next/link"; @@ -38,12 +37,6 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { - InputOTP, - InputOTPGroup, - InputOTPSeparator, - InputOTPSlot, -} from "@/components/ui/input-otp"; import { Label } from "@/components/ui/label"; import { authClient } from "@/lib/auth-client"; import { appRouter } from "@/server/api/root"; @@ -183,12 +176,15 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) { toast.error("Please enter a valid 6-digit code"); return; } + await verifyTwoFactorCode(twoFactorCode); + }; + + const verifyTwoFactorCode = async (code: string) => { + if (isTwoFactorLoading) return; setIsTwoFactorLoading(true); try { - const { error } = await authClient.twoFactor.verifyTotp({ - code: twoFactorCode.replace(/\s/g, ""), - }); + const { error } = await authClient.twoFactor.verifyTotp({ code }); if (error) { toast.error(error.message); @@ -346,27 +342,21 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) { >
- + /> Enter the 6-digit code from your authenticator app From 0a1204f2ff4844a1febfa118ef8f74b7888001a9 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Thu, 22 Oct 2026 11:24:45 +0530 Subject: [PATCH 2/3] fix(auth): keep the six-box look for the 2FA code input --- apps/dokploy/pages/index.tsx | 49 +++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index 4e48cb682..bd40d52ea 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -39,6 +39,7 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { authClient } from "@/lib/auth-client"; +import { cn } from "@/lib/utils"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; import { generateServerSideHelper } from "@/utils/create-server-helpers"; @@ -342,21 +343,39 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) { >
- {/* Plain input instead of InputOTP: Bitwarden's inline suggestion can't fill InputOTP's hidden input (input-otp#152) */} - { - const code = e.target.value.replace(/\D/g, ""); - setTwoFactorCode(code); - if (code.length === 6) verifyTwoFactorCode(code); - }} - autoComplete="one-time-code" - inputMode="numeric" - maxLength={6} - autoFocus - /> + {/* Visible input so password managers can autofill it */} +
+ { + const code = e.target.value.replace(/\D/g, ""); + setTwoFactorCode(code); + if (code.length === 6) verifyTwoFactorCode(code); + }} + autoComplete="one-time-code" + inputMode="numeric" + maxLength={6} + autoFocus + className={cn( + "h-8 border-0 py-0 pr-0 pl-[calc((2rem_-_1ch)/2)] font-mono text-base tracking-[calc(2rem_-_1ch)] focus-visible:ring-0 md:text-base dark:bg-transparent", + twoFactorCode.length === 6 && "caret-transparent", + )} + /> +
+ {[0, 1, 2, 3, 4, 5].map((slot) => ( +
+ ))} +
+
Enter the 6-digit code from your authenticator app From fcfa655d2165ae04e38e62bbc5548359bb82ded2 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Thu, 22 Oct 2026 11:41:34 +0530 Subject: [PATCH 3/3] fix(auth): keep 2FA code input in sync while verifying and editing --- apps/dokploy/pages/index.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index bd40d52ea..6d5adb414 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -71,6 +71,9 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) { const [isTwoFactor, setIsTwoFactor] = useState(false); const [error, setError] = useState(null); const [twoFactorCode, setTwoFactorCode] = useState(""); + const [twoFactorCaret, setTwoFactorCaret] = useState(0); + // Clamped so a caret left over from before the code was cleared can't point past it. + const twoFactorCursor = Math.min(twoFactorCaret, twoFactorCode.length); const [isBackupCodeModalOpen, setIsBackupCodeModalOpen] = useState(false); const [backupCode, setBackupCode] = useState(""); const loginForm = useForm({ @@ -352,15 +355,20 @@ export default function Home({ IS_CLOUD, enforceSSO }: Props) { onChange={(e) => { const code = e.target.value.replace(/\D/g, ""); setTwoFactorCode(code); + setTwoFactorCaret(e.target.selectionStart ?? code.length); if (code.length === 6) verifyTwoFactorCode(code); }} + onSelect={(e) => + setTwoFactorCaret(e.currentTarget.selectionStart ?? 0) + } autoComplete="one-time-code" inputMode="numeric" maxLength={6} + readOnly={isTwoFactorLoading} autoFocus className={cn( "h-8 border-0 py-0 pr-0 pl-[calc((2rem_-_1ch)/2)] font-mono text-base tracking-[calc(2rem_-_1ch)] focus-visible:ring-0 md:text-base dark:bg-transparent", - twoFactorCode.length === 6 && "caret-transparent", + twoFactorCursor === 6 && "caret-transparent", )} />
(
))}