ce76980fe5
- 新增 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 全景分析文档
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { Prisma } from "@prisma/client";
|
|
import bcrypt from "bcryptjs";
|
|
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) => {
|
|
// 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: queryId } });
|
|
if (!user) return NextResponse.json(null);
|
|
|
|
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 */ }
|
|
|
|
return NextResponse.json({
|
|
id: user.id,
|
|
username: user.username,
|
|
avatar: user.avatar,
|
|
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 existing = await requireUser(userId);
|
|
|
|
const updateData: Record<string, unknown> = {};
|
|
|
|
if (body.username !== undefined) {
|
|
const trimmed = validateUsername(body.username);
|
|
if (trimmed !== existing.username) {
|
|
const taken = await prisma.user.findUnique({ where: { username: trimmed } });
|
|
if (taken) throw new ApiError("用户名已被占用", 409);
|
|
}
|
|
updateData.username = trimmed;
|
|
}
|
|
|
|
if (body.newPassword !== undefined) {
|
|
if (!body.currentPassword) throw new ApiError("请输入当前密码");
|
|
const valid = await bcrypt.compare(body.currentPassword, existing.passwordHash);
|
|
if (!valid) throw new ApiError("当前密码错误", 403);
|
|
validatePassword(body.newPassword, "新密码");
|
|
updateData.passwordHash = await bcrypt.hash(body.newPassword, 10);
|
|
}
|
|
|
|
if (body.avatar !== undefined) {
|
|
updateData.avatar = body.avatar;
|
|
}
|
|
|
|
if (body.email !== undefined) {
|
|
if (body.email) validateEmail(body.email);
|
|
updateData.email = body.email || null;
|
|
}
|
|
|
|
if (body.preferences !== undefined) {
|
|
updateData.preferences = JSON.stringify(body.preferences);
|
|
}
|
|
|
|
try {
|
|
const user = await prisma.user.update({
|
|
where: { id: userId },
|
|
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: prefs,
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
|
|
throw new ApiError("用户名已被占用", 409);
|
|
}
|
|
throw e;
|
|
}
|
|
});
|