-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat(settings): add debug mode for superusers #2893
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
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
...e/[workspaceId]/w/components/sidebar/components/settings-modal/components/debug/debug.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
Sg312 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| setIsImporting(false) | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <div className='flex h-full flex-col gap-[16px]'> | ||
| <p className='text-[13px] text-[var(--text-secondary)]'> | ||
| Import a workflow by ID along with its associated copilot chats. | ||
| </p> | ||
|
|
||
| <div className='flex gap-[8px]'> | ||
| <EmcnInput | ||
| value={workflowId} | ||
| onChange={(e) => setWorkflowId(e.target.value)} | ||
| placeholder='Enter workflow ID' | ||
| disabled={isImporting} | ||
| /> | ||
| <Button | ||
| variant='tertiary' | ||
| onClick={handleImport} | ||
| disabled={isImporting || !workflowId.trim()} | ||
| > | ||
| {isImporting ? 'Importing...' : 'Import'} | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.