mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
- Updated `better-auth` to version 1.2.6 in multiple package.json files. - Updated `@better-auth/utils` to version 0.2.4 in server package.json. - Added optional `issuer` field to the 2FA form for enhanced user experience. - Removed unnecessary console log from the profile form component.
349 lines
9.1 KiB
TypeScript
349 lines
9.1 KiB
TypeScript
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
InputOTP,
|
|
InputOTPGroup,
|
|
InputOTPSlot,
|
|
} from "@/components/ui/input-otp";
|
|
import { authClient } from "@/lib/auth-client";
|
|
import { api } from "@/utils/api";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { Fingerprint, QrCode } from "lucide-react";
|
|
import QRCode from "qrcode";
|
|
import { useEffect, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { toast } from "sonner";
|
|
import { z } from "zod";
|
|
|
|
const PasswordSchema = z.object({
|
|
password: z.string().min(8, {
|
|
message: "Password is required",
|
|
}),
|
|
issuer: z.string().optional(),
|
|
});
|
|
|
|
const PinSchema = z.object({
|
|
pin: z.string().min(6, {
|
|
message: "Pin is required",
|
|
}),
|
|
});
|
|
|
|
type TwoFactorSetupData = {
|
|
qrCodeUrl: string;
|
|
secret: string;
|
|
totpURI: string;
|
|
};
|
|
|
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
|
type PinForm = z.infer<typeof PinSchema>;
|
|
|
|
export const Enable2FA = () => {
|
|
const utils = api.useUtils();
|
|
const [data, setData] = useState<TwoFactorSetupData | null>(null);
|
|
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [step, setStep] = useState<"password" | "verify">("password");
|
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
|
|
|
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
|
setIsPasswordLoading(true);
|
|
try {
|
|
const { data: enableData, error } = await authClient.twoFactor.enable({
|
|
password: formData.password,
|
|
issuer: formData.issuer,
|
|
});
|
|
|
|
if (!enableData) {
|
|
throw new Error(error?.message || "Error enabling 2FA");
|
|
}
|
|
|
|
if (enableData.backupCodes) {
|
|
setBackupCodes(enableData.backupCodes);
|
|
}
|
|
|
|
if (enableData.totpURI) {
|
|
const qrCodeUrl = await QRCode.toDataURL(enableData.totpURI);
|
|
|
|
setData({
|
|
qrCodeUrl,
|
|
secret: enableData.totpURI.split("secret=")[1]?.split("&")[0] || "",
|
|
totpURI: enableData.totpURI,
|
|
});
|
|
|
|
setStep("verify");
|
|
toast.success("Scan the QR code with your authenticator app");
|
|
} else {
|
|
throw new Error("No TOTP URI received from server");
|
|
}
|
|
} catch (error) {
|
|
toast.error(
|
|
error instanceof Error ? error.message : "Error setting up 2FA",
|
|
);
|
|
passwordForm.setError("password", {
|
|
message:
|
|
error instanceof Error ? error.message : "Error setting up 2FA",
|
|
});
|
|
} finally {
|
|
setIsPasswordLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleVerifySubmit = async (formData: PinForm) => {
|
|
try {
|
|
const result = await authClient.twoFactor.verifyTotp({
|
|
code: formData.pin,
|
|
});
|
|
|
|
if (result.error) {
|
|
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
|
|
pinForm.setError("pin", {
|
|
message: "Invalid code. Please try again.",
|
|
});
|
|
toast.error("Invalid verification code");
|
|
return;
|
|
}
|
|
|
|
throw result.error;
|
|
}
|
|
|
|
if (!result.data) {
|
|
throw new Error("No response received from server");
|
|
}
|
|
|
|
toast.success("2FA configured successfully");
|
|
utils.user.get.invalidate();
|
|
setIsDialogOpen(false);
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
const errorMessage =
|
|
error.message === "Failed to fetch"
|
|
? "Connection error. Please check your internet connection."
|
|
: error.message;
|
|
|
|
pinForm.setError("pin", {
|
|
message: errorMessage,
|
|
});
|
|
toast.error(errorMessage);
|
|
} else {
|
|
pinForm.setError("pin", {
|
|
message: "Error verifying code",
|
|
});
|
|
toast.error("Error verifying 2FA code");
|
|
}
|
|
}
|
|
};
|
|
|
|
const passwordForm = useForm<PasswordForm>({
|
|
resolver: zodResolver(PasswordSchema),
|
|
defaultValues: {
|
|
password: "",
|
|
},
|
|
});
|
|
|
|
const pinForm = useForm<PinForm>({
|
|
resolver: zodResolver(PinSchema),
|
|
defaultValues: {
|
|
pin: "",
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!isDialogOpen) {
|
|
setStep("password");
|
|
setData(null);
|
|
setBackupCodes([]);
|
|
passwordForm.reset();
|
|
pinForm.reset();
|
|
}
|
|
}, [isDialogOpen, passwordForm, pinForm]);
|
|
|
|
return (
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="ghost">
|
|
<Fingerprint className="size-4 text-muted-foreground" />
|
|
Enable 2FA
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-xl">
|
|
<DialogHeader>
|
|
<DialogTitle>2FA Setup</DialogTitle>
|
|
<DialogDescription>
|
|
{step === "password"
|
|
? "Enter your password to begin 2FA setup"
|
|
: "Scan the QR code and verify with your authenticator app"}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{step === "password" ? (
|
|
<Form {...passwordForm}>
|
|
<form
|
|
id="password-form"
|
|
onSubmit={passwordForm.handleSubmit(handlePasswordSubmit)}
|
|
className="space-y-4"
|
|
>
|
|
<FormField
|
|
control={passwordForm.control}
|
|
name="password"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Password</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="password"
|
|
placeholder="Enter your password"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Enter your password to enable 2FA
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={passwordForm.control}
|
|
name="issuer"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Issuer</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="text"
|
|
placeholder="Enter your issuer"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Enter your password to enable 2FA
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
className="w-full"
|
|
isLoading={isPasswordLoading}
|
|
>
|
|
Continue
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
) : (
|
|
<Form {...pinForm}>
|
|
<form
|
|
id="pin-form"
|
|
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
|
|
className="space-y-6"
|
|
>
|
|
<div className="flex flex-col gap-6 justify-center items-center">
|
|
{data?.qrCodeUrl ? (
|
|
<>
|
|
<div className="flex flex-col items-center gap-4 p-6 border rounded-lg">
|
|
<QrCode className="size-5 text-muted-foreground" />
|
|
<span className="text-sm font-medium">
|
|
Scan this QR code with your authenticator app
|
|
</span>
|
|
<img
|
|
src={data.qrCodeUrl}
|
|
alt="2FA QR Code"
|
|
className="rounded-lg w-48 h-48"
|
|
/>
|
|
<div className="flex flex-col gap-2 text-center">
|
|
<span className="text-sm text-muted-foreground">
|
|
Can't scan the QR code?
|
|
</span>
|
|
<span className="text-xs font-mono bg-muted p-2 rounded">
|
|
{data.secret}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{backupCodes && backupCodes.length > 0 && (
|
|
<div className="w-full space-y-3 border rounded-lg p-4">
|
|
<h4 className="font-medium">Backup Codes</h4>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{backupCodes.map((code, index) => (
|
|
<code
|
|
key={index}
|
|
className="bg-muted p-2 rounded text-sm font-mono"
|
|
>
|
|
{code}
|
|
</code>
|
|
))}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
Save these backup codes in a secure place. You can use
|
|
them to access your account if you lose access to your
|
|
authenticator device.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="flex items-center justify-center w-full h-48 bg-muted rounded-lg">
|
|
<QrCode className="size-8 text-muted-foreground animate-pulse" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<FormField
|
|
control={pinForm.control}
|
|
name="pin"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-col justify-center items-center">
|
|
<FormLabel>Verification Code</FormLabel>
|
|
<FormControl>
|
|
<InputOTP maxLength={6} {...field}>
|
|
<InputOTPGroup>
|
|
<InputOTPSlot index={0} />
|
|
<InputOTPSlot index={1} />
|
|
<InputOTPSlot index={2} />
|
|
<InputOTPSlot index={3} />
|
|
<InputOTPSlot index={4} />
|
|
<InputOTPSlot index={5} />
|
|
</InputOTPGroup>
|
|
</InputOTP>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Enter the 6-digit code from your authenticator app
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-full"
|
|
isLoading={isPasswordLoading}
|
|
>
|
|
Enable 2FA
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|