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:
@@ -65,7 +65,7 @@ export default function AchievementsPage() {
|
||||
setStats(data.stats);
|
||||
setDecisions(data.decisions);
|
||||
setContracts(data.contracts);
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.error("AchievementsPage: fetch failed:", e); }
|
||||
finally { setLoading(false); }
|
||||
})();
|
||||
}, [router]);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { apiHandler, ApiError } from "@/lib/api";
|
||||
import { signToken, setAuthCookie } from "@/lib/auth";
|
||||
|
||||
export const POST = apiHandler(async (req) => {
|
||||
const { username, password } = await req.json();
|
||||
@@ -14,9 +15,12 @@ export const POST = apiHandler(async (req) => {
|
||||
const valid = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!valid) throw new ApiError("用户名或密码错误", 401);
|
||||
|
||||
return NextResponse.json({
|
||||
const token = await signToken(user.id);
|
||||
const res = NextResponse.json({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
});
|
||||
|
||||
return setAuthCookie(res, token);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { clearAuthCookie } from "@/lib/auth";
|
||||
|
||||
export async function POST() {
|
||||
const res = NextResponse.json({ ok: true });
|
||||
return clearAuthCookie(res);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Prisma } from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { apiHandler, ApiError } from "@/lib/api";
|
||||
import { validateUsername, validatePassword } from "@/lib/validation";
|
||||
import { signToken, setAuthCookie } from "@/lib/auth";
|
||||
|
||||
export const POST = apiHandler(async (req) => {
|
||||
const { username, password, avatar } = await req.json();
|
||||
@@ -24,11 +25,14 @@ export const POST = apiHandler(async (req) => {
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
const token = await signToken(user.id);
|
||||
const res = NextResponse.json({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
});
|
||||
|
||||
return setAuthCookie(res, token);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
|
||||
throw new ApiError("用户名已被注册", 409);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -59,8 +59,8 @@ export async function GET(
|
||||
try {
|
||||
const status = await buildRoomStatus(id);
|
||||
if (status && alive) send(status);
|
||||
} catch {
|
||||
/* ignore transient read errors */
|
||||
} catch (e) {
|
||||
console.error("SSE: transient read error:", e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
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 = req.nextUrl.searchParams.get("userId");
|
||||
requireUserId(userId);
|
||||
const userId = await getAuthUserId(req);
|
||||
|
||||
const [decisions, contracts] = await Promise.all([
|
||||
prisma.decision.findMany({
|
||||
where: { userId: userId! },
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 50,
|
||||
}),
|
||||
prisma.weekendPlan.findMany({
|
||||
where: {
|
||||
userId: userId!,
|
||||
userId,
|
||||
status: { in: ["completed", "expired"] },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api";
|
||||
import { apiHandler, ApiError, requireUser } from "@/lib/api";
|
||||
import { getAuthUserId } from "@/lib/auth";
|
||||
|
||||
export const GET = apiHandler(async (req) => {
|
||||
const userId = req.nextUrl.searchParams.get("userId");
|
||||
if (!userId) return NextResponse.json([]);
|
||||
const userId = await getAuthUserId(req);
|
||||
|
||||
const favorites = await prisma.favorite.findMany({
|
||||
where: { userId },
|
||||
@@ -23,9 +23,9 @@ export const GET = apiHandler(async (req) => {
|
||||
});
|
||||
|
||||
export const POST = apiHandler(async (req) => {
|
||||
const { userId, restaurant } = await req.json();
|
||||
const userId = await getAuthUserId(req);
|
||||
const { restaurant } = await req.json();
|
||||
|
||||
requireUserId(userId);
|
||||
if (!restaurant?.id || typeof restaurant.id !== "string") {
|
||||
throw new ApiError("缺少必要字段");
|
||||
}
|
||||
@@ -53,9 +53,9 @@ export const POST = apiHandler(async (req) => {
|
||||
});
|
||||
|
||||
export const DELETE = apiHandler(async (req) => {
|
||||
const { userId, favoriteId } = await req.json();
|
||||
const userId = await getAuthUserId(req);
|
||||
const { favoriteId } = await req.json();
|
||||
|
||||
requireUserId(userId);
|
||||
if (!favoriteId) throw new ApiError("缺少必要字段");
|
||||
|
||||
const fav = await prisma.favorite.findUnique({ where: { id: favoriteId } });
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api";
|
||||
import { apiHandler, ApiError, requireUser } from "@/lib/api";
|
||||
import { getAuthUserId } from "@/lib/auth";
|
||||
|
||||
const MAX_HISTORY = 50;
|
||||
|
||||
export const GET = apiHandler(async (req) => {
|
||||
const userId = req.nextUrl.searchParams.get("userId");
|
||||
if (!userId) return NextResponse.json([]);
|
||||
const userId = await getAuthUserId(req);
|
||||
|
||||
const decisions = await prisma.decision.findMany({
|
||||
where: { userId },
|
||||
@@ -28,10 +28,9 @@ export const GET = apiHandler(async (req) => {
|
||||
});
|
||||
|
||||
export const POST = apiHandler(async (req) => {
|
||||
const { userId, roomId, restaurant, matchType, participants } =
|
||||
await req.json();
|
||||
const userId = await getAuthUserId(req);
|
||||
const { roomId, restaurant, matchType, participants } = await req.json();
|
||||
|
||||
requireUserId(userId);
|
||||
if (!roomId || !restaurant || !matchType) {
|
||||
throw new ApiError("缺少必要字段");
|
||||
}
|
||||
|
||||
@@ -2,17 +2,29 @@ import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api";
|
||||
import { apiHandler, ApiError, requireUser } from "@/lib/api";
|
||||
import { validateUsername, validatePassword, validateEmail } from "@/lib/validation";
|
||||
import { getAuthUserId } from "@/lib/auth";
|
||||
|
||||
export const GET = apiHandler(async (req) => {
|
||||
const userId = req.nextUrl.searchParams.get("id");
|
||||
if (!userId) return NextResponse.json(null);
|
||||
// GET still allows querying by id param (for public profile viewing)
|
||||
// but sensitive fields are only shown to the owner
|
||||
const queryId = req.nextUrl.searchParams.get("id");
|
||||
if (!queryId) return NextResponse.json(null);
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||
const user = await prisma.user.findUnique({ where: { id: queryId } });
|
||||
if (!user) return NextResponse.json(null);
|
||||
|
||||
const decisionCount = await prisma.decision.count({ where: { userId } });
|
||||
const decisionCount = await prisma.decision.count({ where: { userId: queryId } });
|
||||
|
||||
// Check if the requester is the profile owner
|
||||
let isOwner = false;
|
||||
try {
|
||||
const authId = await getAuthUserId(req);
|
||||
isOwner = authId === queryId;
|
||||
} catch {
|
||||
// Not logged in — show public profile only
|
||||
}
|
||||
|
||||
let preferences = {};
|
||||
try { preferences = JSON.parse(user.preferences); } catch { /* fallback */ }
|
||||
@@ -21,18 +33,17 @@ export const GET = apiHandler(async (req) => {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
email: user.email,
|
||||
preferences,
|
||||
email: isOwner ? user.email : undefined,
|
||||
preferences: isOwner ? preferences : undefined,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
decisionCount,
|
||||
});
|
||||
});
|
||||
|
||||
export const PUT = apiHandler(async (req) => {
|
||||
const userId = await getAuthUserId(req);
|
||||
const body = await req.json();
|
||||
const { userId } = body;
|
||||
|
||||
requireUserId(userId);
|
||||
const existing = await requireUser(userId);
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
@@ -184,7 +184,7 @@ export default function BlindboxRoomPage() {
|
||||
setMyIdeas(data.myIdeas ?? []);
|
||||
setDrawnHistory(data.drawn ?? []);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.error("fetchIdeas failed:", e); }
|
||||
}, [room]);
|
||||
|
||||
const fetchSuggestions = useCallback(async () => {
|
||||
@@ -202,7 +202,7 @@ export default function BlindboxRoomPage() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.error("fetchSuggestions failed:", e); }
|
||||
setSuggestions(pickRandom(IDEA_INSPIRATIONS, 4));
|
||||
setSuggestionsSource("static");
|
||||
setSuggestionsLoading(false);
|
||||
@@ -230,7 +230,7 @@ export default function BlindboxRoomPage() {
|
||||
endTime: data.plan.endTime ?? null,
|
||||
});
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.error("fetchAcceptedPlan failed:", e); }
|
||||
}, [room]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -251,7 +251,7 @@ export default function BlindboxRoomPage() {
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (data.pending?.length) setPendingContracts(data.pending);
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.error("fetchPendingContracts failed:", e); }
|
||||
})();
|
||||
}, [isMember]);
|
||||
|
||||
@@ -281,7 +281,7 @@ export default function BlindboxRoomPage() {
|
||||
fetch(`/api/blindbox/plan?mode=pending&userId=${p.id}`)
|
||||
.then((r) => r.json())
|
||||
.then((d) => { if (d.pending?.length) setPendingContracts(d.pending); })
|
||||
.catch(() => {});
|
||||
.catch((e) => { console.error("refreshPendingContracts failed:", e); });
|
||||
}
|
||||
}, ms);
|
||||
|
||||
@@ -314,7 +314,7 @@ export default function BlindboxRoomPage() {
|
||||
setIsMember(true);
|
||||
fetchRoom();
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) { console.error("handleJoinRoom failed:", e); }
|
||||
finally { setJoiningRoom(false); }
|
||||
};
|
||||
|
||||
@@ -1151,7 +1151,7 @@ export default function BlindboxRoomPage() {
|
||||
days: planDays,
|
||||
endTime: data.endTime ?? null,
|
||||
});
|
||||
} catch { /* best-effort */ }
|
||||
} catch (e) { console.error("acceptPlan failed:", e); }
|
||||
}
|
||||
toast.show("契约已接受!");
|
||||
timersRef.current.push(setTimeout(() => {
|
||||
|
||||
@@ -80,7 +80,8 @@ export default function PanicPage() {
|
||||
const data: LocationSuggestion[] = await res.json();
|
||||
setSuggestions(Array.isArray(data) ? data : []);
|
||||
setShowSuggestions(Array.isArray(data) && data.length > 0);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error("PanicPage: fetchSuggestions failed:", e);
|
||||
setSuggestions([]);
|
||||
} finally {
|
||||
setFetchingSuggestions(false);
|
||||
@@ -178,7 +179,8 @@ export default function PanicPage() {
|
||||
try {
|
||||
await joinRoom(roomCode, getUserId());
|
||||
router.push(`/room/${roomCode}`);
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.error("PanicPage: handleJoin failed:", e);
|
||||
setError("房间不存在,请检查房间号");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,8 @@ export default function ProfilePage() {
|
||||
router.push("/");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
console.error("ProfilePage: fetch user failed:", e);
|
||||
setProfile({ ...cached });
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
@@ -93,7 +94,7 @@ export default function ProfilePage() {
|
||||
fetch(`/api/user/favorite?userId=${userId}`)
|
||||
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
|
||||
.then((data) => setFavorites(Array.isArray(data) ? data : []))
|
||||
.catch(() => {})
|
||||
.catch((e) => { console.error("ProfilePage: fetch favorites failed:", e); })
|
||||
.finally(() => setFavLoading(false));
|
||||
}, [userId]);
|
||||
|
||||
@@ -226,8 +227,8 @@ export default function ProfilePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user