chat implemented

need testing
This commit is contained in:
dimitar committed 2026-04-03 03:25:11 +02:00
1 parent c90f8cb1fa
commit 9cbdc35903
40 files changed
+7068 -10

No files matched your search

@@ -0,0 +1,108 @@
/**
* @jest-environment node
*/
import { NextRequest } from "next/server";
import { GET as GET_THREADS } from "../threads/route";
import { POST as START_DM } from "../dm/start/route";
import { GET as GET_MY_DM } from "../dm/my-thread/route";
jest.mock("@clerk/nextjs/server", () => ({
auth: jest.fn(),
}));
jest.mock("@/lib/database", () => ({
getDatabase: jest.fn(),
}));
jest.mock("@/lib/sync-user", () => ({
ensureUserSynced: jest.fn(),
}));
jest.mock("@/lib/chat", () => ({
getOrCreateGymRoomThread: jest.fn(),
listThreadsForUser: jest.fn(),
getOrCreateDmThread: jest.fn(),
isChatError: jest.fn(() => false),
}));
jest.mock("@/lib/logger", () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
}));
describe("/api/chat authz", () => {
const mockAuth = require("@clerk/nextjs/server").auth as jest.Mock;
const mockGetDatabase = require("@/lib/database").getDatabase as jest.Mock;
const mockEnsureUserSynced = require("@/lib/sync-user")
.ensureUserSynced as jest.Mock;
const mockListThreadsForUser = require("@/lib/chat")
.listThreadsForUser as jest.Mock;
const mockGetOrCreateDmThread = require("@/lib/chat")
.getOrCreateDmThread as jest.Mock;
const mockDb = {
getClientTrainerAssignment: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
mockGetDatabase.mockResolvedValue(mockDb);
mockListThreadsForUser.mockResolvedValue([]);
});
it("returns 401 for unauthenticated threads list", async () => {
mockAuth.mockResolvedValue({ userId: null });
const req = new NextRequest("http://localhost/api/chat/threads");
const res = await GET_THREADS(req);
expect(res.status).toBe(401);
});
it("returns 403 when non participant/non staff starts DM", async () => {
mockAuth.mockResolvedValue({ userId: "user_1" });
mockEnsureUserSynced.mockResolvedValue({
id: "user_1",
role: "trainer",
gymId: "gym_1",
});
const req = new NextRequest("http://localhost/api/chat/dm/start", {
method: "POST",
body: JSON.stringify({ trainerId: "trainer_2", clientId: "client_2" }),
});
const res = await START_DM(req);
expect(res.status).toBe(403);
expect(mockGetOrCreateDmThread).not.toHaveBeenCalled();
});
it("returns 403 when non-client requests my-thread endpoint", async () => {
mockAuth.mockResolvedValue({ userId: "trainer_1" });
mockEnsureUserSynced.mockResolvedValue({
id: "trainer_1",
role: "trainer",
gymId: "gym_1",
});
const res = await GET_MY_DM();
expect(res.status).toBe(403);
});
it("returns 404 when client has no active assignment", async () => {
mockAuth.mockResolvedValue({ userId: "client_1" });
mockEnsureUserSynced.mockResolvedValue({
id: "client_1",
role: "client",
gymId: "gym_1",
});
mockDb.getClientTrainerAssignment.mockResolvedValue(null);
const res = await GET_MY_DM();
expect(res.status).toBe(404);
});
});
@@ -0,0 +1,61 @@
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ensureUserSynced } from "@/lib/sync-user";
import { getDatabase } from "@/lib/database";
import { getOrCreateDmThread, isChatError } from "@/lib/chat";
import log from "@/lib/logger";
export async function GET() {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const db = await getDatabase();
const currentUser = await ensureUserSynced(userId, db);
if (!currentUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
if (currentUser.role !== "client") {
return NextResponse.json(
{ error: "Only clients can use this endpoint" },
{ status: 403 },
);
}
const assignment = await db.getClientTrainerAssignment(currentUser.id);
if (!assignment || !assignment.isActive) {
return NextResponse.json(
{ error: "No active trainer assignment found" },
{ status: 404 },
);
}
const thread = await getOrCreateDmThread({
trainerId: assignment.trainerId,
clientId: assignment.clientId,
createdBy: {
id: currentUser.id,
role: currentUser.role,
gymId: currentUser.gymId,
},
});
return NextResponse.json({ thread });
} catch (error) {
if (isChatError(error)) {
return NextResponse.json(
{ error: error.message },
{ status: error.status },
);
}
log.error("Failed to get client DM thread", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ensureUserSynced } from "@/lib/sync-user";
import { getDatabase } from "@/lib/database";
import { getOrCreateDmThread, isChatError } from "@/lib/chat";
import log from "@/lib/logger";
export async function POST(request: NextRequest) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json().catch(() => null);
if (!body || typeof body !== "object") {
return NextResponse.json({ error: "Invalid body" }, { status: 400 });
}
const trainerId = String(body.trainerId ?? "").trim();
const clientId = String(body.clientId ?? "").trim();
if (!trainerId || !clientId) {
return NextResponse.json(
{ error: "trainerId and clientId are required" },
{ status: 400 },
);
}
const db = await getDatabase();
const currentUser = await ensureUserSynced(userId, db);
if (!currentUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const isStaff =
currentUser.role === "superAdmin" || currentUser.role === "admin";
const isParticipant =
currentUser.id === trainerId || currentUser.id === clientId;
if (!isStaff && !isParticipant) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const thread = await getOrCreateDmThread({
trainerId,
clientId,
createdBy: {
id: currentUser.id,
role: currentUser.role,
gymId: currentUser.gymId,
},
});
return NextResponse.json({ thread }, { status: 201 });
} catch (error) {
if (isChatError(error)) {
return NextResponse.json(
{ error: error.message },
{ status: error.status },
);
}
log.error("Failed to start direct message thread", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
@@ -0,0 +1,42 @@
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ensureUserSynced } from "@/lib/sync-user";
import { getDatabase } from "@/lib/database";
import { getOrCreateGymRoomThread, isChatError } from "@/lib/chat";
import log from "@/lib/logger";
export async function GET() {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const db = await getDatabase();
const currentUser = await ensureUserSynced(userId, db);
if (!currentUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const thread = await getOrCreateGymRoomThread({
id: currentUser.id,
role: currentUser.role,
gymId: currentUser.gymId,
});
return NextResponse.json({ thread });
} catch (error) {
if (isChatError(error)) {
return NextResponse.json(
{ error: error.message },
{ status: error.status },
);
}
log.error("Failed to get gym room thread", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
@@ -0,0 +1,181 @@
/**
* @jest-environment node
*/
import { NextRequest } from "next/server";
import { GET, POST } from "../route";
jest.mock("@clerk/nextjs/server", () => ({
auth: jest.fn(),
}));
jest.mock("@/lib/database", () => ({
getDatabase: jest.fn(),
}));
jest.mock("@/lib/sync-user", () => ({
ensureUserSynced: jest.fn(),
}));
jest.mock("@/lib/chat", () => ({
getThreadMessages: jest.fn(),
createThreadMessage: jest.fn(),
isChatError: jest.fn((error: unknown) =>
Boolean(
error && typeof error === "object" && "status" in (error as object),
),
),
}));
jest.mock("@/lib/logger", () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
}));
describe("/api/chat/threads/[id]/messages authz", () => {
const mockAuth = require("@clerk/nextjs/server").auth as jest.Mock;
const mockGetDatabase = require("@/lib/database").getDatabase as jest.Mock;
const mockEnsureUserSynced = require("@/lib/sync-user")
.ensureUserSynced as jest.Mock;
const mockGetThreadMessages = require("@/lib/chat")
.getThreadMessages as jest.Mock;
const mockCreateThreadMessage = require("@/lib/chat")
.createThreadMessage as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockGetDatabase.mockResolvedValue({});
});
it("returns 401 for unauthenticated GET", async () => {
mockAuth.mockResolvedValue({ userId: null });
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages",
);
const response = await GET(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(401);
});
it("returns 403 when synced user is missing on GET", async () => {
mockAuth.mockResolvedValue({ userId: "user_1" });
mockEnsureUserSynced.mockResolvedValue(null);
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages",
);
const response = await GET(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(403);
expect(mockGetThreadMessages).not.toHaveBeenCalled();
});
it("maps ChatError to status code on GET", async () => {
mockAuth.mockResolvedValue({ userId: "user_1" });
mockEnsureUserSynced.mockResolvedValue({
id: "user_1",
role: "client",
gymId: "gym_1",
});
mockGetThreadMessages.mockRejectedValue({
status: 403,
message: "Forbidden",
});
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages",
);
const response = await GET(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(403);
});
it("returns 401 for unauthenticated POST", async () => {
mockAuth.mockResolvedValue({ userId: null });
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages",
{
method: "POST",
body: JSON.stringify({ body: "Hello" }),
},
);
const response = await POST(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(401);
});
it("returns 400 when POST body is invalid", async () => {
mockAuth.mockResolvedValue({ userId: "user_1" });
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages",
{
method: "POST",
body: "not json",
headers: { "Content-Type": "application/json" },
},
);
const response = await POST(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(400);
});
it("returns 403 when synced user is missing on POST", async () => {
mockAuth.mockResolvedValue({ userId: "user_1" });
mockEnsureUserSynced.mockResolvedValue(null);
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages",
{
method: "POST",
body: JSON.stringify({ body: "Hi" }),
},
);
const response = await POST(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(403);
expect(mockCreateThreadMessage).not.toHaveBeenCalled();
});
it("maps ChatError to status code on POST", async () => {
mockAuth.mockResolvedValue({ userId: "user_1" });
mockEnsureUserSynced.mockResolvedValue({
id: "user_1",
role: "client",
gymId: "gym_1",
});
mockCreateThreadMessage.mockRejectedValue({
status: 403,
message: "Forbidden",
});
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages",
{
method: "POST",
body: JSON.stringify({ body: "Hello" }),
},
);
const response = await POST(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(403);
});
});
@@ -0,0 +1,133 @@
/**
* @jest-environment node
*/
import { NextRequest } from "next/server";
import { GET, POST } from "../route";
jest.mock("@clerk/nextjs/server", () => ({
auth: jest.fn(),
}));
jest.mock("@/lib/database", () => ({
getDatabase: jest.fn(),
}));
jest.mock("@/lib/sync-user", () => ({
ensureUserSynced: jest.fn(),
}));
jest.mock("@/lib/chat", () => ({
getThreadMessages: jest.fn(),
createThreadMessage: jest.fn(),
isChatError: jest.fn(() => false),
}));
jest.mock("@/lib/logger", () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
}));
describe("/api/chat/threads/[id]/messages params", () => {
const mockAuth = require("@clerk/nextjs/server").auth as jest.Mock;
const mockGetDatabase = require("@/lib/database").getDatabase as jest.Mock;
const mockEnsureUserSynced = require("@/lib/sync-user")
.ensureUserSynced as jest.Mock;
const mockGetThreadMessages = require("@/lib/chat")
.getThreadMessages as jest.Mock;
const mockCreateThreadMessage = require("@/lib/chat")
.createThreadMessage as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockAuth.mockResolvedValue({ userId: "user_1" });
mockGetDatabase.mockResolvedValue({});
mockEnsureUserSynced.mockResolvedValue({
id: "user_1",
role: "client",
gymId: "gym_1",
});
});
it("parses numeric cursor and limit", async () => {
mockGetThreadMessages.mockResolvedValue({ messages: [], nextCursor: null });
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages?cursor=12345&limit=20",
);
const response = await GET(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(200);
expect(mockGetThreadMessages).toHaveBeenCalledWith(
expect.objectContaining({
threadId: "thread_1",
cursor: 12345,
limit: 20,
}),
);
});
it("ignores invalid cursor and limit query values", async () => {
mockGetThreadMessages.mockResolvedValue({ messages: [], nextCursor: null });
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages?cursor=abc&limit=xyz",
);
const response = await GET(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(200);
expect(mockGetThreadMessages).toHaveBeenCalledWith(
expect.objectContaining({
threadId: "thread_1",
cursor: undefined,
limit: undefined,
}),
);
});
it("filters malformed attachments before create", async () => {
mockCreateThreadMessage.mockResolvedValue({ id: "msg_1" });
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/messages",
{
method: "POST",
body: JSON.stringify({
body: "Hello",
attachments: [
{ url: "https://example.com/file.png", type: "image/png" },
{ url: 123, type: "image/png" },
{ foo: "bar" },
],
}),
},
);
const response = await POST(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(201);
expect(mockCreateThreadMessage).toHaveBeenCalledWith(
expect.objectContaining({
threadId: "thread_1",
body: "Hello",
attachments: [
{
url: "https://example.com/file.png",
type: "image/png",
name: undefined,
},
],
}),
);
});
});
@@ -0,0 +1,148 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ensureUserSynced } from "@/lib/sync-user";
import { getDatabase } from "@/lib/database";
import {
createThreadMessage,
getThreadMessages,
isChatError,
} from "@/lib/chat";
import log from "@/lib/logger";
interface Params {
params: Promise<{ id: string }>;
}
export async function GET(request: NextRequest, { params }: Params) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id: threadId } = await params;
const db = await getDatabase();
const currentUser = await ensureUserSynced(userId, db);
if (!currentUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const cursorParam = request.nextUrl.searchParams.get("cursor");
const limitParam = request.nextUrl.searchParams.get("limit");
const cursor =
cursorParam && Number.isFinite(Number(cursorParam))
? Number(cursorParam)
: undefined;
const limit =
limitParam && Number.isFinite(Number(limitParam))
? Number(limitParam)
: undefined;
const result = await getThreadMessages({
currentUser: {
id: currentUser.id,
role: currentUser.role,
gymId: currentUser.gymId,
},
threadId,
cursor,
limit,
});
return NextResponse.json(result);
} catch (error) {
if (isChatError(error)) {
return NextResponse.json(
{ error: error.message },
{ status: error.status },
);
}
log.error("Failed to fetch chat messages", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id: threadId } = await params;
const body = await request.json().catch(() => null);
if (!body || typeof body !== "object") {
return NextResponse.json({ error: "Invalid body" }, { status: 400 });
}
const messageBody = String(body.body ?? "").trim();
if (!messageBody) {
return NextResponse.json(
{ error: "Message body is required" },
{ status: 400 },
);
}
const clientMessageIdRaw = body.clientMessageId;
const clientMessageId =
typeof clientMessageIdRaw === "string" &&
clientMessageIdRaw.trim().length > 0
? clientMessageIdRaw.trim()
: undefined;
const attachmentsRaw = body.attachments;
const attachments = Array.isArray(attachmentsRaw)
? attachmentsRaw
.filter(
(item): item is { url: string; type: string; name?: string } =>
!!item &&
typeof item === "object" &&
typeof item.url === "string" &&
typeof item.type === "string",
)
.map((item) => ({
url: item.url,
type: item.type,
name: typeof item.name === "string" ? item.name : undefined,
}))
: undefined;
const db = await getDatabase();
const currentUser = await ensureUserSynced(userId, db);
if (!currentUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const message = await createThreadMessage({
currentUser: {
id: currentUser.id,
role: currentUser.role,
gymId: currentUser.gymId,
},
threadId,
body: messageBody,
clientMessageId,
attachments,
});
return NextResponse.json({ message }, { status: 201 });
} catch (error) {
if (isChatError(error)) {
return NextResponse.json(
{ error: error.message },
{ status: error.status },
);
}
log.error("Failed to create chat message", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
@@ -0,0 +1,134 @@
/**
* @jest-environment node
*/
import { NextRequest } from "next/server";
import { POST } from "../route";
jest.mock("@clerk/nextjs/server", () => ({
auth: jest.fn(),
}));
jest.mock("@/lib/database", () => ({
getDatabase: jest.fn(),
}));
jest.mock("@/lib/sync-user", () => ({
ensureUserSynced: jest.fn(),
}));
jest.mock("@/lib/chat", () => ({
markThreadAsRead: jest.fn(),
isChatError: jest.fn((error: unknown) =>
Boolean(
error && typeof error === "object" && "status" in (error as object),
),
),
}));
jest.mock("@/lib/logger", () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
}));
describe("/api/chat/threads/[id]/read authz", () => {
const mockAuth = require("@clerk/nextjs/server").auth as jest.Mock;
const mockGetDatabase = require("@/lib/database").getDatabase as jest.Mock;
const mockEnsureUserSynced = require("@/lib/sync-user")
.ensureUserSynced as jest.Mock;
const mockMarkThreadAsRead = require("@/lib/chat")
.markThreadAsRead as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockGetDatabase.mockResolvedValue({});
});
it("returns 401 for unauthenticated request", async () => {
mockAuth.mockResolvedValue({ userId: null });
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/read",
{
method: "POST",
body: JSON.stringify({ lastReadMessageId: "msg_1" }),
},
);
const response = await POST(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(401);
});
it("returns 403 when synced user is missing", async () => {
mockAuth.mockResolvedValue({ userId: "user_1" });
mockEnsureUserSynced.mockResolvedValue(null);
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/read",
{
method: "POST",
body: JSON.stringify({ lastReadMessageId: "msg_1" }),
},
);
const response = await POST(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(403);
expect(mockMarkThreadAsRead).not.toHaveBeenCalled();
});
it("maps ChatError to status code", async () => {
mockAuth.mockResolvedValue({ userId: "user_1" });
mockEnsureUserSynced.mockResolvedValue({
id: "user_1",
role: "client",
gymId: "gym_1",
});
mockMarkThreadAsRead.mockRejectedValue({
status: 403,
message: "Forbidden",
});
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/read",
{
method: "POST",
body: JSON.stringify({ lastReadMessageId: "msg_1" }),
},
);
const response = await POST(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(403);
});
it("returns success for authorized user", async () => {
mockAuth.mockResolvedValue({ userId: "user_1" });
mockEnsureUserSynced.mockResolvedValue({
id: "user_1",
role: "client",
gymId: "gym_1",
});
mockMarkThreadAsRead.mockResolvedValue(undefined);
const request = new NextRequest(
"http://localhost/api/chat/threads/thread_1/read",
{
method: "POST",
body: JSON.stringify({ lastReadMessageId: "msg_1" }),
},
);
const response = await POST(request, {
params: Promise.resolve({ id: "thread_1" }),
});
expect(response.status).toBe(200);
expect(mockMarkThreadAsRead).toHaveBeenCalled();
});
});
@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ensureUserSynced } from "@/lib/sync-user";
import { getDatabase } from "@/lib/database";
import { isChatError, markThreadAsRead } from "@/lib/chat";
import log from "@/lib/logger";
interface Params {
params: Promise<{ id: string }>;
}
export async function POST(request: NextRequest, { params }: Params) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id: threadId } = await params;
const body = await request.json().catch(() => ({}));
const lastReadMessageId =
body &&
typeof body === "object" &&
typeof body.lastReadMessageId === "string"
? body.lastReadMessageId
: undefined;
const db = await getDatabase();
const currentUser = await ensureUserSynced(userId, db);
if (!currentUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
await markThreadAsRead({
currentUser: {
id: currentUser.id,
role: currentUser.role,
gymId: currentUser.gymId,
},
threadId,
lastReadMessageId,
});
return NextResponse.json({ success: true });
} catch (error) {
if (isChatError(error)) {
return NextResponse.json(
{ error: error.message },
{ status: error.status },
);
}
log.error("Failed to mark chat thread as read", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
@@ -0,0 +1,52 @@
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ensureUserSynced } from "@/lib/sync-user";
import { getDatabase } from "@/lib/database";
import { isChatError, listThreadsForUser } from "@/lib/chat";
import log from "@/lib/logger";
interface Params {
params: Promise<{ id: string }>;
}
export async function GET(_request: Request, { params }: Params) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { id: threadId } = await params;
const db = await getDatabase();
const currentUser = await ensureUserSynced(userId, db);
if (!currentUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const threads = await listThreadsForUser({
id: currentUser.id,
role: currentUser.role,
gymId: currentUser.gymId,
});
const thread = threads.find((item) => item.id === threadId);
if (!thread) {
return NextResponse.json({ error: "Thread not found" }, { status: 404 });
}
return NextResponse.json({ thread });
} catch (error) {
if (isChatError(error)) {
return NextResponse.json(
{ error: error.message },
{ status: error.status },
);
}
log.error("Failed to fetch chat thread", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { ensureUserSynced } from "@/lib/sync-user";
import { getDatabase } from "@/lib/database";
import {
getOrCreateGymRoomThread,
isChatError,
listThreadsForUser,
} from "@/lib/chat";
import log from "@/lib/logger";
export async function GET(request: NextRequest) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const db = await getDatabase();
const currentUser = await ensureUserSynced(userId, db);
if (!currentUser) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const includeGymRoom =
request.nextUrl.searchParams.get("includeGymRoom") !== "false";
if (includeGymRoom && currentUser.gymId) {
await getOrCreateGymRoomThread({
id: currentUser.id,
role: currentUser.role,
gymId: currentUser.gymId,
});
}
const threads = await listThreadsForUser({
id: currentUser.id,
role: currentUser.role,
gymId: currentUser.gymId,
});
return NextResponse.json({ threads });
} catch (error) {
if (isChatError(error)) {
return NextResponse.json(
{ error: error.message },
{ status: error.status },
);
}
log.error("Failed to fetch chat threads", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}