Skip to content

Commit e575ba2

Browse files
Sg312icecrasher321
andauthored
feat(settings): add debug mode for superusers (#2893)
* Superuser debug * Fix * update templates routes to use helper --------- Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
1 parent 5f45db4 commit e575ba2

File tree

10 files changed

+369
-29
lines changed

10 files changed

+369
-29
lines changed

apps/sim/app/api/creators/[id]/verify/route.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { db } from '@sim/db'
2-
import { templateCreators, user } from '@sim/db/schema'
2+
import { templateCreators } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
77
import { generateRequestId } from '@/lib/core/utils/request'
8+
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
89

910
const logger = createLogger('CreatorVerificationAPI')
1011

@@ -23,9 +24,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
2324
}
2425

2526
// Check if user is a super user
26-
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
27-
28-
if (!currentUser[0]?.isSuperUser) {
27+
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
28+
if (!effectiveSuperUser) {
2929
logger.warn(`[${requestId}] Non-super user attempted to verify creator: ${id}`)
3030
return NextResponse.json({ error: 'Only super users can verify creators' }, { status: 403 })
3131
}
@@ -76,9 +76,8 @@ export async function DELETE(
7676
}
7777

7878
// Check if user is a super user
79-
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
80-
81-
if (!currentUser[0]?.isSuperUser) {
79+
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
80+
if (!effectiveSuperUser) {
8281
logger.warn(`[${requestId}] Non-super user attempted to unverify creator: ${id}`)
8382
return NextResponse.json({ error: 'Only super users can unverify creators' }, { status: 403 })
8483
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { db } from '@sim/db'
2+
import { copilotChats, workflow, workspace } from '@sim/db/schema'
3+
import { createLogger } from '@sim/logger'
4+
import { eq } from 'drizzle-orm'
5+
import { type NextRequest, NextResponse } from 'next/server'
6+
import { getSession } from '@/lib/auth'
7+
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
8+
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
9+
import {
10+
loadWorkflowFromNormalizedTables,
11+
saveWorkflowToNormalizedTables,
12+
} from '@/lib/workflows/persistence/utils'
13+
import { sanitizeForExport } from '@/lib/workflows/sanitization/json-sanitizer'
14+
15+
const logger = createLogger('SuperUserImportWorkflow')
16+
17+
interface ImportWorkflowRequest {
18+
workflowId: string
19+
targetWorkspaceId: string
20+
}
21+
22+
/**
23+
* POST /api/superuser/import-workflow
24+
*
25+
* Superuser endpoint to import a workflow by ID along with its copilot chats.
26+
* This creates a copy of the workflow in the target workspace with new IDs.
27+
* Only the workflow structure and copilot chats are copied - no deployments,
28+
* webhooks, triggers, or other sensitive data.
29+
*
30+
* Requires both isSuperUser flag AND superUserModeEnabled setting.
31+
*/
32+
export async function POST(request: NextRequest) {
33+
try {
34+
const session = await getSession()
35+
if (!session?.user?.id) {
36+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
37+
}
38+
39+
const { effectiveSuperUser, isSuperUser, superUserModeEnabled } =
40+
await verifyEffectiveSuperUser(session.user.id)
41+
42+
if (!effectiveSuperUser) {
43+
logger.warn('Non-effective-superuser attempted to access import-workflow endpoint', {
44+
userId: session.user.id,
45+
isSuperUser,
46+
superUserModeEnabled,
47+
})
48+
return NextResponse.json({ error: 'Forbidden: Superuser access required' }, { status: 403 })
49+
}
50+
51+
const body: ImportWorkflowRequest = await request.json()
52+
const { workflowId, targetWorkspaceId } = body
53+
54+
if (!workflowId) {
55+
return NextResponse.json({ error: 'workflowId is required' }, { status: 400 })
56+
}
57+
58+
if (!targetWorkspaceId) {
59+
return NextResponse.json({ error: 'targetWorkspaceId is required' }, { status: 400 })
60+
}
61+
62+
// Verify target workspace exists
63+
const [targetWorkspace] = await db
64+
.select({ id: workspace.id, ownerId: workspace.ownerId })
65+
.from(workspace)
66+
.where(eq(workspace.id, targetWorkspaceId))
67+
.limit(1)
68+
69+
if (!targetWorkspace) {
70+
return NextResponse.json({ error: 'Target workspace not found' }, { status: 404 })
71+
}
72+
73+
// Get the source workflow
74+
const [sourceWorkflow] = await db
75+
.select()
76+
.from(workflow)
77+
.where(eq(workflow.id, workflowId))
78+
.limit(1)
79+
80+
if (!sourceWorkflow) {
81+
return NextResponse.json({ error: 'Source workflow not found' }, { status: 404 })
82+
}
83+
84+
// Load the workflow state from normalized tables
85+
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
86+
87+
if (!normalizedData) {
88+
return NextResponse.json(
89+
{ error: 'Workflow has no normalized data - cannot import' },
90+
{ status: 400 }
91+
)
92+
}
93+
94+
// Use existing export logic to create export format
95+
const workflowState = {
96+
blocks: normalizedData.blocks,
97+
edges: normalizedData.edges,
98+
loops: normalizedData.loops,
99+
parallels: normalizedData.parallels,
100+
metadata: {
101+
name: sourceWorkflow.name,
102+
description: sourceWorkflow.description ?? undefined,
103+
color: sourceWorkflow.color,
104+
},
105+
}
106+
107+
const exportData = sanitizeForExport(workflowState)
108+
109+
// Use existing import logic (parseWorkflowJson regenerates IDs automatically)
110+
const { data: importedData, errors } = parseWorkflowJson(JSON.stringify(exportData))
111+
112+
if (!importedData || errors.length > 0) {
113+
return NextResponse.json(
114+
{ error: `Failed to parse workflow: ${errors.join(', ')}` },
115+
{ status: 400 }
116+
)
117+
}
118+
119+
// Create new workflow record
120+
const newWorkflowId = crypto.randomUUID()
121+
const now = new Date()
122+
123+
await db.insert(workflow).values({
124+
id: newWorkflowId,
125+
userId: session.user.id,
126+
workspaceId: targetWorkspaceId,
127+
folderId: null, // Don't copy folder association
128+
name: `[Debug Import] ${sourceWorkflow.name}`,
129+
description: sourceWorkflow.description,
130+
color: sourceWorkflow.color,
131+
lastSynced: now,
132+
createdAt: now,
133+
updatedAt: now,
134+
isDeployed: false, // Never copy deployment status
135+
runCount: 0,
136+
variables: sourceWorkflow.variables || {},
137+
})
138+
139+
// Save using existing persistence logic
140+
const saveResult = await saveWorkflowToNormalizedTables(newWorkflowId, importedData)
141+
142+
if (!saveResult.success) {
143+
// Clean up the workflow record if save failed
144+
await db.delete(workflow).where(eq(workflow.id, newWorkflowId))
145+
return NextResponse.json(
146+
{ error: `Failed to save workflow state: ${saveResult.error}` },
147+
{ status: 500 }
148+
)
149+
}
150+
151+
// Copy copilot chats associated with the source workflow
152+
const sourceCopilotChats = await db
153+
.select()
154+
.from(copilotChats)
155+
.where(eq(copilotChats.workflowId, workflowId))
156+
157+
let copilotChatsImported = 0
158+
159+
for (const chat of sourceCopilotChats) {
160+
await db.insert(copilotChats).values({
161+
userId: session.user.id,
162+
workflowId: newWorkflowId,
163+
title: chat.title ? `[Import] ${chat.title}` : null,
164+
messages: chat.messages,
165+
model: chat.model,
166+
conversationId: null, // Don't copy conversation ID
167+
previewYaml: chat.previewYaml,
168+
planArtifact: chat.planArtifact,
169+
config: chat.config,
170+
createdAt: new Date(),
171+
updatedAt: new Date(),
172+
})
173+
copilotChatsImported++
174+
}
175+
176+
logger.info('Superuser imported workflow', {
177+
userId: session.user.id,
178+
sourceWorkflowId: workflowId,
179+
newWorkflowId,
180+
targetWorkspaceId,
181+
copilotChatsImported,
182+
})
183+
184+
return NextResponse.json({
185+
success: true,
186+
newWorkflowId,
187+
copilotChatsImported,
188+
})
189+
} catch (error) {
190+
logger.error('Error importing workflow', error)
191+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
192+
}
193+
}

apps/sim/app/api/templates/[id]/approve/route.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
77
import { generateRequestId } from '@/lib/core/utils/request'
8-
import { verifySuperUser } from '@/lib/templates/permissions'
8+
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
99

1010
const logger = createLogger('TemplateApprovalAPI')
1111

@@ -25,8 +25,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
2525
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
2626
}
2727

28-
const { isSuperUser } = await verifySuperUser(session.user.id)
29-
if (!isSuperUser) {
28+
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
29+
if (!effectiveSuperUser) {
3030
logger.warn(`[${requestId}] Non-super user attempted to approve template: ${id}`)
3131
return NextResponse.json({ error: 'Only super users can approve templates' }, { status: 403 })
3232
}
@@ -71,8 +71,8 @@ export async function DELETE(
7171
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
7272
}
7373

74-
const { isSuperUser } = await verifySuperUser(session.user.id)
75-
if (!isSuperUser) {
74+
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
75+
if (!effectiveSuperUser) {
7676
logger.warn(`[${requestId}] Non-super user attempted to reject template: ${id}`)
7777
return NextResponse.json({ error: 'Only super users can reject templates' }, { status: 403 })
7878
}

apps/sim/app/api/templates/[id]/reject/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
77
import { generateRequestId } from '@/lib/core/utils/request'
8-
import { verifySuperUser } from '@/lib/templates/permissions'
8+
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
99

1010
const logger = createLogger('TemplateRejectionAPI')
1111

@@ -25,8 +25,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
2525
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
2626
}
2727

28-
const { isSuperUser } = await verifySuperUser(session.user.id)
29-
if (!isSuperUser) {
28+
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
29+
if (!effectiveSuperUser) {
3030
logger.warn(`[${requestId}] Non-super user attempted to reject template: ${id}`)
3131
return NextResponse.json({ error: 'Only super users can reject templates' }, { status: 403 })
3232
}

apps/sim/app/api/templates/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
templateCreators,
44
templateStars,
55
templates,
6-
user,
76
workflow,
87
workflowDeploymentVersion,
98
} from '@sim/db/schema'
@@ -14,6 +13,7 @@ import { v4 as uuidv4 } from 'uuid'
1413
import { z } from 'zod'
1514
import { getSession } from '@/lib/auth'
1615
import { generateRequestId } from '@/lib/core/utils/request'
16+
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
1717
import {
1818
extractRequiredCredentials,
1919
sanitizeCredentials,
@@ -70,8 +70,8 @@ export async function GET(request: NextRequest) {
7070
logger.debug(`[${requestId}] Fetching templates with params:`, params)
7171

7272
// Check if user is a super user
73-
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
74-
const isSuperUser = currentUser[0]?.isSuperUser || false
73+
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
74+
const isSuperUser = effectiveSuperUser
7575

7676
// Build query conditions
7777
const conditions = []

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1477,7 +1477,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
14771477
toolCall.name === 'mark_todo_in_progress' ||
14781478
toolCall.name === 'tool_search_tool_regex' ||
14791479
toolCall.name === 'user_memory' ||
1480-
toolCall.name === 'edit_responsd' ||
1480+
toolCall.name === 'edit_respond' ||
14811481
toolCall.name === 'debug_respond' ||
14821482
toolCall.name === 'plan_respond'
14831483
)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { createLogger } from '@sim/logger'
5+
import { useQueryClient } from '@tanstack/react-query'
6+
import { useParams } from 'next/navigation'
7+
import { Button, Input as EmcnInput } from '@/components/emcn'
8+
import { workflowKeys } from '@/hooks/queries/workflows'
9+
10+
const logger = createLogger('DebugSettings')
11+
12+
/**
13+
* Debug settings component for superusers.
14+
* Allows importing workflows by ID for debugging purposes.
15+
*/
16+
export function Debug() {
17+
const params = useParams()
18+
const queryClient = useQueryClient()
19+
const workspaceId = params?.workspaceId as string
20+
21+
const [workflowId, setWorkflowId] = useState('')
22+
const [isImporting, setIsImporting] = useState(false)
23+
24+
const handleImport = async () => {
25+
if (!workflowId.trim()) return
26+
27+
setIsImporting(true)
28+
29+
try {
30+
const response = await fetch('/api/superuser/import-workflow', {
31+
method: 'POST',
32+
headers: { 'Content-Type': 'application/json' },
33+
body: JSON.stringify({
34+
workflowId: workflowId.trim(),
35+
targetWorkspaceId: workspaceId,
36+
}),
37+
})
38+
39+
const data = await response.json()
40+
41+
if (response.ok) {
42+
await queryClient.invalidateQueries({ queryKey: workflowKeys.list(workspaceId) })
43+
setWorkflowId('')
44+
logger.info('Workflow imported successfully', {
45+
originalWorkflowId: workflowId.trim(),
46+
newWorkflowId: data.newWorkflowId,
47+
copilotChatsImported: data.copilotChatsImported,
48+
})
49+
}
50+
} catch (error) {
51+
logger.error('Failed to import workflow', error)
52+
} finally {
53+
setIsImporting(false)
54+
}
55+
}
56+
57+
return (
58+
<div className='flex h-full flex-col gap-[16px]'>
59+
<p className='text-[13px] text-[var(--text-secondary)]'>
60+
Import a workflow by ID along with its associated copilot chats.
61+
</p>
62+
63+
<div className='flex gap-[8px]'>
64+
<EmcnInput
65+
value={workflowId}
66+
onChange={(e) => setWorkflowId(e.target.value)}
67+
placeholder='Enter workflow ID'
68+
disabled={isImporting}
69+
/>
70+
<Button
71+
variant='tertiary'
72+
onClick={handleImport}
73+
disabled={isImporting || !workflowId.trim()}
74+
>
75+
{isImporting ? 'Importing...' : 'Import'}
76+
</Button>
77+
</div>
78+
</div>
79+
)
80+
}

0 commit comments

Comments
 (0)