-
Notifications
You must be signed in to change notification settings - Fork 0
compleated 20-feat #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| import { get, put } from "@vercel/blob"; | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { getCurrentUserId, getProjectWithAccess } from "@/lib/auth"; | ||
| import { prisma } from "@/lib/prisma"; | ||
| import type { CanvasEdge, CanvasNode } from "@/types/canvas"; | ||
|
|
||
| interface CanvasBody { | ||
| nodes?: unknown; | ||
| edges?: unknown; | ||
| } | ||
|
|
||
| interface CanvasSnapshot { | ||
| nodes: CanvasNode[]; | ||
| edges: CanvasEdge[]; | ||
| } | ||
|
|
||
| /** | ||
| * Validate canvas JSON body. Accepts an object with `nodes` and `edges` | ||
| * arrays; anything else is rejected. | ||
| */ | ||
| function parseCanvasBody(body: unknown): CanvasSnapshot | null { | ||
| if (body === null || typeof body !== "object") return null; | ||
| const { nodes, edges } = body as CanvasBody; | ||
| if (!Array.isArray(nodes) || !Array.isArray(edges)) return null; | ||
| return { | ||
| nodes: nodes as CanvasNode[], | ||
| edges: edges as CanvasEdge[], | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * PUT /api/projects/[projectId]/canvas | ||
| * | ||
| * Upload the latest canvas JSON to Vercel Blob and store the blob URL on | ||
| * the Prisma project record (`canvasJsonPath`). | ||
| */ | ||
| export async function PUT( | ||
| request: Request, | ||
| { params }: { params: Promise<{ projectId: string }> }, | ||
| ) { | ||
| const { projectId } = await params; | ||
|
|
||
| const project = await getProjectWithAccess(projectId); | ||
| if (!project) { | ||
| // getProjectWithAccess returns null for both no-session and no access. | ||
| const userId = await getCurrentUserId(); | ||
| if (!userId) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
| return NextResponse.json({ error: "Forbidden" }, { status: 403 }); | ||
| } | ||
|
|
||
| let body: unknown = null; | ||
| try { | ||
| body = await request.json(); | ||
| } catch { | ||
| body = null; | ||
| } | ||
|
|
||
| const snapshot = parseCanvasBody(body); | ||
| if (!snapshot) { | ||
| return NextResponse.json( | ||
| { error: "Invalid canvas payload. Expected { nodes, edges }." }, | ||
| { status: 400 }, | ||
| ); | ||
| } | ||
|
|
||
| const pathname = `canvas/${projectId}.json`; | ||
| const blob = await put(pathname, JSON.stringify(snapshot), { | ||
| access: "private", | ||
| contentType: "application/json", | ||
| addRandomSuffix: false, | ||
| allowOverwrite: true, | ||
| }); | ||
|
|
||
| const updated = await prisma.project.update({ | ||
| where: { id: projectId }, | ||
| data: { canvasJsonPath: blob.url }, | ||
| select: { id: true, canvasJsonPath: true, updatedAt: true }, | ||
|
Comment on lines
+69
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Prevent stale concurrent saves from overwriting newer snapshots. All collaborators overwrite the same pathname without a revision check. If an older request completes after a newer one, its stale snapshot becomes authoritative. Persist a revision/version and conditionally commit the Blob URL, or otherwise reject superseded writes. 🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| return NextResponse.json({ | ||
| canvasJsonPath: updated.canvasJsonPath, | ||
| updatedAt: updated.updatedAt, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * GET /api/projects/[projectId]/canvas | ||
| * | ||
| * Read the project's saved blob URL from Prisma, fetch the canvas JSON | ||
| * from Vercel Blob, and return it to the editor. | ||
| */ | ||
| export async function GET( | ||
| _request: Request, | ||
| { params }: { params: Promise<{ projectId: string }> }, | ||
| ) { | ||
| const { projectId } = await params; | ||
|
|
||
| const project = await getProjectWithAccess(projectId); | ||
| if (!project) { | ||
| const userId = await getCurrentUserId(); | ||
| if (!userId) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
| return NextResponse.json({ error: "Forbidden" }, { status: 403 }); | ||
| } | ||
|
|
||
| const record = await prisma.project.findUnique({ | ||
| where: { id: projectId }, | ||
| select: { canvasJsonPath: true }, | ||
| }); | ||
|
|
||
| if (!record?.canvasJsonPath) { | ||
| return NextResponse.json({ | ||
| nodes: [] as CanvasNode[], | ||
| edges: [] as CanvasEdge[], | ||
| canvasJsonPath: null, | ||
| }); | ||
| } | ||
|
|
||
| try { | ||
| const result = await get(record.canvasJsonPath, { | ||
| access: "private", | ||
| useCache: false, | ||
| }); | ||
|
|
||
| if (!result || result.statusCode !== 200 || !result.stream) { | ||
| return NextResponse.json( | ||
| { error: "Failed to fetch canvas blob" }, | ||
| { status: 502 }, | ||
| ); | ||
| } | ||
|
|
||
| const text = await new Response(result.stream).text(); | ||
| let data: unknown = null; | ||
| try { | ||
| data = JSON.parse(text) as unknown; | ||
| } catch { | ||
| return NextResponse.json( | ||
| { error: "Invalid canvas blob contents" }, | ||
| { status: 502 }, | ||
| ); | ||
| } | ||
|
|
||
| const snapshot = parseCanvasBody(data); | ||
| if (!snapshot) { | ||
| return NextResponse.json( | ||
| { error: "Invalid canvas blob contents" }, | ||
| { status: 502 }, | ||
| ); | ||
| } | ||
|
|
||
| return NextResponse.json({ | ||
| nodes: snapshot.nodes, | ||
| edges: snapshot.edges, | ||
| canvasJsonPath: record.canvasJsonPath, | ||
| }); | ||
| } catch { | ||
| return NextResponse.json( | ||
| { error: "Failed to load canvas blob" }, | ||
| { status: 502 }, | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Validate node and edge contents before persisting them.
The casts do not perform runtime validation. Any arrays—including malformed objects—are accepted, stored, and later passed into React Flow as
CanvasNode[]/CanvasEdge[], allowing a bad client payload to poison the saved canvas. Add schema validation and reasonable payload/count limits.🤖 Prompt for AI Agents