fix: unify panic room code format and validate room join id
This commit is contained in:
@@ -74,11 +74,11 @@ describe("POST /api/room/[id]/join", () => {
|
||||
it("returns 404 when room not found", async () => {
|
||||
mockAtomicUpdate.mockResolvedValue(null);
|
||||
|
||||
const req = createRequest("/api/room/NONEXIST/join", {
|
||||
const req = createRequest("/api/room/ABC123/join", {
|
||||
method: "POST",
|
||||
body: { userId: "user-1" },
|
||||
});
|
||||
const ctx = createRouteContext({ id: "NONEXIST" });
|
||||
const ctx = createRouteContext({ id: "ABC123" });
|
||||
const res = await POST(req, ctx);
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
@@ -92,4 +92,14 @@ describe("POST /api/room/[id]/join", () => {
|
||||
const res = await POST(req, ctx);
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("returns 400 when room id format is invalid", async () => {
|
||||
const req = createRequest("/api/room/1234/join", {
|
||||
method: "POST",
|
||||
body: { userId: "user-1" },
|
||||
});
|
||||
const ctx = createRouteContext({ id: "1234" });
|
||||
const res = await POST(req, ctx);
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,14 +2,16 @@ import { NextResponse } from "next/server";
|
||||
import { atomicUpdateRoom } from "@/lib/roomRepository";
|
||||
import { notify } from "@/lib/roomEvents";
|
||||
import { apiHandler, ApiError, requireUserId } from "@/lib/api";
|
||||
import { validatePanicRoomId } from "@/lib/validation";
|
||||
|
||||
export const POST = apiHandler(async (req, { params }) => {
|
||||
const { id } = await params;
|
||||
const roomId = validatePanicRoomId(id);
|
||||
const { userId } = await req.json();
|
||||
|
||||
requireUserId(userId);
|
||||
|
||||
const updated = await atomicUpdateRoom(id, (data) => {
|
||||
const updated = await atomicUpdateRoom(roomId, (data) => {
|
||||
if (data.kickedUsers.includes(userId)) {
|
||||
throw new ApiError("你已被移出该房间", 403);
|
||||
}
|
||||
@@ -24,10 +26,10 @@ export const POST = apiHandler(async (req, { params }) => {
|
||||
|
||||
if (!updated) throw new ApiError("房间不存在或已过期", 404);
|
||||
|
||||
notify(id);
|
||||
notify(roomId);
|
||||
|
||||
return NextResponse.json({
|
||||
roomId: id,
|
||||
roomId,
|
||||
userCount: updated.users.length,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,7 @@ const mockFetch = vi.fn().mockResolvedValue({
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
|
||||
import PanicPage from "./page";
|
||||
import { joinRoom } from "@/lib/room";
|
||||
|
||||
const toastCtx: ToastContextValue = { show: vi.fn() };
|
||||
|
||||
@@ -97,4 +98,20 @@ describe("PanicPage", () => {
|
||||
renderPage();
|
||||
expect(screen.getAllByText("餐厅").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("normalizes room code as 6-char alphanumeric when joining", async () => {
|
||||
renderPage();
|
||||
const input = screen.getByPlaceholderText("输入 6 位房间号");
|
||||
fireEvent.change(input, { target: { value: "ab12-cd!" } });
|
||||
expect(input).toHaveValue("AB12CD");
|
||||
|
||||
const submitButton = screen.getByLabelText("加入房间");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(joinRoom)).toHaveBeenCalledWith("AB12CD", "user-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+15
-10
@@ -170,15 +170,16 @@ export default function PanicPage() {
|
||||
|
||||
const handleJoin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (roomCode.length !== 4) {
|
||||
setError("请输入 4 位房间号");
|
||||
if (roomCode.length !== 6) {
|
||||
setError("请输入 6 位房间号");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
await joinRoom(roomCode, getUserId());
|
||||
router.push(`/room/${roomCode}`);
|
||||
const normalizedRoomCode = roomCode.toUpperCase();
|
||||
await joinRoom(normalizedRoomCode, getUserId());
|
||||
router.push(`/room/${normalizedRoomCode}`);
|
||||
} catch (e) {
|
||||
console.error("PanicPage: handleJoin failed:", e);
|
||||
setError("房间不存在,请检查房间号");
|
||||
@@ -562,13 +563,17 @@ export default function PanicPage() {
|
||||
<form onSubmit={handleJoin} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={4}
|
||||
placeholder="输入 4 位房间号"
|
||||
inputMode="text"
|
||||
pattern="[A-Za-z0-9]*"
|
||||
maxLength={6}
|
||||
placeholder="输入 6 位房间号"
|
||||
value={roomCode}
|
||||
onChange={(e) => {
|
||||
setRoomCode(e.target.value.replace(/\D/g, "").slice(0, 4));
|
||||
const normalized = e.target.value
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]/g, "")
|
||||
.slice(0, 6);
|
||||
setRoomCode(normalized);
|
||||
setError("");
|
||||
}}
|
||||
disabled={loading}
|
||||
@@ -576,7 +581,7 @@ export default function PanicPage() {
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || roomCode.length !== 4}
|
||||
disabled={loading || roomCode.length !== 6}
|
||||
aria-label="加入房间"
|
||||
className="flex h-11 w-11 items-center justify-center rounded-xl bg-elevated text-secondary ring-1 ring-subtle transition-colors hover:bg-subtle disabled:opacity-30"
|
||||
>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
validateIdeaContent,
|
||||
validateRoomName,
|
||||
requireString,
|
||||
validatePanicRoomId,
|
||||
} from "@/lib/validation";
|
||||
|
||||
describe("validateUsername", () => {
|
||||
@@ -125,3 +126,16 @@ describe("requireString", () => {
|
||||
expect(() => requireString(123, "字段")).toThrow(ApiError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validatePanicRoomId", () => {
|
||||
it("accepts 6-char alphanumeric room id and normalizes uppercase", () => {
|
||||
expect(validatePanicRoomId("ab12cd")).toBe("AB12CD");
|
||||
expect(validatePanicRoomId("ROOM01")).toBe("ROOM01");
|
||||
});
|
||||
|
||||
it("rejects invalid room id format", () => {
|
||||
expect(() => validatePanicRoomId("1234")).toThrow(ApiError);
|
||||
expect(() => validatePanicRoomId("1234567")).toThrow(ApiError);
|
||||
expect(() => validatePanicRoomId("12-45a")).toThrow(ApiError);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,3 +48,16 @@ export function requireString(value: unknown, fieldName: string): string {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const PANIC_ROOM_ID_REGEX = /^[A-Z0-9]{6}$/;
|
||||
|
||||
export function validatePanicRoomId(raw: unknown): string {
|
||||
if (!raw || typeof raw !== "string") {
|
||||
throw new ApiError("roomId 不能为空");
|
||||
}
|
||||
const roomId = raw.trim().toUpperCase();
|
||||
if (!PANIC_ROOM_ID_REGEX.test(roomId)) {
|
||||
throw new ApiError("房间号格式无效,应为 6 位字母数字", 400);
|
||||
}
|
||||
return roomId;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user