feat(api): add CRUD endpoints for friendships and friend requests

This commit is contained in:
Chneemann 2026-09-03 04:23:48 +02:00
parent d4066752c6
commit e07036c190
No known key found for this signature in database
2 changed files with 298 additions and 0 deletions

View file

@ -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<NextResponse>} 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<NextResponse>} 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 },
);
}
}

153
app/api/friends/route.ts Normal file
View file

@ -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<NextResponse>} 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<NextResponse>} 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 },
);
}
}