"use client"; import { KeyRound, Lock, UserRound } from "lucide-react"; import { signIn } from "next-auth/react"; import { useTranslations } from "next-intl"; import { type FormEvent, useEffect, useId, useRef, useState } from "react"; import { precheckLogin } from "@/actions/auth-precheck"; import { AuthAlert } from "@/components/auth/auth-alert"; import { AuthField } from "@/components/auth/auth-field"; import { type CaptchaPublicConfig, CaptchaWidget, readCaptchaToken, } from "@/components/auth/captcha-widget"; import { PasswordToggle } from "@/components/auth/password-toggle"; import Link from "@/components/link"; import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry"; export type LoginFormVariant = /** Full page: labelled fields, plus the register / forgot-password footer. */ | "page" /** Homepage sidebar: visible labels, no footer, tighter spacing. */ | "compact"; export interface LoginFormProps { captcha?: CaptchaPublicConfig; nonce?: string; variant?: LoginFormVariant; /** Where to land after a successful sign-in. */ redirectTo?: string; } /** * The single sign-in form for both `/login` and the homepage sidebar. It runs a * cheap server-side precheck first so the visitor gets a specific reason * (bad password, unverified email, 2FA required, captcha) instead of NextAuth's * generic failure, then completes the credentials sign-in. */ export function LoginForm({ captcha = { provider: "none" }, nonce, variant = "page", redirectTo = "/me", }: LoginFormProps = {}) { const t = useTranslations("pages.login"); const fieldId = useId(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); const [code, setCode] = useState(""); const [needs2fa, setNeeds2fa] = useState(false); const [error, setError] = useState(null); const [unverified, setUnverified] = useState(false); const [pending, setPending] = useState(false); const [locked, setLocked] = useState(false); const codeRef = useRef(null); // The 2FA field only exists after the pre-check asks for a code; move focus // there automatically so the visitor can keep typing without reaching for // the mouse (the lint rule rules out a bare `autoFocus`). useEffect(() => { if (needs2fa) codeRef.current?.focus(); }, [needs2fa]); const showFooter = variant === "page"; const compact = variant === "compact"; const lockCredentials = needs2fa || locked ? "opacity-50 pointer-events-none" : ""; async function onSubmit(e: FormEvent) { e.preventDefault(); if (locked) return; setError(null); setUnverified(false); setPending(true); try { if (!needs2fa) { const captchaToken = readCaptchaToken(e.currentTarget, captcha); const pre = await precheckLogin(username, password, captchaToken); if (pre === "invalid") { setError(t("errorInvalidCredentials")); return; } if (pre === "locked") { setLocked(true); setError(t("errorLocked")); return; } if (pre === "captcha") { setError(t("errorCaptcha")); return; } if (pre === "unverified") { setError(t("errorUnverified")); setUnverified(true); return; } if (pre === "twofactor") { setNeeds2fa(true); return; } } const res = await signInWithTransientRetry(() => signIn("credentials", { username, password, code, redirect: false, }), ); if (!res || res.error) { setError( needs2fa ? t("errorInvalid2fa") : t("errorInvalidCredentials"), ); return; } window.location.href = redirectTo; } catch { setError(needs2fa ? t("errorInvalid2fa") : t("errorInvalidCredentials")); } finally { setPending(false); } } return ( <> {needs2fa && (

{t("subtitle2fa")}

)}
setShowPassword((v) => !v)} labelShow={t("showPassword")} labelHide={t("hidePassword")} /> } /> {needs2fa ? ( ) : ( )} {error && {error}} {unverified && (

{t("verifyEmailCta")}

)} {showFooter && (

{t("noAccount")}{" "} {t("createOne")}

)} ); }