fix: 评论流程两个断点(mock 登录 cookie 透传 + 评论后 ISR 缓存失效)

- mock-social 500:route handler 里 headers() 不可写,session cookie 无法落浏览器。
  改为 auth.api.signInEmail({ asResponse: true }) 并把 Set-Cookie 显式透传给响应。
  浏览器验证:mock 登录后有 session cookie,get-session 返回演示用户。
- 评论提交后详情页仍显示旧评分(ISR 1h):submitReview/deleteMyReview/setReviewStatus
  增加 revalidatePath(tools/[slug] + rankings + 首页)。
  浏览器验证:提交评论后 'Based on N reviews' 即时 2→3。
- 全链路回归:登录→写评论(星级/正文/优缺点)→提交出现→聚合更新→Helpful 计数 全部 PASS
This commit is contained in:
AI Directory Platform
2026-09-09 09:24:28 +00:00
parent 14af0aaa3a
commit f4f48f7293
2 changed files with 35 additions and 12 deletions
@@ -134,7 +134,10 @@ export async function submitReview(
metadata: { rating: data.rating }, metadata: { rating: data.rating },
}); });
// 失效详情页 ISR 缓存(评分/评论数即时反映)+ 榜单页
revalidatePath("/[locale]/tools/[slug]", "page"); revalidatePath("/[locale]/tools/[slug]", "page");
revalidatePath("/[locale]/rankings", "page");
revalidatePath("/[locale]", "page");
return { success: true, reviewId }; return { success: true, reviewId };
} }
@@ -314,6 +317,7 @@ export async function deleteMyReview(toolId: string) {
.where(and(eq(review.userId, user.id), eq(review.toolId, toolId))); .where(and(eq(review.userId, user.id), eq(review.toolId, toolId)));
await recalcToolRating(toolId); await recalcToolRating(toolId);
revalidatePath("/[locale]/tools/[slug]", "page"); revalidatePath("/[locale]/tools/[slug]", "page");
revalidatePath("/[locale]/rankings", "page");
return { success: true as const }; return { success: true as const };
} }
@@ -335,6 +339,7 @@ export async function setReviewStatus(reviewId: string, status: ReviewStatus) {
if (row) { if (row) {
await recalcToolRating(row.toolId); await recalcToolRating(row.toolId);
revalidatePath("/[locale]/tools/[slug]", "page"); revalidatePath("/[locale]/tools/[slug]", "page");
revalidatePath("/[locale]/rankings", "page");
} }
return { success: true }; return { success: true };
} }
@@ -1,20 +1,19 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { user, account } from "@/lib/db/schema"; import { user, account } from "@/lib/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
import { hashPassword } from "better-auth/crypto"; import { hashPassword } from "better-auth/crypto";
/** /**
* POST /api/auth/mock-social { provider: "google" | "github" } * POST /api/auth/mock-social { provider: "google" | "github" }
* 说明:开发/演示环境的第三方登录 Mock。 * 说明:开发/演示环境的第三方登录 Mock。
* 未配置真实 OAuth 凭据时,前端显示第三方按钮,点击走此端点: * - 未配置真实 OAuth 凭据时启用;生产配置 AUTH_GOOGLE_ID/AUTH_GITHUB_ID 后自动 403。
* 直接创建(或复用)对应 provider 的演示用户并建立 session —— 一键登录。 * - 复用/创建对应 provider 的演示用户,然后通过 better-auth 的 signInEmail 建立 session,
* 生产环境配置了 AUTH_GOOGLE_ID / AUTH_GITHUB_ID 后此端点自动禁用(403)。 * 并把 Set-Cookie 头显式透传给浏览器(route handler 里 headers() 不可写,必须走 response)。
*/ */
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
// 双保险:配置了真实凭据时拒绝 mock
if (process.env.AUTH_GOOGLE_ID || process.env.AUTH_GITHUB_ID) { if (process.env.AUTH_GOOGLE_ID || process.env.AUTH_GITHUB_ID) {
return NextResponse.json({ error: "MOCK_DISABLED" }, { status: 403 }); return NextResponse.json({ error: "MOCK_DISABLED" }, { status: 403 });
} }
@@ -27,7 +26,6 @@ export async function POST(req: NextRequest) {
const email = `${provider}-user@demo.dev`; const email = `${provider}-user@demo.dev`;
const name = provider === "google" ? "Google Demo User" : "GitHub Demo User"; const name = provider === "google" ? "Google Demo User" : "GitHub Demo User";
// 复用或创建演示用户
let [u] = await db.select().from(user).where(eq(user.email, email)).limit(1); let [u] = await db.select().from(user).where(eq(user.email, email)).limit(1);
if (!u) { if (!u) {
[u] = await db [u] = await db
@@ -42,11 +40,10 @@ export async function POST(req: NextRequest) {
.returning(); .returning();
} }
// 确保 credential 密码存在(一键登录用)
await db await db
.insert(account) .insert(account)
.values({ .values({
id: `acc_mock_${provider}`, id: nanoid(),
accountId: u.id, accountId: u.id,
providerId: "credential", providerId: "credential",
userId: u.id, userId: u.id,
@@ -54,11 +51,32 @@ export async function POST(req: NextRequest) {
}) })
.onConflictDoNothing(); .onConflictDoNothing();
// 建立 session // 用一个模拟请求头调用 signInEmail;返回的 set-cookie 需要透传
const session = await auth.api.signInEmail({ const requestHeaders = new Headers(req.headers);
requestHeaders.set("cookie", req.headers.get("cookie") || "");
const res = await auth.api.signInEmail({
body: { email, password: "password123" }, body: { email, password: "password123" },
headers: await headers(), headers: requestHeaders,
asResponse: true,
}); });
return NextResponse.json({ ok: true, user: session?.user ?? null }); if (!res.ok) {
const text = await res.text();
return NextResponse.json(
{ error: "MOCK_SIGNIN_FAILED", detail: text.slice(0, 200) },
{ status: 500 }
);
}
const body = await res.json();
const out = NextResponse.json({ ok: true, user: body.user ?? null });
// 透传全部 Set-Cookie(session token)
const setCookies = res.headers.getSetCookie?.() ?? [];
for (const c of setCookies) {
out.headers.append("set-cookie", c);
}
return out;
} }