This commit is contained in:
2025-08-18 17:26:49 +02:00
parent 72445ac6e9
commit 3f777f67ae
8 changed files with 130 additions and 16 deletions

View File

@@ -24,6 +24,7 @@
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.7", "tw-animate-css": "^1.3.7",
"zod": "^4.0.17",
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",

View File

@@ -28,7 +28,8 @@
"react-dom": "19.1.0", "react-dom": "19.1.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.7" "tw-animate-css": "^1.3.7",
"zod": "^4.0.17"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",

View File

@@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { auth } from "../../../../auth/auth"; import { auth } from "../../../../auth/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { idSchema, validateParams } from "@/lib/validation";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -17,9 +18,15 @@ export async function POST(
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 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({ const movie = await prisma.movie.update({
where: { where: {
id: (await params).id, id: data.id,
}, },
data: { data: {
approved: true, approved: true,

View File

@@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { auth } from "../../auth/auth"; import { auth } from "../../auth/auth";
import { scheduleMovieSchema, scheduleIdSchema, validateRequestData, validateSearchParams } from "@/lib/validation";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -14,7 +15,13 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 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 { try {
const schedule = await prisma.movieSchedule.create({ const schedule = await prisma.movieSchedule.create({
@@ -60,12 +67,13 @@ export async function DELETE(req: Request) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
const { searchParams } = new URL(req.url); // Validate search params
const scheduleId = searchParams.get('id'); const { data, error } = validateSearchParams(req.url, scheduleIdSchema);
if (error) {
if (!scheduleId) { return NextResponse.json({ error }, { status: 400 });
return NextResponse.json({ error: "Schedule ID required" }, { status: 400 });
} }
const { id: scheduleId } = data;
try { try {
await prisma.movieSchedule.delete({ await prisma.movieSchedule.delete({

View File

@@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { auth } from "../../../auth/auth"; import { auth } from "../../../auth/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { idSchema, validateParams } from "@/lib/validation";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -16,7 +17,13 @@ export async function POST(
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 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; const userId = session.user.id;
// Check if the user has already voted for this movie // Check if the user has already voted for this movie

View File

@@ -3,16 +3,23 @@ import { PrismaClient } from "@prisma/client";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { auth } from "../auth/auth"; import { auth } from "../auth/auth";
import { movieSuggestionSchema, validateRequestData } from "@/lib/validation";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
export async function POST(req: Request) { export async function POST(req: Request) {
const { title, description, posterUrl, suggestedBy } = await req.json();
const session = await auth.api.getSession({ headers: await headers() }); const session = await auth.api.getSession({ headers: await headers() });
if (!session || !session.user?.id) { if (!session || !session.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 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) // Check if there's already a pending request for this movie (not approved)
const existingPendingMovies = await prisma.movie.findMany({ const existingPendingMovies = await prisma.movie.findMany({
where: { where: {

View File

@@ -2,24 +2,27 @@
import auth from "@/lib/auth-config"; import auth from "@/lib/auth-config";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { searchMovieSchema, validateSearchParams } from "@/lib/validation";
export async function GET(req: Request) { 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() }); const session = await auth.api.getSession({ headers: await headers() });
if (!session) { if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
if (!query) { // Validate search params
return NextResponse.json({ error: "Query is required" }, { status: 400 }); const { data, error } = validateSearchParams(req.url, searchMovieSchema);
if (error) {
return NextResponse.json({ error }, { status: 400 });
} }
const { query } = data;
const res = await fetch( const res = await fetch(
`https://api.themoviedb.org/3/search/movie?api_key=${process.env.TMDB_API_KEY}&query=${query}` `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);
} }

80
src/lib/validation.ts Normal file
View File

@@ -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<string, string>, 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<string, string> = {};
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" };
}
}