diff --git a/app/api/sites/add/route.ts b/app/api/sites/add/route.ts index 6c97c3a..86a13a6 100644 --- a/app/api/sites/add/route.ts +++ b/app/api/sites/add/route.ts @@ -1,18 +1,15 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; +import { z } from "zod/v4"; -interface Body { - name: string; - description: string; -} +const schema = z.object({ + name: z.string(), + description: z.string(), +}); export async function POST(request: NextRequest) { try { - const body: Body = await request.json(); - - if (!body.name) { - return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); - } + const body = schema.parse(await request.json()); const site = await prisma.site.create({ data: { @@ -24,6 +21,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ site }, { status: 201 }); } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/sites/delete/route.ts b/app/api/sites/delete/route.ts index ff6dd87..44adf72 100644 --- a/app/api/sites/delete/route.ts +++ b/app/api/sites/delete/route.ts @@ -1,19 +1,25 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; +import { z } from "zod/v4"; +const schema = z.object({ + siteId: z.string(), +}); export async function DELETE(request: NextRequest) { const searchParams = request.nextUrl.searchParams; - const siteId = searchParams.get("siteId"); + const siteId = schema.parse({ siteId: searchParams.get("siteId") }); try { const site = await prisma.site.delete({ - where: { id: Number(siteId) }, + where: { id: Number(siteId.siteId) }, }); return NextResponse.json(site); - } catch (error) { - console.error(error); - return NextResponse.json({ error: "Failed to delete site" }, { status: 500 }); + } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } \ No newline at end of file diff --git a/app/api/sites/edit/route.ts b/app/api/sites/edit/route.ts index 882dd07..84aa104 100644 --- a/app/api/sites/edit/route.ts +++ b/app/api/sites/edit/route.ts @@ -1,27 +1,35 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; -import { Network } from "@/app/types"; +import { z } from "zod/v4"; -interface Body { - id: string; - name: string; - description: string; - networks: Network[]; -} +const schema = z.object({ + id: z.string(), + name: z.string(), + description: z.string(), + networks: z.array(z.object({ + id: z.string().optional(), + name: z.string().optional(), + ipv4Subnet: z.string().optional(), + ipv6Subnet: z.string().optional(), + gateway: z.string().optional(), + })), +}); export async function POST(request: NextRequest) { - const body: Body = await request.json(); - const { id, name, description, networks } = body; - try { + const body = schema.parse(await request.json()); + const { id, name, description, networks } = body; + const site = await prisma.site.update({ where: { id: Number(id) }, data: { name, description }, }); return NextResponse.json(site); - } catch (error) { - console.error(error); - return NextResponse.json({ error: "Failed to update site" }, { status: 500 }); + } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/sites/get/route.ts b/app/api/sites/get/route.ts index db65bfb..213c557 100644 --- a/app/api/sites/get/route.ts +++ b/app/api/sites/get/route.ts @@ -1,18 +1,19 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; +import { z } from "zod/v4"; -interface QueryParams { - siteId: string; -} +const schema = z.object({ + siteId: z.string(), +}); export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; - const siteId = searchParams.get("siteId"); + const siteId = schema.parse({ siteId: searchParams.get("siteId") }); try { const site = await prisma.site.findUnique({ where: { - id: Number(siteId), + id: Number(siteId.siteId), }, include: { networks: { @@ -24,8 +25,10 @@ export async function GET(request: NextRequest) { }); return NextResponse.json({ site }); - } catch (error) { - console.error("Error fetching site:", error); - return NextResponse.json({ error: "Failed to fetch site" }, { status: 500 }); + } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } \ No newline at end of file diff --git a/app/api/sites/get_all/route.ts b/app/api/sites/get_all/route.ts index 232a139..839a0e9 100644 --- a/app/api/sites/get_all/route.ts +++ b/app/api/sites/get_all/route.ts @@ -2,12 +2,19 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; import Fuse from 'fuse.js'; import { Site } from "@/app/types"; +import { z } from "zod/v4"; + +const schema = z.object({ + currentPage: z.string().optional(), + itemPerPage: z.string().optional(), + search: z.string().optional(), +}); export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; - const currentPage = Number(searchParams.get("currentPage")) || 1; - const itemPerPage = Number(searchParams.get("itemPerPage")) || 10; - const search = searchParams.get("search") || ""; + const currentPage = Number(schema.parse({ currentPage: searchParams.get("currentPage") }).currentPage) || 1; + const itemPerPage = Number(schema.parse({ itemPerPage: searchParams.get("itemPerPage") }).itemPerPage) || 10; + const search = schema.parse({ search: searchParams.get("search") }).search || ""; try { if (!search) { @@ -77,7 +84,9 @@ export async function GET(request: NextRequest) { itemPerPage }, { status: 200 }); } catch (error: any) { - console.error("Search error:", error); + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/sites/networks/add/route.ts b/app/api/sites/networks/add/route.ts index c017163..5cd8fa1 100644 --- a/app/api/sites/networks/add/route.ts +++ b/app/api/sites/networks/add/route.ts @@ -1,17 +1,18 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; +import { z } from "zod/v4"; -interface Body { - id: string; - siteId: string; - name: string; - ipv4Subnet?: string; - ipv6Subnet?: string; - gateway?: string; -} +const schema = z.object({ + id: z.string(), + siteId: z.string(), + name: z.string().min(2), + ipv4Subnet: z.string().optional(), + ipv6Subnet: z.string().optional(), + gateway: z.string().optional(), +}); export async function POST(request: NextRequest) { - const body: Body = await request.json(); + const body = schema.parse(await request.json()); try { const network = await prisma.network.create({ @@ -25,8 +26,10 @@ export async function POST(request: NextRequest) { }); return NextResponse.json({ network }, { status: 201 }); - } catch (error) { - console.error("Error creating network:", error); - return NextResponse.json({ error: "Failed to create network" }, { status: 500 }); + } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/sites/networks/delete/route.ts b/app/api/sites/networks/delete/route.ts index c72465f..55e9586 100644 --- a/app/api/sites/networks/delete/route.ts +++ b/app/api/sites/networks/delete/route.ts @@ -1,20 +1,27 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; +import { z } from "zod/v4"; + +const schema = z.object({ + networkId: z.string(), +}); export async function DELETE(request: NextRequest) { const searchParams = request.nextUrl.searchParams; - const networkId = searchParams.get("networkId"); + const network = schema.parse({ networkId: searchParams.get("networkId") }); try { await prisma.network.delete({ where: { - id: Number(networkId), + id: Number(network.networkId), }, }); return NextResponse.json({ message: "Network deleted successfully" }); - } catch (error) { - console.error("Error deleting network:", error); - return NextResponse.json({ error: "Failed to delete network" }, { status: 500 }); + } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } \ No newline at end of file diff --git a/app/api/sites/networks/edit/route.ts b/app/api/sites/networks/edit/route.ts index 2897f0b..21ee88d 100644 --- a/app/api/sites/networks/edit/route.ts +++ b/app/api/sites/networks/edit/route.ts @@ -1,16 +1,17 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; +import { z } from "zod/v4"; -interface Body { - id: string; - name: string; - ipv4Subnet?: string; - ipv6Subnet?: string; - gateway?: string; -} +const schema = z.object({ + id: z.string(), + name: z.string().min(2), + ipv4Subnet: z.string().optional(), + ipv6Subnet: z.string().optional(), + gateway: z.string().optional(), +}); export async function POST(request: NextRequest) { - const body: Body = await request.json(); + const body = schema.parse(await request.json()); try { const network = await prisma.network.update({ @@ -26,8 +27,10 @@ export async function POST(request: NextRequest) { }); return NextResponse.json({ network }, { status: 201 }); - } catch (error) { - console.error("Error editing network:", error); - return NextResponse.json({ error: "Failed to edit network" }, { status: 500 }); + } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } \ No newline at end of file diff --git a/app/api/user/change/password/route.ts b/app/api/user/change/password/route.ts index ea1a581..133793d 100644 --- a/app/api/user/change/password/route.ts +++ b/app/api/user/change/password/route.ts @@ -2,20 +2,17 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; import jwt from "jsonwebtoken"; import bcrypt from "bcryptjs"; +import { z } from "zod/v4"; -interface Body { - token: string; - old_password: string; - password: string; -} +const schema = z.object({ + token: z.string(), + old_password: z.string(), + password: z.string().min(8, "Password must be at least 8 characters long").max(32, "Password must be at most 32 characters long").regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,32}$/, "Password must contain at least one lowercase letter, one uppercase letter, one digit, and one special character"), +}); export async function POST(request: NextRequest) { try { - const body: Body = await request.json(); - - if (!body.token || !body.old_password || !body.password) { - return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); - } + const body = schema.parse(await request.json()); if(!process.env.JWT_SECRET) { return NextResponse.json({ error: "No JWT secret found" }, { status: 500 }); @@ -52,6 +49,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: "Password updated successfully" }, { status: 200 }); } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/user/change/profile/route.ts b/app/api/user/change/profile/route.ts index cfdcf0a..a80433f 100644 --- a/app/api/user/change/profile/route.ts +++ b/app/api/user/change/profile/route.ts @@ -1,21 +1,18 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "@/app/prisma"; import jwt from "jsonwebtoken"; +import { z } from "zod/v4"; -interface Body { - token: string; - username: string; - name: string; - email: string; -} +const schema = z.object({ + token: z.string(), + username: z.string().min(3, "Username must be at least 3 characters long").max(32, "Username must be at most 32 characters long"), + name: z.string().min(3, "Name must be at least 3 characters long").max(32, "Name must be at most 32 characters long"), + email: z.string().email("Invalid email address"), +}); export async function POST(request: NextRequest) { try { - const body: Body = await request.json(); - - if (!body.username || !body.name || !body.email) { - return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); - } + const body = schema.parse(await request.json()); if(!process.env.JWT_SECRET) { return NextResponse.json({ error: "No JWT secret found" }, { status: 500 }); @@ -46,6 +43,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: "Profile updated successfully" }, { status: 200 }); } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/user/create/route.ts b/app/api/user/create/route.ts index 0a3487b..be1be46 100644 --- a/app/api/user/create/route.ts +++ b/app/api/user/create/route.ts @@ -1,21 +1,18 @@ import { NextRequest, NextResponse } from "next/server"; import bcrypt from 'bcryptjs'; import prisma from "@/app/prisma"; +import { z } from "zod/v4"; -interface Body { - username: string; - name: string; - email: string; - password: string; -} +const schema = z.object({ + username: z.string().min(3, "Username must be at least 3 characters long").max(32, "Username must be at most 32 characters long"), + name: z.string().min(3, "Name must be at least 3 characters long").max(32, "Name must be at most 32 characters long"), + email: z.string().email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters long").max(32, "Password must be at most 32 characters long").regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,32}$/, "Password must contain at least one lowercase letter, one uppercase letter, one digit, and one special character"), +}); export async function POST(request: NextRequest) { try { - const body: Body = await request.json(); - - if (!body.username || !body.name || !body.email || !body.password) { - return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); - } + const body = schema.parse(await request.json()); const user = await prisma.user.create({ data: { @@ -29,6 +26,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ user }, { status: 201 }); } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/user/login/route.ts b/app/api/user/login/route.ts index 71c93f4..430e2d1 100644 --- a/app/api/user/login/route.ts +++ b/app/api/user/login/route.ts @@ -2,20 +2,17 @@ import { NextRequest, NextResponse } from "next/server"; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import prisma from "@/app/prisma"; +import { z } from "zod/v4"; -interface Body { - email: string; - password: string; - remember?: boolean; -} +const schema = z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters long").max(32, "Password must be at most 32 characters long").regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,32}$/, "Password must contain at least one lowercase letter, one uppercase letter, one digit, and one special character"), + remember: z.boolean().optional(), +}); export async function POST(request: NextRequest) { try { - const body: Body = await request.json(); - - if (!body.email || !body.password) { - return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); - } + const body = schema.parse(await request.json()); const user = await prisma.user.findUnique({ where: { @@ -53,6 +50,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: "Login successful", token }, { status: 200 }); } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/user/validate/route.ts b/app/api/user/validate/route.ts index f985fd3..8db9ec1 100644 --- a/app/api/user/validate/route.ts +++ b/app/api/user/validate/route.ts @@ -1,18 +1,15 @@ import { NextRequest, NextResponse } from "next/server"; import jwt from 'jsonwebtoken'; import prisma from "@/app/prisma"; +import { z } from "zod/v4"; -interface Body { - token: string; -} +const schema = z.object({ + token: z.string(), +}); export async function POST(request: NextRequest) { try { - const body: Body = await request.json(); - - if (!body.token) { - return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); - } + const body = schema.parse(await request.json()); if(!process.env.JWT_SECRET) { return NextResponse.json({ error: "No JWT secret found" }, { status: 500 }); @@ -33,6 +30,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: "Valid", username: user.username, name: user.name, email: user.email }, { status: 200 }); } catch (error: any) { + if(error instanceof z.ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/package-lock.json b/package-lock.json index ea8405d..6fb8fbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "lucide-react": "^0.511.0", "next": "15.3.2", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "zod": "^3.25.17" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.7", @@ -2315,6 +2316,15 @@ "engines": { "node": ">=18" } + }, + "node_modules/zod": { + "version": "3.25.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.17.tgz", + "integrity": "sha512-8hQzQ/kMOIFbwOgPrm9Sf9rtFHpFUMy4HvN0yEB0spw14aYi0uT5xG5CE2DB9cd51GWNsz+DNO7se1kztHMKnw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 9fa9333..e0d6c76 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "lucide-react": "^0.511.0", "next": "15.3.2", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "zod": "^3.25.17" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.7",