Files
jiang13-bbs/frontend/app/login/page.tsx
freefire 4b93739468 feat: 后台增加开放消息开关
关闭后前台隐藏私聊/群聊入口并由后端拦截;同步去掉公告页重复计数胶囊。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 04:16:10 +08:00

140 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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";
import { useAllowMessages } from "@/components/MessagesPolicyProvider";
// 仅允许同源相对路径跳转,拦截 //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 allowMessages = useAllowMessages();
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 ? "、评论" : ""}{allowMessages ? "、发消息" : ""}与点赞
</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>
);
}