diff --git a/src/app/api/search/user/route.ts b/src/app/api/search/user/route.ts new file mode 100644 index 0000000..a8b6414 --- /dev/null +++ b/src/app/api/search/user/route.ts @@ -0,0 +1,79 @@ +import { auth } from '@/auth'; +import { prisma } from '@/prisma'; +import { searchUserSchema, searchUserResponseSchema } from './validation'; +import { + returnZodTypeCheckedResponse, + userAuthenticated, +} from '@/lib/apiHelpers'; +import { + ErrorResponseSchema, + ZodErrorResponseSchema, +} from '@/app/api/validation'; + +export const GET = auth(async function GET(req) { + const authCheck = userAuthenticated(req); + if (!authCheck.continue) + return returnZodTypeCheckedResponse( + ErrorResponseSchema, + authCheck.response, + authCheck.metadata, + ); + + const dataRaw = Object.fromEntries(new URL(req.url).searchParams); + const data = await searchUserSchema.safeParseAsync(dataRaw); + if (!data.success) + return returnZodTypeCheckedResponse( + ZodErrorResponseSchema, + { + success: false, + message: 'Invalid request data', + errors: data.error.issues, + }, + { status: 400 }, + ); + const { query, count, page, sort_by, sort_order } = data.data; + + const dbUsers = await prisma.user.findMany({ + where: { + OR: [ + { name: { contains: query } }, + { first_name: { contains: query } }, + { last_name: { contains: query } }, + ], + }, + orderBy: { + [sort_by]: sort_order, + }, + skip: (page - 1) * count, + take: count, + select: { + id: true, + name: true, + first_name: true, + last_name: true, + timezone: true, + image: true, + }, + }); + + const userCount = await prisma.user.count({ + where: { + OR: [ + { name: { contains: query } }, + { first_name: { contains: query } }, + { last_name: { contains: query } }, + ], + }, + }); + + return returnZodTypeCheckedResponse( + searchUserResponseSchema, + { + success: true, + users: dbUsers, + total_count: userCount, + total_pages: Math.ceil(userCount / count), + }, + { status: 200 }, + ); +}); diff --git a/src/app/api/search/user/swagger.ts b/src/app/api/search/user/swagger.ts new file mode 100644 index 0000000..90ca54e --- /dev/null +++ b/src/app/api/search/user/swagger.ts @@ -0,0 +1,33 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { searchUserResponseSchema, searchUserSchema } from './validation'; +import { + invalidRequestDataResponse, + notAuthenticatedResponse, + serverReturnedDataValidationErrorResponse, + userNotFoundResponse, +} from '@/lib/defaultApiResponses'; + +export default function registerSwaggerPaths(registry: OpenAPIRegistry) { + registry.registerPath({ + method: 'get', + path: '/api/search/user', + request: { + query: searchUserSchema, + }, + responses: { + 200: { + description: 'User search results', + content: { + 'application/json': { + schema: searchUserResponseSchema, + }, + }, + }, + ...invalidRequestDataResponse, + ...notAuthenticatedResponse, + ...userNotFoundResponse, + ...serverReturnedDataValidationErrorResponse, + }, + tags: ['Search'], + }); +} diff --git a/src/app/api/search/user/validation.ts b/src/app/api/search/user/validation.ts new file mode 100644 index 0000000..c1662b0 --- /dev/null +++ b/src/app/api/search/user/validation.ts @@ -0,0 +1,20 @@ +import zod from 'zod/v4'; +import { PublicUserSchema } from '../../user/validation'; + +export const searchUserSchema = zod.object({ + query: zod.string().optional().default(''), + count: zod.coerce.number().min(1).max(100).default(10), + page: zod.coerce.number().min(1).default(1), + sort_by: zod + .enum(['created_at', 'name', 'first_name', 'last_name', 'id']) + .optional() + .default('created_at'), + sort_order: zod.enum(['asc', 'desc']).optional().default('desc'), +}); + +export const searchUserResponseSchema = zod.object({ + success: zod.boolean(), + users: zod.array(PublicUserSchema), + total_count: zod.number(), + total_pages: zod.number(), +});