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
+4 -3
View File
@@ -1,12 +1,13 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireMembership } from "@/lib/blindbox";
import { apiHandler, ApiError, requireUserId } from "@/lib/api";
import { apiHandler, ApiError } from "@/lib/api";
import { getAuthUserId } from "@/lib/auth";
export const POST = apiHandler(async (req) => {
const { roomId, userId } = await req.json();
const userId = await getAuthUserId(req);
const { roomId } = await req.json();
requireUserId(userId);
if (!roomId || typeof roomId !== "string") throw new ApiError("roomId 不能为空");
await requireMembership(roomId, userId);
+4 -3
View File
@@ -1,10 +1,11 @@
import { NextResponse } from "next/server";
import { apiHandler, ApiError, requireUserId } from "@/lib/api";
import { apiHandler, ApiError } from "@/lib/api";
import { refinePlan } from "@/lib/ai";
import { getAuthUserId } from "@/lib/auth";
export const POST = apiHandler(async (req) => {
const { userId, instruction, days } = await req.json();
requireUserId(userId);
await getAuthUserId(req);
const { instruction, days } = await req.json();
if (!instruction?.trim()) throw new ApiError("指令不能为空", 400);
if (!Array.isArray(days) || days.length === 0) throw new ApiError("days 无效", 400);
+13 -12
View File
@@ -1,7 +1,8 @@
import { NextResponse } from "next/server";
import { requireMembership } from "@/lib/blindbox";
import { apiHandler, ApiError, requireUserId } from "@/lib/api";
import { apiHandler, ApiError } from "@/lib/api";
import { runPlanGeneration } from "@/lib/blindboxPlanGen";
import { getAuthUserId } from "@/lib/auth";
interface AvailableTime {
date: string;
@@ -10,9 +11,9 @@ interface AvailableTime {
}
export const POST = apiHandler(async (req) => {
const { roomId, userId, availableTime } = await req.json();
const userId = await getAuthUserId(req);
const { roomId, availableTime } = await req.json();
requireUserId(userId);
if (!roomId) throw new ApiError("roomId 不能为空");
await requireMembership(roomId, userId);
@@ -27,7 +28,7 @@ export const POST = apiHandler(async (req) => {
throw new ApiError("请选择有效的可用时间");
}
const result = await runPlanGeneration(roomId, userId!, at);
const result = await runPlanGeneration(roomId, userId, at);
return NextResponse.json({
id: result.id,
@@ -69,14 +70,15 @@ function computeEndTime(planData: string, now: Date): Date | null {
}
return base;
} catch {
} catch (e) {
console.error("computeEndTime failed:", e);
return null;
}
}
export const PATCH = apiHandler(async (req) => {
const { planId, userId, action, days } = await req.json();
requireUserId(userId);
const userId = await getAuthUserId(req);
const { planId, action, days } = await req.json();
if (!planId) throw new ApiError("planId 不能为空");
const { prisma } = await import("@/lib/prisma");
@@ -129,10 +131,9 @@ export const PATCH = apiHandler(async (req) => {
});
export const GET = apiHandler(async (req) => {
const userId = await getAuthUserId(req);
const { searchParams } = new URL(req.url);
const mode = searchParams.get("mode") || "latest";
const userId = searchParams.get("userId");
requireUserId(userId);
const { prisma } = await import("@/lib/prisma");
@@ -141,7 +142,7 @@ export const GET = apiHandler(async (req) => {
if (!roomId) throw new ApiError("roomId 不能为空");
const plan = await prisma.weekendPlan.findFirst({
where: { roomId, userId: userId!, status: "accepted" },
where: { roomId, userId, status: "accepted" },
orderBy: { createdAt: "desc" },
select: { id: true, planData: true, endTime: true, createdAt: true },
});
@@ -156,7 +157,7 @@ export const GET = apiHandler(async (req) => {
if (mode === "pending") {
const plans = await prisma.weekendPlan.findMany({
where: {
userId: userId!,
userId,
status: "accepted",
endTime: { not: null, lt: new Date() },
},
@@ -190,7 +191,7 @@ export const GET = apiHandler(async (req) => {
if (mode === "history") {
const plans = await prisma.weekendPlan.findMany({
where: {
userId: userId!,
userId,
status: { in: ["completed", "expired"] },
},
orderBy: { createdAt: "desc" },
+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 }));
@@ -33,7 +33,7 @@ export const POST = apiHandler(async (req) => {
reason: alt.reason,
};
}
} catch { /* ignore, use fallback */ }
} catch (e) { console.error("suggest-item: POI search failed, using fallback:", e); }
return {
activity: alt.activity,
poi: alt.searchQuery,
+4 -3
View File
@@ -1,13 +1,14 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireMembership } from "@/lib/blindbox";
import { apiHandler, requireUserId } from "@/lib/api";
import { apiHandler } from "@/lib/api";
import { getAuthUserId } from "@/lib/auth";
import { tagIdea } from "@/lib/ai";
export const POST = apiHandler(async (req) => {
const { roomId, userId } = await req.json();
const userId = await getAuthUserId(req);
const { roomId } = await req.json();
requireUserId(userId);
await requireMembership(roomId, userId);
// Find all untagged ideas in this room (any member's ideas)
+5 -7
View File
@@ -1,7 +1,8 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getRoomByCode, requireMembership } from "@/lib/blindbox";
import { apiHandler, ApiError, requireUserId } from "@/lib/api";
import { apiHandler, ApiError } from "@/lib/api";
import { getAuthUserId } from "@/lib/auth";
export const GET = apiHandler(async (_req, { params }) => {
const { code } = await params;
@@ -27,10 +28,9 @@ export const GET = apiHandler(async (_req, { params }) => {
});
export const PATCH = apiHandler(async (req, { params }) => {
const userId = await getAuthUserId(req);
const { code } = await params;
const { userId, city, address, lat, lng } = await req.json();
requireUserId(userId);
const { city, address, lat, lng } = await req.json();
const room = await prisma.blindBoxRoom.findUnique({
where: { code: code.toUpperCase() },
@@ -67,10 +67,8 @@ export const PATCH = apiHandler(async (req, { params }) => {
});
export const DELETE = apiHandler(async (req, { params }) => {
const userId = await getAuthUserId(req);
const { code } = await params;
const { userId } = await req.json();
requireUserId(userId);
const room = await prisma.blindBoxRoom.findUnique({
where: { code: code.toUpperCase() },
+4 -3
View File
@@ -1,11 +1,12 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { apiHandler, ApiError, requireUserId } from "@/lib/api";
import { apiHandler, ApiError } from "@/lib/api";
import { getAuthUserId } from "@/lib/auth";
export const POST = apiHandler(async (req) => {
const { userId, code } = await req.json();
const userId = await getAuthUserId(req);
const { code } = await req.json();
requireUserId(userId);
if (!code || typeof code !== "string") throw new ApiError("请输入房间号");
const room = await prisma.blindBoxRoom.findUnique({
+4 -4
View File
@@ -1,13 +1,13 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { generateUniqueRoomCode } from "@/lib/blindbox";
import { apiHandler, requireUserId, requireUser } from "@/lib/api";
import { apiHandler, requireUser } from "@/lib/api";
import { validateRoomName } from "@/lib/validation";
import { getAuthUserId } from "@/lib/auth";
export const POST = apiHandler(async (req) => {
const { userId, name } = await req.json();
requireUserId(userId);
const userId = await getAuthUserId(req);
const { name } = await req.json();
const roomName = validateRoomName(name);
+3 -2
View File
@@ -1,9 +1,10 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { apiHandler, requireUserId } from "@/lib/api";
import { apiHandler } from "@/lib/api";
import { getAuthUserId } from "@/lib/auth";
export const GET = apiHandler(async (req) => {
const userId = requireUserId(req.nextUrl.searchParams.get("userId"));
const userId = await getAuthUserId(req);
const memberships = await prisma.blindBoxMember.findMany({
where: { userId },
+10 -9
View File
@@ -1,9 +1,10 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireMembership } from "@/lib/blindbox";
import { apiHandler, ApiError, requireUserId } from "@/lib/api";
import { apiHandler, ApiError } from "@/lib/api";
import { validateIdeaContent, requireString } from "@/lib/validation";
import { tagIdea } from "@/lib/ai";
import { getAuthUserId } from "@/lib/auth";
const TAG_TIMEOUT_MS = 60_000;
@@ -26,13 +27,13 @@ function applyTags(ideaId: string, content: string) {
},
});
})
.catch(() => {});
.catch((e) => { console.error("updateIdeaTags: background tag update failed:", e); });
}
export const POST = apiHandler(async (req) => {
const { roomId, userId, content } = await req.json();
const userId = await getAuthUserId(req);
const { roomId, content } = await req.json();
requireUserId(userId);
requireString(roomId, "roomId");
const trimmedContent = validateIdeaContent(content);
@@ -48,7 +49,7 @@ export const POST = apiHandler(async (req) => {
});
export const GET = apiHandler(async (req) => {
const userId = requireUserId(req.nextUrl.searchParams.get("userId"));
const userId = await getAuthUserId(req);
const roomId = requireString(req.nextUrl.searchParams.get("roomId"), "roomId");
await requireMembership(roomId, userId);
@@ -88,9 +89,9 @@ export const GET = apiHandler(async (req) => {
});
export const PUT = apiHandler(async (req) => {
const { ideaId, userId, content } = await req.json();
const userId = await getAuthUserId(req);
const { ideaId, content } = await req.json();
requireUserId(userId);
requireString(ideaId, "ideaId");
const trimmedContent = validateIdeaContent(content);
@@ -107,9 +108,9 @@ export const PUT = apiHandler(async (req) => {
});
export const DELETE = apiHandler(async (req) => {
const { ideaId, userId } = await req.json();
const userId = await getAuthUserId(req);
const { ideaId } = await req.json();
requireUserId(userId);
requireString(ideaId, "ideaId");
const { count } = await prisma.blindBoxIdea.deleteMany({
+4 -4
View File
@@ -1,18 +1,18 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireMembership } from "@/lib/blindbox";
import { apiHandler, ApiError, requireUserId } from "@/lib/api";
import { apiHandler, ApiError } from "@/lib/api";
import { getAuthUserId } from "@/lib/auth";
import { suggestIdeas } from "@/lib/ai";
export const GET = apiHandler(async (req) => {
const userId = await getAuthUserId(req);
const { searchParams } = new URL(req.url);
const roomId = searchParams.get("roomId");
const userId = searchParams.get("userId");
requireUserId(userId);
if (!roomId) throw new ApiError("roomId 不能为空");
await requireMembership(roomId, userId!);
await requireMembership(roomId, userId);
const recentIdeas = await prisma.blindBoxIdea.findMany({
where: { roomId, status: "in_pool" },