diff --git a/app/(app)/tasks/useTaskForm.ts b/app/(app)/tasks/useTaskForm.ts index be6a1d6..b2265c3 100644 --- a/app/(app)/tasks/useTaskForm.ts +++ b/app/(app)/tasks/useTaskForm.ts @@ -6,6 +6,7 @@ import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { TaskPriority, TaskStatus, DbTask } from "@/types/task"; +import { taskSchema } from "@/lib/schemas/task"; /** * Custom React hook that encapsulates form state management, field updates, assignee toggling, @@ -65,18 +66,14 @@ export function useTaskForm( }; /** - * Validates form inputs, combines date and time values, and submits the payload - * via API depending on whether the form is in create or edit mode. + * Validates form input against Zod schemas and dispatches POST or PATCH network requests to persist task changes. * - * @async * @param {React.SubmitEvent} e - The form submission event. */ const handleSubmit = (e: React.SubmitEvent) => { e.preventDefault(); setError(null); - if (!form.title.trim()) return; - const finalDate = form.dueDate || todayString; const combinedDateTime = new Date(`${finalDate}T${form.dueTime}`); @@ -85,15 +82,24 @@ export function useTaskForm( return; } + const validationResult = taskSchema.safeParse({ + ...form, + dueDate: combinedDateTime.toISOString(), + }); + + if (!validationResult.success) { + setError(validationResult.error.issues[0].message); + return; + } + startTransition(async () => { try { const response = await fetch("/api/tasks", { method: isEditMode ? "PATCH" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ + ...validationResult.data, id: initialData?.id, - ...form, - dueDate: combinedDateTime.toISOString(), }), }); diff --git a/app/api/tasks/route.ts b/app/api/tasks/route.ts index 9809da8..a8a37e1 100644 --- a/app/api/tasks/route.ts +++ b/app/api/tasks/route.ts @@ -1,21 +1,21 @@ /** * @file api/tasks/route.ts - * @description API endpoint for creating and updating task records with strict existence checks. + * @description API endpoint for creating and updating task records with strict Zod validation. */ import { NextResponse } from "next/server"; import { auth } from "@/auth"; import { TaskService } from "@/services/task.service"; -import { TaskPayload } from "@/types/task"; +import { taskSchema } from "@/lib/schemas/task"; /** * Validates the incoming task request by checking user authentication, parsing the JSON payload, - * and ensuring all required task fields (and optional ID if specified) are present. + * and running Zod validation against the task schema. * * @async * @param {Request} request - The incoming HTTP request. * @param {boolean} [requireId=false] - Whether a task ID is mandatory in the payload body. - * @returns {Promise<{ userId?: string; body?: TaskPayload & { id?: string }; error?: NextResponse }>} Validation results containing user ID, parsed body, or a NextResponse error. + * @returns {Promise<{ userId?: string; body?: any; error?: NextResponse }>} Validation results containing user ID, parsed body, or a NextResponse error. */ async function validateTaskRequest(request: Request, requireId = false) { const session = await auth(); @@ -23,19 +23,22 @@ async function validateTaskRequest(request: Request, requireId = false) { return { error: new NextResponse("Unauthorized", { status: 401 }) }; } - const body: TaskPayload & { id?: string } = await request.json(); + const jsonBody = await request.json().catch(() => null); + if (!jsonBody) { + return { error: new NextResponse("Invalid JSON payload", { status: 400 }) }; + } - if ( - (requireId && !body.id) || - !body.title || - !body.description || - !body.dueDate || - !body.status || - !body.priority - ) { - return { - error: new NextResponse("Missing required task fields", { status: 400 }), - }; + const validationResult = taskSchema.safeParse(jsonBody); + + if (!validationResult.success) { + const errorMessage = validationResult.error.issues[0].message; + return { error: new NextResponse(errorMessage, { status: 400 }) }; + } + + const body = validationResult.data; + + if (requireId && !body.id) { + return { error: new NextResponse("Missing task ID", { status: 400 }) }; } return { userId: session.user.id, body }; diff --git a/lib/schemas/task.ts b/lib/schemas/task.ts new file mode 100644 index 0000000..a2172a6 --- /dev/null +++ b/lib/schemas/task.ts @@ -0,0 +1,32 @@ +/** + * @file lib/schemas/task.ts + * @description Zod validation schema for task creation and modification operations, enforcing constraints on title, description, status, priority, dates, and assignees. + */ + +import { z } from "zod"; +import { taskStatusEnum, taskPriorityEnum } from "@/db/schema"; + +/** + * Zod validation schema for task entities and form submissions. + */ +export const taskSchema = z.object({ + id: z.uuid().optional(), + title: z + .string() + .min(1, "Title is required") + .max(255, "Title is too long (max 255 characters)"), + description: z + .string() + .min(1, "Description is required") + .max(2000, "Description is too long (max 2000 characters)"), + status: z.enum(taskStatusEnum.enumValues), + priority: z.enum(taskPriorityEnum.enumValues), + dueDate: z + .string() + .min(1, "Due date is required") + .transform((val) => new Date(val)), + dueTime: z + .string() + .regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, "Invalid time format"), + assignees: z.array(z.uuid()).optional(), +});