diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index 4fba5a3..2447de6 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { Prisma } from '@prisma/client' import prisma from '@/lib/prisma' import { generateHash } from '@/utils/hash' -import { ApiError, routeWrapper } from '@/utils/api' +import { ApiError, routeWrapper, getQueryParams } from '@/utils/api' export const sanitizeUserSelect = () => { const fields = Object.keys(Prisma.UserScalarFieldEnum) @@ -29,11 +29,9 @@ export const checkUserBody = async (body: any, id: string | null = null) => { export const GET = routeWrapper( async (req: NextRequest) => { - const skip = req.nextUrl.searchParams.get('skip') - const take = req.nextUrl.searchParams.get('take') + const queryParams = getQueryParams(req.nextUrl) const users = await prisma.user.findMany({ - skip: skip && take ? Number(skip) : undefined, - take: skip && take ? Number(take) : undefined, + ...queryParams, select: sanitizeUserSelect() }) return NextResponse.json(users) diff --git a/src/store/user.ts b/src/store/user.ts index 0835467..dfb2edf 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -1,5 +1,5 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' -import { User } from '@prisma/client' +import { Prisma, User } from '@prisma/client' export const userApi = createApi({ reducerPath: 'userApi', @@ -10,7 +10,7 @@ export const userApi = createApi({ query: (id) => `users/${id}`, providesTags: (_result, _err, id) => [{ type: 'User', id }], }), - getUsers: build.query({ + getUsers: build.query({ query: (params) => ({ url: 'users', params diff --git a/src/utils/api.ts b/src/utils/api.ts index a76ce45..f2c349f 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -3,7 +3,11 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import authOptions from '@/lib/auth' +import { NextURL } from 'next/dist/server/web/next-url' +/** + * Custom error class representing API errors with a specific status code. + */ export class ApiError extends Error { public readonly statusCode: number @@ -15,6 +19,11 @@ export class ApiError extends Error { } } +/** + * Sanitizes sensitive information from the request body for logging purposes. + * @param consumedBody - The request body to be sanitized. + * @returns Sanitized request body. + */ const sanitizeBody = (consumedBody: any) => { const sanitizedText = '*****' const sanitizedBody = { ...consumedBody } @@ -23,6 +32,10 @@ const sanitizeBody = (consumedBody: any) => { return sanitizedBody } +/** + * Logs request details to the console for debugging purposes. + * @param req - The incoming request object. + */ const logRequest = (req: NextRequest) => { if (process.env.NODE_ENV === 'test') return const { method, url, consumedBody } = req @@ -33,18 +46,32 @@ const logRequest = (req: NextRequest) => { console.info('---') } +/** + * Logs error details to the console for debugging purposes. + * @param error - The error object to be logged. + */ const logError = (error: any) => { if (process.env.NODE_ENV === 'test') return console.info(`Error: ${JSON.stringify(error)}`) console.info('---') } +/** + * Extracts a human-readable error message from the given error string. + * @param message - The error message string. + * @returns Human-readable error message. + */ const getErrorMessage = (message: string) => { if (message.includes('Unique constraint failed on the fields: (`username`)')) return 'Username exists' if (message.includes('Unique constraint failed on the fields: (`email`)')) return 'Email exists' return 'Server failure' } +/** + * Wraps a route handler function with error handling and request logging. + * @param routeHandler - The route handler function to be wrapped. + * @returns Wrapped route handler function. + */ export const routeWrapper = ( routeHandler: ( req: NextRequest, context?: any) => Promise @@ -70,13 +97,37 @@ export const routeWrapper = ( } } +/** + * Retrieves the user session information from the server session. + * @returns User session information. + * @throws ApiError if the user is not authenticated. + */ export const withSessionUser = async () => { const session = await getServerSession(authOptions) if (!session) throw new ApiError('Unauthorized', 401) return session?.user } +/** + * Checks if the provided user ID matches the user ID in the session. + * @param userId - The user ID to be checked. + * @throws ApiError if the user IDs do not match, indicating unauthorized access. + */ export const checkUserMatchesSession = async (userId: string | undefined) => { const session = await getServerSession(authOptions) if (session?.user?.id !== userId) throw new ApiError('Unauthorized', 401) } + +/** + * Parses and extracts query parameters from a Next.js URL object. + * @param nextUrl - Next.js URL object containing query parameters. + * @returns Parsed query parameters as an object. + */ +export const getQueryParams = (nextUrl: NextURL) => { + const queryParams = Array.from(nextUrl.searchParams.entries()).reduce((acc, [key, value]) => { + const numericValue = !Number.isNaN(Number(value)) ? Number(value) : value + return { ...acc, [key]: numericValue } + }, {} as { [key: string]: string | number }) + + return queryParams +}