From 77f4264a5b9364e43defb7901825cb586cf26115 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 21 Sep 2025 15:06:33 +0000 Subject: [PATCH] fix(workspace): harden /api/workspaces GET against DB errors; gate socket init on NEXT_PUBLIC_SOCKET_URL to avoid failed WebSocket attempts in prod without socket server --- apps/sim/app/api/health/route.ts | 40 ++++++------ apps/sim/app/api/workspaces/route.ts | 62 ++++++++++--------- apps/sim/app/workspace/[workspaceId]/page.tsx | 6 +- apps/sim/contexts/socket-context.tsx | 11 +++- 4 files changed, 65 insertions(+), 54 deletions(-) diff --git a/apps/sim/app/api/health/route.ts b/apps/sim/app/api/health/route.ts index a7d92b0ac75..6e4704ac9d4 100644 --- a/apps/sim/app/api/health/route.ts +++ b/apps/sim/app/api/health/route.ts @@ -1,24 +1,24 @@ import { NextResponse } from 'next/server' export async function GET() { - try { - return NextResponse.json( - { - status: 'healthy', - timestamp: new Date().toISOString(), - service: 'sim-app', - }, - { status: 200 }, - ) - } catch (error) { - return NextResponse.json( - { - status: 'unhealthy', - timestamp: new Date().toISOString(), - service: 'sim-app', - error: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 }, - ) - } + try { + return NextResponse.json( + { + status: 'healthy', + timestamp: new Date().toISOString(), + service: 'sim-app', + }, + { status: 200 } + ) + } catch (error) { + return NextResponse.json( + { + status: 'unhealthy', + timestamp: new Date().toISOString(), + service: 'sim-app', + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } } diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index b184ca6e864..c94796618e8 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -16,40 +16,46 @@ export async function GET() { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - // Get all workspaces where the user has permissions - const userWorkspaces = await db - .select({ - workspace: workspace, - permissionType: permissions.permissionType, - }) - .from(permissions) - .innerJoin(workspace, eq(permissions.entityId, workspace.id)) - .where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace'))) - .orderBy(desc(workspace.createdAt)) + try { + // Get all workspaces where the user has permissions + const userWorkspaces = await db + .select({ + workspace: workspace, + permissionType: permissions.permissionType, + }) + .from(permissions) + .innerJoin(workspace, eq(permissions.entityId, workspace.id)) + .where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace'))) + .orderBy(desc(workspace.createdAt)) - if (userWorkspaces.length === 0) { - // Create a default workspace for the user - const defaultWorkspace = await createDefaultWorkspace(session.user.id, session.user.name) + if (userWorkspaces.length === 0) { + // Create a default workspace for the user + const defaultWorkspace = await createDefaultWorkspace(session.user.id, session.user.name) - // Migrate existing workflows to the default workspace - await migrateExistingWorkflows(session.user.id, defaultWorkspace.id) + // Migrate existing workflows to the default workspace + await migrateExistingWorkflows(session.user.id, defaultWorkspace.id) - return NextResponse.json({ workspaces: [defaultWorkspace] }) - } + return NextResponse.json({ workspaces: [defaultWorkspace] }) + } - // If user has workspaces but might have orphaned workflows, migrate them - await ensureWorkflowsHaveWorkspace(session.user.id, userWorkspaces[0].workspace.id) + // If user has workspaces but might have orphaned workflows, migrate them + await ensureWorkflowsHaveWorkspace(session.user.id, userWorkspaces[0].workspace.id) - // Format the response with permission information - const workspacesWithPermissions = userWorkspaces.map( - ({ workspace: workspaceDetails, permissionType }) => ({ - ...workspaceDetails, - role: permissionType === 'admin' ? 'owner' : 'member', // Map admin to owner for compatibility - permissions: permissionType, - }) - ) + // Format the response with permission information + const workspacesWithPermissions = userWorkspaces.map( + ({ workspace: workspaceDetails, permissionType }) => ({ + ...workspaceDetails, + role: permissionType === 'admin' ? 'owner' : 'member', + permissions: permissionType, + }) + ) - return NextResponse.json({ workspaces: workspacesWithPermissions }) + return NextResponse.json({ workspaces: workspacesWithPermissions }) + } catch (error) { + logger.error('Failed to fetch user workspaces', { error, userId: session.user.id }) + // Degrade gracefully to avoid hard-blocking the workspace page + return NextResponse.json({ workspaces: [] }) + } } // POST /api/workspaces - Create a new workspace diff --git a/apps/sim/app/workspace/[workspaceId]/page.tsx b/apps/sim/app/workspace/[workspaceId]/page.tsx index d4c027ee694..5fa78bac9da 100644 --- a/apps/sim/app/workspace/[workspaceId]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/page.tsx @@ -1,10 +1,6 @@ import { redirect } from 'next/navigation' -export default function WorkspacePage({ - params, -}: { - params: { workspaceId: string } -}) { +export default function WorkspacePage({ params }: { params: { workspaceId: string } }) { const { workspaceId } = params redirect(`/workspace/${workspaceId}/w`) } diff --git a/apps/sim/contexts/socket-context.tsx b/apps/sim/contexts/socket-context.tsx index 56160cf3e9c..b14592b3cf2 100644 --- a/apps/sim/contexts/socket-context.tsx +++ b/apps/sim/contexts/socket-context.tsx @@ -164,7 +164,16 @@ export function SocketProvider({ children, user }: SocketProviderProps) { // Generate initial token for socket authentication const token = await generateSocketToken() - const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || 'http://localhost:3002' + // Only attempt to connect if socket URL is explicitly configured in production. + // In environments without a socket server, skip initialization gracefully. + const configuredSocketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') + const socketUrl = configuredSocketUrl || 'http://localhost:3002' + + if (!configuredSocketUrl && getEnv('NODE_ENV') === 'production') { + logger.warn('Socket server URL is not configured; skipping socket initialization') + setIsConnecting(false) + return + } logger.info('Attempting to connect to Socket.IO server', { url: socketUrl,