fix: 修复后台文章计数为0 + 分类编辑补全描述字段 + CSP放行外部图片 + 更新关于页描述

- 文章管理:计数请求补 page=1 参数,命中分页接口返回正确的 total
- 分类管理:编辑模式新增描述输入框,保存时一并提交 description
- CSP:img-src 加入 https: 允许加载外部图片
- 关于页:数据存储描述从 JSON 文件更正为 SQLite 数据库
- Footer:添加 ICP 备案号
This commit is contained in:
胡旭
2026-06-24 13:51:48 +08:00
parent 3707eddfd4
commit 18e915bcbb
69 changed files with 6818 additions and 1422 deletions
+48 -19
View File
@@ -1,26 +1,57 @@
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { checkAuth, createSession, SESSION_KEY } from "@/lib/auth";
import { registerFailedAttempt, clearAttempts, isLocked } from "@/lib/rate-limit";
const ADMIN_PASSWORD = "asui2026"; // 后续可改环境变量
const SESSION_KEY = "admin_session";
const SESSION_VALUE = "authenticated";
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "asui2026";
const SESSION_MAX_AGE = 60 * 60 * 24 * 7; // 7 天
/** 取客户端 IP 作为限流 key,兼容代理转发头。 */
function clientKey(request: NextRequest): string {
const fwd = request.headers.get("x-forwarded-for");
const ip = fwd ? fwd.split(",")[0].trim() : request.headers.get("x-real-ip") || "unknown";
return `login:${ip}`;
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (body.password === ADMIN_PASSWORD) {
const cookieStore = await cookies();
cookieStore.set(SESSION_KEY, SESSION_VALUE, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 7 days
path: "/",
});
return NextResponse.json({ ok: true });
const key = clientKey(request);
const lock = isLocked(key);
if (lock.locked) {
return NextResponse.json(
{ error: `尝试次数过多,请 ${Math.ceil(lock.retryAfterSec / 60)} 分钟后再试` },
{ status: 429 }
);
}
return NextResponse.json({ error: "密码错误" }, { status: 401 });
let body: { password?: string };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "请求格式错误" }, { status: 400 });
}
if (typeof body.password !== "string" || body.password !== ADMIN_PASSWORD) {
const result = registerFailedAttempt(key);
if (result.locked) {
return NextResponse.json(
{ error: "密码错误次数过多,账户已锁定 15 分钟" },
{ status: 429 }
);
}
return NextResponse.json({ error: "密码错误" }, { status: 401 });
}
clearAttempts(key);
const token = await createSession(SESSION_MAX_AGE);
const cookieStore = await cookies();
cookieStore.set(SESSION_KEY, token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: SESSION_MAX_AGE,
path: "/",
});
return NextResponse.json({ ok: true });
}
export async function DELETE() {
@@ -30,7 +61,5 @@ export async function DELETE() {
}
export async function GET() {
const cookieStore = await cookies();
const session = cookieStore.get(SESSION_KEY);
return NextResponse.json({ authenticated: session?.value === SESSION_VALUE });
return NextResponse.json({ authenticated: await checkAuth() });
}