"use client"; import { signIn } from "next-auth/react"; import { useTranslations } from "next-intl"; import { type FormEvent, useId, useState } from "react"; import { precheckLogin } from "@/actions/auth-precheck"; import { type CaptchaPublicConfig, CaptchaWidget, readCaptchaToken, } from "@/components/auth/captcha-widget"; 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 [pending, setPending] = useState(false); const showFooter = variant === "page"; const labelled = variant === "compact"; const lockCredentials = needs2fa ? "opacity-50 pointer-events-none" : ""; async function onSubmit(e: FormEvent) { e.preventDefault(); setError(null); 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 === "captcha") { setError(t("errorCaptcha")); return; } if (pre === "unverified") { setError(t("errorUnverified")); 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")}

)}
setUsername(e.target.value)} placeholder={t("usernamePlaceholder")} autoComplete="username" disabled={needs2fa} required className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none disabled:opacity-50" style={{ backgroundColor: "var(--color-background)", color: "var(--color-text-readable)", borderColor: needs2fa ? "color-mix(in srgb, var(--color-text-muted) 15%, transparent)" : "color-mix(in srgb, var(--color-text-muted) 25%, transparent)", }} />
setPassword(e.target.value)} placeholder={t("passwordPlaceholder")} autoComplete="current-password" disabled={needs2fa} required className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none disabled:opacity-50" style={{ backgroundColor: "var(--color-background)", color: "var(--color-text-readable)", borderColor: needs2fa ? "color-mix(in srgb, var(--color-text-muted) 15%, transparent)" : "color-mix(in srgb, var(--color-text-muted) 25%, transparent)", }} />
{needs2fa ? (
setCode(e.target.value)} placeholder={t("codePlaceholder")} inputMode="numeric" autoComplete="one-time-code" className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none" style={{ backgroundColor: "var(--color-background)", color: "var(--color-text-readable)", borderColor: "color-mix(in srgb, var(--color-primary) 30%, transparent)", }} />
) : ( )} {error && (

{error}

)} {showFooter && (

{t("noAccount")}{" "} {t("createOne")} {" "} ยท{" "} {t("forgotPassword")}

)} ); }