230 lines
6.4 KiB
TypeScript
230 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import { signIn } from "next-auth/react";
|
|
import { type FormEvent, useState } from "react";
|
|
import { precheckLogin } from "@/actions/auth-precheck";
|
|
import {
|
|
type CaptchaPublicConfig,
|
|
CaptchaWidget,
|
|
readCaptchaToken,
|
|
} from "@/components/auth/captcha-widget";
|
|
import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry";
|
|
|
|
export function HomeLoginForm({
|
|
captcha = { provider: "none" },
|
|
nonce,
|
|
}: {
|
|
captcha?: CaptchaPublicConfig;
|
|
nonce?: string;
|
|
} = {}) {
|
|
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<string | null>(null);
|
|
const [pending, setPending] = useState(false);
|
|
|
|
async function onSubmit(e: FormEvent<HTMLFormElement>) {
|
|
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("Invalid username or password");
|
|
return;
|
|
}
|
|
if (pre === "captcha") {
|
|
setError("Captcha verification failed. Please try again.");
|
|
return;
|
|
}
|
|
if (pre === "unverified") {
|
|
setError("Please verify your email before signing in.");
|
|
return;
|
|
}
|
|
if (pre === "twofactor") {
|
|
setNeeds2fa(true);
|
|
return;
|
|
}
|
|
}
|
|
const res = await signInWithTransientRetry(() =>
|
|
signIn("credentials", {
|
|
username,
|
|
password,
|
|
code,
|
|
redirect: false,
|
|
}),
|
|
);
|
|
if (!res || res.error) {
|
|
setError(
|
|
needs2fa ? "Invalid 2FA code" : "Invalid username or password",
|
|
);
|
|
return;
|
|
}
|
|
window.location.href = "/";
|
|
} catch {
|
|
setError(needs2fa ? "Invalid 2FA code" : "Invalid username or password");
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={onSubmit} className="relative flex flex-col gap-4">
|
|
<div className="space-y-1">
|
|
<label
|
|
htmlFor="login-username"
|
|
className="block text-xs font-bold uppercase tracking-wider"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
>
|
|
Username
|
|
</label>
|
|
<input
|
|
id="login-username"
|
|
type="text"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder="Your username"
|
|
autoComplete="username"
|
|
disabled={needs2fa}
|
|
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)",
|
|
}}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between">
|
|
<label
|
|
htmlFor="login-password"
|
|
className="block text-xs font-bold uppercase tracking-wider"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
>
|
|
Password
|
|
</label>
|
|
</div>
|
|
<div className="relative">
|
|
<input
|
|
id="login-password"
|
|
type={showPassword ? "text" : "password"}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Your password"
|
|
autoComplete="current-password"
|
|
disabled={needs2fa}
|
|
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)",
|
|
}}
|
|
required
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-sm font-bold opacity-60 hover:opacity-100 transition-opacity"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
tabIndex={-1}
|
|
>
|
|
{showPassword ? "Hide" : "Show"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{needs2fa ? (
|
|
<div className="space-y-1 animate-scale-in">
|
|
<label
|
|
htmlFor="login-2fa"
|
|
className="block text-xs font-bold uppercase tracking-wider"
|
|
style={{ color: "var(--color-text-muted)" }}
|
|
>
|
|
2FA Code
|
|
</label>
|
|
<input
|
|
id="login-2fa"
|
|
type="text"
|
|
value={code}
|
|
onChange={(e) => setCode(e.target.value)}
|
|
placeholder="Enter your 2FA code"
|
|
inputMode="numeric"
|
|
autoComplete="one-time-code"
|
|
className="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)",
|
|
}}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<CaptchaWidget captcha={captcha} nonce={nonce} />
|
|
)}
|
|
|
|
{error ? (
|
|
<p
|
|
className="animate-fade-in-up m-0 text-center text-sm font-semibold"
|
|
style={{ color: "var(--color-danger)" }}
|
|
>
|
|
{error}
|
|
</p>
|
|
) : null}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={pending}
|
|
className="btn-shine w-full cursor-pointer rounded-xl font-extrabold text-sm py-3.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg disabled:opacity-60"
|
|
style={{
|
|
background: "var(--color-primary)",
|
|
color: "var(--color-primary-foreground)",
|
|
boxShadow:
|
|
"0 4px 20px color-mix(in srgb, var(--color-primary) 30%, transparent)",
|
|
}}
|
|
>
|
|
{pending ? (
|
|
<span className="inline-flex items-center gap-2">
|
|
<svg
|
|
className="animate-spin h-4 w-4"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
role="img"
|
|
aria-label="Loading"
|
|
>
|
|
<circle
|
|
className="opacity-25"
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
strokeWidth="4"
|
|
/>
|
|
<path
|
|
className="opacity-75"
|
|
fill="currentColor"
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
/>
|
|
</svg>
|
|
Please wait...
|
|
</span>
|
|
) : needs2fa ? (
|
|
"Verify"
|
|
) : (
|
|
"Login"
|
|
)}
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|