import express from "express"; import { z } from "zod"; import { Prisma, PrismaClient } from "../generated/client"; type SortField = "name" | "createdAt" | "updatedAt"; type SortDirection = "asc" | "desc"; type BuildDrawingsCacheKey = (keyParts: { userId: string; searchTerm: string; collectionFilter: string; includeData: boolean; sortField: SortField; sortDirection: SortDirection; }) => string; type EnsureTrashCollection = ( db: Prisma.TransactionClient | PrismaClient, userId: string ) => Promise; type LogAuditEvent = (params: { userId: string; action: string; resource?: string; ipAddress?: string; userAgent?: string; details?: Record; }) => Promise; type DashboardRouteDeps = { prisma: PrismaClient; requireAuth: express.RequestHandler; asyncHandler: ( fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise ) => express.RequestHandler; parseJsonField: (rawValue: string | null | undefined, fallback: T) => T; sanitizeText: (input: unknown, maxLength?: number) => string; validateImportedDrawing: (data: unknown) => boolean; drawingCreateSchema: z.ZodTypeAny; drawingUpdateSchema: z.ZodTypeAny; respondWithValidationErrors: (res: express.Response, issues: z.ZodIssue[]) => void; collectionNameSchema: z.ZodTypeAny; ensureTrashCollection: EnsureTrashCollection; invalidateDrawingsCache: () => void; buildDrawingsCacheKey: BuildDrawingsCacheKey; getCachedDrawingsBody: (key: string) => Buffer | null; cacheDrawingsResponse: (key: string, payload: unknown) => Buffer; MAX_PAGE_SIZE: number; config: { nodeEnv: string; enableAuditLogging: boolean; }; logAuditEvent: LogAuditEvent; }; export const registerDashboardRoutes = ( app: express.Express, deps: DashboardRouteDeps ) => { const { prisma, requireAuth, asyncHandler, parseJsonField, sanitizeText, validateImportedDrawing, drawingCreateSchema, drawingUpdateSchema, respondWithValidationErrors, collectionNameSchema, ensureTrashCollection, invalidateDrawingsCache, buildDrawingsCacheKey, getCachedDrawingsBody, cacheDrawingsResponse, MAX_PAGE_SIZE, config, logAuditEvent, } = deps; app.get("/drawings", requireAuth, asyncHandler(async (req, res) => { if (!req.user) { return res.status(401).json({ error: "Unauthorized" }); } const { search, collectionId, includeData, limit, offset, sortField, sortDirection } = req.query; const where: Prisma.DrawingWhereInput = { userId: req.user.id }; const searchTerm = typeof search === "string" && search.trim().length > 0 ? search.trim() : undefined; if (searchTerm) { where.name = { contains: searchTerm }; } let collectionFilterKey = "default"; if (collectionId === "null") { where.collectionId = null; collectionFilterKey = "null"; } else if (collectionId) { const normalizedCollectionId = String(collectionId); if (normalizedCollectionId === "trash") { where.collectionId = "trash"; collectionFilterKey = "trash"; } else { const collection = await prisma.collection.findFirst({ where: { id: normalizedCollectionId, userId: req.user.id }, }); if (!collection) { return res.status(404).json({ error: "Collection not found" }); } where.collectionId = normalizedCollectionId; collectionFilterKey = `id:${normalizedCollectionId}`; } } else { where.OR = [{ collectionId: { not: "trash" } }, { collectionId: null }]; } const shouldIncludeData = typeof includeData === "string" ? includeData.toLowerCase() === "true" || includeData === "1" : false; const parsedSortField: SortField = sortField === "name" || sortField === "createdAt" || sortField === "updatedAt" ? sortField : "updatedAt"; const parsedSortDirection: SortDirection = sortDirection === "asc" || sortDirection === "desc" ? sortDirection : parsedSortField === "name" ? "asc" : "desc"; const rawLimit = limit ? Number.parseInt(limit as string, 10) : undefined; const rawOffset = offset ? Number.parseInt(offset as string, 10) : undefined; const parsedLimit = rawLimit !== undefined && Number.isFinite(rawLimit) ? Math.min(Math.max(rawLimit, 1), MAX_PAGE_SIZE) : undefined; const parsedOffset = rawOffset !== undefined && Number.isFinite(rawOffset) ? Math.max(rawOffset, 0) : undefined; const cacheKey = buildDrawingsCacheKey({ userId: req.user.id, searchTerm: searchTerm ?? "", collectionFilter: collectionFilterKey, includeData: shouldIncludeData, sortField: parsedSortField, sortDirection: parsedSortDirection, }) + `:${parsedLimit}:${parsedOffset}`; const cachedBody = getCachedDrawingsBody(cacheKey); if (cachedBody) { res.setHeader("X-Cache", "HIT"); res.setHeader("Content-Type", "application/json"); return res.send(cachedBody); } const summarySelect: Prisma.DrawingSelect = { id: true, name: true, collectionId: true, preview: true, version: true, createdAt: true, updatedAt: true, }; const orderBy: Prisma.DrawingOrderByWithRelationInput = parsedSortField === "name" ? { name: parsedSortDirection } : parsedSortField === "createdAt" ? { createdAt: parsedSortDirection } : { updatedAt: parsedSortDirection }; const queryOptions: Prisma.DrawingFindManyArgs = { where, orderBy }; if (parsedLimit !== undefined) queryOptions.take = parsedLimit; if (parsedOffset !== undefined) queryOptions.skip = parsedOffset; if (!shouldIncludeData) queryOptions.select = summarySelect; const [drawings, totalCount] = await Promise.all([ prisma.drawing.findMany(queryOptions), prisma.drawing.count({ where }), ]); let responsePayload: any[] = drawings as any[]; if (shouldIncludeData) { responsePayload = (drawings as any[]).map((d: any) => ({ ...d, elements: parseJsonField(d.elements, []), appState: parseJsonField(d.appState, {}), files: parseJsonField(d.files, {}), })); } const finalResponse = { drawings: responsePayload, totalCount, limit: parsedLimit, offset: parsedOffset, }; const body = cacheDrawingsResponse(cacheKey, finalResponse); res.setHeader("X-Cache", "MISS"); res.setHeader("Content-Type", "application/json"); return res.send(body); })); app.get("/drawings/:id", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const drawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id, }, }); if (!drawing) { return res.status(404).json({ error: "Drawing not found", message: "Drawing does not exist" }); } return res.json({ ...drawing, elements: parseJsonField(drawing.elements, []), appState: parseJsonField(drawing.appState, {}), files: parseJsonField(drawing.files, {}), }); })); app.post("/drawings", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const isImportedDrawing = req.headers["x-imported-file"] === "true"; if (isImportedDrawing && !validateImportedDrawing(req.body)) { return res.status(400).json({ error: "Invalid imported drawing file", message: "The imported file contains potentially malicious content or invalid structure", }); } const parsed = drawingCreateSchema.safeParse(req.body); if (!parsed.success) { return respondWithValidationErrors(res, parsed.error.issues); } const payload = parsed.data as { name?: string; collectionId?: string | null; elements: unknown[]; appState: Record; preview?: string | null; files?: Record; }; const drawingName = payload.name ?? "Untitled Drawing"; const targetCollectionId = payload.collectionId === undefined ? null : payload.collectionId; if (targetCollectionId && targetCollectionId !== "trash") { const collection = await prisma.collection.findFirst({ where: { id: targetCollectionId, userId: req.user.id }, }); if (!collection) return res.status(404).json({ error: "Collection not found" }); } else if (targetCollectionId === "trash") { await ensureTrashCollection(prisma, req.user.id); } const newDrawing = await prisma.drawing.create({ data: { name: drawingName, elements: JSON.stringify(payload.elements), appState: JSON.stringify(payload.appState), userId: req.user.id, collectionId: targetCollectionId, preview: payload.preview ?? null, files: JSON.stringify(payload.files ?? {}), }, }); invalidateDrawingsCache(); return res.json({ ...newDrawing, elements: parseJsonField(newDrawing.elements, []), appState: parseJsonField(newDrawing.appState, {}), files: parseJsonField(newDrawing.files, {}), }); })); app.put("/drawings/:id", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const existingDrawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id }, }); if (!existingDrawing) return res.status(404).json({ error: "Drawing not found" }); const parsed = drawingUpdateSchema.safeParse(req.body); if (!parsed.success) { if (config.nodeEnv === "development") { console.error("[API] Validation failed", { id, errors: parsed.error.issues }); } return respondWithValidationErrors(res, parsed.error.issues); } const payload = parsed.data as { name?: string; collectionId?: string | null; elements?: unknown[]; appState?: Record; preview?: string | null; files?: Record; version?: number; }; const isSceneUpdate = payload.elements !== undefined || payload.appState !== undefined || payload.files !== undefined; const data: Prisma.DrawingUpdateInput = { version: { increment: 1 } }; if (payload.name !== undefined) data.name = payload.name; if (payload.elements !== undefined) data.elements = JSON.stringify(payload.elements); if (payload.appState !== undefined) data.appState = JSON.stringify(payload.appState); if (payload.files !== undefined) data.files = JSON.stringify(payload.files); if (payload.preview !== undefined) data.preview = payload.preview; if (payload.collectionId !== undefined) { if (payload.collectionId === "trash") { await ensureTrashCollection(prisma, req.user.id); (data as Prisma.DrawingUncheckedUpdateInput).collectionId = "trash"; } else if (payload.collectionId) { const collection = await prisma.collection.findFirst({ where: { id: payload.collectionId, userId: req.user.id }, }); if (!collection) return res.status(404).json({ error: "Collection not found" }); (data as Prisma.DrawingUncheckedUpdateInput).collectionId = payload.collectionId; } else { (data as Prisma.DrawingUncheckedUpdateInput).collectionId = null; } } const updateWhere: Prisma.DrawingWhereInput = { id, userId: req.user.id }; if (isSceneUpdate && payload.version !== undefined) { updateWhere.version = payload.version; } const updateResult = await prisma.drawing.updateMany({ where: updateWhere, data, }); if (updateResult.count === 0) { if (isSceneUpdate && payload.version !== undefined) { const latestDrawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id }, select: { version: true }, }); return res.status(409).json({ error: "Conflict", code: "VERSION_CONFLICT", message: "Drawing has changed since this editor state was loaded.", currentVersion: latestDrawing?.version ?? null, }); } return res.status(404).json({ error: "Drawing not found" }); } const updatedDrawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id }, }); if (!updatedDrawing) { return res.status(404).json({ error: "Drawing not found" }); } invalidateDrawingsCache(); return res.json({ ...updatedDrawing, elements: parseJsonField(updatedDrawing.elements, []), appState: parseJsonField(updatedDrawing.appState, {}), files: parseJsonField(updatedDrawing.files, {}), }); })); app.delete("/drawings/:id", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const drawing = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } }); if (!drawing) return res.status(404).json({ error: "Drawing not found" }); const deleteResult = await prisma.drawing.deleteMany({ where: { id, userId: req.user.id }, }); if (deleteResult.count === 0) { return res.status(404).json({ error: "Drawing not found" }); } invalidateDrawingsCache(); if (config.enableAuditLogging) { await logAuditEvent({ userId: req.user.id, action: "drawing_deleted", resource: `drawing:${id}`, ipAddress: req.ip || req.connection.remoteAddress || undefined, userAgent: req.headers["user-agent"] || undefined, details: { drawingId: id, drawingName: drawing.name }, }); } return res.json({ success: true }); })); app.post("/drawings/:id/duplicate", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const original = await prisma.drawing.findFirst({ where: { id, userId: req.user.id } }); if (!original) return res.status(404).json({ error: "Original drawing not found" }); if (original.collectionId === "trash") { await ensureTrashCollection(prisma, req.user.id); } const newDrawing = await prisma.drawing.create({ data: { name: `${original.name} (Copy)`, elements: original.elements, appState: original.appState, files: original.files, userId: req.user.id, collectionId: original.collectionId, version: 1, }, }); invalidateDrawingsCache(); return res.json({ ...newDrawing, elements: parseJsonField(newDrawing.elements, []), appState: parseJsonField(newDrawing.appState, {}), files: parseJsonField(newDrawing.files, {}), }); })); app.get("/collections", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const collections = await prisma.collection.findMany({ where: { userId: req.user.id }, orderBy: { createdAt: "desc" }, }); return res.json(collections); })); app.post("/collections", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const parsed = collectionNameSchema.safeParse(req.body.name); if (!parsed.success) { return res.status(400).json({ error: "Validation error", message: "Collection name must be between 1 and 100 characters", }); } const sanitizedName = sanitizeText(parsed.data, 100); const newCollection = await prisma.collection.create({ data: { name: sanitizedName, userId: req.user.id }, }); return res.json(newCollection); })); app.put("/collections/:id", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const existingCollection = await prisma.collection.findFirst({ where: { id, userId: req.user.id }, }); if (!existingCollection) return res.status(404).json({ error: "Collection not found" }); const parsed = collectionNameSchema.safeParse(req.body.name); if (!parsed.success) { return res.status(400).json({ error: "Validation error", message: "Collection name must be between 1 and 100 characters", }); } const sanitizedName = sanitizeText(parsed.data, 100); const updateResult = await prisma.collection.updateMany({ where: { id, userId: req.user.id }, data: { name: sanitizedName }, }); if (updateResult.count === 0) { return res.status(404).json({ error: "Collection not found" }); } const updatedCollection = await prisma.collection.findFirst({ where: { id, userId: req.user.id }, }); if (!updatedCollection) { return res.status(404).json({ error: "Collection not found" }); } return res.json(updatedCollection); })); app.delete("/collections/:id", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const { id } = req.params; const collection = await prisma.collection.findFirst({ where: { id, userId: req.user.id }, }); if (!collection) return res.status(404).json({ error: "Collection not found" }); await prisma.$transaction([ prisma.drawing.updateMany({ where: { collectionId: id, userId: req.user.id }, data: { collectionId: null }, }), prisma.collection.deleteMany({ where: { id, userId: req.user.id } }), ]); invalidateDrawingsCache(); if (config.enableAuditLogging) { await logAuditEvent({ userId: req.user.id, action: "collection_deleted", resource: `collection:${id}`, ipAddress: req.ip || req.connection.remoteAddress || undefined, userAgent: req.headers["user-agent"] || undefined, details: { collectionId: id, collectionName: collection.name }, }); } return res.json({ success: true }); })); app.get("/library", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const libraryId = `user_${req.user.id}`; const library = await prisma.library.findUnique({ where: { id: libraryId } }); if (!library) return res.json({ items: [] }); return res.json({ items: parseJsonField(library.items, []) }); })); app.put("/library", requireAuth, asyncHandler(async (req, res) => { if (!req.user) return res.status(401).json({ error: "Unauthorized" }); const { items } = req.body; if (!Array.isArray(items)) { return res.status(400).json({ error: "Items must be an array" }); } const libraryId = `user_${req.user.id}`; const library = await prisma.library.upsert({ where: { id: libraryId }, update: { items: JSON.stringify(items) }, create: { id: libraryId, items: JSON.stringify(items) }, }); return res.json({ items: parseJsonField(library.items, []) }); })); };