fix: 修复后台文章计数为0 + 分类编辑补全描述字段 + CSP放行外部图片 + 更新关于页描述
- 文章管理:计数请求补 page=1 参数,命中分页接口返回正确的 total - 分类管理:编辑模式新增描述输入框,保存时一并提交 description - CSP:img-src 加入 https: 允许加载外部图片 - 关于页:数据存储描述从 JSON 文件更正为 SQLite 数据库 - Footer:添加 ICP 备案号
This commit is contained in:
+48
-19
@@ -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() });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user