diff --git a/app/api/projects/[id]/route.ts b/app/api/projects/[id]/route.ts index 0020aa3..260c485 100644 --- a/app/api/projects/[id]/route.ts +++ b/app/api/projects/[id]/route.ts @@ -1,8 +1,14 @@ import { NextResponse } from "next/server" +import { revalidatePath } from "next/cache" import { getServerSession } from "next-auth" import { authOptions } from "@/lib/auth" import prisma from "@/lib/db" +import { + normalizeBoolean, + normalizeLongFormField, + normalizeOptionalString, +} from "@/lib/project-validation" type FeatureInput = { name: string @@ -34,6 +40,13 @@ export async function GET(req: Request, { params }: RouteParams) { }, include: { features: true, + user: { + select: { + id: true, + name: true, + email: true, + }, + }, }, }) @@ -77,10 +90,12 @@ export async function PUT(req: Request, { params }: RouteParams) { features = [], } = data - const featureList: FeatureInput[] = Array.isArray(features) ? features : [] + const sanitizedTitle = normalizeOptionalString(title) + const sanitizedDescription = normalizeOptionalString(description) + const sanitizedTechnologies = normalizeOptionalString(technologies) // Validate required fields - if (!title || !description || !technologies) { + if (!sanitizedTitle || !sanitizedDescription || !sanitizedTechnologies) { return NextResponse.json({ error: "Missing required fields" }, { status: 400 }) } @@ -97,38 +112,62 @@ export async function PUT(req: Request, { params }: RouteParams) { } // Update project + const sanitizedFeatureList: FeatureInput[] = (Array.isArray(features) ? features : []) + .map((feature) => { + const name = normalizeOptionalString(feature?.name) + + if (!name) { + return null + } + + return { + name, + description: normalizeLongFormField(feature?.description), + } + }) + .filter((feature): feature is FeatureInput => feature !== null) + const project = await prisma.project.update({ where: { id: projectId, }, data: { - title, - description, - technologies, - link, - githubUrl, - imageUrl, - logoUrl, - featured: Boolean(featured), - developmentProcess, - challengesFaced, - futurePlans, - logContent, + title: sanitizedTitle, + description: sanitizedDescription, + technologies: sanitizedTechnologies, + link: normalizeOptionalString(link), + githubUrl: normalizeOptionalString(githubUrl), + imageUrl: normalizeOptionalString(imageUrl), + logoUrl: normalizeOptionalString(logoUrl), + featured: normalizeBoolean(featured), + developmentProcess: normalizeLongFormField(developmentProcess), + challengesFaced: normalizeLongFormField(challengesFaced), + futurePlans: normalizeLongFormField(futurePlans), + logContent: normalizeLongFormField(logContent), features: { deleteMany: {}, - create: featureList - .filter((feature): feature is FeatureInput => Boolean(feature?.name)) - .map((feature) => ({ - name: feature.name, - description: feature.description ?? null, - })), + create: sanitizedFeatureList, }, }, include: { features: true, + user: { + select: { + id: true, + name: true, + email: true, + }, + }, }, }) + revalidatePath("/projects") + revalidatePath("/") + revalidatePath("/dashboard") + revalidatePath("/dashboard/projects") + revalidatePath(`/dashboard/projects/${projectId}`) + revalidatePath(`/dashboard/projects/${projectId}/view`) + return NextResponse.json({ project }) } catch (error) { console.error("Error updating project:", error) @@ -167,6 +206,13 @@ export async function DELETE(req: Request, { params }: RouteParams) { }, }) + revalidatePath("/projects") + revalidatePath("/") + revalidatePath("/dashboard") + revalidatePath("/dashboard/projects") + revalidatePath(`/dashboard/projects/${projectId}`) + revalidatePath(`/dashboard/projects/${projectId}/view`) + return NextResponse.json({ success: true }) } catch (error) { console.error("Error deleting project:", error) diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index a7a98fa..dd7b96d 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -1,8 +1,14 @@ import { NextResponse } from "next/server" import { revalidatePath } from "next/cache" import { getServerSession } from "next-auth" + import { authOptions } from "@/lib/auth" import prisma from "@/lib/db" +import { + normalizeBoolean, + normalizeLongFormField, + normalizeOptionalString, +} from "@/lib/project-validation" type FeatureInput = { name: string @@ -122,40 +128,59 @@ export async function POST(req: Request) { features = [], } = data - const featureList: FeatureInput[] = Array.isArray(features) ? features : [] + const sanitizedTitle = normalizeOptionalString(title) + const sanitizedDescription = normalizeOptionalString(description) + const sanitizedTechnologies = normalizeOptionalString(technologies) // Validate required fields - if (!title || !description || !technologies) { + if (!sanitizedTitle || !sanitizedDescription || !sanitizedTechnologies) { return NextResponse.json({ error: "Missing required fields" }, { status: 400 }) } + const sanitizedFeatureList: FeatureInput[] = (Array.isArray(features) ? features : []) + .map((feature) => { + const name = normalizeOptionalString(feature?.name) + + if (!name) { + return null + } + + return { + name, + description: normalizeLongFormField(feature?.description), + } + }) + .filter((feature): feature is FeatureInput => feature !== null) + // Create project with features const project = await prisma.project.create({ data: { - title, - description, - technologies, - link, - githubUrl, - imageUrl, - logoUrl, - featured: Boolean(featured), - developmentProcess, - challengesFaced, - futurePlans, - logContent, + title: sanitizedTitle, + description: sanitizedDescription, + technologies: sanitizedTechnologies, + link: normalizeOptionalString(link), + githubUrl: normalizeOptionalString(githubUrl), + imageUrl: normalizeOptionalString(imageUrl), + logoUrl: normalizeOptionalString(logoUrl), + featured: normalizeBoolean(featured), + developmentProcess: normalizeLongFormField(developmentProcess), + challengesFaced: normalizeLongFormField(challengesFaced), + futurePlans: normalizeLongFormField(futurePlans), + logContent: normalizeLongFormField(logContent), userId, features: { - create: featureList - .filter((feature): feature is FeatureInput => Boolean(feature?.name)) - .map((feature) => ({ - name: feature.name, - description: feature.description ?? null, - })), + create: sanitizedFeatureList, }, }, include: { features: true, + user: { + select: { + id: true, + name: true, + email: true, + }, + }, }, }) @@ -163,6 +188,8 @@ export async function POST(req: Request) { revalidatePath("/") revalidatePath("/dashboard") revalidatePath("/dashboard/projects") + revalidatePath(`/dashboard/projects/${project.id}`) + revalidatePath(`/dashboard/projects/${project.id}/view`) return NextResponse.json({ project }) } catch (error) { diff --git a/app/dashboard/projects/[id]/page.tsx b/app/dashboard/projects/[id]/page.tsx index a816e32..0847b3e 100644 --- a/app/dashboard/projects/[id]/page.tsx +++ b/app/dashboard/projects/[id]/page.tsx @@ -27,6 +27,7 @@ import type { FeatureDraft } from "@/components/feature-input" import type { Project } from "@/types/database" import { AnimatedSection } from "@/components/animated-section" import { AnimatedList } from "@/components/animated-list" +import { PROJECT_LONGFORM_MAX_LENGTH } from "@/lib/project-validation" interface EditProjectPageProps { params: { @@ -47,6 +48,7 @@ export default function EditProjectPage({ params }: EditProjectPageProps) { const logFileInputRef = useRef(null) const [uploadingLog, setUploadingLog] = useState(false) const [projectId, setProjectId] = useState(null) + const formattedLongFormLimit = new Intl.NumberFormat().format(PROJECT_LONGFORM_MAX_LENGTH) useEffect(() => { if (!params?.id) { @@ -132,6 +134,10 @@ export default function EditProjectPage({ params }: EditProjectPageProps) { const developmentProcess = formData.get("developmentProcess") as string const challengesFaced = formData.get("challengesFaced") as string const futurePlans = formData.get("futurePlans") as string + const safeLogContent = + logContent.length > PROJECT_LONGFORM_MAX_LENGTH + ? logContent.slice(0, PROJECT_LONGFORM_MAX_LENGTH) + : logContent try { const response = await fetch(`/api/projects/${projectId}`, { @@ -151,7 +157,7 @@ export default function EditProjectPage({ params }: EditProjectPageProps) { developmentProcess, challengesFaced, futurePlans, - logContent, + logContent: safeLogContent, features: features .filter((feature) => feature.name.trim().length > 0) .map((feature) => ({ @@ -235,12 +241,19 @@ export default function EditProjectPage({ params }: EditProjectPageProps) { try { // Read file content const text = await file.text() - setLogContent(text) - - toast({ - title: "Log file loaded", - description: "Log file has been loaded successfully.", - }) + if (text.length > PROJECT_LONGFORM_MAX_LENGTH) { + setLogContent(text.slice(0, PROJECT_LONGFORM_MAX_LENGTH)) + toast({ + title: "Log truncated", + description: `The uploaded log exceeded ${formattedLongFormLimit} characters and was truncated to fit the storage limit.`, + }) + } else { + setLogContent(text) + toast({ + title: "Log file loaded", + description: "Log file has been loaded successfully.", + }) + } } catch (error) { console.error("Log file reading error:", error) toast({ @@ -392,36 +405,48 @@ export default function EditProjectPage({ params }: EditProjectPageProps) {