diff --git a/src/app/api/comments/public/public.controller.ts b/src/app/api/comments/public/public.controller.ts index b0766c09f..75d56ed27 100644 --- a/src/app/api/comments/public/public.controller.ts +++ b/src/app/api/comments/public/public.controller.ts @@ -6,6 +6,8 @@ import { getPaginationLimit } from '@/utils/pagination' import { getSearchParams } from '@/utils/request' import { decode, encode } from 'js-base64' import { NextRequest, NextResponse } from 'next/server' +import { ValidateUuid } from '@api/core/utils/validateUuid' +import { PublicResource } from '@api/core/types/public' type TaskAndCommentIdParams = { params: Promise<{ id: string }> @@ -53,6 +55,7 @@ export const getAllCommentsPublic = async (req: NextRequest) => { export const getOneCommentPublic = async (req: NextRequest, { params }: TaskAndCommentIdParams) => { const { id } = await params + ValidateUuid(id, PublicResource.Comments) const user = await authenticate(req) const commentService = new CommentService(user) @@ -66,6 +69,7 @@ export const getOneCommentPublic = async (req: NextRequest, { params }: TaskAndC export const deleteOneCommentPublic = async (req: NextRequest, { params }: TaskAndCommentIdParams) => { const { id } = await params + ValidateUuid(id, PublicResource.Comments) const user = await authenticate(req) const commentService = new CommentService(user) diff --git a/src/app/api/core/types/public.ts b/src/app/api/core/types/public.ts new file mode 100644 index 000000000..8789a03ef --- /dev/null +++ b/src/app/api/core/types/public.ts @@ -0,0 +1,5 @@ +export enum PublicResource { + Tasks = 'task', + Templates = 'template', + Comments = 'comment', +} diff --git a/src/app/api/core/utils/validateUuid.ts b/src/app/api/core/utils/validateUuid.ts new file mode 100644 index 000000000..10d9e5f7b --- /dev/null +++ b/src/app/api/core/utils/validateUuid.ts @@ -0,0 +1,24 @@ +import httpStatus from 'http-status' +import z from 'zod' +import APIError from '@api/core/exceptions/api' +import { PublicResource } from '@api/core/types/public' + +/** + * Validates that an ID parameter is a valid UUID. + * + * This utility is intended to be used in service-layer retrieval flows + * where an ID is passed via API params. If the ID is not a valid UUID, + * it throws a 404 error immediately instead of letting the request hit Prisma, + * thereby avoiding Prisma P2023 errors and providing a controlled, + * consistent API response. Mainly used for public APIs. + * + * @param id - The resource ID to validate + * @param resourceType - The type of resource being retrieved + * + * @throws {APIError} 404 NOT_FOUND if the ID is not a valid UUID + */ +export const ValidateUuid = (id: string, resourceType: PublicResource) => { + if (!z.string().uuid().safeParse(id).success) { + throw new APIError(httpStatus.NOT_FOUND, `The requested ${resourceType} was not found`) + } +} diff --git a/src/app/api/tasks/public/public.controller.ts b/src/app/api/tasks/public/public.controller.ts index 0e7ade4a1..da3b945c3 100644 --- a/src/app/api/tasks/public/public.controller.ts +++ b/src/app/api/tasks/public/public.controller.ts @@ -10,6 +10,8 @@ import { decode, encode } from 'js-base64' import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { PublicTasksService } from '@api/tasks/public/public.service' +import { ValidateUuid } from '@api/core/utils/validateUuid' +import { PublicResource } from '@api/core/types/public' export const getAllTasksPublic = async (req: NextRequest) => { const user = await authenticate(req) @@ -54,6 +56,7 @@ export const getAllTasksPublic = async (req: NextRequest) => { export const getOneTaskPublic = async (req: NextRequest, { params }: IdParams) => { const { id } = await params + ValidateUuid(id, PublicResource.Tasks) const user = await authenticate(req) const tasksService = new PublicTasksService(user) const task = await tasksService.getOneTask(id) @@ -78,6 +81,7 @@ export const createTaskPublic = async (req: NextRequest) => { export const updateTaskPublic = async (req: NextRequest, { params }: IdParams) => { const { id } = await params + ValidateUuid(id, PublicResource.Tasks) const user = await authenticate(req) const data = PublicTaskUpdateDtoSchema.parse(await req.json()) @@ -90,6 +94,7 @@ export const updateTaskPublic = async (req: NextRequest, { params }: IdParams) = export const deleteOneTaskPublic = async (req: NextRequest, { params }: IdParams) => { const { id } = await params + ValidateUuid(id, PublicResource.Tasks) const recursive = req.nextUrl.searchParams.get('recursive') const user = await authenticate(req) const tasksService = new PublicTasksService(user) diff --git a/src/app/api/tasks/templates/public/public.controller.ts b/src/app/api/tasks/templates/public/public.controller.ts index 2d40b3213..d7d912a4d 100644 --- a/src/app/api/tasks/templates/public/public.controller.ts +++ b/src/app/api/tasks/templates/public/public.controller.ts @@ -5,6 +5,8 @@ import { TemplatesService } from '@/app/api/tasks/templates/templates.service' import { defaultLimit } from '@/constants/public-api' import { getSearchParams } from '@/utils/request' import { encode, decode } from 'js-base64' +import { ValidateUuid } from '@api/core/utils/validateUuid' +import { PublicResource } from '@api/core/types/public' import { NextRequest, NextResponse } from 'next/server' @@ -24,6 +26,7 @@ export const getTaskTemplatesPublic = async (req: NextRequest) => { export const getTaskTemplatePublic = async (req: NextRequest, { params }: IdParams) => { const { id } = await params + ValidateUuid(id, PublicResource.Tasks) const user = await authenticate(req) const templatesService = new TemplatesService(user) const template = await templatesService.getOneTemplate(id)