From ad7b4f1ab0b3ef2f71e9a70078716aed50cdbf64 Mon Sep 17 00:00:00 2001 From: schererleander Date: Fri, 26 Dec 2025 18:08:48 +0100 Subject: feat(auth): add two-factor authentication support --- src/app/settings/two-factor-form.tsx | 202 +++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 src/app/settings/two-factor-form.tsx (limited to 'src/app') diff --git a/src/app/settings/two-factor-form.tsx b/src/app/settings/two-factor-form.tsx new file mode 100644 index 0000000..5dccada --- /dev/null +++ b/src/app/settings/two-factor-form.tsx @@ -0,0 +1,202 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { Shield, Loader2, Copy } from "lucide-react" +import { toast } from "sonner" +import Image from "next/image" + +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" + +interface TwoFactorFormProps { + twoFactorEnabled: boolean +} + +export function TwoFactorForm({ twoFactorEnabled: initialTwoFactorEnabled }: TwoFactorFormProps) { + const router = useRouter() + const [twoFactorEnabled, setTwoFactorEnabled] = useState(initialTwoFactorEnabled) + const [is2FALoading, setIs2FALoading] = useState(false) + const [setupData, setSetupData] = useState<{ secret: string; qrCode: string } | null>(null) + const [verificationCode, setVerificationCode] = useState("") + const [isDialogOpen, setIsDialogOpen] = useState(false) + + const start2FASetup = async () => { + setIs2FALoading(true) + try { + const res = await fetch("/api/user/2fa", { method: "PUT" }) + const data = await res.json() + if (data.error) throw new Error(data.error) + setSetupData(data) + setIsDialogOpen(true) + } catch { + toast.error("Failed to start 2FA setup") + } finally { + setIs2FALoading(false) + } + } + + const verifyAndEnable2FA = async () => { + if (verificationCode.length !== 6) { + toast.error("Please enter a 6-digit code") + return + } + + setIs2FALoading(true) + try { + const res = await fetch("/api/user/2fa", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + code: verificationCode, + secret: setupData?.secret, + }), + }) + const data = await res.json() + + if (data.error) throw new Error(data.error) + + setTwoFactorEnabled(true) + setIsDialogOpen(false) + toast.success("Two-factor authentication enabled") + router.refresh() + } catch { + toast.error("Invalid verification code") + } finally { + setIs2FALoading(false) + setVerificationCode("") + } + } + + const disable2FA = async () => { + if (!confirm("Are you sure you want to disable 2FA? This will make your account less secure.")) return + + setIs2FALoading(true) + try { + const res = await fetch("/api/user/2fa", { method: "DELETE" }) + const data = await res.json() + + if (data.error) throw new Error(data.error) + + setTwoFactorEnabled(false) + toast.success("Two-factor authentication disabled") + router.refresh() + } catch { + toast.error("Failed to disable 2FA") + } finally { + setIs2FALoading(false) + } + } + + const copyToClipboard = () => { + if (setupData?.secret) { + navigator.clipboard.writeText(setupData.secret) + toast.success("Secret copied to clipboard") + } + } + + return ( + + + + + Two-Factor Authentication + + + Add an extra layer of security to your account + + + +
+
+

Status: {twoFactorEnabled ? "Enabled" : "Disabled"}

+

+ {twoFactorEnabled + ? "Your account is secured with 2FA." + : "Protect your account by enabling 2FA."} +

+
+ {twoFactorEnabled ? ( + + ) : ( + + + + + + + Set up Two-Factor Authentication + + Scan the QR code with your authenticator app (like Google Authenticator or Authy). + + + + {setupData && ( +
+
+ 2FA QR Code +
+ +
+ + {setupData.secret} + + +
+ +
+ + setVerificationCode(e.target.value.slice(0, 6))} + maxLength={6} + /> +
+ + +
+ )} +
+
+ )} +
+
+
+ ) +} -- cgit v1.3.1