diff --git a/app/api/tasks/[id]/route.ts b/app/api/tasks/[id]/route.ts new file mode 100644 index 0000000..e7bc215 --- /dev/null +++ b/app/api/tasks/[id]/route.ts @@ -0,0 +1,73 @@ +/** + * @file app/api/tasks/[id]/route.ts + * @description API route handler for updating a task's status patch endpoint, ensuring user authorization and valid status transitions. + */ + +import { NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { taskStatusEnum } from "@/db/schema"; +import { TaskService } from "@/services/task.service"; +import { RouteContext, TaskStatus } from "@/types/tasks"; + +/** + * Handles PATCH requests to update a specific task's status. + * Verifies user authentication, validates the incoming status against allowed schema values, + * checks permissions via the task service, and performs the update. + * + * @async + * @param {Request} request - The incoming HTTP request containing the status update payload. + * @param {RouteContext} context - The route context containing dynamic route parameters. + * @returns {Promise} A JSON response indicating success with the updated task or an error message. + */ +export async function PATCH(request: Request, context: RouteContext) { + try { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id: taskId } = await context.params; + const body = await request.json(); + const { status } = body as { status: unknown }; + + // Validate status against enum values using TaskStatus type guard/check + const validStatuses = taskStatusEnum.enumValues; + if ( + !status || + typeof status !== "string" || + !validStatuses.includes(status as TaskStatus) + ) { + return NextResponse.json( + { error: "Invalid or missing status value" }, + { status: 400 }, + ); + } + + const typedStatus = status as TaskStatus; + + // Execute update & verification via Service in a single step + const updatedTask = await TaskService.updateStatusIfAuthorized( + taskId, + session.user.id, + typedStatus, + ); + + if (!updatedTask) { + return NextResponse.json( + { error: "Task not found or access denied" }, + { status: 403 }, + ); + } + + return NextResponse.json( + { success: true, task: updatedTask }, + { status: 200 }, + ); + } catch (error) { + console.error("Error updating task status:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} diff --git a/services/task.service.ts b/services/task.service.ts index e277f66..adf4ec4 100644 --- a/services/task.service.ts +++ b/services/task.service.ts @@ -1,6 +1,6 @@ /** * @file services/task.service.ts - * @description Business logic service handling task permissions and database queries. + * @description Business logic service handling task permissions, database queries, and status updates. */ import { db } from "@/db"; @@ -9,6 +9,7 @@ import { taskAssigneesTable, type Task as DbTask, } from "@/db/schema"; +import { TaskStatus } from "@/types/tasks"; import { and, eq, or, exists } from "drizzle-orm"; /** @@ -52,4 +53,48 @@ export class TaskService { return task as DbTask | undefined; } + + /** + * Updates a task's status if the user is authorized as either the owner or an assignee. + * + * @async + * @param {string} taskId - The unique identifier of the task to update. + * @param {string} userId - The unique identifier of the user performing the update. + * @param {TaskStatus} status - The new status to apply to the task. + * @returns {Promise} The updated task object, or null if the update failed or user is unauthorized. + */ + static async updateStatusIfAuthorized( + taskId: string, + userId: string, + status: TaskStatus, + ) { + const [updatedTask] = await db + .update(tasksTable) + .set({ + status: status, + updatedAt: new Date(), + }) + .where( + and( + eq(tasksTable.id, taskId), + or( + eq(tasksTable.userId, userId), + exists( + db + .select({ taskId: taskAssigneesTable.taskId }) + .from(taskAssigneesTable) + .where( + and( + eq(taskAssigneesTable.taskId, taskId), + eq(taskAssigneesTable.userId, userId), + ), + ), + ), + ), + ), + ) + .returning(); + + return (updatedTask as DbTask | undefined) || null; + } }