refactor(P0): JWT 认证、并发安全、错误日志三项安全加固

- 新增 JWT httpOnly cookie 认证链路 (jose),登录/注册签发 token,
  所有用户和盲盒 API 改为从 cookie 提取 userId,不再信任客户端传值
- 新增 /api/auth/logout 端点清除认证 cookie
- GET /api/user 区分 owner/非 owner,非 owner 不暴露 email
- atomicUpdateRoom 新增 per-room 应用层互斥锁,防止 SQLite 下并发 lost update
- 修复 getRoomData 中 fire-and-forget delete 改为 await
- 37 个静默 catch 块跨 17 个文件添加 console.error 日志
- 新增 REFACTOR_PLAN.md 全景分析文档
This commit is contained in:
2026-03-02 17:24:26 +08:00
parent 99120a7042
commit ce76980fe5
41 changed files with 528 additions and 144 deletions
+8 -6
View File
@@ -1,23 +1,24 @@
import { NextRequest } from "next/server";
import { requireMembership } from "@/lib/blindbox";
import { requireUserId } from "@/lib/api";
import { runPlanGeneration } from "@/lib/blindboxPlanGen";
import { getAuthUserId } from "@/lib/auth";
function encodeSSE(event: string, data: string): string {
return `event: ${event}\ndata: ${data}\n\n`;
}
export async function POST(req: Request): Promise<Response> {
export async function POST(req: NextRequest): Promise<Response> {
let roomId: string;
let userId: string;
let availableTime: { date: string; startHour: number; endHour: number };
try {
userId = await getAuthUserId(req);
const body = await req.json();
roomId = body.roomId;
userId = body.userId;
availableTime = body.availableTime;
requireUserId(userId);
if (!roomId) {
return new Response(
JSON.stringify({ error: "roomId 不能为空" }),
@@ -41,8 +42,9 @@ export async function POST(req: Request): Promise<Response> {
}
} catch (e) {
const message = e instanceof Error ? e.message : "请求参数错误";
const status = e instanceof Error && "status" in e ? (e as { status: number }).status : 400;
return new Response(JSON.stringify({ error: message }), {
status: 400,
status,
headers: { "Content-Type": "application/json" },
});
}
@@ -55,7 +57,7 @@ export async function POST(req: Request): Promise<Response> {
};
try {
const result = await runPlanGeneration(roomId!, userId!, availableTime!, (message) => {
const result = await runPlanGeneration(roomId, userId, availableTime, (message) => {
push("status", message);
});
push("plan", JSON.stringify({ id: result.id, days: result.days, createdAt: result.createdAt }));