From 5af59b68296e937663dc85b80e8352408efdded0 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 31 Mar 2026 01:01:44 +0530 Subject: [PATCH 1/5] feat: add chat segment lookup endpoint --- app/api/chats/[id]/segment/route.ts | 35 ++++++ .../__tests__/getChatSegmentHandler.test.ts | 96 ++++++++++++++++ .../__tests__/resolveAccessibleRoom.test.ts | 108 ++++++++++++++++++ lib/chats/getChatSegmentHandler.ts | 32 ++++++ lib/chats/resolveAccessibleRoom.ts | 85 ++++++++++++++ .../selectSegmentRoomByRoomId.ts | 25 ++++ 6 files changed, 381 insertions(+) create mode 100644 app/api/chats/[id]/segment/route.ts create mode 100644 lib/chats/__tests__/getChatSegmentHandler.test.ts create mode 100644 lib/chats/__tests__/resolveAccessibleRoom.test.ts create mode 100644 lib/chats/getChatSegmentHandler.ts create mode 100644 lib/chats/resolveAccessibleRoom.ts create mode 100644 lib/supabase/segment_rooms/selectSegmentRoomByRoomId.ts diff --git a/app/api/chats/[id]/segment/route.ts b/app/api/chats/[id]/segment/route.ts new file mode 100644 index 00000000..f0bbdb72 --- /dev/null +++ b/app/api/chats/[id]/segment/route.ts @@ -0,0 +1,35 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { getChatSegmentHandler } from "@/lib/chats/getChatSegmentHandler"; + +/** + * OPTIONS handler for CORS preflight requests. + * + * @returns A NextResponse with CORS headers. + */ +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: getCorsHeaders(), + }); +} + +/** + * GET /api/chats/[id]/segment + * + * Retrieves the segment associated with a chat room. + * Returns 404 when the room does not exist or is not accessible by the caller. + * + * @param request - The incoming request object. + * @param root0 - The route context object. + * @param root0.params - The dynamic route params containing chat id. + * @returns A NextResponse with segment linkage data or an error. + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +): Promise { + const { id } = await params; + return getChatSegmentHandler(request, id); +} diff --git a/lib/chats/__tests__/getChatSegmentHandler.test.ts b/lib/chats/__tests__/getChatSegmentHandler.test.ts new file mode 100644 index 00000000..00e6ce0e --- /dev/null +++ b/lib/chats/__tests__/getChatSegmentHandler.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { NextRequest, NextResponse } from "next/server"; +import { getChatSegmentHandler } from "@/lib/chats/getChatSegmentHandler"; +import { resolveAccessibleRoom } from "@/lib/chats/resolveAccessibleRoom"; +import { selectSegmentRoomByRoomId } from "@/lib/supabase/segment_rooms/selectSegmentRoomByRoomId"; + +vi.mock("@/lib/chats/resolveAccessibleRoom", () => ({ + resolveAccessibleRoom: vi.fn(), +})); + +vi.mock("@/lib/supabase/segment_rooms/selectSegmentRoomByRoomId", () => ({ + selectSegmentRoomByRoomId: vi.fn(), +})); + +const createRequest = () => new NextRequest("http://localhost/api/chats/chat-id/segment"); + +describe("getChatSegmentHandler", () => { + const roomId = "123e4567-e89b-42d3-a456-426614174000"; + const segmentId = "123e4567-e89b-42d3-a456-426614174003"; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns validation/auth response from resolver", async () => { + vi.mocked(resolveAccessibleRoom).mockResolvedValue( + NextResponse.json({ status: "error", error: "Unauthorized" }, { status: 401 }), + ); + + const response = await getChatSegmentHandler(createRequest(), roomId); + const body = await response.json(); + + expect(response.status).toBe(401); + expect(body).toEqual({ + status: "error", + error: "Unauthorized", + }); + expect(selectSegmentRoomByRoomId).not.toHaveBeenCalled(); + }); + + it("returns linked segment when segment_room exists", async () => { + vi.mocked(resolveAccessibleRoom).mockResolvedValue({ + room: { + id: roomId, + account_id: "11111111-1111-1111-1111-111111111111", + artist_id: null, + topic: "Test", + updated_at: null, + }, + accountId: "11111111-1111-1111-1111-111111111111", + }); + vi.mocked(selectSegmentRoomByRoomId).mockResolvedValue({ + room_id: roomId, + segment_id: segmentId, + id: "123e4567-e89b-42d3-a456-426614174004", + created_at: null, + updated_at: null, + }); + + const response = await getChatSegmentHandler(createRequest(), roomId); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ + status: "success", + room_id: roomId, + segment_id: segmentId, + segment_exists: true, + }); + }); + + it("returns null segment when no segment_room exists", async () => { + vi.mocked(resolveAccessibleRoom).mockResolvedValue({ + room: { + id: roomId, + account_id: "11111111-1111-1111-1111-111111111111", + artist_id: null, + topic: "Test", + updated_at: null, + }, + accountId: "11111111-1111-1111-1111-111111111111", + }); + vi.mocked(selectSegmentRoomByRoomId).mockResolvedValue(null); + + const response = await getChatSegmentHandler(createRequest(), roomId); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ + status: "success", + room_id: roomId, + segment_id: null, + segment_exists: false, + }); + }); +}); diff --git a/lib/chats/__tests__/resolveAccessibleRoom.test.ts b/lib/chats/__tests__/resolveAccessibleRoom.test.ts new file mode 100644 index 00000000..71cd829d --- /dev/null +++ b/lib/chats/__tests__/resolveAccessibleRoom.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { NextRequest, NextResponse } from "next/server"; +import { resolveAccessibleRoom } from "@/lib/chats/resolveAccessibleRoom"; +import { validateAuthContext } from "@/lib/auth/validateAuthContext"; +import { canAccessAccount } from "@/lib/organizations/canAccessAccount"; +import selectRoom from "@/lib/supabase/rooms/selectRoom"; + +vi.mock("@/lib/auth/validateAuthContext", () => ({ + validateAuthContext: vi.fn(), +})); + +vi.mock("@/lib/organizations/canAccessAccount", () => ({ + canAccessAccount: vi.fn(), +})); + +vi.mock("@/lib/supabase/rooms/selectRoom", () => ({ + default: vi.fn(), +})); + +const createRequest = () => new NextRequest("http://localhost/api/chats/chat-id/segment"); + +describe("resolveAccessibleRoom", () => { + const accountId = "11111111-1111-1111-1111-111111111111"; + const roomId = "123e4567-e89b-42d3-a456-426614174000"; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 400 for invalid chat id", async () => { + const result = await resolveAccessibleRoom(createRequest(), "invalid-id"); + expect(result).toBeInstanceOf(NextResponse); + + const response = result as NextResponse; + const body = await response.json(); + expect(response.status).toBe(400); + expect(body).toEqual({ + status: "error", + error: "id must be a valid UUID", + }); + }); + + it("returns auth error response when auth validation fails", async () => { + vi.mocked(validateAuthContext).mockResolvedValue( + NextResponse.json({ status: "error", error: "Unauthorized" }, { status: 401 }), + ); + + const result = await resolveAccessibleRoom(createRequest(), roomId); + expect(result).toBeInstanceOf(NextResponse); + + const response = result as NextResponse; + expect(response.status).toBe(401); + }); + + it("returns 404 when room does not exist", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ accountId, orgId: null }); + vi.mocked(selectRoom).mockResolvedValue(null); + + const result = await resolveAccessibleRoom(createRequest(), roomId); + expect(result).toBeInstanceOf(NextResponse); + + const response = result as NextResponse; + expect(response.status).toBe(404); + }); + + it("returns 404 when user has no access to room owner account", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ accountId, orgId: null }); + vi.mocked(selectRoom).mockResolvedValue({ + id: roomId, + account_id: "123e4567-e89b-42d3-a456-426614174002", + artist_id: null, + topic: "Test", + updated_at: null, + }); + vi.mocked(canAccessAccount).mockResolvedValue(false); + + const result = await resolveAccessibleRoom(createRequest(), roomId); + expect(result).toBeInstanceOf(NextResponse); + + const response = result as NextResponse; + expect(response.status).toBe(404); + }); + + it("returns room when requester is the room owner", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ accountId, orgId: null }); + vi.mocked(selectRoom).mockResolvedValue({ + id: roomId, + account_id: accountId, + artist_id: null, + topic: "Test", + updated_at: null, + }); + + const result = await resolveAccessibleRoom(createRequest(), roomId); + + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual({ + room: { + id: roomId, + account_id: accountId, + artist_id: null, + topic: "Test", + updated_at: null, + }, + accountId, + }); + }); +}); diff --git a/lib/chats/getChatSegmentHandler.ts b/lib/chats/getChatSegmentHandler.ts new file mode 100644 index 00000000..bf08762f --- /dev/null +++ b/lib/chats/getChatSegmentHandler.ts @@ -0,0 +1,32 @@ +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { resolveAccessibleRoom } from "@/lib/chats/resolveAccessibleRoom"; +import { selectSegmentRoomByRoomId } from "@/lib/supabase/segment_rooms/selectSegmentRoomByRoomId"; + +/** + * Handles GET /api/chats/[id]/segment. + * + * Returns the segment associated with a chat room if one exists. + * + * @param request - The incoming request object for auth context. + * @param id - The chat room ID from route params. + * @returns A NextResponse with segment linkage data or an error. + */ +export async function getChatSegmentHandler(request: Request, id: string): Promise { + const roomResult = await resolveAccessibleRoom(request, id); + if (roomResult instanceof NextResponse) { + return roomResult; + } + + const segmentRoom = await selectSegmentRoomByRoomId(roomResult.room.id); + + return NextResponse.json( + { + status: "success", + room_id: roomResult.room.id, + segment_id: segmentRoom?.segment_id || null, + segment_exists: Boolean(segmentRoom?.segment_id), + }, + { status: 200, headers: getCorsHeaders() }, + ); +} diff --git a/lib/chats/resolveAccessibleRoom.ts b/lib/chats/resolveAccessibleRoom.ts new file mode 100644 index 00000000..1ebcd4f5 --- /dev/null +++ b/lib/chats/resolveAccessibleRoom.ts @@ -0,0 +1,85 @@ +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { validateAuthContext } from "@/lib/auth/validateAuthContext"; +import { canAccessAccount } from "@/lib/organizations/canAccessAccount"; +import selectRoom from "@/lib/supabase/rooms/selectRoom"; +import type { Tables } from "@/types/database.types"; + +const chatIdSchema = z.string().uuid("id must be a valid UUID"); + +interface ResolveAccessibleRoomResult { + room: Tables<"rooms">; + accountId: string; +} + +/** + * Resolves a chat room and validates access for the authenticated caller. + * + * @param request - The incoming request object for auth context. + * @param id - The chat room ID from route params. + * @returns The room and authenticated accountId, or an error NextResponse. + */ +export async function resolveAccessibleRoom( + request: Request, + id: string, +): Promise { + const parsedId = chatIdSchema.safeParse(id); + if (!parsedId.success) { + return NextResponse.json( + { + status: "error", + error: parsedId.error.issues[0]?.message || "Invalid chat ID", + }, + { status: 400, headers: getCorsHeaders() }, + ); + } + + const authResult = await validateAuthContext(request); + if (authResult instanceof NextResponse) { + return authResult; + } + + const { accountId } = authResult; + const room = await selectRoom(parsedId.data); + + if (!room) { + return NextResponse.json( + { + status: "error", + error: "Chat room not found", + }, + { status: 404, headers: getCorsHeaders() }, + ); + } + + const roomAccountId = room.account_id; + if (!roomAccountId) { + return NextResponse.json( + { + status: "error", + error: "Chat room is missing account_id", + }, + { status: 500, headers: getCorsHeaders() }, + ); + } + + if (roomAccountId !== accountId) { + const hasAccess = await canAccessAccount({ + currentAccountId: accountId, + targetAccountId: roomAccountId, + }); + + if (!hasAccess) { + return NextResponse.json( + { + status: "error", + error: "Chat room not found", + }, + { status: 404, headers: getCorsHeaders() }, + ); + } + } + + return { room, accountId }; +} diff --git a/lib/supabase/segment_rooms/selectSegmentRoomByRoomId.ts b/lib/supabase/segment_rooms/selectSegmentRoomByRoomId.ts new file mode 100644 index 00000000..b0d7bee2 --- /dev/null +++ b/lib/supabase/segment_rooms/selectSegmentRoomByRoomId.ts @@ -0,0 +1,25 @@ +import supabase from "@/lib/supabase/serverClient"; +import type { Tables } from "@/types/database.types"; + +/** + * Retrieves a segment_rooms row by room_id. + * + * @param roomId - The chat room ID. + * @returns The first matching segment_rooms row or null if none exists. + */ +export async function selectSegmentRoomByRoomId( + roomId: string, +): Promise | null> { + const { data, error } = await supabase + .from("segment_rooms") + .select("*") + .eq("room_id", roomId) + .maybeSingle(); + + if (error) { + console.error("[ERROR] selectSegmentRoomByRoomId:", error); + throw error; + } + + return data; +} From 2a9766736f7e01e1a4d6304092e9ed101ca3f86e Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 31 Mar 2026 01:14:26 +0530 Subject: [PATCH 2/5] refactor: align chat segment endpoint with shared room resolver --- .../__tests__/resolveAccessibleRoom.test.ts | 23 ++++++++----- lib/chats/resolveAccessibleRoom.ts | 32 ++++++------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/lib/chats/__tests__/resolveAccessibleRoom.test.ts b/lib/chats/__tests__/resolveAccessibleRoom.test.ts index 71cd829d..3e34c1bd 100644 --- a/lib/chats/__tests__/resolveAccessibleRoom.test.ts +++ b/lib/chats/__tests__/resolveAccessibleRoom.test.ts @@ -2,21 +2,21 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { NextRequest, NextResponse } from "next/server"; import { resolveAccessibleRoom } from "@/lib/chats/resolveAccessibleRoom"; import { validateAuthContext } from "@/lib/auth/validateAuthContext"; -import { canAccessAccount } from "@/lib/organizations/canAccessAccount"; import selectRoom from "@/lib/supabase/rooms/selectRoom"; +import { buildGetChatsParams } from "@/lib/chats/buildGetChatsParams"; vi.mock("@/lib/auth/validateAuthContext", () => ({ validateAuthContext: vi.fn(), })); -vi.mock("@/lib/organizations/canAccessAccount", () => ({ - canAccessAccount: vi.fn(), -})); - vi.mock("@/lib/supabase/rooms/selectRoom", () => ({ default: vi.fn(), })); +vi.mock("@/lib/chats/buildGetChatsParams", () => ({ + buildGetChatsParams: vi.fn(), +})); + const createRequest = () => new NextRequest("http://localhost/api/chats/chat-id/segment"); describe("resolveAccessibleRoom", () => { @@ -63,7 +63,7 @@ describe("resolveAccessibleRoom", () => { expect(response.status).toBe(404); }); - it("returns 404 when user has no access to room owner account", async () => { + it("returns 403 when user has no access to room owner account", async () => { vi.mocked(validateAuthContext).mockResolvedValue({ accountId, orgId: null }); vi.mocked(selectRoom).mockResolvedValue({ id: roomId, @@ -72,13 +72,16 @@ describe("resolveAccessibleRoom", () => { topic: "Test", updated_at: null, }); - vi.mocked(canAccessAccount).mockResolvedValue(false); + vi.mocked(buildGetChatsParams).mockResolvedValue({ + params: { account_ids: [accountId] }, + error: null, + }); const result = await resolveAccessibleRoom(createRequest(), roomId); expect(result).toBeInstanceOf(NextResponse); const response = result as NextResponse; - expect(response.status).toBe(404); + expect(response.status).toBe(403); }); it("returns room when requester is the room owner", async () => { @@ -90,6 +93,10 @@ describe("resolveAccessibleRoom", () => { topic: "Test", updated_at: null, }); + vi.mocked(buildGetChatsParams).mockResolvedValue({ + params: { account_ids: [accountId] }, + error: null, + }); const result = await resolveAccessibleRoom(createRequest(), roomId); diff --git a/lib/chats/resolveAccessibleRoom.ts b/lib/chats/resolveAccessibleRoom.ts index 1ebcd4f5..6c3be38b 100644 --- a/lib/chats/resolveAccessibleRoom.ts +++ b/lib/chats/resolveAccessibleRoom.ts @@ -2,8 +2,8 @@ import { NextResponse } from "next/server"; import { z } from "zod"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { validateAuthContext } from "@/lib/auth/validateAuthContext"; -import { canAccessAccount } from "@/lib/organizations/canAccessAccount"; import selectRoom from "@/lib/supabase/rooms/selectRoom"; +import { buildGetChatsParams } from "@/lib/chats/buildGetChatsParams"; import type { Tables } from "@/types/database.types"; const chatIdSchema = z.string().uuid("id must be a valid UUID"); @@ -53,30 +53,16 @@ export async function resolveAccessibleRoom( ); } - const roomAccountId = room.account_id; - if (!roomAccountId) { - return NextResponse.json( - { - status: "error", - error: "Chat room is missing account_id", - }, - { status: 500, headers: getCorsHeaders() }, - ); - } - - if (roomAccountId !== accountId) { - const hasAccess = await canAccessAccount({ - currentAccountId: accountId, - targetAccountId: roomAccountId, - }); + const { params } = await buildGetChatsParams({ + account_id: accountId, + }); - if (!hasAccess) { + // If params.account_ids is undefined, it means admin access (all records) + if (params.account_ids && room.account_id) { + if (!params.account_ids.includes(room.account_id)) { return NextResponse.json( - { - status: "error", - error: "Chat room not found", - }, - { status: 404, headers: getCorsHeaders() }, + { status: "error", error: "Access denied to this chat" }, + { status: 403, headers: getCorsHeaders() }, ); } } From a14a63fc5bb7d7b747ffaff674ea33680d7ace27 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 31 Mar 2026 02:02:50 +0530 Subject: [PATCH 3/5] refactor: rename segment room resolver to validateChatAccess --- lib/chats/__tests__/getChatSegmentHandler.test.ts | 12 ++++++------ ...ibleRoom.test.ts => validateChatAccess.test.ts} | 14 +++++++------- lib/chats/getChatSegmentHandler.ts | 4 ++-- ...olveAccessibleRoom.ts => validateChatAccess.ts} | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) rename lib/chats/__tests__/{resolveAccessibleRoom.test.ts => validateChatAccess.test.ts} (86%) rename lib/chats/{resolveAccessibleRoom.ts => validateChatAccess.ts} (97%) diff --git a/lib/chats/__tests__/getChatSegmentHandler.test.ts b/lib/chats/__tests__/getChatSegmentHandler.test.ts index 00e6ce0e..18cfb92c 100644 --- a/lib/chats/__tests__/getChatSegmentHandler.test.ts +++ b/lib/chats/__tests__/getChatSegmentHandler.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { NextRequest, NextResponse } from "next/server"; import { getChatSegmentHandler } from "@/lib/chats/getChatSegmentHandler"; -import { resolveAccessibleRoom } from "@/lib/chats/resolveAccessibleRoom"; +import { validateChatAccess } from "@/lib/chats/validateChatAccess"; import { selectSegmentRoomByRoomId } from "@/lib/supabase/segment_rooms/selectSegmentRoomByRoomId"; -vi.mock("@/lib/chats/resolveAccessibleRoom", () => ({ - resolveAccessibleRoom: vi.fn(), +vi.mock("@/lib/chats/validateChatAccess", () => ({ + validateChatAccess: vi.fn(), })); vi.mock("@/lib/supabase/segment_rooms/selectSegmentRoomByRoomId", () => ({ @@ -23,7 +23,7 @@ describe("getChatSegmentHandler", () => { }); it("returns validation/auth response from resolver", async () => { - vi.mocked(resolveAccessibleRoom).mockResolvedValue( + vi.mocked(validateChatAccess).mockResolvedValue( NextResponse.json({ status: "error", error: "Unauthorized" }, { status: 401 }), ); @@ -39,7 +39,7 @@ describe("getChatSegmentHandler", () => { }); it("returns linked segment when segment_room exists", async () => { - vi.mocked(resolveAccessibleRoom).mockResolvedValue({ + vi.mocked(validateChatAccess).mockResolvedValue({ room: { id: roomId, account_id: "11111111-1111-1111-1111-111111111111", @@ -70,7 +70,7 @@ describe("getChatSegmentHandler", () => { }); it("returns null segment when no segment_room exists", async () => { - vi.mocked(resolveAccessibleRoom).mockResolvedValue({ + vi.mocked(validateChatAccess).mockResolvedValue({ room: { id: roomId, account_id: "11111111-1111-1111-1111-111111111111", diff --git a/lib/chats/__tests__/resolveAccessibleRoom.test.ts b/lib/chats/__tests__/validateChatAccess.test.ts similarity index 86% rename from lib/chats/__tests__/resolveAccessibleRoom.test.ts rename to lib/chats/__tests__/validateChatAccess.test.ts index 3e34c1bd..02b044b9 100644 --- a/lib/chats/__tests__/resolveAccessibleRoom.test.ts +++ b/lib/chats/__tests__/validateChatAccess.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; import { NextRequest, NextResponse } from "next/server"; -import { resolveAccessibleRoom } from "@/lib/chats/resolveAccessibleRoom"; +import { validateChatAccess } from "@/lib/chats/validateChatAccess"; import { validateAuthContext } from "@/lib/auth/validateAuthContext"; import selectRoom from "@/lib/supabase/rooms/selectRoom"; import { buildGetChatsParams } from "@/lib/chats/buildGetChatsParams"; @@ -19,7 +19,7 @@ vi.mock("@/lib/chats/buildGetChatsParams", () => ({ const createRequest = () => new NextRequest("http://localhost/api/chats/chat-id/segment"); -describe("resolveAccessibleRoom", () => { +describe("validateChatAccess", () => { const accountId = "11111111-1111-1111-1111-111111111111"; const roomId = "123e4567-e89b-42d3-a456-426614174000"; @@ -28,7 +28,7 @@ describe("resolveAccessibleRoom", () => { }); it("returns 400 for invalid chat id", async () => { - const result = await resolveAccessibleRoom(createRequest(), "invalid-id"); + const result = await validateChatAccess(createRequest(), "invalid-id"); expect(result).toBeInstanceOf(NextResponse); const response = result as NextResponse; @@ -45,7 +45,7 @@ describe("resolveAccessibleRoom", () => { NextResponse.json({ status: "error", error: "Unauthorized" }, { status: 401 }), ); - const result = await resolveAccessibleRoom(createRequest(), roomId); + const result = await validateChatAccess(createRequest(), roomId); expect(result).toBeInstanceOf(NextResponse); const response = result as NextResponse; @@ -56,7 +56,7 @@ describe("resolveAccessibleRoom", () => { vi.mocked(validateAuthContext).mockResolvedValue({ accountId, orgId: null }); vi.mocked(selectRoom).mockResolvedValue(null); - const result = await resolveAccessibleRoom(createRequest(), roomId); + const result = await validateChatAccess(createRequest(), roomId); expect(result).toBeInstanceOf(NextResponse); const response = result as NextResponse; @@ -77,7 +77,7 @@ describe("resolveAccessibleRoom", () => { error: null, }); - const result = await resolveAccessibleRoom(createRequest(), roomId); + const result = await validateChatAccess(createRequest(), roomId); expect(result).toBeInstanceOf(NextResponse); const response = result as NextResponse; @@ -98,7 +98,7 @@ describe("resolveAccessibleRoom", () => { error: null, }); - const result = await resolveAccessibleRoom(createRequest(), roomId); + const result = await validateChatAccess(createRequest(), roomId); expect(result).not.toBeInstanceOf(NextResponse); expect(result).toEqual({ diff --git a/lib/chats/getChatSegmentHandler.ts b/lib/chats/getChatSegmentHandler.ts index bf08762f..965733f1 100644 --- a/lib/chats/getChatSegmentHandler.ts +++ b/lib/chats/getChatSegmentHandler.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; -import { resolveAccessibleRoom } from "@/lib/chats/resolveAccessibleRoom"; +import { validateChatAccess } from "@/lib/chats/validateChatAccess"; import { selectSegmentRoomByRoomId } from "@/lib/supabase/segment_rooms/selectSegmentRoomByRoomId"; /** @@ -13,7 +13,7 @@ import { selectSegmentRoomByRoomId } from "@/lib/supabase/segment_rooms/selectSe * @returns A NextResponse with segment linkage data or an error. */ export async function getChatSegmentHandler(request: Request, id: string): Promise { - const roomResult = await resolveAccessibleRoom(request, id); + const roomResult = await validateChatAccess(request, id); if (roomResult instanceof NextResponse) { return roomResult; } diff --git a/lib/chats/resolveAccessibleRoom.ts b/lib/chats/validateChatAccess.ts similarity index 97% rename from lib/chats/resolveAccessibleRoom.ts rename to lib/chats/validateChatAccess.ts index 6c3be38b..26cc1e9c 100644 --- a/lib/chats/resolveAccessibleRoom.ts +++ b/lib/chats/validateChatAccess.ts @@ -20,7 +20,7 @@ interface ResolveAccessibleRoomResult { * @param id - The chat room ID from route params. * @returns The room and authenticated accountId, or an error NextResponse. */ -export async function resolveAccessibleRoom( +export async function validateChatAccess( request: Request, id: string, ): Promise { From fe6b77262cda168d21dde0c379abf4f49a82f44e Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 31 Mar 2026 02:40:36 +0530 Subject: [PATCH 4/5] fix: use NextRequest in chat access and segment handler --- lib/chats/getChatSegmentHandler.ts | 3 ++- lib/chats/validateChatAccess.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/chats/getChatSegmentHandler.ts b/lib/chats/getChatSegmentHandler.ts index 965733f1..74357637 100644 --- a/lib/chats/getChatSegmentHandler.ts +++ b/lib/chats/getChatSegmentHandler.ts @@ -1,3 +1,4 @@ +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { validateChatAccess } from "@/lib/chats/validateChatAccess"; @@ -12,7 +13,7 @@ import { selectSegmentRoomByRoomId } from "@/lib/supabase/segment_rooms/selectSe * @param id - The chat room ID from route params. * @returns A NextResponse with segment linkage data or an error. */ -export async function getChatSegmentHandler(request: Request, id: string): Promise { +export async function getChatSegmentHandler(request: NextRequest, id: string): Promise { const roomResult = await validateChatAccess(request, id); if (roomResult instanceof NextResponse) { return roomResult; diff --git a/lib/chats/validateChatAccess.ts b/lib/chats/validateChatAccess.ts index 26cc1e9c..80373f2e 100644 --- a/lib/chats/validateChatAccess.ts +++ b/lib/chats/validateChatAccess.ts @@ -1,3 +1,4 @@ +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { z } from "zod"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; @@ -21,7 +22,7 @@ interface ResolveAccessibleRoomResult { * @returns The room and authenticated accountId, or an error NextResponse. */ export async function validateChatAccess( - request: Request, + request: NextRequest, id: string, ): Promise { const parsedId = chatIdSchema.safeParse(id); From 58f8bb8e7453aa26e7fc93b5c5f12545a31391c4 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 31 Mar 2026 02:42:28 +0530 Subject: [PATCH 5/5] style: format segment handler --- lib/chats/getChatSegmentHandler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/chats/getChatSegmentHandler.ts b/lib/chats/getChatSegmentHandler.ts index 74357637..ab3d8518 100644 --- a/lib/chats/getChatSegmentHandler.ts +++ b/lib/chats/getChatSegmentHandler.ts @@ -13,7 +13,10 @@ import { selectSegmentRoomByRoomId } from "@/lib/supabase/segment_rooms/selectSe * @param id - The chat room ID from route params. * @returns A NextResponse with segment linkage data or an error. */ -export async function getChatSegmentHandler(request: NextRequest, id: string): Promise { +export async function getChatSegmentHandler( + request: NextRequest, + id: string, +): Promise { const roomResult = await validateChatAccess(request, id); if (roomResult instanceof NextResponse) { return roomResult;