Public chrome and width live only under (site). /admin and /client inherit the root layout alone, so housekeeping width is no longer tied to the public template. Co-authored-by: Cursor <[email protected]>
159 lines
3.9 KiB
TypeScript
159 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { signIn } from "next-auth/react";
|
|
import { useTranslations } from "next-intl";
|
|
import { type FormEvent, useState } from "react";
|
|
import { precheckLogin } from "@/actions/auth-precheck";
|
|
import { ContentCard } from "@/components/public/ui";
|
|
|
|
export default function LoginPage() {
|
|
const t = useTranslations("pages.login");
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
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) {
|
|
e.preventDefault();
|
|
setError(null);
|
|
setPending(true);
|
|
try {
|
|
if (!needs2fa) {
|
|
const pre = await precheckLogin(username, password);
|
|
if (pre === "invalid") {
|
|
setError(t("errorInvalidCredentials"));
|
|
return;
|
|
}
|
|
if (pre === "twofactor") {
|
|
setNeeds2fa(true);
|
|
return;
|
|
}
|
|
}
|
|
const res = await signIn("credentials", {
|
|
username,
|
|
password,
|
|
code,
|
|
redirect: false,
|
|
});
|
|
if (!res || res.error) {
|
|
setError(
|
|
needs2fa ? t("errorInvalid2fa") : t("errorInvalidCredentials"),
|
|
);
|
|
return;
|
|
}
|
|
window.location.href = "/";
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main style={{ maxWidth: 420, margin: "2rem auto" }}>
|
|
<ContentCard
|
|
icon={needs2fa ? "🔒" : "🔑"}
|
|
title={t("title")}
|
|
subtitle={needs2fa ? t("subtitle2fa") : t("subtitle")}
|
|
>
|
|
<form onSubmit={onSubmit} style={{ display: "grid", gap: "0.7rem" }}>
|
|
<input
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
placeholder={t("usernamePlaceholder")}
|
|
autoComplete="username"
|
|
disabled={needs2fa}
|
|
/>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder={t("passwordPlaceholder")}
|
|
autoComplete="current-password"
|
|
disabled={needs2fa}
|
|
/>
|
|
{needs2fa ? (
|
|
<input
|
|
value={code}
|
|
onChange={(e) => setCode(e.target.value)}
|
|
placeholder={t("codePlaceholder")}
|
|
inputMode="numeric"
|
|
autoComplete="one-time-code"
|
|
// biome-ignore lint/a11y/noAutofocus: focus the only relevant field in the 2FA step
|
|
autoFocus
|
|
/>
|
|
) : null}
|
|
<button type="submit" className="btn btn-primary" disabled={pending}>
|
|
{pending ? t("pleaseWait") : needs2fa ? t("verify") : t("signIn")}
|
|
</button>
|
|
</form>
|
|
|
|
{!needs2fa ? (
|
|
<>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.5rem",
|
|
margin: "1rem 0",
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
flex: 1,
|
|
height: 1,
|
|
background: "var(--border-color)",
|
|
}}
|
|
/>
|
|
<span className="muted">{t("or")}</span>
|
|
<span
|
|
style={{
|
|
flex: 1,
|
|
height: 1,
|
|
background: "var(--border-color)",
|
|
}}
|
|
/>
|
|
</div>
|
|
<div style={{ display: "grid", gap: "0.5rem" }}>
|
|
<button
|
|
type="button"
|
|
className="btn btn-outline"
|
|
onClick={() => signIn("discord", { callbackUrl: "/" })}
|
|
>
|
|
{t("continueWithDiscord")}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="btn btn-outline"
|
|
onClick={() => signIn("google", { callbackUrl: "/" })}
|
|
>
|
|
{t("continueWithGoogle")}
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : null}
|
|
|
|
{error ? (
|
|
<p
|
|
style={{
|
|
color: "var(--color-danger)",
|
|
textAlign: "center",
|
|
marginBottom: 0,
|
|
}}
|
|
>
|
|
{error}
|
|
</p>
|
|
) : null}
|
|
<p
|
|
className="muted"
|
|
style={{ textAlign: "center", marginBottom: 0, marginTop: "1rem" }}
|
|
>
|
|
{t("noAccount")} <Link href="/register">{t("createOne")}</Link> ·{" "}
|
|
<Link href="/forgot">{t("forgotPassword")}</Link>
|
|
</p>
|
|
</ContentCard>
|
|
</main>
|
|
);
|
|
}
|