Files
jiang13-bbs/frontend/app/page.tsx
freefire 055688a6cf refactor: 完成前后端认证体系重构与安全增强
本提交重构了全栈的认证与安全体系,包含以下核心变更:
1. **后端安全增强**:
   - 使用常量时间比较修复CSRF校验时序漏洞
   - 重构refresh token存储为哈希+密文,支持轮转宽限期与盗用检测
   - 新增cookie前缀自动配置,支持__Host-前缀强化生产环境安全
   - 优化登出逻辑为单设备登出,合并用户信息与未读通知接口
   - 增加数据库迁移脚本,兼容旧版refresh token数据升级
   - 新增定时清理过期refresh token任务

2. **前端安全与体验优化**:
   - 新增cookie工具库,统一管理认证cookie名与转发逻辑
   - 修复开放重定向漏洞,实现安全跳转校验
   - 重构SSR请求逻辑,仅转发必要的access token cookie
   - 优化middleware轮转逻辑,修复并发请求去重与边缘部署兼容性
   - 修复登录态静默校正逻辑,兼容旧版cookie路径与名称
   - 新增环境变量示例配置文件

3. **工程化改进**:
   修复Next.js类型文件导入路径,统一前后端配置与命名规范
2026-09-12 15:23:13 +08:00

200 lines
8.3 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.
import Link from "next/link";
import type { Metadata } from "next";
import { cookies } from "next/headers";
import { PenSquare, SearchX, MessagesSquare, Check, ArrowRight } from "lucide-react";
import { fetchPosts, fetchBoards } from "@/lib/api";
import { authCookieHeader } from "@/lib/cookies";
import Pagination from "@/components/Pagination";
import PostRow from "@/components/PostRow";
import PinnedPosts from "@/components/PinnedPosts";
export const metadata: Metadata = {
title: "姜十三论坛 · 技术讨论社区",
description: "姜十三论坛,分享技术、讨论问题、交流心得的社区",
};
interface HomePageProps {
searchParams: Promise<{ q?: string; page?: string }>;
}
export default async function HomePage({ searchParams }: HomePageProps) {
const { q, page: pageStr } = await searchParams;
const keyword = q?.trim() || "";
const page = Math.max(1, parseInt(pageStr || "1", 10) || 1);
// 仅转发最小化的 access cookie 供后端填充 liked 状态(不携带 refresh 等)
const cookie = authCookieHeader(await cookies());
const [postsData, boardsData] = await Promise.all([
fetchPosts(page, 20, undefined, "latest", keyword, cookie),
fetchBoards(),
]);
// 第一页且非搜索时:管理员置顶独立成区,其余进入普通列表
const split = page === 1 && !keyword;
const pinned = split ? postsData.posts.filter((p) => p.pinned > 0) : [];
const rows = split ? postsData.posts.filter((p) => p.pinned <= 0) : postsData.posts;
return (
<div>
{/* 页头 */}
<div className="flex items-end justify-between gap-4 mb-6">
<div>
<h1 className="text-[26px] sm:text-[32px] font-extrabold tracking-tight" style={{ color: "var(--ink)" }}>
{keyword ? (
<>
搜索“<span style={{ color: "var(--accent)" }}>{keyword}</span>”
</>
) : (
"最新讨论"
)}
</h1>
<p className="meta mt-2">
{keyword
? `共找到 ${postsData.total} 篇相关帖子`
: `共 ${postsData.total} 篇帖子 · ${boardsData.boards.length} 个板块,欢迎加入讨论`}
</p>
</div>
<Link href="/compose" className="btn btn-primary hidden sm:inline-flex">
<PenSquare size={16} /> 写新帖
</Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_290px] gap-6">
{/* 主列 */}
<div>
<PinnedPosts posts={pinned} />
<div className="panel overflow-hidden">
{rows.length > 0 ? (
<>
<div className="px-4 sm:px-5 py-3.5 flex items-center justify-between" style={{ borderBottom: "1px solid var(--line)" }}>
<span className="font-bold text-[15px]" style={{ color: "var(--ink)" }}>
{keyword ? "搜索结果" : "全部讨论"}
</span>
<span className="meta text-xs">共 {postsData.total} 篇</span>
</div>
{rows.map((post) => (
<PostRow key={post.id} post={post} boardHref />
))}
</>
) : (
<div className="text-center py-20 px-6">
<span
className="inline-flex w-14 h-14 rounded-full items-center justify-center mb-4"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
>
{keyword ? <SearchX size={26} /> : <MessagesSquare size={26} />}
</span>
<p className="font-bold text-lg" style={{ color: "var(--ink)" }}>
{keyword ? "没有找到相关帖子" : "还没有帖子"}
</p>
<p className="meta mt-1.5">
{keyword ? `换个关键词试试,或者发起“${keyword}”相关的话题` : "成为第一个发起讨论的人吧"}
</p>
{keyword ? (
<div className="flex items-center justify-center gap-3 mt-6">
<Link href="/" className="btn btn-line">
清除搜索
</Link>
<Link href="/compose" className="btn btn-primary">
<PenSquare size={15} /> 发起这个话题
</Link>
</div>
) : (
<Link href="/compose" className="btn btn-primary mt-6">
<PenSquare size={15} /> 写新帖
</Link>
)}
</div>
)}
</div>
<Pagination
total={postsData.total}
page={page}
size={20}
basePath="/"
query={keyword ? { q: keyword } : undefined}
/>
</div>
{/* 侧栏 */}
<aside className="space-y-5">
{/* 发帖引导 */}
{!keyword && (
<div
className="p-5"
style={{
background: "var(--accent-soft)",
borderRadius: "var(--r)",
}}
>
<p className="font-extrabold text-[16px] mb-1" style={{ color: "var(--ink)" }}>
有什么想聊的?
</p>
<p className="text-[13px] leading-relaxed mb-4" style={{ color: "var(--ink-2)" }}>
分享你的想法、问题或经验,社区里有人正在等你的回答。
</p>
<Link
href="/compose"
className="btn btn-primary w-full justify-center"
>
<PenSquare size={15} /> 发起讨论
</Link>
</div>
)}
<div className="panel p-5">
<p className="font-bold text-[15px] mb-3.5" style={{ color: "var(--ink)" }}>板块导航</p>
<div className="space-y-1">
{boardsData.boards.map((b, i) => (
<Link
key={b.id}
href={`/board/${b.id}`}
className="group flex items-center gap-3 py-1.5 px-2 -mx-2 rounded-xl transition-colors hover:bg-[var(--panel-2)]"
>
<span
className={`av-${i % 8} w-8 h-8 rounded-xl flex items-center justify-center text-[13px] font-bold shrink-0`}
>
{b.name.slice(0, 1)}
</span>
<span
className="text-sm font-medium flex-1 truncate group-hover:text-[var(--accent)] transition-colors"
style={{ color: "var(--ink)" }}
>
{b.name}
</span>
<ArrowRight size={14} className="opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all" style={{ color: "var(--accent)" }} />
</Link>
))}
</div>
</div>
<div className="panel p-5">
<p className="font-bold text-[15px] mb-3.5" style={{ color: "var(--ink)" }}>社区守则</p>
<ul className="space-y-3 text-[13.5px] leading-relaxed" style={{ color: "var(--ink-2)" }}>
<li className="flex gap-2.5">
<span className="shrink-0 w-5 h-5 rounded-full flex items-center justify-center mt-0.5" style={{ background: "var(--accent-soft)", color: "var(--accent)" }}>
<Check size={12} strokeWidth={3} />
</span>
标题清晰,内容支持 Markdown
</li>
<li className="flex gap-2.5">
<span className="shrink-0 w-5 h-5 rounded-full flex items-center justify-center mt-0.5" style={{ background: "var(--accent-soft)", color: "var(--accent)" }}>
<Check size={12} strokeWidth={3} />
</span>
友善讨论,就事论事
</li>
<li className="flex gap-2.5">
<span className="shrink-0 w-5 h-5 rounded-full flex items-center justify-center mt-0.5" style={{ background: "var(--accent-soft)", color: "var(--accent)" }}>
<Check size={12} strokeWidth={3} />
</span>
违规内容可向管理员举报
</li>
</ul>
</div>
</aside>
</div>
</div>
);
}