diff --git a/apps/sim/app/api/creators/[id]/verify/route.ts b/apps/sim/app/api/creators/[id]/verify/route.ts index 45cd2dc0b0..6ce9e8b3c1 100644 --- a/apps/sim/app/api/creators/[id]/verify/route.ts +++ b/apps/sim/app/api/creators/[id]/verify/route.ts @@ -1,10 +1,11 @@ import { db } from '@sim/db' -import { templateCreators, user } from '@sim/db/schema' +import { templateCreators } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' +import { verifyEffectiveSuperUser } from '@/lib/templates/permissions' const logger = createLogger('CreatorVerificationAPI') @@ -23,9 +24,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } // Check if user is a super user - const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1) - - if (!currentUser[0]?.isSuperUser) { + const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id) + if (!effectiveSuperUser) { logger.warn(`[${requestId}] Non-super user attempted to verify creator: ${id}`) return NextResponse.json({ error: 'Only super users can verify creators' }, { status: 403 }) } @@ -76,9 +76,8 @@ export async function DELETE( } // Check if user is a super user - const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1) - - if (!currentUser[0]?.isSuperUser) { + const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id) + if (!effectiveSuperUser) { logger.warn(`[${requestId}] Non-super user attempted to unverify creator: ${id}`) return NextResponse.json({ error: 'Only super users can unverify creators' }, { status: 403 }) } diff --git a/apps/sim/app/api/superuser/import-workflow/route.ts b/apps/sim/app/api/superuser/import-workflow/route.ts new file mode 100644 index 0000000000..3998792993 --- /dev/null +++ b/apps/sim/app/api/superuser/import-workflow/route.ts @@ -0,0 +1,193 @@ +import { db } from '@sim/db' +import { copilotChats, workflow, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { verifyEffectiveSuperUser } from '@/lib/templates/permissions' +import { parseWorkflowJson } from '@/lib/workflows/operations/import-export' +import { + loadWorkflowFromNormalizedTables, + saveWorkflowToNormalizedTables, +} from '@/lib/workflows/persistence/utils' +import { sanitizeForExport } from '@/lib/workflows/sanitization/json-sanitizer' + +const logger = createLogger('SuperUserImportWorkflow') + +interface ImportWorkflowRequest { + workflowId: string + targetWorkspaceId: string +} + +/** + * 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 async function POST(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 body: ImportWorkflowRequest = await request.json() + const { workflowId, targetWorkspaceId } = body + + if (!workflowId) { + return NextResponse.json({ error: 'workflowId is required' }, { status: 400 }) + } + + if (!targetWorkspaceId) { + return NextResponse.json({ error: 'targetWorkspaceId is required' }, { status: 400 }) + } + + // Verify target workspace exists + const [targetWorkspace] = await db + .select({ id: workspace.id, ownerId: workspace.ownerId }) + .from(workspace) + .where(eq(workspace.id, targetWorkspaceId)) + .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, + color: sourceWorkflow.color, + }, + } + + 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 = crypto.randomUUID() + const now = new Date() + + await db.insert(workflow).values({ + id: newWorkflowId, + userId: session.user.id, + workspaceId: targetWorkspaceId, + folderId: null, // Don't copy folder association + name: `[Debug Import] ${sourceWorkflow.name}`, + description: sourceWorkflow.description, + color: sourceWorkflow.color, + 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() + .from(copilotChats) + .where(eq(copilotChats.workflowId, workflowId)) + + let copilotChatsImported = 0 + + for (const chat of sourceCopilotChats) { + await db.insert(copilotChats).values({ + userId: session.user.id, + workflowId: newWorkflowId, + title: chat.title ? `[Import] ${chat.title}` : null, + messages: chat.messages, + 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(), + }) + 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 }) + } +} diff --git a/apps/sim/app/api/templates/[id]/approve/route.ts b/apps/sim/app/api/templates/[id]/approve/route.ts index c15c1916ee..0492ae5845 100644 --- a/apps/sim/app/api/templates/[id]/approve/route.ts +++ b/apps/sim/app/api/templates/[id]/approve/route.ts @@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' -import { verifySuperUser } from '@/lib/templates/permissions' +import { verifyEffectiveSuperUser } from '@/lib/templates/permissions' const logger = createLogger('TemplateApprovalAPI') @@ -25,8 +25,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { isSuperUser } = await verifySuperUser(session.user.id) - if (!isSuperUser) { + const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id) + if (!effectiveSuperUser) { logger.warn(`[${requestId}] Non-super user attempted to approve template: ${id}`) return NextResponse.json({ error: 'Only super users can approve templates' }, { status: 403 }) } @@ -71,8 +71,8 @@ export async function DELETE( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { isSuperUser } = await verifySuperUser(session.user.id) - if (!isSuperUser) { + const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id) + if (!effectiveSuperUser) { logger.warn(`[${requestId}] Non-super user attempted to reject template: ${id}`) return NextResponse.json({ error: 'Only super users can reject templates' }, { status: 403 }) } diff --git a/apps/sim/app/api/templates/[id]/reject/route.ts b/apps/sim/app/api/templates/[id]/reject/route.ts index af5ed2e12b..99e50e52a9 100644 --- a/apps/sim/app/api/templates/[id]/reject/route.ts +++ b/apps/sim/app/api/templates/[id]/reject/route.ts @@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' -import { verifySuperUser } from '@/lib/templates/permissions' +import { verifyEffectiveSuperUser } from '@/lib/templates/permissions' const logger = createLogger('TemplateRejectionAPI') @@ -25,8 +25,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { isSuperUser } = await verifySuperUser(session.user.id) - if (!isSuperUser) { + const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id) + if (!effectiveSuperUser) { logger.warn(`[${requestId}] Non-super user attempted to reject template: ${id}`) return NextResponse.json({ error: 'Only super users can reject templates' }, { status: 403 }) } diff --git a/apps/sim/app/api/templates/route.ts b/apps/sim/app/api/templates/route.ts index 7177aa0050..2985684e4b 100644 --- a/apps/sim/app/api/templates/route.ts +++ b/apps/sim/app/api/templates/route.ts @@ -3,7 +3,6 @@ import { templateCreators, templateStars, templates, - user, workflow, workflowDeploymentVersion, } from '@sim/db/schema' @@ -14,6 +13,7 @@ import { v4 as uuidv4 } from 'uuid' import { z } from 'zod' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' +import { verifyEffectiveSuperUser } from '@/lib/templates/permissions' import { extractRequiredCredentials, sanitizeCredentials, @@ -70,8 +70,8 @@ export async function GET(request: NextRequest) { logger.debug(`[${requestId}] Fetching templates with params:`, params) // Check if user is a super user - const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1) - const isSuperUser = currentUser[0]?.isSuperUser || false + const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id) + const isSuperUser = effectiveSuperUser // Build query conditions const conditions = [] diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index 0992aaaea0..826085970d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -1477,7 +1477,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: toolCall.name === 'mark_todo_in_progress' || toolCall.name === 'tool_search_tool_regex' || toolCall.name === 'user_memory' || - toolCall.name === 'edit_responsd' || + toolCall.name === 'edit_respond' || toolCall.name === 'debug_respond' || toolCall.name === 'plan_respond' ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/debug/debug.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/debug/debug.tsx new file mode 100644 index 0000000000..b36d32a3a4 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/debug/debug.tsx @@ -0,0 +1,80 @@ +'use client' + +import { useState } from 'react' +import { createLogger } from '@sim/logger' +import { useQueryClient } from '@tanstack/react-query' +import { useParams } from 'next/navigation' +import { Button, Input as EmcnInput } from '@/components/emcn' +import { workflowKeys } from '@/hooks/queries/workflows' + +const logger = createLogger('DebugSettings') + +/** + * Debug settings component for superusers. + * Allows importing workflows by ID for debugging purposes. + */ +export function Debug() { + const params = useParams() + const queryClient = useQueryClient() + const workspaceId = params?.workspaceId as string + + const [workflowId, setWorkflowId] = useState('') + const [isImporting, setIsImporting] = useState(false) + + const handleImport = async () => { + if (!workflowId.trim()) return + + setIsImporting(true) + + try { + const response = await fetch('/api/superuser/import-workflow', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + workflowId: workflowId.trim(), + targetWorkspaceId: workspaceId, + }), + }) + + const data = await response.json() + + if (response.ok) { + await queryClient.invalidateQueries({ queryKey: workflowKeys.list(workspaceId) }) + setWorkflowId('') + logger.info('Workflow imported successfully', { + originalWorkflowId: workflowId.trim(), + newWorkflowId: data.newWorkflowId, + copilotChatsImported: data.copilotChatsImported, + }) + } + } catch (error) { + logger.error('Failed to import workflow', error) + } finally { + setIsImporting(false) + } + } + + return ( +
+ Import a workflow by ID along with its associated copilot chats. +
+ +