diff --git a/CLAUDE.md b/CLAUDE.md index 4ee4cafd..a64148a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -144,6 +144,74 @@ export async function selectTableName({ - All API routes should have JSDoc comments - Run `pnpm lint` before committing +## Test-Driven Development (TDD) + +**CRITICAL: Always write tests BEFORE implementing new features or fixing bugs.** + +### TDD Workflow + +1. **Write failing tests first** - Create tests in `lib/[domain]/__tests__/[filename].test.ts` that describe the expected behavior +2. **Run tests to verify they fail** - `pnpm test path/to/test.ts` +3. **Implement the code** - Write the minimum code needed to make tests pass +4. **Run tests to verify they pass** - All tests should be green +5. **Refactor if needed** - Clean up while keeping tests green + +### Test File Location + +Tests live alongside the code they test: +``` +lib/ +├── chats/ +│ ├── __tests__/ +│ │ └── updateChatHandler.test.ts +│ ├── updateChatHandler.ts +│ └── validateUpdateChatBody.ts +``` + +### Test Pattern + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextRequest, NextResponse } from "next/server"; + +// Mock dependencies +vi.mock("@/lib/networking/getCorsHeaders", () => ({ + getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })), +})); + +describe("functionName", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("successful cases", () => { + it("does something when condition is met", async () => { + // Arrange + vi.mocked(dependency).mockResolvedValue(mockData); + + // Act + const result = await functionName(input); + + // Assert + expect(result.status).toBe(200); + }); + }); + + describe("error cases", () => { + it("returns 400 when validation fails", async () => { + // Test error handling + }); + }); +}); +``` + +### When to Write Tests + +- **New API endpoints**: Write tests for all success and error paths +- **New handlers**: Test business logic with mocked dependencies +- **Bug fixes**: Write a failing test that reproduces the bug, then fix it +- **Validation functions**: Test all valid and invalid input combinations + ## Authentication **Never use `account_id` in request bodies or tool schemas.** Always derive the account ID from authentication: diff --git a/app/api/chats/route.ts b/app/api/chats/route.ts index ca58f6cf..dbfe7cfb 100644 --- a/app/api/chats/route.ts +++ b/app/api/chats/route.ts @@ -3,6 +3,7 @@ import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { createChatHandler } from "@/lib/chats/createChatHandler"; import { getChatsHandler } from "@/lib/chats/getChatsHandler"; +import { updateChatHandler } from "@/lib/chats/updateChatHandler"; /** * OPTIONS handler for CORS preflight requests. @@ -52,3 +53,21 @@ export async function GET(request: NextRequest): Promise { export async function POST(request: NextRequest): Promise { return createChatHandler(request); } + +/** + * PATCH /api/chats + * + * Update a chat room's topic (display name). + * + * Authentication: x-api-key header or Authorization Bearer token required. + * + * Body parameters: + * - chatId (required): UUID of the chat room to update + * - topic (required): New display name for the chat (3-50 characters) + * + * @param request - The request object + * @returns A NextResponse with the updated chat or an error + */ +export async function PATCH(request: NextRequest): Promise { + return updateChatHandler(request); +} diff --git a/lib/chats/__tests__/updateChatHandler.test.ts b/lib/chats/__tests__/updateChatHandler.test.ts new file mode 100644 index 00000000..cbb33603 --- /dev/null +++ b/lib/chats/__tests__/updateChatHandler.test.ts @@ -0,0 +1,167 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextRequest, NextResponse } from "next/server"; +import { updateChatHandler } from "../updateChatHandler"; + +vi.mock("@/lib/networking/getCorsHeaders", () => ({ + getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })), +})); + +vi.mock("@/lib/chats/validateUpdateChatBody", () => ({ + validateUpdateChatBody: vi.fn(), +})); + +vi.mock("@/lib/supabase/rooms/updateRoom", () => ({ + updateRoom: vi.fn(), +})); + +import { validateUpdateChatBody } from "@/lib/chats/validateUpdateChatBody"; +import { updateRoom } from "@/lib/supabase/rooms/updateRoom"; + +describe("updateChatHandler", () => { + const mockRequest = () => { + return new NextRequest("http://localhost/api/chats", { + method: "PATCH", + headers: { "x-api-key": "test-key", "Content-Type": "application/json" }, + body: JSON.stringify({}), + }); + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("successful update", () => { + it("updates chat topic and returns success response", async () => { + const chatId = "123e4567-e89b-12d3-a456-426614174001"; + const accountId = "123e4567-e89b-12d3-a456-426614174000"; + const newTopic = "My Updated Chat"; + + vi.mocked(validateUpdateChatBody).mockResolvedValue({ + chatId, + topic: newTopic, + }); + + vi.mocked(updateRoom).mockResolvedValue({ + id: chatId, + account_id: accountId, + artist_id: null, + topic: newTopic, + updated_at: "2024-01-02T00:00:00Z", + }); + + const request = mockRequest(); + const response = await updateChatHandler(request); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body).toEqual({ + status: "success", + chat: { + id: chatId, + account_id: accountId, + topic: newTopic, + updated_at: "2024-01-02T00:00:00Z", + artist_id: null, + }, + }); + + expect(updateRoom).toHaveBeenCalledWith(chatId, { topic: newTopic }); + }); + }); + + describe("validation errors", () => { + it("returns 400 from validation when body is invalid", async () => { + vi.mocked(validateUpdateChatBody).mockResolvedValue( + NextResponse.json( + { status: "error", error: "chatId must be a valid UUID" }, + { status: 400 }, + ), + ); + + const request = mockRequest(); + const response = await updateChatHandler(request); + + expect(response.status).toBe(400); + expect(updateRoom).not.toHaveBeenCalled(); + }); + + it("returns 401 from validation when auth fails", async () => { + vi.mocked(validateUpdateChatBody).mockResolvedValue( + NextResponse.json( + { status: "error", error: "Unauthorized" }, + { status: 401 }, + ), + ); + + const request = mockRequest(); + const response = await updateChatHandler(request); + + expect(response.status).toBe(401); + expect(updateRoom).not.toHaveBeenCalled(); + }); + + it("returns 404 from validation when chat not found", async () => { + vi.mocked(validateUpdateChatBody).mockResolvedValue( + NextResponse.json( + { status: "error", error: "Chat room not found" }, + { status: 404 }, + ), + ); + + const request = mockRequest(); + const response = await updateChatHandler(request); + + expect(response.status).toBe(404); + expect(updateRoom).not.toHaveBeenCalled(); + }); + + it("returns 403 from validation when access denied", async () => { + vi.mocked(validateUpdateChatBody).mockResolvedValue( + NextResponse.json( + { status: "error", error: "Access denied to this chat" }, + { status: 403 }, + ), + ); + + const request = mockRequest(); + const response = await updateChatHandler(request); + + expect(response.status).toBe(403); + expect(updateRoom).not.toHaveBeenCalled(); + }); + }); + + describe("update errors", () => { + it("returns 500 when updateRoom fails", async () => { + vi.mocked(validateUpdateChatBody).mockResolvedValue({ + chatId: "123e4567-e89b-12d3-a456-426614174001", + topic: "New Topic", + }); + + vi.mocked(updateRoom).mockResolvedValue(null); + + const request = mockRequest(); + const response = await updateChatHandler(request); + + expect(response.status).toBe(500); + const body = await response.json(); + expect(body.error).toContain("Failed to update"); + }); + + it("returns 500 when updateRoom throws", async () => { + vi.mocked(validateUpdateChatBody).mockResolvedValue({ + chatId: "123e4567-e89b-12d3-a456-426614174001", + topic: "New Topic", + }); + + vi.mocked(updateRoom).mockRejectedValue(new Error("Database error")); + + const request = mockRequest(); + const response = await updateChatHandler(request); + + expect(response.status).toBe(500); + const body = await response.json(); + expect(body.error).toBe("Database error"); + }); + }); +}); diff --git a/lib/chats/__tests__/validateUpdateChatBody.test.ts b/lib/chats/__tests__/validateUpdateChatBody.test.ts new file mode 100644 index 00000000..d3328d98 --- /dev/null +++ b/lib/chats/__tests__/validateUpdateChatBody.test.ts @@ -0,0 +1,418 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NextRequest, NextResponse } from "next/server"; +import { validateUpdateChatBody } from "../validateUpdateChatBody"; + +vi.mock("@/lib/networking/getCorsHeaders", () => ({ + getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })), +})); + +vi.mock("@/lib/auth/validateAuthContext", () => ({ + validateAuthContext: vi.fn(), +})); + +vi.mock("@/lib/supabase/rooms/selectRoom", () => ({ + default: vi.fn(), +})); + +vi.mock("@/lib/chats/buildGetChatsParams", () => ({ + buildGetChatsParams: vi.fn(), +})); + +import { validateAuthContext } from "@/lib/auth/validateAuthContext"; +import selectRoom from "@/lib/supabase/rooms/selectRoom"; +import { buildGetChatsParams } from "@/lib/chats/buildGetChatsParams"; + +describe("validateUpdateChatBody", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const createRequest = (body: object) => { + return new NextRequest("http://localhost/api/chats", { + method: "PATCH", + headers: { "Content-Type": "application/json", "x-api-key": "test-key" }, + body: JSON.stringify(body), + }); + }; + + describe("successful validation", () => { + it("returns validated data when user owns the chat", async () => { + const chatId = "123e4567-e89b-12d3-a456-426614174000"; + const accountId = "123e4567-e89b-12d3-a456-426614174001"; + const topic = "Valid Topic"; + const room = { + id: chatId, + account_id: accountId, + artist_id: null, + topic: "Old Topic", + updated_at: "2024-01-01T00:00:00Z", + }; + + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId, + orgId: null, + authToken: "test-key", + }); + + vi.mocked(selectRoom).mockResolvedValue(room); + + vi.mocked(buildGetChatsParams).mockResolvedValue({ + params: { account_ids: [accountId] }, + error: null, + }); + + const request = createRequest({ chatId, topic }); + const result = await validateUpdateChatBody(request); + + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual({ chatId, topic }); + }); + + it("accepts topic at minimum length (3 chars)", async () => { + const chatId = "123e4567-e89b-12d3-a456-426614174000"; + const accountId = "123e4567-e89b-12d3-a456-426614174001"; + const topic = "abc"; + const room = { + id: chatId, + account_id: accountId, + artist_id: null, + topic: "Old", + updated_at: "2024-01-01T00:00:00Z", + }; + + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId, + orgId: null, + authToken: "test-key", + }); + + vi.mocked(selectRoom).mockResolvedValue(room); + + vi.mocked(buildGetChatsParams).mockResolvedValue({ + params: { account_ids: [accountId] }, + error: null, + }); + + const request = createRequest({ chatId, topic }); + const result = await validateUpdateChatBody(request); + + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual({ chatId, topic }); + }); + + it("accepts topic at maximum length (50 chars)", async () => { + const chatId = "123e4567-e89b-12d3-a456-426614174000"; + const accountId = "123e4567-e89b-12d3-a456-426614174001"; + const topic = "a".repeat(50); + const room = { + id: chatId, + account_id: accountId, + artist_id: null, + topic: "Old", + updated_at: "2024-01-01T00:00:00Z", + }; + + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId, + orgId: null, + authToken: "test-key", + }); + + vi.mocked(selectRoom).mockResolvedValue(room); + + vi.mocked(buildGetChatsParams).mockResolvedValue({ + params: { account_ids: [accountId] }, + error: null, + }); + + const request = createRequest({ chatId, topic }); + const result = await validateUpdateChatBody(request); + + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual({ chatId, topic }); + }); + + it("allows org key to access member's chat", async () => { + const chatId = "123e4567-e89b-12d3-a456-426614174000"; + const memberAccountId = "123e4567-e89b-12d3-a456-426614174001"; + const orgId = "123e4567-e89b-12d3-a456-426614174002"; + const topic = "Valid Topic"; + const room = { + id: chatId, + account_id: memberAccountId, + artist_id: null, + topic: "Old", + updated_at: "2024-01-01T00:00:00Z", + }; + + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: orgId, + orgId, + authToken: "org-key", + }); + + vi.mocked(selectRoom).mockResolvedValue(room); + + vi.mocked(buildGetChatsParams).mockResolvedValue({ + params: { account_ids: [memberAccountId, "other-member"] }, + error: null, + }); + + const request = createRequest({ chatId, topic }); + const result = await validateUpdateChatBody(request); + + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual({ chatId, topic }); + }); + + it("allows admin access when account_ids is undefined", async () => { + const chatId = "123e4567-e89b-12d3-a456-426614174000"; + const accountId = "123e4567-e89b-12d3-a456-426614174001"; + const topic = "Valid Topic"; + const room = { + id: chatId, + account_id: "any-account", + artist_id: null, + topic: "Old", + updated_at: "2024-01-01T00:00:00Z", + }; + + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId, + orgId: "admin-org", + authToken: "admin-key", + }); + + vi.mocked(selectRoom).mockResolvedValue(room); + + vi.mocked(buildGetChatsParams).mockResolvedValue({ + params: {}, // No account_ids = admin access + error: null, + }); + + const request = createRequest({ chatId, topic }); + const result = await validateUpdateChatBody(request); + + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual({ chatId, topic }); + }); + }); + + describe("body validation errors", () => { + it("returns 400 when chatId is missing", async () => { + const request = createRequest({ topic: "Valid Topic" }); + + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.status).toBe("error"); + }); + + it("returns 400 when chatId is not a valid UUID", async () => { + const request = createRequest({ chatId: "not-a-uuid", topic: "Valid Topic" }); + + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error).toContain("UUID"); + }); + + it("returns 400 when topic is missing", async () => { + const request = createRequest({ chatId: "123e4567-e89b-12d3-a456-426614174000" }); + + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(400); + }); + + it("returns 400 when topic is too short", async () => { + const request = createRequest({ + chatId: "123e4567-e89b-12d3-a456-426614174000", + topic: "ab", + }); + + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error).toContain("3 and 50"); + }); + + it("returns 400 when topic is too long", async () => { + const request = createRequest({ + chatId: "123e4567-e89b-12d3-a456-426614174000", + topic: "a".repeat(51), + }); + + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error).toContain("3 and 50"); + }); + }); + + describe("JSON parsing errors", () => { + it("handles invalid JSON gracefully", async () => { + const request = new NextRequest("http://localhost/api/chats", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: "not valid json", + }); + + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(400); + }); + + it("handles empty body gracefully", async () => { + const request = new NextRequest("http://localhost/api/chats", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + }); + + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(400); + }); + }); + + describe("authentication errors", () => { + it("returns 401 when auth fails", async () => { + vi.mocked(validateAuthContext).mockResolvedValue( + NextResponse.json( + { status: "error", error: "Unauthorized" }, + { status: 401 }, + ), + ); + + const request = createRequest({ + chatId: "123e4567-e89b-12d3-a456-426614174000", + topic: "Valid Topic", + }); + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(401); + }); + }); + + describe("room not found errors", () => { + it("returns 404 when chat does not exist", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "123e4567-e89b-12d3-a456-426614174001", + orgId: null, + authToken: "test-key", + }); + + vi.mocked(selectRoom).mockResolvedValue(null); + + const request = createRequest({ + chatId: "123e4567-e89b-12d3-a456-426614174000", + topic: "Valid Topic", + }); + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(404); + const body = await response.json(); + expect(body.error).toContain("not found"); + }); + }); + + describe("access denied errors", () => { + it("returns 403 when user tries to update another user's chat", async () => { + const userAccountId = "123e4567-e89b-12d3-a456-426614174001"; + const otherAccountId = "123e4567-e89b-12d3-a456-426614174002"; + const chatId = "123e4567-e89b-12d3-a456-426614174000"; + const room = { + id: chatId, + account_id: otherAccountId, + artist_id: null, + topic: "Old Topic", + updated_at: "2024-01-01T00:00:00Z", + }; + + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: userAccountId, + orgId: null, + authToken: "test-key", + }); + + vi.mocked(selectRoom).mockResolvedValue(room); + + vi.mocked(buildGetChatsParams).mockResolvedValue({ + params: { account_ids: [userAccountId] }, + error: null, + }); + + const request = createRequest({ + chatId, + topic: "Valid Topic", + }); + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(403); + const body = await response.json(); + expect(body.error).toContain("Access denied"); + }); + + it("returns 403 when org key cannot access non-member's chat", async () => { + const orgId = "123e4567-e89b-12d3-a456-426614174001"; + const nonMemberAccountId = "123e4567-e89b-12d3-a456-426614174002"; + const chatId = "123e4567-e89b-12d3-a456-426614174000"; + const room = { + id: chatId, + account_id: nonMemberAccountId, + artist_id: null, + topic: "Old Topic", + updated_at: "2024-01-01T00:00:00Z", + }; + + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: orgId, + orgId, + authToken: "org-key", + }); + + vi.mocked(selectRoom).mockResolvedValue(room); + + vi.mocked(buildGetChatsParams).mockResolvedValue({ + params: { account_ids: ["member-1", "member-2"] }, // non-member not in list + error: null, + }); + + const request = createRequest({ + chatId, + topic: "Valid Topic", + }); + const result = await validateUpdateChatBody(request); + + expect(result).toBeInstanceOf(NextResponse); + const response = result as NextResponse; + expect(response.status).toBe(403); + const body = await response.json(); + expect(body.error).toContain("Access denied"); + }); + }); +}); diff --git a/lib/chats/updateChatHandler.ts b/lib/chats/updateChatHandler.ts new file mode 100644 index 00000000..dba51a74 --- /dev/null +++ b/lib/chats/updateChatHandler.ts @@ -0,0 +1,50 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { validateUpdateChatBody } from "./validateUpdateChatBody"; +import { updateRoom } from "@/lib/supabase/rooms/updateRoom"; + +/** + * Handles PATCH /api/chats - Update a chat room's topic. + * + * @param request - The NextRequest object + * @returns NextResponse with updated chat data or error + */ +export async function updateChatHandler(request: NextRequest): Promise { + const validated = await validateUpdateChatBody(request); + if (validated instanceof NextResponse) { + return validated; + } + + const { chatId, topic } = validated; + + try { + const updated = await updateRoom(chatId, { topic }); + if (!updated) { + return NextResponse.json( + { status: "error", error: "Failed to update chat" }, + { status: 500, headers: getCorsHeaders() }, + ); + } + + return NextResponse.json( + { + status: "success", + chat: { + id: updated.id, + account_id: updated.account_id, + topic: updated.topic, + updated_at: updated.updated_at, + artist_id: updated.artist_id, + }, + }, + { status: 200, headers: getCorsHeaders() }, + ); + } catch (error) { + const message = error instanceof Error ? error.message : "Server error"; + return NextResponse.json( + { status: "error", error: message }, + { status: 500, headers: getCorsHeaders() }, + ); + } +} diff --git a/lib/chats/validateUpdateChatBody.ts b/lib/chats/validateUpdateChatBody.ts new file mode 100644 index 00000000..63b48f38 --- /dev/null +++ b/lib/chats/validateUpdateChatBody.ts @@ -0,0 +1,104 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { validateAuthContext } from "@/lib/auth/validateAuthContext"; +import selectRoom from "@/lib/supabase/rooms/selectRoom"; +import { buildGetChatsParams } from "./buildGetChatsParams"; +import { z } from "zod"; + +/** + * Zod schema for PATCH /api/chats request body. + */ +export const updateChatBodySchema = z.object({ + chatId: z.string().uuid("chatId must be a valid UUID"), + topic: z + .string({ message: "topic is required" }) + .min(3, "topic must be between 3 and 50 characters") + .max(50, "topic must be between 3 and 50 characters"), +}); + +export type UpdateChatBody = z.infer; + +/** + * Validated update chat request data. + */ +export interface ValidatedUpdateChat { + chatId: string; + topic: string; +} + +/** + * Validates request for PATCH /api/chats. + * Parses JSON, validates schema, authenticates, verifies room exists, and checks access. + * + * @param request - The NextRequest object + * @returns A NextResponse with an error if validation fails, or the validated data if validation passes. + */ +export async function validateUpdateChatBody( + request: NextRequest, +): Promise { + // Parse JSON body + let body: unknown; + try { + body = await request.json(); + } catch { + body = {}; + } + + // Validate body schema + const result = updateChatBodySchema.safeParse(body); + if (!result.success) { + const firstError = result.error.issues[0]; + return NextResponse.json( + { + status: "error", + missing_fields: firstError.path, + error: firstError.message, + }, + { + status: 400, + headers: getCorsHeaders(), + }, + ); + } + + const { chatId, topic } = result.data; + + // Validate authentication + const authResult = await validateAuthContext(request); + if (authResult instanceof NextResponse) { + return authResult; + } + + const { accountId, orgId } = authResult; + + // Verify room exists + const room = await selectRoom(chatId); + if (!room) { + return NextResponse.json( + { status: "error", error: "Chat room not found" }, + { status: 404, headers: getCorsHeaders() }, + ); + } + + // Check access control + const { params } = await buildGetChatsParams({ + account_id: accountId, + org_id: orgId, + }); + + // 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: "Access denied to this chat" }, + { status: 403, headers: getCorsHeaders() }, + ); + } + } + + return { + chatId, + topic, + }; +} diff --git a/lib/supabase/rooms/updateRoom.ts b/lib/supabase/rooms/updateRoom.ts new file mode 100644 index 00000000..20714531 --- /dev/null +++ b/lib/supabase/rooms/updateRoom.ts @@ -0,0 +1,30 @@ +import supabase from "../serverClient"; +import type { Tables, TablesUpdate } from "@/types/database.types"; + +type Room = Tables<"rooms">; + +/** + * Updates a room's topic by ID. + * + * @param roomId - The ID of the room to update + * @param updates - The fields to update + * @returns The updated room data or null if not found or error + */ +export async function updateRoom( + roomId: string, + updates: TablesUpdate<"rooms">, +): Promise { + const { data, error } = await supabase + .from("rooms") + .update(updates) + .eq("id", roomId) + .select("*") + .single(); + + if (error) { + console.error("[ERROR] updateRoom:", error); + return null; + } + + return data; +}