修复 SSE 成员校验未生效的问题
This commit is contained in:
@@ -103,7 +103,13 @@
|
||||
|
||||
## 中优问题(P2)
|
||||
|
||||
### P2-1 SSE 成员校验逻辑未真正启用(前端未传 `userId`)
|
||||
### P2-1 SSE 成员校验逻辑未真正启用(前端未传 `userId`)【已完成】
|
||||
- 修复状态:✅ 已完成(2026-03-03)
|
||||
- 修复内容:
|
||||
- `useRoomPolling` 改为显式接收 `userId`,SSE 连接带上 `?userId=...`;
|
||||
- `room/[id]/page` 传入当前用户 ID;
|
||||
- `GET /api/room/[id]/events` 强制 `userId` 必填并做成员校验(无 userId 返回 401,非成员返回 403);
|
||||
- 更新 hook 测试覆盖 SSE URL 与缺失 userId 场景。
|
||||
- 证据:
|
||||
- `src/app/api/room/[id]/events/route.ts:14-18`(仅在 query 存在 `userId` 时校验)
|
||||
- `src/hooks/useRoomPolling.ts:32`(实际建立 SSE 连接时未携带 `userId`)
|
||||
|
||||
@@ -12,15 +12,25 @@ export async function GET(
|
||||
|
||||
const url = new URL(req.url);
|
||||
const userId = url.searchParams.get("userId");
|
||||
if (!userId) {
|
||||
return new Response(JSON.stringify({ error: "missing_user_id" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const data = await getRoomData(id);
|
||||
if (data && !data.users.includes(userId)) {
|
||||
return new Response(JSON.stringify({ error: "not_a_member" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
const data = await getRoomData(id);
|
||||
if (!data) {
|
||||
return new Response(JSON.stringify({ error: "room_not_found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (!data.users.includes(userId)) {
|
||||
return new Response(JSON.stringify({ error: "not_a_member" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function RoomPage() {
|
||||
const {
|
||||
userCount, match, matchType, matchLikes, runnerUps, likeCounts, swipeCounts,
|
||||
restaurants, notFound, mutate, creatorId, locked, users, userProfiles, scene,
|
||||
} = useRoomPolling(roomId);
|
||||
} = useRoomPolling(roomId, userId || undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!roomId) return;
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, waitFor, act } from "@testing-library/react";
|
||||
|
||||
let esInstances: { onmessage?: (e: MessageEvent) => void; onerror?: () => void; onopen?: () => void; close: ReturnType<typeof vi.fn> }[] = [];
|
||||
let esInstances: {
|
||||
url: string;
|
||||
onmessage?: (e: MessageEvent) => void;
|
||||
onerror?: () => void;
|
||||
onopen?: () => void;
|
||||
close: ReturnType<typeof vi.fn>;
|
||||
}[] = [];
|
||||
|
||||
class MockEventSource {
|
||||
onmessage: ((e: MessageEvent) => void) | null = null;
|
||||
onerror: (() => void) | null = null;
|
||||
onopen: (() => void) | null = null;
|
||||
url: string;
|
||||
onmessage: ((e: MessageEvent) => void) | undefined;
|
||||
onerror: (() => void) | undefined;
|
||||
onopen: (() => void) | undefined;
|
||||
close = vi.fn();
|
||||
|
||||
constructor(_url: string) {
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
esInstances.push(this);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +60,7 @@ afterEach(() => {
|
||||
|
||||
describe("useRoomPolling", () => {
|
||||
it("returns room data from SWR", () => {
|
||||
const { result } = renderHook(() => useRoomPolling("ROOM01"));
|
||||
const { result } = renderHook(() => useRoomPolling("ROOM01", "user-1"));
|
||||
|
||||
expect(result.current.userCount).toBe(2);
|
||||
expect(result.current.users).toEqual(["user-1", "user-2"]);
|
||||
@@ -60,12 +68,18 @@ describe("useRoomPolling", () => {
|
||||
});
|
||||
|
||||
it("creates EventSource connection", () => {
|
||||
renderHook(() => useRoomPolling("ROOM01"));
|
||||
renderHook(() => useRoomPolling("ROOM01", "user-1"));
|
||||
expect(esInstances.length).toBeGreaterThan(0);
|
||||
expect(esInstances[0].url).toContain("/api/room/ROOM01/events?userId=user-1");
|
||||
});
|
||||
|
||||
it("does not create EventSource when userId is missing", () => {
|
||||
renderHook(() => useRoomPolling("ROOM01"));
|
||||
expect(esInstances.length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns defaults when no roomId", () => {
|
||||
const { result } = renderHook(() => useRoomPolling(undefined));
|
||||
const { result } = renderHook(() => useRoomPolling(undefined, "user-1"));
|
||||
expect(result.current.userCount).toBe(2);
|
||||
});
|
||||
|
||||
@@ -78,12 +92,12 @@ describe("useRoomPolling", () => {
|
||||
mutate: vi.fn(),
|
||||
} as never);
|
||||
|
||||
const { result } = renderHook(() => useRoomPolling("ROOM01"));
|
||||
const { result } = renderHook(() => useRoomPolling("ROOM01", "user-1"));
|
||||
expect(result.current.notFound).toBe(true);
|
||||
});
|
||||
|
||||
it("cleans up EventSource on unmount", () => {
|
||||
const { unmount } = renderHook(() => useRoomPolling("ROOM01"));
|
||||
const { unmount } = renderHook(() => useRoomPolling("ROOM01", "user-1"));
|
||||
const es = esInstances[esInstances.length - 1];
|
||||
unmount();
|
||||
expect(es.close).toHaveBeenCalled();
|
||||
|
||||
@@ -10,7 +10,10 @@ function backoffMs(attempt: number): number {
|
||||
return Math.min(2000 * Math.pow(2, attempt) + Math.random() * 500, 60_000);
|
||||
}
|
||||
|
||||
export function useRoomPolling(roomId: string | undefined) {
|
||||
export function useRoomPolling(
|
||||
roomId: string | undefined,
|
||||
userId?: string,
|
||||
) {
|
||||
const { data, error, isLoading, mutate } = useSWR<RoomStatus>(
|
||||
roomId ? `/api/room/${roomId}` : null,
|
||||
fetcher,
|
||||
@@ -28,8 +31,10 @@ export function useRoomPolling(roomId: string | undefined) {
|
||||
const notFound = error?.message === "NOT_FOUND";
|
||||
|
||||
useEffect(() => {
|
||||
if (!roomId) return;
|
||||
const es = new EventSource(`/api/room/${roomId}/events`);
|
||||
if (!roomId || !userId) return;
|
||||
const es = new EventSource(
|
||||
`/api/room/${roomId}/events?userId=${encodeURIComponent(userId)}`,
|
||||
);
|
||||
|
||||
const clearFallback = () => {
|
||||
if (fallbackRef.current) {
|
||||
@@ -68,7 +73,7 @@ export function useRoomPolling(roomId: string | undefined) {
|
||||
es.close();
|
||||
clearFallback();
|
||||
};
|
||||
}, [roomId, mutate]);
|
||||
}, [roomId, userId, mutate]);
|
||||
|
||||
return {
|
||||
userCount: data?.userCount ?? 0,
|
||||
|
||||
Reference in New Issue
Block a user