Skip to content

Commit 9da689b

Browse files
committed
Fix
1 parent e1bea05 commit 9da689b

File tree

4 files changed

+90
-145
lines changed

4 files changed

+90
-145
lines changed

apps/sim/app/api/superuser/import-workflow/route.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { db } from '@sim/db'
2-
import { copilotChats, user, workflow, workspace } from '@sim/db/schema'
2+
import { copilotChats, workflow, workspace } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { eq } from 'drizzle-orm'
5-
import { NextRequest, NextResponse } from 'next/server'
5+
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
7+
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
78
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
89
import {
910
loadWorkflowFromNormalizedTables,
@@ -25,6 +26,8 @@ interface ImportWorkflowRequest {
2526
* This creates a copy of the workflow in the target workspace with new IDs.
2627
* Only the workflow structure and copilot chats are copied - no deployments,
2728
* webhooks, triggers, or other sensitive data.
29+
*
30+
* Requires both isSuperUser flag AND superUserModeEnabled setting.
2831
*/
2932
export async function POST(request: NextRequest) {
3033
try {
@@ -33,16 +36,14 @@ export async function POST(request: NextRequest) {
3336
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
3437
}
3538

36-
// Verify the user is a superuser
37-
const [currentUser] = await db
38-
.select({ isSuperUser: user.isSuperUser })
39-
.from(user)
40-
.where(eq(user.id, session.user.id))
41-
.limit(1)
39+
const { effectiveSuperUser, isSuperUser, superUserModeEnabled } =
40+
await verifyEffectiveSuperUser(session.user.id)
4241

43-
if (!currentUser?.isSuperUser) {
44-
logger.warn('Non-superuser attempted to access import-workflow endpoint', {
42+
if (!effectiveSuperUser) {
43+
logger.warn('Non-effective-superuser attempted to access import-workflow endpoint', {
4544
userId: session.user.id,
45+
isSuperUser,
46+
superUserModeEnabled,
4647
})
4748
return NextResponse.json({ error: 'Forbidden: Superuser access required' }, { status: 403 })
4849
}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/debug/debug.tsx

Lines changed: 30 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,30 @@
11
'use client'
22

33
import { useState } from 'react'
4-
import { useParams, useRouter } from 'next/navigation'
5-
import { useQueryClient } from '@tanstack/react-query'
6-
import { AlertTriangle, Download, ExternalLink, Loader2 } from 'lucide-react'
74
import { createLogger } from '@sim/logger'
8-
import { Button } from '@/components/ui/button'
9-
import { Input } from '@/components/ui/input'
10-
import { Label } from '@/components/ui/label'
5+
import { useQueryClient } from '@tanstack/react-query'
6+
import { useParams } from 'next/navigation'
7+
import { Button, Input as EmcnInput } from '@/components/emcn'
118
import { workflowKeys } from '@/hooks/queries/workflows'
129

1310
const logger = createLogger('DebugSettings')
1411

15-
interface ImportResult {
16-
success: boolean
17-
newWorkflowId?: string
18-
copilotChatsImported?: number
19-
error?: string
20-
}
21-
2212
/**
2313
* Debug settings component for superusers.
2414
* Allows importing workflows by ID for debugging purposes.
2515
*/
2616
export function Debug() {
2717
const params = useParams()
28-
const router = useRouter()
2918
const queryClient = useQueryClient()
3019
const workspaceId = params?.workspaceId as string
3120

3221
const [workflowId, setWorkflowId] = useState('')
3322
const [isImporting, setIsImporting] = useState(false)
34-
const [result, setResult] = useState<ImportResult | null>(null)
3523

3624
const handleImport = async () => {
3725
if (!workflowId.trim()) return
3826

3927
setIsImporting(true)
40-
setResult(null)
4128

4229
try {
4330
const response = await fetch('/api/superuser/import-workflow', {
@@ -51,126 +38,42 @@ export function Debug() {
5138

5239
const data = await response.json()
5340

54-
if (!response.ok) {
55-
setResult({ success: false, error: data.error || 'Failed to import workflow' })
56-
return
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+
})
5749
}
58-
59-
// Invalidate workflow list cache to show the new workflow immediately
60-
await queryClient.invalidateQueries({ queryKey: workflowKeys.list(workspaceId) })
61-
62-
setResult({
63-
success: true,
64-
newWorkflowId: data.newWorkflowId,
65-
copilotChatsImported: data.copilotChatsImported,
66-
})
67-
68-
setWorkflowId('')
69-
logger.info('Workflow imported successfully', {
70-
originalWorkflowId: workflowId.trim(),
71-
newWorkflowId: data.newWorkflowId,
72-
copilotChatsImported: data.copilotChatsImported,
73-
})
7450
} catch (error) {
7551
logger.error('Failed to import workflow', error)
76-
setResult({ success: false, error: 'An unexpected error occurred' })
7752
} finally {
7853
setIsImporting(false)
7954
}
8055
}
8156

82-
const handleNavigateToWorkflow = () => {
83-
if (result?.newWorkflowId) {
84-
router.push(`/workspace/${workspaceId}/w/${result.newWorkflowId}`)
85-
}
86-
}
87-
88-
const handleKeyDown = (e: React.KeyboardEvent) => {
89-
if (e.key === 'Enter' && !isImporting && workflowId.trim()) {
90-
handleImport()
91-
}
92-
}
93-
9457
return (
95-
<div className="flex flex-col gap-6 p-1">
96-
<div className="flex items-center gap-2 rounded-lg border border-amber-500/20 bg-amber-500/10 p-4">
97-
<AlertTriangle className="h-5 w-5 flex-shrink-0 text-amber-500" />
98-
<p className="text-sm text-amber-200">
99-
This is a superuser debug feature. Use with caution. Imported workflows and copilot chats
100-
will be copied to your current workspace.
101-
</p>
102-
</div>
103-
104-
<div className="flex flex-col gap-4">
105-
<div>
106-
<h3 className="mb-1 text-base font-medium text-white">Import Workflow by ID</h3>
107-
<p className="text-sm text-muted-foreground">
108-
Enter a workflow ID to import it along with its associated copilot chats into your
109-
current workspace. Only the workflow structure and copilot conversations will be copied
110-
- no deployments, webhooks, or triggers.
111-
</p>
112-
</div>
113-
114-
<div className="flex flex-col gap-2">
115-
<Label htmlFor="workflow-id">Workflow ID</Label>
116-
<div className="flex gap-2">
117-
<Input
118-
id="workflow-id"
119-
value={workflowId}
120-
onChange={(e) => setWorkflowId(e.target.value)}
121-
onKeyDown={handleKeyDown}
122-
placeholder="Enter workflow ID (e.g., abc123-def456-...)"
123-
disabled={isImporting}
124-
className="flex-1"
125-
/>
126-
<Button onClick={handleImport} disabled={isImporting || !workflowId.trim()}>
127-
{isImporting ? (
128-
<>
129-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
130-
Importing...
131-
</>
132-
) : (
133-
<>
134-
<Download className="mr-2 h-4 w-4" />
135-
Import
136-
</>
137-
)}
138-
</Button>
139-
</div>
140-
</div>
141-
142-
{result && (
143-
<div
144-
className={`rounded-lg border p-4 ${
145-
result.success
146-
? 'border-green-500/20 bg-green-500/10'
147-
: 'border-red-500/20 bg-red-500/10'
148-
}`}
149-
>
150-
{result.success ? (
151-
<div className="flex flex-col gap-2">
152-
<p className="font-medium text-green-400">Workflow imported successfully!</p>
153-
<p className="text-sm text-green-300">
154-
New workflow ID: <code className="font-mono">{result.newWorkflowId}</code>
155-
</p>
156-
<p className="text-sm text-green-300">
157-
Copilot chats imported: {result.copilotChatsImported}
158-
</p>
159-
<Button
160-
variant="outline"
161-
size="sm"
162-
onClick={handleNavigateToWorkflow}
163-
className="mt-2 w-fit"
164-
>
165-
<ExternalLink className="mr-2 h-4 w-4" />
166-
Open Workflow
167-
</Button>
168-
</div>
169-
) : (
170-
<p className="text-red-400">{result.error}</p>
171-
)}
172-
</div>
173-
)}
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>
17477
</div>
17578
</div>
17679
)

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,13 @@ type SettingsSection =
9595
| 'workflow-mcp-servers'
9696
| 'debug'
9797

98-
type NavigationSection = 'account' | 'subscription' | 'tools' | 'system' | 'enterprise' | 'superuser'
98+
type NavigationSection =
99+
| 'account'
100+
| 'subscription'
101+
| 'tools'
102+
| 'system'
103+
| 'enterprise'
104+
| 'superuser'
99105

100106
type NavigationItem = {
101107
id: SettingsSection
@@ -202,6 +208,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
202208
const { data: session } = useSession()
203209
const queryClient = useQueryClient()
204210
const { data: organizationsData } = useOrganizations()
211+
const { data: generalSettings } = useGeneralSettings()
205212
const { data: subscriptionData } = useSubscriptionData({ enabled: isBillingEnabled })
206213
const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders()
207214

@@ -298,8 +305,10 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
298305
return false
299306
}
300307

301-
// requiresSuperUser: only show if user is a superuser
302-
if (item.requiresSuperUser && !isSuperUser) {
308+
// requiresSuperUser: only show if user is a superuser AND has superuser mode enabled
309+
const superUserModeEnabled = generalSettings?.superUserModeEnabled ?? false
310+
const effectiveSuperUser = isSuperUser && superUserModeEnabled
311+
if (item.requiresSuperUser && !effectiveSuperUser) {
303312
return false
304313
}
305314

@@ -316,6 +325,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
316325
isAdmin,
317326
permissionConfig,
318327
isSuperUser,
328+
generalSettings?.superUserModeEnabled,
319329
])
320330

321331
// Memoized callbacks to prevent infinite loops in child components
@@ -344,9 +354,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
344354
[activeSection]
345355
)
346356

347-
// React Query hook automatically loads and syncs settings
348-
useGeneralSettings()
349-
350357
// Apply initial section from store when modal opens
351358
useEffect(() => {
352359
if (open && initialSection) {

apps/sim/lib/templates/permissions.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { db } from '@sim/db'
2-
import { member, templateCreators, templates, user } from '@sim/db/schema'
2+
import { member, settings, templateCreators, templates, user } from '@sim/db/schema'
33
import { and, eq, or } from 'drizzle-orm'
44

55
export type CreatorPermissionLevel = 'member' | 'admin'
66

77
/**
8-
* Verifies if a user is a super user.
8+
* Verifies if a user is a super user (database flag only).
99
*
1010
* @param userId - The ID of the user to check
1111
* @returns Object with isSuperUser boolean
@@ -15,6 +15,40 @@ export async function verifySuperUser(userId: string): Promise<{ isSuperUser: bo
1515
return { isSuperUser: currentUser?.isSuperUser || false }
1616
}
1717

18+
/**
19+
* Verifies if a user is an effective super user (database flag AND settings toggle).
20+
* This should be used for features that can be disabled by the user's settings toggle.
21+
*
22+
* @param userId - The ID of the user to check
23+
* @returns Object with effectiveSuperUser boolean and component values
24+
*/
25+
export async function verifyEffectiveSuperUser(userId: string): Promise<{
26+
effectiveSuperUser: boolean
27+
isSuperUser: boolean
28+
superUserModeEnabled: boolean
29+
}> {
30+
const [currentUser] = await db
31+
.select({ isSuperUser: user.isSuperUser })
32+
.from(user)
33+
.where(eq(user.id, userId))
34+
.limit(1)
35+
36+
const [userSettings] = await db
37+
.select({ superUserModeEnabled: settings.superUserModeEnabled })
38+
.from(settings)
39+
.where(eq(settings.userId, userId))
40+
.limit(1)
41+
42+
const isSuperUser = currentUser?.isSuperUser || false
43+
const superUserModeEnabled = userSettings?.superUserModeEnabled ?? false
44+
45+
return {
46+
effectiveSuperUser: isSuperUser && superUserModeEnabled,
47+
isSuperUser,
48+
superUserModeEnabled,
49+
}
50+
}
51+
1852
/**
1953
* Fetches a template and verifies the user has permission to modify it.
2054
* Combines template existence check and creator permission check in one call.

0 commit comments

Comments
 (0)