fix: 服务端验证强化 — 房间ID/坐标/swipe/盲盒竞态/空格

- #15: 房间 ID 扩展为 6 位字母数字,createRoom 用 P2002 重试替代 find-then-create
- #16: 盲盒编辑/删除改用 updateMany/deleteMany 原子操作,防止 TOCTOU
- #17: lat/lng 用 Number.isFinite + 范围校验 (-90~90, -180~180)
- #18: swipe action 必须为 'like' 或 'pass'
- #19: user PUT 的 JSON.parse(preferences) 加 try/catch
- #26: requireString 拒绝纯空格字符串
This commit is contained in:
2026-02-26 20:19:56 +08:00
parent 93f20747e4
commit dfb3cfa136
6 changed files with 41 additions and 31 deletions
+9 -13
View File
@@ -55,17 +55,14 @@ export const PUT = apiHandler(async (req) => {
requireString(ideaId, "ideaId");
const trimmedContent = validateIdeaContent(content);
const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } });
if (!idea) throw new ApiError("想法不存在", 404);
if (idea.userId !== userId) throw new ApiError("只能编辑自己的想法", 403);
if (idea.status !== "in_pool") throw new ApiError("已抽中的想法不能编辑");
const updated = await prisma.blindBoxIdea.update({
where: { id: ideaId },
const { count } = await prisma.blindBoxIdea.updateMany({
where: { id: ideaId, userId, status: "in_pool" },
data: { content: trimmedContent },
});
return NextResponse.json({ id: updated.id, content: updated.content });
if (count === 0) throw new ApiError("想法不存在、已被抽中或无权编辑", 404);
return NextResponse.json({ id: ideaId, content: trimmedContent });
});
export const DELETE = apiHandler(async (req) => {
@@ -74,12 +71,11 @@ export const DELETE = apiHandler(async (req) => {
requireUserId(userId);
requireString(ideaId, "ideaId");
const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } });
if (!idea) throw new ApiError("想法不存在", 404);
if (idea.userId !== userId) throw new ApiError("只能删除自己的想法", 403);
if (idea.status !== "in_pool") throw new ApiError("已抽中的想法不能删除");
const { count } = await prisma.blindBoxIdea.deleteMany({
where: { id: ideaId, userId, status: "in_pool" },
});
await prisma.blindBoxIdea.delete({ where: { id: ideaId } });
if (count === 0) throw new ApiError("想法不存在、已被抽中或无权删除", 404);
return NextResponse.json({ deleted: true });
});
+3
View File
@@ -11,6 +11,9 @@ export const POST = apiHandler(async (req, { params }) => {
if (restaurantId == null || !action) {
throw new ApiError("restaurantId and action are required");
}
if (action !== "like" && action !== "pass") {
throw new ApiError("action must be 'like' or 'pass'");
}
const rid = String(restaurantId);
+5 -2
View File
@@ -117,7 +117,10 @@ export const POST = apiHandler(async (req) => {
const sceneConfig = getSceneConfig(scene === "drink" ? "drink" : "eat");
if (!lat || !lng) {
const numLat = Number(lat);
const numLng = Number(lng);
if (!Number.isFinite(numLat) || !Number.isFinite(numLng) ||
numLat < -90 || numLat > 90 || numLng < -180 || numLng > 180) {
throw new ApiError("无法获取位置信息,请允许定位权限后重试");
}
@@ -125,7 +128,7 @@ export const POST = apiHandler(async (req) => {
const url = new URL("https://restapi.amap.com/v5/place/around");
url.searchParams.set("key", apiKey);
url.searchParams.set("location", `${lng},${lat}`);
url.searchParams.set("location", `${numLng},${numLat}`);
url.searchParams.set("radius", String(radius));
url.searchParams.set("types", sceneConfig.poiTypes);
url.searchParams.set("show_fields", "business,photos");
+4 -1
View File
@@ -73,12 +73,15 @@ export const PUT = apiHandler(async (req) => {
data: updateData,
});
let prefs = {};
try { prefs = JSON.parse(user.preferences); } catch { /* fallback */ }
return NextResponse.json({
id: user.id,
username: user.username,
avatar: user.avatar,
email: user.email,
preferences: JSON.parse(user.preferences),
preferences: prefs,
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {