关闭评论后前台隐藏评论能力;外观/设置/公告改用 AdminField 与 Stepper。 Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
4.4 KiB
TypeScript
138 lines
4.4 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { useRouter, useSearchParams } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { MessagesSquare } from "lucide-react";
|
||
import { apiLogin, apiMe, apiGetSettings } from "@/lib/api";
|
||
import { hasAuthCookieHint } from "@/lib/cookies";
|
||
import { useAllowComments } from "@/components/CommentsPolicyProvider";
|
||
|
||
// 仅允许同源相对路径跳转,拦截 //host、/\\host 与绝对 URL 形式的开放重定向
|
||
function safeRedirect(to: string | null): string {
|
||
if (!to || !to.startsWith("/") || to.startsWith("//") || to.startsWith("/\\")) {
|
||
return "/";
|
||
}
|
||
return to;
|
||
}
|
||
|
||
function RegisterHint() {
|
||
const [allow, setAllow] = useState(true);
|
||
useEffect(() => {
|
||
apiGetSettings()
|
||
.then((s) => setAllow(s.allow_register))
|
||
.catch(() => {});
|
||
}, []);
|
||
if (!allow) {
|
||
return (
|
||
<p className="meta text-center mt-5">当前站点暂不开放注册</p>
|
||
);
|
||
}
|
||
return (
|
||
<p className="meta text-center mt-5">
|
||
还没有账号?
|
||
<Link href="/register" className="ml-1 font-semibold" style={{ color: "var(--accent)" }}>
|
||
立即注册
|
||
</Link>
|
||
</p>
|
||
);
|
||
}
|
||
|
||
export default function LoginPage() {
|
||
const router = useRouter();
|
||
const searchParams = useSearchParams();
|
||
const allowComments = useAllowComments();
|
||
const redirect = safeRedirect(searchParams.get("redirect"));
|
||
const [username, setUsername] = useState("");
|
||
const [password, setPassword] = useState("");
|
||
const [error, setError] = useState("");
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
// 已登录用户访问登录页:直接回落到安全的跳转目标
|
||
useEffect(() => {
|
||
if (!hasAuthCookieHint()) return;
|
||
apiMe()
|
||
.then((res) => {
|
||
if (res.user) router.replace(redirect);
|
||
})
|
||
.catch(() => {});
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, []);
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setError("");
|
||
setLoading(true);
|
||
try {
|
||
const res = await apiLogin(username, password);
|
||
// 登录成功后后端通过 Set-Cookie 写入 HttpOnly JWT,前端无需存储 token
|
||
if (res.user) {
|
||
// root layout 在客户端导航中持久化,push 后再 refresh 让其重新取登录态
|
||
router.push(redirect);
|
||
router.refresh();
|
||
} else {
|
||
setError(res.error || "登录失败");
|
||
}
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="max-w-md mx-auto pt-8 sm:pt-14">
|
||
<div className="mb-7 text-center">
|
||
<Link
|
||
href="/"
|
||
aria-label="返回首页"
|
||
className="inline-flex w-14 h-14 rounded-2xl items-center justify-center mb-4 transition-transform duration-300 hover:rotate-[-6deg]"
|
||
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
|
||
>
|
||
<MessagesSquare size={26} />
|
||
</Link>
|
||
<h1 className="text-[28px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
|
||
欢迎回来
|
||
</h1>
|
||
<p className="text-sm mt-2" style={{ color: "var(--ink-2)" }}>
|
||
登录后发帖{allowComments ? "、评论" : ""}与点赞
|
||
</p>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="panel p-6 sm:p-7 space-y-4">
|
||
{error && <div className="alert-error" role="alert">{error}</div>}
|
||
<div>
|
||
<label className="field-label" htmlFor="login-username">用户名</label>
|
||
<input
|
||
id="login-username"
|
||
type="text"
|
||
value={username}
|
||
onChange={(e) => setUsername(e.target.value)}
|
||
required
|
||
autoFocus
|
||
autoComplete="username"
|
||
className="field"
|
||
placeholder="你的用户名"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="field-label" htmlFor="login-password">密码</label>
|
||
<input
|
||
id="login-password"
|
||
type="password"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
required
|
||
autoComplete="current-password"
|
||
className="field"
|
||
placeholder="输入密码"
|
||
/>
|
||
</div>
|
||
<button type="submit" disabled={loading} className="btn btn-primary w-full justify-center mt-2">
|
||
{loading ? "登录中..." : "登录"}
|
||
</button>
|
||
</form>
|
||
|
||
<RegisterHint />
|
||
</div>
|
||
);
|
||
}
|