import { db } from '@sim/db' import { copilotChats, workflow, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { importWorkflowAsSuperuserContract } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { loadCopilotChatMessages } from '@/lib/copilot/chat/lifecycle' import { appendCopilotChatMessages } from '@/lib/copilot/chat/messages-store' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { verifyEffectiveSuperUser } from '@/lib/permissions/super-user' import { parseWorkflowJson } from '@/lib/workflows/operations/import-export' import { loadWorkflowFromNormalizedTables, saveWorkflowToNormalizedTables, } from '@/lib/workflows/persistence/utils' import { sanitizeForExport } from '@/lib/workflows/sanitization/json-sanitizer' import { deduplicateWorkflowName } from '@/lib/workflows/utils' const logger = createLogger('SuperUserImportWorkflow') /** * POST /api/superuser/import-workflow * * Superuser endpoint to import a workflow by ID along with its copilot chats. * This creates a copy of the workflow in the target workspace with new IDs. * Only the workflow structure and copilot chats are copied - no deployments, * webhooks, triggers, or other sensitive data. * * Requires both isSuperUser flag AND superUserModeEnabled setting. */ export const POST = withRouteHandler(async (request: NextRequest) => { try { const session = await getSession() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const { effectiveSuperUser, isSuperUser, superUserModeEnabled } = await verifyEffectiveSuperUser(session.user.id) if (!effectiveSuperUser) { logger.warn('Non-effective-superuser attempted to access import-workflow endpoint', { userId: session.user.id, isSuperUser, superUserModeEnabled, }) return NextResponse.json({ error: 'Forbidden: Superuser access required' }, { status: 403 }) } const parsed = await parseRequest( importWorkflowAsSuperuserContract, request, {}, { validationErrorResponse: (error) => { const missingPath = error.issues[0]?.path[0] if (missingPath === 'workflowId') { return NextResponse.json({ error: 'workflowId is required' }, { status: 400 }) } if (missingPath === 'targetWorkspaceId') { return NextResponse.json({ error: 'targetWorkspaceId is required' }, { status: 400 }) } return NextResponse.json({ error: 'workflowId is required' }, { status: 400 }) }, } ) if (!parsed.success) return parsed.response const { workflowId, targetWorkspaceId } = parsed.data.body // Verify target workspace exists const [targetWorkspace] = await db .select({ id: workspace.id, ownerId: workspace.ownerId }) .from(workspace) .where(and(eq(workspace.id, targetWorkspaceId), isNull(workspace.archivedAt))) .limit(1) if (!targetWorkspace) { return NextResponse.json({ error: 'Target workspace not found' }, { status: 404 }) } // Get the source workflow const [sourceWorkflow] = await db .select() .from(workflow) .where(eq(workflow.id, workflowId)) .limit(1) if (!sourceWorkflow) { return NextResponse.json({ error: 'Source workflow not found' }, { status: 404 }) } // Load the workflow state from normalized tables const normalizedData = await loadWorkflowFromNormalizedTables(workflowId) if (!normalizedData) { return NextResponse.json( { error: 'Workflow has no normalized data - cannot import' }, { status: 400 } ) } // Use existing export logic to create export format const workflowState = { blocks: normalizedData.blocks, edges: normalizedData.edges, loops: normalizedData.loops, parallels: normalizedData.parallels, metadata: { name: sourceWorkflow.name, description: sourceWorkflow.description ?? undefined, }, } const exportData = sanitizeForExport(workflowState) // Use existing import logic (parseWorkflowJson regenerates IDs automatically) const { data: importedData, errors } = parseWorkflowJson(JSON.stringify(exportData)) if (!importedData || errors.length > 0) { return NextResponse.json( { error: `Failed to parse workflow: ${errors.join(', ')}` }, { status: 400 } ) } // Create new workflow record const newWorkflowId = generateId() const now = new Date() const dedupedName = await deduplicateWorkflowName( `[Debug Import] ${sourceWorkflow.name}`, targetWorkspaceId, null ) await db.insert(workflow).values({ id: newWorkflowId, userId: session.user.id, workspaceId: targetWorkspaceId, folderId: null, name: dedupedName, description: sourceWorkflow.description, lastSynced: now, createdAt: now, updatedAt: now, isDeployed: false, // Never copy deployment status runCount: 0, variables: sourceWorkflow.variables || {}, }) // Save using existing persistence logic const saveResult = await saveWorkflowToNormalizedTables(newWorkflowId, importedData) if (!saveResult.success) { // Clean up the workflow record if save failed await db.delete(workflow).where(eq(workflow.id, newWorkflowId)) return NextResponse.json( { error: `Failed to save workflow state: ${saveResult.error}` }, { status: 500 } ) } // Copy copilot chats associated with the source workflow const sourceCopilotChats = await db .select({ id: copilotChats.id, title: copilotChats.title, model: copilotChats.model, previewYaml: copilotChats.previewYaml, planArtifact: copilotChats.planArtifact, config: copilotChats.config, }) .from(copilotChats) .where(eq(copilotChats.workflowId, workflowId)) let copilotChatsImported = 0 for (const chat of sourceCopilotChats) { const sourceMessages = await loadCopilotChatMessages(chat.id) await db.transaction(async (tx) => { const [imported] = await tx .insert(copilotChats) .values({ userId: session.user.id, workflowId: newWorkflowId, title: chat.title ? `[Import] ${chat.title}` : null, model: chat.model, conversationId: null, // Don't copy conversation ID previewYaml: chat.previewYaml, planArtifact: chat.planArtifact, config: chat.config, createdAt: new Date(), updatedAt: new Date(), }) .returning({ id: copilotChats.id }) if (imported && sourceMessages.length > 0) { await appendCopilotChatMessages( imported.id, sourceMessages, { chatModel: chat.model }, tx ) } }) copilotChatsImported++ } logger.info('Superuser imported workflow', { userId: session.user.id, sourceWorkflowId: workflowId, newWorkflowId, targetWorkspaceId, copilotChatsImported, }) return NextResponse.json({ success: true, newWorkflowId, copilotChatsImported, }) } catch (error) { logger.error('Error importing workflow', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } })