feat(tasks): integrate zod validation for task creation and updates
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 47s

This commit is contained in:
Chneemann 2026-08-23 10:54:20 +02:00
parent 3812afab5a
commit f99d1f15c4
No known key found for this signature in database
3 changed files with 64 additions and 23 deletions

View file

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

View file

@ -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 };

32
lib/schemas/task.ts Normal file
View file

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