From e07036c1906e7c829e6cfa32d7d7dc9ecd1c2306 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Thu, 3 Sep 2026 04:23:48 +0200 Subject: [PATCH] feat(api): add CRUD endpoints for friendships and friend requests --- app/api/friends/[friendshipId]/route.ts | 145 ++++++++++++++++++++++ app/api/friends/route.ts | 153 ++++++++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 app/api/friends/[friendshipId]/route.ts create mode 100644 app/api/friends/route.ts diff --git a/app/api/friends/[friendshipId]/route.ts b/app/api/friends/[friendshipId]/route.ts new file mode 100644 index 0000000..5f84ff1 --- /dev/null +++ b/app/api/friends/[friendshipId]/route.ts @@ -0,0 +1,145 @@ +/** + * @file app/api/friends/[friendshipId]/route.ts + * @description API route handler for updating (PATCH) or removing (DELETE) an existing friendship relation. + */ + +import { auth } from "@/auth"; +import { db } from "@/db"; +import { friendships } from "@/db/schema"; +import { and, eq, or } from "drizzle-orm"; +import { NextResponse } from "next/server"; + +/** + * Updates the status of a specific friendship request (e.g., to ACCEPTED or BLOCKED). + * + * @async + * @function PATCH + * @param {Request} req - The incoming HTTP request containing the updated status in JSON format. + * @param {Object} context - The route context. + * @param {Promise<{ friendshipId: string }>} context.params - The route parameters containing the friendship identifier. + * @returns {Promise} The updated friendship object or an error response. + */ +export async function PATCH( + req: Request, + { params }: { params: Promise<{ friendshipId: string }> }, +) { + try { + const { friendshipId } = await params; + const session = await auth(); + const { status } = await req.json(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + if (!["ACCEPTED", "BLOCKED"].includes(status)) { + return NextResponse.json( + { error: "Invalid status provided" }, + { status: 400 }, + ); + } + + const [existingFriendship] = await db + .select() + .from(friendships) + .where(eq(friendships.id, friendshipId)) + .limit(1); + + if (!existingFriendship) { + return NextResponse.json( + { error: "Friendship request not found" }, + { status: 404 }, + ); + } + + // Only the receiver can ACCEPT a pending request + if ( + status === "ACCEPTED" && + existingFriendship.receiverId !== session.user.id + ) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + // Only involved users can BLOCK + if ( + status === "BLOCKED" && + existingFriendship.senderId !== session.user.id && + existingFriendship.receiverId !== session.user.id + ) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const [updatedFriendship] = await db + .update(friendships) + .set({ + status, + updatedAt: new Date(), + }) + .where(eq(friendships.id, friendshipId)) + .returning(); + + return NextResponse.json(updatedFriendship); + } catch (error) { + console.error("API Friendship PATCH error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} + +/** + * Deletes an existing friendship relation. + * + * @async + * @function DELETE + * @param {Request} req - The incoming HTTP request. + * @param {Object} context - The route context. + * @param {Promise<{ friendshipId: string }>} context.params - The route parameters containing the friendship identifier. + * @returns {Promise} A success JSON response or an error response. + */ +export async function DELETE( + req: Request, + { params }: { params: Promise<{ friendshipId: string }> }, +) { + try { + const { friendshipId } = await params; + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const [existingFriendship] = await db + .select() + .from(friendships) + .where( + and( + eq(friendships.id, friendshipId), + or( + eq(friendships.senderId, session.user.id), + eq(friendships.receiverId, session.user.id), + ), + ), + ) + .limit(1); + + if (!existingFriendship) { + return NextResponse.json( + { error: "Friendship not found or forbidden" }, + { status: 404 }, + ); + } + + // Delete Friendship entry + await db.delete(friendships).where(eq(friendships.id, friendshipId)); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("API Friendship DELETE error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} diff --git a/app/api/friends/route.ts b/app/api/friends/route.ts new file mode 100644 index 0000000..0b95fc6 --- /dev/null +++ b/app/api/friends/route.ts @@ -0,0 +1,153 @@ +/** + * @file app/api/friends/route.ts + * @description API route handlers for managing user friendships, supporting fetching user friendships via GET and creating new friend requests via POST. + */ + +import { auth } from "@/auth"; +import { db } from "@/db"; +import { friendships, users } from "@/db/schema"; +import { and, eq, or } from "drizzle-orm"; +import { NextResponse } from "next/server"; + +/** + * Handles GET requests to retrieve all friendships and friend requests for the authenticated user. + * + * @async + * @function GET + * @returns {Promise} A JSON response containing the list of friendships or an error object. + */ +export async function GET() { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const currentUserId = session.user.id; + + // Fetch all friendships where current user is either sender or receiver + const list = await db.query.friendships.findMany({ + where: or( + eq(friendships.senderId, currentUserId), + eq(friendships.receiverId, currentUserId), + ), + with: { + sender: { + columns: { + id: true, + username: true, + color: true, + status: true, + }, + }, + receiver: { + columns: { + id: true, + username: true, + color: true, + status: true, + }, + }, + }, + }); + + return NextResponse.json(list); + } catch (error) { + console.error("API Friends GET error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} + +/** + * Handles POST requests to create a new friend request based on a target username. + * + * @async + * @function POST + * @param {Request} req - The incoming HTTP request containing the target username in the JSON body. + * @returns {Promise} A JSON response containing the created friendship record or an error message. + */ +export async function POST(req: Request) { + try { + const session = await auth(); + const { username } = await req.json(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const trimmedUsername = username?.trim(); + + if (!trimmedUsername) { + return NextResponse.json( + { error: "Username is required" }, + { status: 400 }, + ); + } + + // Find the target user by username + const [targetUser] = await db + .select() + .from(users) + .where(eq(users.username, trimmedUsername)) + .limit(1); + + if (!targetUser) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + // Prevent sending a request to oneself + if (targetUser.id === session.user.id) { + return NextResponse.json( + { error: "You cannot add yourself as a friend" }, + { status: 400 }, + ); + } + + // Check if a friendship or request already exists between these users + const [existingFriendship] = await db + .select() + .from(friendships) + .where( + or( + and( + eq(friendships.senderId, session.user.id), + eq(friendships.receiverId, targetUser.id), + ), + and( + eq(friendships.senderId, targetUser.id), + eq(friendships.receiverId, session.user.id), + ), + ), + ) + .limit(1); + + if (existingFriendship) { + return NextResponse.json( + { error: "A friendship or request already exists with this user" }, + { status: 400 }, + ); + } + + // Create new friend request + const [newFriendship] = await db + .insert(friendships) + .values({ + senderId: session.user.id, + receiverId: targetUser.id, + status: "PENDING", + }) + .returning(); + + return NextResponse.json(newFriendship, { status: 201 }); + } catch (error) { + console.error("API Friends POST error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +}