import { NextRequest, NextResponse } from "next/server"; import { db, isDatabaseAvailable } from "@/lib/db"; import { ensureDatabaseInitialized } from "@/lib/db/init"; import { projects } from "@/lib/schema/db"; import { eq, and } from "drizzle-orm"; import { getCurrentUser } from "@/lib/auth/session"; import { requireCsrf } from "@/lib/auth/csrf"; import { sanitizeText } from "@/lib/ingest/sanitize"; import { isProActive } from "@/lib/billing/access"; import { resolveGuestContext, applyGuestCookie, type GuestContext, } from "@/lib/auth/guest"; export const runtime = "nodejs"; const PROJECT_NAME_MAX_LENGTH = 120; const PROJECT_COLORS = ["slate", "blue", "emerald", "amber", "rose", "violet"]; async function resolveScope(req: NextRequest): Promise<{ user: Awaited>; userId: string; guest: GuestContext | null; }> { const user = await getCurrentUser(req); if (user) return { user, userId: user.id, guest: null }; const guest = resolveGuestContext(req); return { user: null, userId: guest.bucket, guest }; } function isProUser(user: Awaited>): boolean { return isProActive(user); } /** GET /api/projects/[id] — single project, ownership-checked, left ungated like the list endpoint. */ export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const { id } = await params; const dbAvailable = await isDatabaseAvailable(); if (!dbAvailable) { return NextResponse.json({ error: "Database unavailable." }, { status: 503 }); } await ensureDatabaseInitialized(); const scope = await resolveScope(req); const rows = await db .select() .from(projects) .where(and(eq(projects.id, id), eq(projects.userId, scope.userId))) .limit(1); if (rows.length === 0) { return applyGuestCookie( NextResponse.json({ error: "Project not found." }, { status: 404 }), scope.guest ); } const p = rows[0]; return applyGuestCookie( NextResponse.json({ success: true, project: { id: p.id, name: p.name, color: p.color, createdAt: p.createdAt.toISOString(), updatedAt: p.updatedAt.toISOString(), }, }), scope.guest ); } catch (error: any) { console.error("GET /api/projects/[id] error:", error); return NextResponse.json({ error: "Failed to load project." }, { status: 500 }); } } /** PATCH /api/projects/[id] — rename / recolor. Pro-only, ownership-checked. */ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const csrfBlocked = requireCsrf(req); if (csrfBlocked) return csrfBlocked; const { id } = await params; const dbAvailable = await isDatabaseAvailable(); if (!dbAvailable) { return NextResponse.json({ error: "Database unavailable." }, { status: 503 }); } await ensureDatabaseInitialized(); const scope = await resolveScope(req); if (!isProUser(scope.user)) { return NextResponse.json({ error: "pro_required" }, { status: 402 }); } const existing = await db .select({ id: projects.id }) .from(projects) .where(and(eq(projects.id, id), eq(projects.userId, scope.userId))) .limit(1); if (existing.length === 0) { return applyGuestCookie( NextResponse.json({ error: "Project not found." }, { status: 404 }), scope.guest ); } const body = await req.json().catch(() => null); const updates: { name?: string; color?: string | null; updatedAt: Date } = { updatedAt: new Date(), }; if (typeof body?.name === "string") { const name = sanitizeText(body.name, PROJECT_NAME_MAX_LENGTH); if (!name) { return NextResponse.json({ error: "Project name is required." }, { status: 400 }); } updates.name = name; } if (body?.color !== undefined) { updates.color = PROJECT_COLORS.includes(body.color) ? body.color : null; } const [updated] = await db .update(projects) .set(updates) .where(eq(projects.id, id)) .returning(); return applyGuestCookie( NextResponse.json({ success: true, project: { id: updated.id, name: updated.name, color: updated.color, createdAt: updated.createdAt.toISOString(), updatedAt: updated.updatedAt.toISOString(), }, }), scope.guest ); } catch (error: any) { if (error?.code === "23505") { return NextResponse.json( { error: "A project with this name already exists." }, { status: 409 } ); } console.error("PATCH /api/projects/[id] error:", error); return NextResponse.json({ error: "Failed to update project." }, { status: 500 }); } } /** DELETE /api/projects/[id] — Pro-only, ownership-checked. Receipts detach via ON DELETE SET NULL. */ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const csrfBlocked = requireCsrf(req); if (csrfBlocked) return csrfBlocked; const { id } = await params; const dbAvailable = await isDatabaseAvailable(); if (!dbAvailable) { return NextResponse.json({ error: "Database unavailable." }, { status: 503 }); } await ensureDatabaseInitialized(); const scope = await resolveScope(req); if (!isProUser(scope.user)) { return NextResponse.json({ error: "pro_required" }, { status: 402 }); } const existing = await db .select({ id: projects.id }) .from(projects) .where(and(eq(projects.id, id), eq(projects.userId, scope.userId))) .limit(1); if (existing.length === 0) { return applyGuestCookie( NextResponse.json({ error: "Project not found." }, { status: 404 }), scope.guest ); } await db.delete(projects).where(eq(projects.id, id)); return applyGuestCookie(NextResponse.json({ success: true }), scope.guest); } catch (error: any) { console.error("DELETE /api/projects/[id] error:", error); return NextResponse.json({ error: "Failed to delete project." }, { status: 500 }); } }