Files
portfolio/app/api/projects/[id]/route.ts
2026-01-12 14:36:10 +00:00

223 lines
6.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { apiCache } from '@/lib/cache';
import { checkRateLimit, getRateLimitHeaders, requireSessionAuth } from '@/lib/auth';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import { generateUniqueSlug } from '@/lib/slug';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: idParam } = await params;
const id = parseInt(idParam);
if (!Number.isFinite(id)) {
return NextResponse.json({ error: 'Invalid project id' }, { status: 400 });
}
const project = await prisma.project.findUnique({
where: { id }
});
if (!project) {
return NextResponse.json(
{ error: 'Project not found' },
{ status: 404 }
);
}
return NextResponse.json(project);
} catch (error) {
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Project table does not exist. Returning 404.');
}
return NextResponse.json(
{ error: 'Project not found' },
{ status: 404 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error fetching project:', error);
}
return NextResponse.json(
{ error: 'Failed to fetch project' },
{ status: 500 }
);
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Rate limiting for PUT requests
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
if (!checkRateLimit(ip, 5, 60000)) { // 5 requests per minute for PUT
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(ip, 5, 60000)
}
}
);
}
// Check if this is an admin request
const isAdminRequest = request.headers.get('x-admin-request') === 'true';
if (!isAdminRequest) {
return NextResponse.json(
{ error: 'Admin access required' },
{ status: 403 }
);
}
const authError = requireSessionAuth(request);
if (authError) return authError;
const { id: idParam } = await params;
const id = parseInt(idParam);
if (!Number.isFinite(id)) {
return NextResponse.json({ error: 'Invalid project id' }, { status: 400 });
}
const data = await request.json();
// Remove difficulty field if it exists (since we're removing it)
const { difficulty, slug, defaultLocale, ...projectData } = data;
// Keep slug stable by default; only update if explicitly provided,
// or if the project currently has no slug (e.g. after migration).
const existing = await prisma.project.findUnique({
where: { id },
select: { slug: true, title: true },
});
const nextSlug =
typeof slug === 'string' && slug.trim()
? slug.trim()
: existing?.slug?.trim()
? existing.slug
: await generateUniqueSlug({
base: String(projectData.title || existing?.title || 'project'),
isTaken: async (candidate) => {
const found = await prisma.project.findUnique({
where: { slug: candidate },
select: { id: true },
});
return !!found && found.id !== id;
},
});
const project = await prisma.project.update({
where: { id },
data: {
...projectData,
slug: nextSlug,
defaultLocale: typeof defaultLocale === 'string' && defaultLocale ? defaultLocale : undefined,
updatedAt: new Date(),
// Keep existing difficulty if not provided
...(difficulty ? { difficulty } : {})
}
});
// Invalidate cache after successful update
await apiCache.invalidateProject(id);
await apiCache.invalidateAll();
return NextResponse.json(project);
} catch (error) {
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Project table does not exist.');
}
return NextResponse.json(
{ error: 'Database table not found. Please run migrations.' },
{ status: 503 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error updating project:', error);
}
return NextResponse.json(
{ error: 'Failed to update project', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Rate limiting for DELETE requests
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
if (!checkRateLimit(ip, 3, 60000)) { // 3 requests per minute for DELETE (more restrictive)
return new NextResponse(
JSON.stringify({ error: 'Rate limit exceeded' }),
{
status: 429,
headers: {
'Content-Type': 'application/json',
...getRateLimitHeaders(ip, 3, 60000)
}
}
);
}
// Check if this is an admin request
const isAdminRequest = request.headers.get('x-admin-request') === 'true';
if (!isAdminRequest) {
return NextResponse.json(
{ error: 'Admin access required' },
{ status: 403 }
);
}
const authError = requireSessionAuth(request);
if (authError) return authError;
const { id: idParam } = await params;
const id = parseInt(idParam);
if (!Number.isFinite(id)) {
return NextResponse.json({ error: 'Invalid project id' }, { status: 400 });
}
await prisma.project.delete({
where: { id }
});
// Invalidate cache after successful deletion
await apiCache.invalidateProject(id);
await apiCache.invalidateAll();
return NextResponse.json({ success: true });
} catch (error) {
// Handle missing database table gracefully
if (error instanceof PrismaClientKnownRequestError && error.code === 'P2021') {
if (process.env.NODE_ENV === 'development') {
console.warn('Project table does not exist.');
}
return NextResponse.json(
{ error: 'Database table not found. Please run migrations.' },
{ status: 503 }
);
}
if (process.env.NODE_ENV === 'development') {
console.error('Error deleting project:', error);
}
return NextResponse.json(
{ error: 'Failed to delete project' },
{ status: 500 }
);
}
}