From 19edcaeeb50ed0f884c724ecc687ea2a4332891e Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 18:17:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8F=90=E5=8F=96=20requireUserId/?= =?UTF-8?q?requireUser/requireMembership=20=E6=A0=A1=E9=AA=8C=E5=B7=A5?= =?UTF-8?q?=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 requireUserId:统一 14 处 userId 非空校验,返回 401 - 新增 requireUser:统一 4 处用户存在性检查,返回 404 - validateMembership 升级为 requireMembership,直接抛 403 - 混合校验拆分为 auth(401) + 字段(400),状态码更准确 --- src/app/api/blindbox/draw/route.ts | 9 ++++----- src/app/api/blindbox/room/[code]/route.ts | 4 ++-- src/app/api/blindbox/room/join/route.ts | 4 ++-- src/app/api/blindbox/room/route.ts | 7 +++---- src/app/api/blindbox/rooms/route.ts | 5 ++--- src/app/api/blindbox/route.ts | 19 ++++++++----------- src/app/api/room/[id]/join/route.ts | 4 ++-- src/app/api/room/[id]/manage/route.ts | 7 +++---- src/app/api/room/[id]/swipe/route.ts | 7 ++++--- src/app/api/room/[id]/undo/route.ts | 7 +++---- src/app/api/user/favorite/route.ts | 11 ++++++----- src/app/api/user/history/route.ts | 8 ++++---- src/app/api/user/route.ts | 8 +++----- src/lib/api.ts | 16 ++++++++++++++++ src/lib/blindbox.ts | 7 +++++-- 15 files changed, 67 insertions(+), 56 deletions(-) diff --git a/src/app/api/blindbox/draw/route.ts b/src/app/api/blindbox/draw/route.ts index cab04e2..062de1e 100644 --- a/src/app/api/blindbox/draw/route.ts +++ b/src/app/api/blindbox/draw/route.ts @@ -1,16 +1,15 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { validateMembership } from "@/lib/blindbox"; -import { apiHandler, ApiError } from "@/lib/api"; +import { requireMembership } from "@/lib/blindbox"; +import { apiHandler, ApiError, requireUserId } from "@/lib/api"; export const POST = apiHandler(async (req) => { const { roomId, userId } = await req.json(); - if (!userId || typeof userId !== "string") throw new ApiError("请先登录", 401); + requireUserId(userId); if (!roomId || typeof roomId !== "string") throw new ApiError("roomId 不能为空"); - const isMember = await validateMembership(roomId, userId); - if (!isMember) throw new ApiError("你不是这个房间的成员", 403); + await requireMembership(roomId, userId); const pool = await prisma.blindBoxIdea.findMany({ where: { roomId, status: "in_pool" }, diff --git a/src/app/api/blindbox/room/[code]/route.ts b/src/app/api/blindbox/room/[code]/route.ts index 5ba4ab9..d67408d 100644 --- a/src/app/api/blindbox/room/[code]/route.ts +++ b/src/app/api/blindbox/room/[code]/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { getRoomByCode } from "@/lib/blindbox"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId } from "@/lib/api"; export const GET = apiHandler(async (_req, { params }) => { const { code } = await params; @@ -26,7 +26,7 @@ export const DELETE = apiHandler(async (req, { params }) => { const { code } = await params; const { userId } = await req.json(); - if (!userId) throw new ApiError("缺少用户 ID"); + requireUserId(userId); const room = await prisma.blindBoxRoom.findUnique({ where: { code: code.toUpperCase() }, diff --git a/src/app/api/blindbox/room/join/route.ts b/src/app/api/blindbox/room/join/route.ts index 8ac094c..98b3208 100644 --- a/src/app/api/blindbox/room/join/route.ts +++ b/src/app/api/blindbox/room/join/route.ts @@ -1,11 +1,11 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId } from "@/lib/api"; export const POST = apiHandler(async (req) => { const { userId, code } = await req.json(); - if (!userId || typeof userId !== "string") throw new ApiError("请先登录", 401); + requireUserId(userId); if (!code || typeof code !== "string") throw new ApiError("请输入房间号"); const room = await prisma.blindBoxRoom.findUnique({ diff --git a/src/app/api/blindbox/room/route.ts b/src/app/api/blindbox/room/route.ts index c5942e8..35b2e19 100644 --- a/src/app/api/blindbox/room/route.ts +++ b/src/app/api/blindbox/room/route.ts @@ -1,18 +1,17 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { generateUniqueRoomCode } from "@/lib/blindbox"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api"; export const POST = apiHandler(async (req) => { const { userId, name } = await req.json(); - if (!userId || typeof userId !== "string") throw new ApiError("请先登录", 401); + requireUserId(userId); const roomName = (name || "").trim() || "我们的周末"; if (roomName.length > 30) throw new ApiError("房间名不能超过 30 个字"); - const user = await prisma.user.findUnique({ where: { id: userId } }); - if (!user) throw new ApiError("用户不存在", 404); + await requireUser(userId); const code = await generateUniqueRoomCode(); diff --git a/src/app/api/blindbox/rooms/route.ts b/src/app/api/blindbox/rooms/route.ts index 6983317..84400c9 100644 --- a/src/app/api/blindbox/rooms/route.ts +++ b/src/app/api/blindbox/rooms/route.ts @@ -1,10 +1,9 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, requireUserId } from "@/lib/api"; export const GET = apiHandler(async (req) => { - const userId = req.nextUrl.searchParams.get("userId"); - if (!userId) throw new ApiError("请先登录", 401); + const userId = requireUserId(req.nextUrl.searchParams.get("userId")); const memberships = await prisma.blindBoxMember.findMany({ where: { userId }, diff --git a/src/app/api/blindbox/route.ts b/src/app/api/blindbox/route.ts index 64237bf..5dd4665 100644 --- a/src/app/api/blindbox/route.ts +++ b/src/app/api/blindbox/route.ts @@ -1,20 +1,19 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { validateMembership } from "@/lib/blindbox"; -import { apiHandler, ApiError } from "@/lib/api"; +import { requireMembership } from "@/lib/blindbox"; +import { apiHandler, ApiError, requireUserId } from "@/lib/api"; export const POST = apiHandler(async (req) => { const { roomId, userId, content } = await req.json(); - if (!userId || typeof userId !== "string") throw new ApiError("请先登录", 401); + requireUserId(userId); if (!roomId || typeof roomId !== "string") throw new ApiError("roomId 不能为空"); if (!content || typeof content !== "string" || content.trim().length === 0) { throw new ApiError("内容不能为空"); } if (content.trim().length > 200) throw new ApiError("内容不能超过 200 字"); - const isMember = await validateMembership(roomId, userId); - if (!isMember) throw new ApiError("你不是这个房间的成员", 403); + await requireMembership(roomId, userId); const idea = await prisma.blindBoxIdea.create({ data: { roomId, userId, content: content.trim() }, @@ -25,13 +24,11 @@ export const POST = apiHandler(async (req) => { export const GET = apiHandler(async (req) => { const roomId = req.nextUrl.searchParams.get("roomId"); - const userId = req.nextUrl.searchParams.get("userId"); + const userId = requireUserId(req.nextUrl.searchParams.get("userId")); if (!roomId) throw new ApiError("缺少 roomId"); - if (!userId) throw new ApiError("请先登录", 401); - const isMember = await validateMembership(roomId, userId); - if (!isMember) throw new ApiError("你不是这个房间的成员", 403); + await requireMembership(roomId, userId); const [poolCount, myIdeas, drawn] = await Promise.all([ prisma.blindBoxIdea.count({ @@ -58,7 +55,7 @@ export const GET = apiHandler(async (req) => { export const PUT = apiHandler(async (req) => { const { ideaId, userId, content } = await req.json(); - if (!userId) throw new ApiError("请先登录", 401); + requireUserId(userId); if (!ideaId) throw new ApiError("缺少 ideaId"); if (!content || typeof content !== "string" || content.trim().length === 0) { throw new ApiError("内容不能为空"); @@ -81,7 +78,7 @@ export const PUT = apiHandler(async (req) => { export const DELETE = apiHandler(async (req) => { const { ideaId, userId } = await req.json(); - if (!userId) throw new ApiError("请先登录", 401); + requireUserId(userId); if (!ideaId) throw new ApiError("缺少 ideaId"); const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } }); diff --git a/src/app/api/room/[id]/join/route.ts b/src/app/api/room/[id]/join/route.ts index fb901ae..230357e 100644 --- a/src/app/api/room/[id]/join/route.ts +++ b/src/app/api/room/[id]/join/route.ts @@ -1,13 +1,13 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; import { notify } from "@/lib/roomEvents"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId } from "@/lib/api"; export const POST = apiHandler(async (req, { params }) => { const { id } = await params; const { userId } = await req.json(); - if (!userId) throw new ApiError("userId required"); + requireUserId(userId); const updated = await atomicUpdateRoom(id, (data) => { if (data.kickedUsers.includes(userId)) { diff --git a/src/app/api/room/[id]/manage/route.ts b/src/app/api/room/[id]/manage/route.ts index bf900b3..b2b76b3 100644 --- a/src/app/api/room/[id]/manage/route.ts +++ b/src/app/api/room/[id]/manage/route.ts @@ -1,15 +1,14 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; import { notify } from "@/lib/roomEvents"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId } from "@/lib/api"; export const POST = apiHandler(async (req, { params }) => { const { id } = await params; const { userId, action, targetUserId } = await req.json(); - if (!userId || !action) { - throw new ApiError("userId and action required"); - } + requireUserId(userId); + if (!action) throw new ApiError("action required"); const updated = await atomicUpdateRoom(id, (data) => { if (data.creatorId !== userId) { diff --git a/src/app/api/room/[id]/swipe/route.ts b/src/app/api/room/[id]/swipe/route.ts index c448003..835368e 100644 --- a/src/app/api/room/[id]/swipe/route.ts +++ b/src/app/api/room/[id]/swipe/route.ts @@ -1,14 +1,15 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; import { notify } from "@/lib/roomEvents"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId } from "@/lib/api"; export const POST = apiHandler(async (req, { params }) => { const { id } = await params; const { userId, restaurantId, action } = await req.json(); - if (!userId || restaurantId == null || !action) { - throw new ApiError("userId, restaurantId, and action are required"); + requireUserId(userId); + if (restaurantId == null || !action) { + throw new ApiError("restaurantId and action are required"); } const rid = String(restaurantId); diff --git a/src/app/api/room/[id]/undo/route.ts b/src/app/api/room/[id]/undo/route.ts index 651f398..f493849 100644 --- a/src/app/api/room/[id]/undo/route.ts +++ b/src/app/api/room/[id]/undo/route.ts @@ -1,15 +1,14 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; import { notify } from "@/lib/roomEvents"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId } from "@/lib/api"; export const POST = apiHandler(async (req, { params }) => { const { id } = await params; const { userId, restaurantId } = await req.json(); - if (!userId || restaurantId == null) { - throw new ApiError("userId and restaurantId are required"); - } + requireUserId(userId); + if (restaurantId == null) throw new ApiError("restaurantId is required"); const rid = String(restaurantId); diff --git a/src/app/api/user/favorite/route.ts b/src/app/api/user/favorite/route.ts index 6fecf41..dd97664 100644 --- a/src/app/api/user/favorite/route.ts +++ b/src/app/api/user/favorite/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api"; export const GET = apiHandler(async (req) => { const userId = req.nextUrl.searchParams.get("userId"); @@ -24,10 +24,10 @@ export const GET = apiHandler(async (req) => { export const POST = apiHandler(async (req) => { const { userId, restaurant } = await req.json(); - if (!userId || !restaurant) throw new ApiError("缺少必要字段"); + requireUserId(userId); + if (!restaurant) throw new ApiError("缺少必要字段"); - const user = await prisma.user.findUnique({ where: { id: userId } }); - if (!user) throw new ApiError("请先设置个人资料", 404); + await requireUser(userId); const existing = await prisma.favorite.findFirst({ where: { @@ -53,7 +53,8 @@ export const POST = apiHandler(async (req) => { export const DELETE = apiHandler(async (req) => { const { userId, favoriteId } = await req.json(); - if (!userId || !favoriteId) throw new ApiError("缺少必要字段"); + requireUserId(userId); + if (!favoriteId) throw new ApiError("缺少必要字段"); const fav = await prisma.favorite.findUnique({ where: { id: favoriteId } }); if (!fav || fav.userId !== userId) throw new ApiError("收藏不存在", 404); diff --git a/src/app/api/user/history/route.ts b/src/app/api/user/history/route.ts index 6c40f01..b68e669 100644 --- a/src/app/api/user/history/route.ts +++ b/src/app/api/user/history/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api"; const MAX_HISTORY = 50; @@ -31,12 +31,12 @@ export const POST = apiHandler(async (req) => { const { userId, roomId, restaurant, matchType, participants } = await req.json(); - if (!userId || !roomId || !restaurant || !matchType) { + requireUserId(userId); + if (!roomId || !restaurant || !matchType) { throw new ApiError("缺少必要字段"); } - const user = await prisma.user.findUnique({ where: { id: userId } }); - if (!user) throw new ApiError("用户未注册", 404); + await requireUser(userId); const existing = await prisma.decision.findFirst({ where: { userId, roomId }, diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 03c8bc0..6c6f8e1 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import bcrypt from "bcryptjs"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api"; export const GET = apiHandler(async (req) => { const userId = req.nextUrl.searchParams.get("id"); @@ -27,10 +27,8 @@ export const PUT = apiHandler(async (req) => { const body = await req.json(); const { userId } = body; - if (!userId) throw new ApiError("缺少用户 ID"); - - const existing = await prisma.user.findUnique({ where: { id: userId } }); - if (!existing) throw new ApiError("用户不存在", 404); + requireUserId(userId); + const existing = await requireUser(userId); const updateData: Record = {}; diff --git a/src/lib/api.ts b/src/lib/api.ts index d2d7da1..33d1e46 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; export class ApiError extends Error { constructor( @@ -9,6 +10,21 @@ export class ApiError extends Error { } } +/** Validates that value is a non-empty string; throws 401 otherwise. */ +export function requireUserId(value: unknown): string { + if (!value || typeof value !== "string") { + throw new ApiError("请先登录", 401); + } + return value; +} + +/** Finds user by ID; throws 404 if not found. */ +export async function requireUser(userId: string) { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) throw new ApiError("用户不存在", 404); + return user; +} + type RouteContext = { params: Promise> }; type RouteHandler = ( diff --git a/src/lib/blindbox.ts b/src/lib/blindbox.ts index 7376c5a..c9e5b4a 100644 --- a/src/lib/blindbox.ts +++ b/src/lib/blindbox.ts @@ -1,4 +1,5 @@ import { prisma } from "@/lib/prisma"; +import { ApiError } from "@/lib/api"; export function generateRoomCode(): string { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; @@ -18,11 +19,13 @@ export async function generateUniqueRoomCode(): Promise { throw new Error("无法生成唯一房间号"); } -export async function validateMembership(roomId: string, userId: string) { +/** Throws 403 if user is not a member of the room. */ +export async function requireMembership(roomId: string, userId: string) { const member = await prisma.blindBoxMember.findUnique({ where: { roomId_userId: { roomId, userId } }, }); - return !!member; + if (!member) throw new ApiError("你不是这个房间的成员", 403); + return member; } export async function getRoomByCode(code: string) {