import { arktypeResolver } from "@hookform/resolvers/arktype"; import { type } from "arktype"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { AuthLayout } from "~/client/components/auth-layout"; import { Button } from "~/client/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form"; import { Input } from "~/client/components/ui/input"; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/client/components/ui/input-otp"; import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors"; import { ResetPasswordDialog } from "../components/reset-password-dialog"; import { useNavigate } from "@tanstack/react-router"; import { normalizeUsername } from "~/lib/username"; import { cn } from "~/client/lib/utils"; import { SsoLoginSection } from "~/client/modules/sso/components/sso-login-section"; const loginSchema = type({ username: "2<=string<=50", password: "string>=1", }); type LoginFormValues = typeof loginSchema.inferIn; type LoginPageProps = { error?: string; }; export function LoginPage({ error }: LoginPageProps = {}) { const navigate = useNavigate(); const [showResetDialog, setShowResetDialog] = useState(false); const [isLoggingIn, setIsLoggingIn] = useState(false); const [requires2FA, setRequires2FA] = useState(false); const [totpCode, setTotpCode] = useState(""); const [isVerifying2FA, setIsVerifying2FA] = useState(false); const [trustDevice, setTrustDevice] = useState(false); const errorCode = decodeLoginError(error); const errorDescription = errorCode ? getLoginErrorDescription(errorCode) : null; const form = useForm({ resolver: arktypeResolver(loginSchema), defaultValues: { username: "", password: "", }, }); const onSubmit = async (values: LoginFormValues) => { const { data, error } = await authClient.signIn.username({ username: normalizeUsername(values.username), password: values.password, fetchOptions: { onRequest: () => { setIsLoggingIn(true); }, onResponse: () => { setIsLoggingIn(false); }, }, }); if (error) { console.error(error); toast.error("Login failed", { description: error.message }); return; } if ("twoFactorRedirect" in data && data.twoFactorRedirect) { setRequires2FA(true); return; } const d = await authClient.getSession(); if (data.user && !d.data?.user.hasDownloadedResticPassword) { void navigate({ to: "/download-recovery-key" }); } else { void navigate({ to: "/volumes" }); } }; const handleVerify2FA = async () => { if (totpCode.length !== 6) { toast.error("Please enter a 6-digit code"); return; } const { data, error } = await authClient.twoFactor.verifyTotp({ code: totpCode, trustDevice, fetchOptions: { onRequest: () => { setIsVerifying2FA(true); }, onResponse: () => { setIsVerifying2FA(false); }, }, }); if (error) { console.error(error); toast.error("Verification failed", { description: error.message }); setTotpCode(""); return; } if (data) { toast.success("Login successful"); const session = await authClient.getSession(); if (session.data?.user && !session.data.user.hasDownloadedResticPassword) { void navigate({ to: "/download-recovery-key" }); } else { void navigate({ to: "/volumes" }); } } }; const handleBackToLogin = () => { setRequires2FA(false); setTotpCode(""); setTrustDevice(false); form.reset(); }; if (requires2FA) { return (
setTrustDevice(e.target.checked)} className="h-4 w-4" />
); } return (
{errorDescription}
( Username )} /> (
Password
)} />
); }