From d94d1ec077124f5145948dd72681ada9f658a63a Mon Sep 17 00:00:00 2001 From: Chneemann Date: Sat, 5 Sep 2026 10:00:33 +0200 Subject: [PATCH] feat(api): add CRUD endpoints for conversations and direct messages --- app/api/dm/[conversationId]/route.ts | 94 +++++++++++++++ app/api/dm/messages/[messageId]/route.ts | 143 +++++++++++++++++++++++ app/api/dm/route.ts | 87 ++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 app/api/dm/[conversationId]/route.ts create mode 100644 app/api/dm/messages/[messageId]/route.ts create mode 100644 app/api/dm/route.ts diff --git a/app/api/dm/[conversationId]/route.ts b/app/api/dm/[conversationId]/route.ts new file mode 100644 index 0000000..30d8ed9 --- /dev/null +++ b/app/api/dm/[conversationId]/route.ts @@ -0,0 +1,94 @@ +/** + * @file app/api/dm/[conversationId]/route.ts + * @description API route handler for creating new direct messages within a specific conversation. + */ + +import { auth } from "@/auth"; +import { db } from "@/db"; +import { conversations, directMessages } from "@/db/schema"; +import { and, eq, or } from "drizzle-orm"; +import { NextResponse } from "next/server"; + +/** Regular expression to validate UUID format for conversation IDs. */ +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +/** + * Handles POST requests to send a new direct message in a conversation. + * + * @async + * @function POST + * @param {Request} req - The incoming HTTP request containing the message content. + * @param {Object} context - The route context. + * @param {Promise<{ conversationId: string }>} context.params - The route parameters containing the conversation ID. + * @returns {Promise} The JSON response with the created message or an error status. + */ +export async function POST( + req: Request, + { params }: { params: Promise<{ conversationId: string }> }, +) { + try { + const { conversationId } = await params; + + if (!UUID_REGEX.test(conversationId)) { + return NextResponse.json( + { error: "Invalid Conversation ID" }, + { status: 400 }, + ); + } + + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { content } = await req.json(); + if (!content || !content.trim()) { + return NextResponse.json( + { error: "Message content cannot be empty" }, + { status: 400 }, + ); + } + + // Sicherstellen, dass der User Teil der Unterhaltung ist + const [conversation] = await db + .select({ id: conversations.id }) + .from(conversations) + .where( + and( + eq(conversations.id, conversationId), + or( + eq(conversations.userOneId, session.user.id), + eq(conversations.userTwoId, session.user.id), + ), + ), + ) + .limit(1); + + if (!conversation) { + return NextResponse.json( + { error: "Conversation not found or forbidden" }, + { status: 404 }, + ); + } + + const [newMessage] = await db + .insert(directMessages) + .values({ + content: content.trim(), + conversationId, + senderId: session.user.id, + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + return NextResponse.json(newMessage, { status: 201 }); + } catch (error) { + console.error("API Direct Messages POST error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} diff --git a/app/api/dm/messages/[messageId]/route.ts b/app/api/dm/messages/[messageId]/route.ts new file mode 100644 index 0000000..8412303 --- /dev/null +++ b/app/api/dm/messages/[messageId]/route.ts @@ -0,0 +1,143 @@ +/** + * @file app/api/dm/messages/[messageId]/route.ts + * @description API route handlers for updating (PATCH) and deleting (DELETE) direct messages by their unique ID, including validation and permission checks. + */ + +import { auth } from "@/auth"; +import { db } from "@/db"; +import { directMessages } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { NextResponse } from "next/server"; + +/** + * Regular expression for validating UUID version 1-5 strings. + * + * @constant {RegExp} + */ +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +/** + * Handles PATCH requests to update the content of an existing direct message. + * + * @async + * @function PATCH + * @param {Request} req - The incoming HTTP request object containing the updated message content. + * @param {Object} context - The route context containing dynamic parameters. + * @param {Promise<{ messageId: string }>} context.params - A promise resolving to the route parameters including the message ID. + * @returns {Promise} The updated message JSON response or an appropriate error response. + */ +export async function PATCH( + req: Request, + { params }: { params: Promise<{ messageId: string }> }, +) { + try { + const { messageId } = await params; + + if (!UUID_REGEX.test(messageId)) { + return NextResponse.json( + { error: "Invalid Message ID" }, + { status: 400 }, + ); + } + + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { content } = await req.json(); + if (!content || !content.trim()) { + return NextResponse.json( + { error: "Message content cannot be empty" }, + { status: 400 }, + ); + } + + const [existingMessage] = await db + .select() + .from(directMessages) + .where(eq(directMessages.id, messageId)) + .limit(1); + + if (!existingMessage) { + return NextResponse.json({ error: "Message not found" }, { status: 404 }); + } + + if (existingMessage.senderId !== session.user.id) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const [updatedMessage] = await db + .update(directMessages) + .set({ + content: content.trim(), + updatedAt: new Date(), + }) + .where(eq(directMessages.id, messageId)) + .returning(); + + return NextResponse.json(updatedMessage); + } catch (error) { + console.error("API Direct Message PATCH error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} + +/** + * Handles DELETE requests to remove a direct message by its unique ID. + * + * @async + * @function DELETE + * @param {Request} req - The incoming HTTP request object. + * @param {Object} context - The route context containing dynamic parameters. + * @param {Promise<{ messageId: string }>} context.params - A promise resolving to the route parameters including the message ID. + * @returns {Promise} A success JSON response or an appropriate error response. + */ +export async function DELETE( + req: Request, + { params }: { params: Promise<{ messageId: string }> }, +) { + try { + const { messageId } = await params; + + if (!UUID_REGEX.test(messageId)) { + return NextResponse.json( + { error: "Invalid Message ID" }, + { status: 400 }, + ); + } + + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const [existingMessage] = await db + .select() + .from(directMessages) + .where(eq(directMessages.id, messageId)) + .limit(1); + + if (!existingMessage) { + return NextResponse.json({ error: "Message not found" }, { status: 404 }); + } + + if (existingMessage.senderId !== session.user.id) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + await db.delete(directMessages).where(eq(directMessages.id, messageId)); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("API Direct Message DELETE error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} diff --git a/app/api/dm/route.ts b/app/api/dm/route.ts new file mode 100644 index 0000000..aa385d9 --- /dev/null +++ b/app/api/dm/route.ts @@ -0,0 +1,87 @@ +/** + * @file app/api/dm/route.ts + * @description API route handler for creating or retrieving direct message conversations between users. + */ + +import { auth } from "@/auth"; +import { db } from "@/db"; +import { conversations } from "@/db/schema"; +import { and, eq, or } from "drizzle-orm"; +import { NextResponse } from "next/server"; + +/** + * Regular expression pattern for validating UUID format. + * + * @type {RegExp} + */ +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +/** + * Handles HTTP POST requests to initiate or fetch a direct message conversation with a specified recipient. + * + * @async + * @function POST + * @param {Request} req - The incoming HTTP request containing the recipient identifier in JSON format. + * @returns {Promise} The JSON response containing the conversation object or an error message. + */ +export async function POST(req: Request) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { recipientId } = await req.json(); + + if ( + !recipientId || + !UUID_REGEX.test(recipientId) || + recipientId === session.user.id + ) { + return NextResponse.json( + { error: "Invalid recipient ID" }, + { status: 400 }, + ); + } + + // Bestehende Konversation suchen + const [existingConversation] = await db + .select() + .from(conversations) + .where( + or( + and( + eq(conversations.userOneId, session.user.id), + eq(conversations.userTwoId, recipientId), + ), + and( + eq(conversations.userOneId, recipientId), + eq(conversations.userTwoId, session.user.id), + ), + ), + ) + .limit(1); + + if (existingConversation) { + return NextResponse.json(existingConversation); + } + + // Neue Konversation anlegen + const [newConversation] = await db + .insert(conversations) + .values({ + userOneId: session.user.id, + userTwoId: recipientId, + }) + .returning(); + + return NextResponse.json(newConversation, { status: 201 }); + } catch (error) { + console.error("API Conversations POST error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +}