From 3f777f67ae826ff26d6e38efa22dd57a584d8323 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:26:49 +0200 Subject: [PATCH] zod shit --- bun.lock | 1 + package.json | 3 +- .../api/admin/movies/[id]/approve/route.ts | 9 ++- src/app/api/admin/schedule/route.ts | 20 +++-- src/app/api/movies/[id]/vote/route.ts | 9 ++- src/app/api/movies/route.ts | 9 ++- src/app/api/movies/search/route.ts | 15 ++-- src/lib/validation.ts | 80 +++++++++++++++++++ 8 files changed, 130 insertions(+), 16 deletions(-) create mode 100644 src/lib/validation.ts diff --git a/bun.lock b/bun.lock index a40c303..54902a0 100644 --- a/bun.lock +++ b/bun.lock @@ -24,6 +24,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.7", + "zod": "^4.0.17", }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/package.json b/package.json index 3f4463f..2dd9c22 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "react-dom": "19.1.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", - "tw-animate-css": "^1.3.7" + "tw-animate-css": "^1.3.7", + "zod": "^4.0.17" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/src/app/api/admin/movies/[id]/approve/route.ts b/src/app/api/admin/movies/[id]/approve/route.ts index dd2f220..4fd5d51 100644 --- a/src/app/api/admin/movies/[id]/approve/route.ts +++ b/src/app/api/admin/movies/[id]/approve/route.ts @@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client"; import { NextResponse } from "next/server"; import { auth } from "../../../../auth/auth"; import { headers } from "next/headers"; +import { idSchema, validateParams } from "@/lib/validation"; const prisma = new PrismaClient(); @@ -17,9 +18,15 @@ export async function POST( return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } + // Validate params + const { data, error } = validateParams(await params, idSchema); + if (error) { + return NextResponse.json({ error }, { status: 400 }); + } + const movie = await prisma.movie.update({ where: { - id: (await params).id, + id: data.id, }, data: { approved: true, diff --git a/src/app/api/admin/schedule/route.ts b/src/app/api/admin/schedule/route.ts index 4072109..42ddfa6 100644 --- a/src/app/api/admin/schedule/route.ts +++ b/src/app/api/admin/schedule/route.ts @@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client"; import { NextResponse } from "next/server"; import { headers } from "next/headers"; import { auth } from "../../auth/auth"; +import { scheduleMovieSchema, scheduleIdSchema, validateRequestData, validateSearchParams } from "@/lib/validation"; const prisma = new PrismaClient(); @@ -14,7 +15,13 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { movieId, date } = await req.json(); + // Validate request data + const { data, error } = await validateRequestData(req, scheduleMovieSchema); + if (error) { + return NextResponse.json({ error }, { status: 400 }); + } + + const { movieId, date } = data; try { const schedule = await prisma.movieSchedule.create({ @@ -60,12 +67,13 @@ export async function DELETE(req: Request) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const { searchParams } = new URL(req.url); - const scheduleId = searchParams.get('id'); - - if (!scheduleId) { - return NextResponse.json({ error: "Schedule ID required" }, { status: 400 }); + // Validate search params + const { data, error } = validateSearchParams(req.url, scheduleIdSchema); + if (error) { + return NextResponse.json({ error }, { status: 400 }); } + + const { id: scheduleId } = data; try { await prisma.movieSchedule.delete({ diff --git a/src/app/api/movies/[id]/vote/route.ts b/src/app/api/movies/[id]/vote/route.ts index 6d19441..b1f435f 100644 --- a/src/app/api/movies/[id]/vote/route.ts +++ b/src/app/api/movies/[id]/vote/route.ts @@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client"; import { NextResponse } from "next/server"; import { auth } from "../../../auth/auth"; import { headers } from "next/headers"; +import { idSchema, validateParams } from "@/lib/validation"; const prisma = new PrismaClient(); @@ -16,7 +17,13 @@ export async function POST( return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const movieId = (await params).id; + // Validate params + const { data, error } = validateParams(await params, idSchema); + if (error) { + return NextResponse.json({ error }, { status: 400 }); + } + + const movieId = data.id; const userId = session.user.id; // Check if the user has already voted for this movie diff --git a/src/app/api/movies/route.ts b/src/app/api/movies/route.ts index 97f607e..92c32bf 100644 --- a/src/app/api/movies/route.ts +++ b/src/app/api/movies/route.ts @@ -3,16 +3,23 @@ import { PrismaClient } from "@prisma/client"; import { NextRequest, NextResponse } from "next/server"; import { headers } from "next/headers"; import { auth } from "../auth/auth"; +import { movieSuggestionSchema, validateRequestData } from "@/lib/validation"; const prisma = new PrismaClient(); export async function POST(req: Request) { - const { title, description, posterUrl, suggestedBy } = await req.json(); const session = await auth.api.getSession({ headers: await headers() }); if (!session || !session.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } + // Validate request data + const { data, error } = await validateRequestData(req, movieSuggestionSchema); + if (error) { + return NextResponse.json({ error }, { status: 400 }); + } + const { title, description, posterUrl, suggestedBy } = data as any; + // Check if there's already a pending request for this movie (not approved) const existingPendingMovies = await prisma.movie.findMany({ where: { diff --git a/src/app/api/movies/search/route.ts b/src/app/api/movies/search/route.ts index cb2991a..057224a 100644 --- a/src/app/api/movies/search/route.ts +++ b/src/app/api/movies/search/route.ts @@ -2,24 +2,27 @@ import auth from "@/lib/auth-config"; import { headers } from "next/headers"; import { NextResponse } from "next/server"; +import { searchMovieSchema, validateSearchParams } from "@/lib/validation"; export async function GET(req: Request) { - const { searchParams } = new URL(req.url); - const query = searchParams.get("query"); const session = await auth.api.getSession({ headers: await headers() }); if (!session) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - if (!query) { - return NextResponse.json({ error: "Query is required" }, { status: 400 }); + // Validate search params + const { data, error } = validateSearchParams(req.url, searchMovieSchema); + if (error) { + return NextResponse.json({ error }, { status: 400 }); } + + const { query } = data; const res = await fetch( `https://api.themoviedb.org/3/search/movie?api_key=${process.env.TMDB_API_KEY}&query=${query}` ); - const data = await res.json(); + const apiData = await res.json(); - return NextResponse.json(data.results); + return NextResponse.json(apiData.results); } diff --git a/src/lib/validation.ts b/src/lib/validation.ts new file mode 100644 index 0000000..4e0b758 --- /dev/null +++ b/src/lib/validation.ts @@ -0,0 +1,80 @@ +import { z } from 'zod'; + +// Movie suggestion schema +export const movieSuggestionSchema = z.object({ + title: z.string().min(1, "Title is required").max(255, "Title must be less than 255 characters"), + description: z.string().min(1, "Description is required").max(1000, "Description must be less than 1000 characters"), + posterUrl: z.string().url("Invalid URL format"), + suggestedBy: z.string().min(1, "Suggested by is required").max(100, "Suggested by must be less than 100 characters"), +}); + +// Movie vote schema +export const movieVoteSchema = z.object({ + movieId: z.string().min(1, "Movie ID is required"), +}); + +// Schedule movie schema +export const scheduleMovieSchema = z.object({ + movieId: z.string().min(1, "Movie ID is required"), + date: z.string().datetime("Invalid date format"), +}); + +// Search movie schema +export const searchMovieSchema = z.object({ + query: z.string().min(1, "Query is required").max(100, "Query must be less than 100 characters"), +}); + +// Schedule ID schema +export const scheduleIdSchema = z.object({ + id: z.string().min(1, "Schedule ID is required"), +}); + +// Generic ID schema +export const idSchema = z.object({ + id: z.string().min(1, "ID is required"), +}); + +// Utility function to validate request data +export async function validateRequestData(request: Request, schema: z.ZodSchema) { + try { + const data = await request.json(); + return { data: schema.parse(data) as any, error: null }; + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessage = error.errors.map(err => err.message).join(", "); + return { data: null, error: errorMessage }; + } + return { data: null, error: "Invalid JSON" }; + } +} + +// Utility function to validate URL parameters +export function validateParams(params: Record, schema: z.ZodSchema) { + try { + return { data: schema.parse(params) as any, error: null }; + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessage = error.errors.map(err => err.message).join(", "); + return { data: null, error: errorMessage }; + } + return { data: null, error: "Invalid parameters" }; + } +} + +// Utility function to validate search parameters +export function validateSearchParams(url: string, schema: z.ZodSchema) { + try { + const { searchParams } = new URL(url); + const params: Record = {}; + for (const [key, value] of searchParams.entries()) { + params[key] = value; + } + return { data: schema.parse(params) as any, error: null }; + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessage = error.errors.map(err => err.message).join(", "); + return { data: null, error: errorMessage }; + } + return { data: null, error: "Invalid search parameters" }; + } +} \ No newline at end of file