From 9da689bc8ebbc948567ed80acdc0a9bf4607dbff Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 19 Jan 2026 15:58:07 -0800 Subject: [PATCH] Fix --- .../api/superuser/import-workflow/route.ts | 21 +-- .../settings-modal/components/debug/debug.tsx | 155 ++++-------------- .../settings-modal/settings-modal.tsx | 19 ++- apps/sim/lib/templates/permissions.ts | 38 ++++- 4 files changed, 89 insertions(+), 144 deletions(-) diff --git a/apps/sim/app/api/superuser/import-workflow/route.ts b/apps/sim/app/api/superuser/import-workflow/route.ts index d174faf2f..399879299 100644 --- a/apps/sim/app/api/superuser/import-workflow/route.ts +++ b/apps/sim/app/api/superuser/import-workflow/route.ts @@ -1,9 +1,10 @@ import { db } from '@sim/db' -import { copilotChats, user, workflow, workspace } from '@sim/db/schema' +import { copilotChats, workflow, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' -import { NextRequest, NextResponse } from 'next/server' +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, @@ -25,6 +26,8 @@ interface ImportWorkflowRequest { * 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 { @@ -33,16 +36,14 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - // Verify the user is a superuser - const [currentUser] = await db - .select({ isSuperUser: user.isSuperUser }) - .from(user) - .where(eq(user.id, session.user.id)) - .limit(1) + const { effectiveSuperUser, isSuperUser, superUserModeEnabled } = + await verifyEffectiveSuperUser(session.user.id) - if (!currentUser?.isSuperUser) { - logger.warn('Non-superuser attempted to access import-workflow endpoint', { + 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 }) } 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 index 7070571b1..b36d32a3a 100644 --- 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 @@ -1,43 +1,30 @@ 'use client' import { useState } from 'react' -import { useParams, useRouter } from 'next/navigation' -import { useQueryClient } from '@tanstack/react-query' -import { AlertTriangle, Download, ExternalLink, Loader2 } from 'lucide-react' import { createLogger } from '@sim/logger' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' +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') -interface ImportResult { - success: boolean - newWorkflowId?: string - copilotChatsImported?: number - error?: string -} - /** * Debug settings component for superusers. * Allows importing workflows by ID for debugging purposes. */ export function Debug() { const params = useParams() - const router = useRouter() const queryClient = useQueryClient() const workspaceId = params?.workspaceId as string const [workflowId, setWorkflowId] = useState('') const [isImporting, setIsImporting] = useState(false) - const [result, setResult] = useState(null) const handleImport = async () => { if (!workflowId.trim()) return setIsImporting(true) - setResult(null) try { const response = await fetch('/api/superuser/import-workflow', { @@ -51,126 +38,42 @@ export function Debug() { const data = await response.json() - if (!response.ok) { - setResult({ success: false, error: data.error || 'Failed to import workflow' }) - return + 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, + }) } - - // Invalidate workflow list cache to show the new workflow immediately - await queryClient.invalidateQueries({ queryKey: workflowKeys.list(workspaceId) }) - - setResult({ - success: true, - newWorkflowId: data.newWorkflowId, - copilotChatsImported: data.copilotChatsImported, - }) - - setWorkflowId('') - logger.info('Workflow imported successfully', { - originalWorkflowId: workflowId.trim(), - newWorkflowId: data.newWorkflowId, - copilotChatsImported: data.copilotChatsImported, - }) } catch (error) { logger.error('Failed to import workflow', error) - setResult({ success: false, error: 'An unexpected error occurred' }) } finally { setIsImporting(false) } } - const handleNavigateToWorkflow = () => { - if (result?.newWorkflowId) { - router.push(`/workspace/${workspaceId}/w/${result.newWorkflowId}`) - } - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !isImporting && workflowId.trim()) { - handleImport() - } - } - return ( -
-
- -

- This is a superuser debug feature. Use with caution. Imported workflows and copilot chats - will be copied to your current workspace. -

-
+
+

+ Import a workflow by ID along with its associated copilot chats. +

-
-
-

Import Workflow by ID

-

- Enter a workflow ID to import it along with its associated copilot chats into your - current workspace. Only the workflow structure and copilot conversations will be copied - - no deployments, webhooks, or triggers. -

-
- -
- -
- setWorkflowId(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Enter workflow ID (e.g., abc123-def456-...)" - disabled={isImporting} - className="flex-1" - /> - -
-
- - {result && ( -
- {result.success ? ( -
-

Workflow imported successfully!

-

- New workflow ID: {result.newWorkflowId} -

-

- Copilot chats imported: {result.copilotChatsImported} -

- -
- ) : ( -

{result.error}

- )} -
- )} +
+ setWorkflowId(e.target.value)} + placeholder='Enter workflow ID' + disabled={isImporting} + /> +
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx index 6b1a152aa..f862a1290 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx @@ -95,7 +95,13 @@ type SettingsSection = | 'workflow-mcp-servers' | 'debug' -type NavigationSection = 'account' | 'subscription' | 'tools' | 'system' | 'enterprise' | 'superuser' +type NavigationSection = + | 'account' + | 'subscription' + | 'tools' + | 'system' + | 'enterprise' + | 'superuser' type NavigationItem = { id: SettingsSection @@ -202,6 +208,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { const { data: session } = useSession() const queryClient = useQueryClient() const { data: organizationsData } = useOrganizations() + const { data: generalSettings } = useGeneralSettings() const { data: subscriptionData } = useSubscriptionData({ enabled: isBillingEnabled }) const { data: ssoProvidersData, isLoading: isLoadingSSO } = useSSOProviders() @@ -298,8 +305,10 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { return false } - // requiresSuperUser: only show if user is a superuser - if (item.requiresSuperUser && !isSuperUser) { + // requiresSuperUser: only show if user is a superuser AND has superuser mode enabled + const superUserModeEnabled = generalSettings?.superUserModeEnabled ?? false + const effectiveSuperUser = isSuperUser && superUserModeEnabled + if (item.requiresSuperUser && !effectiveSuperUser) { return false } @@ -316,6 +325,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { isAdmin, permissionConfig, isSuperUser, + generalSettings?.superUserModeEnabled, ]) // Memoized callbacks to prevent infinite loops in child components @@ -344,9 +354,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { [activeSection] ) - // React Query hook automatically loads and syncs settings - useGeneralSettings() - // Apply initial section from store when modal opens useEffect(() => { if (open && initialSection) { diff --git a/apps/sim/lib/templates/permissions.ts b/apps/sim/lib/templates/permissions.ts index 60288928c..7c17b92fc 100644 --- a/apps/sim/lib/templates/permissions.ts +++ b/apps/sim/lib/templates/permissions.ts @@ -1,11 +1,11 @@ import { db } from '@sim/db' -import { member, templateCreators, templates, user } from '@sim/db/schema' +import { member, settings, templateCreators, templates, user } from '@sim/db/schema' import { and, eq, or } from 'drizzle-orm' export type CreatorPermissionLevel = 'member' | 'admin' /** - * Verifies if a user is a super user. + * Verifies if a user is a super user (database flag only). * * @param userId - The ID of the user to check * @returns Object with isSuperUser boolean @@ -15,6 +15,40 @@ export async function verifySuperUser(userId: string): Promise<{ isSuperUser: bo return { isSuperUser: currentUser?.isSuperUser || false } } +/** + * Verifies if a user is an effective super user (database flag AND settings toggle). + * This should be used for features that can be disabled by the user's settings toggle. + * + * @param userId - The ID of the user to check + * @returns Object with effectiveSuperUser boolean and component values + */ +export async function verifyEffectiveSuperUser(userId: string): Promise<{ + effectiveSuperUser: boolean + isSuperUser: boolean + superUserModeEnabled: boolean +}> { + const [currentUser] = await db + .select({ isSuperUser: user.isSuperUser }) + .from(user) + .where(eq(user.id, userId)) + .limit(1) + + const [userSettings] = await db + .select({ superUserModeEnabled: settings.superUserModeEnabled }) + .from(settings) + .where(eq(settings.userId, userId)) + .limit(1) + + const isSuperUser = currentUser?.isSuperUser || false + const superUserModeEnabled = userSettings?.superUserModeEnabled ?? false + + return { + effectiveSuperUser: isSuperUser && superUserModeEnabled, + isSuperUser, + superUserModeEnabled, + } +} + /** * Fetches a template and verifies the user has permission to modify it. * Combines template existence check and creator permission check in one call.