Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/sim/app/api/auth/socket-token/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { headers } from 'next/headers'
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('SocketToken')

export async function POST() {
try {
Expand All @@ -10,11 +13,23 @@ export async function POST() {
})

if (!response) {
logger.error('Failed to generate socket token - no response from auth API')
return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 })
}

return NextResponse.json({ token: response.token })
} catch (error) {
logger.error('Error generating socket token:', {
error,
errorMessage: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : undefined,
})

// Return more specific error if it's an auth issue
if (error instanceof Error && error.message.includes('Unauthorized')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 })
}
}
141 changes: 101 additions & 40 deletions apps/sim/app/api/workspaces/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,76 +10,137 @@ const logger = createLogger('Workspaces')

// Get all workspaces for the current user
export async function GET() {
const session = await getSession()
try {
const session = await getSession()

if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
if (!session?.user?.id) {
logger.warn('GET /api/workspaces - No session or user ID', { session })
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

logger.info('GET /api/workspaces - Fetching workspaces for user', { userId: session.user.id })

// Get all workspaces where the user has permissions
const userWorkspaces = await db
.select({
workspace: workspace,
permissionType: permissions.permissionType,
// 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))

logger.info('GET /api/workspaces - Found workspaces', {
userId: session.user.id,
count: userWorkspaces.length
})
.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
logger.info('GET /api/workspaces - Creating default workspace', { userId: session.user.id })
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', // Map admin to owner for compatibility
permissions: permissionType,
})
)

return NextResponse.json({ workspaces: workspacesWithPermissions })
return NextResponse.json({ workspaces: workspacesWithPermissions })
} catch (error) {
logger.error('GET /api/workspaces - Error fetching workspaces', {
error,
errorMessage: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : undefined
})

// Return a more specific error message if possible
if (error instanceof Error && error.message.includes('Database')) {
return NextResponse.json(
{ error: 'Database connection error. Please try again later.' },
{ status: 503 }
)
}

return NextResponse.json(
{ error: 'Failed to fetch workspaces' },
{ status: 500 }
)
}
}

// POST /api/workspaces - Create a new workspace
export async function POST(req: Request) {
const session = await getSession()
try {
const session = await getSession()

if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
if (!session?.user?.id) {
logger.warn('POST /api/workspaces - No session or user ID')
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

try {
const { name } = await req.json()

if (!name) {
return NextResponse.json({ error: 'Name is required' }, { status: 400 })
}

logger.info('POST /api/workspaces - Creating workspace', {
userId: session.user.id,
name
})

const newWorkspace = await createWorkspace(session.user.id, name)

logger.info('POST /api/workspaces - Workspace created successfully', {
userId: session.user.id,
workspaceId: newWorkspace.id
})

return NextResponse.json({ workspace: newWorkspace })
} catch (error) {
console.error('Error creating workspace:', error)
return NextResponse.json({ error: 'Failed to create workspace' }, { status: 500 })
logger.error('POST /api/workspaces - Error creating workspace', {
error,
errorMessage: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : undefined
})

if (error instanceof Error && error.message.includes('Database')) {
return NextResponse.json(
{ error: 'Database connection error. Please try again later.' },
{ status: 503 }
)
}

return NextResponse.json(
{ error: 'Failed to create workspace' },
{ status: 500 }
)
}
}

// Helper function to create a default workspace
async function createDefaultWorkspace(userId: string, userName?: string | null) {
const workspaceName = userName ? `${userName}'s Workspace` : 'My Workspace'
return createWorkspace(userId, workspaceName)
try {
const workspaceName = userName ? `${userName}'s Workspace` : 'My Workspace'
return await createWorkspace(userId, workspaceName)
} catch (error) {
logger.error('Failed to create default workspace', { userId, userName, error })
throw error
}
}

// Helper function to create a workspace
Expand Down
70 changes: 50 additions & 20 deletions apps/sim/contexts/socket-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,28 @@ export function SocketProvider({ children, user }: SocketProviderProps) {

// Helper function to generate a fresh socket token
const generateSocketToken = async (): Promise<string> => {
// Avoid overlapping token requests
const res = await fetch('/api/auth/socket-token', {
method: 'POST',
credentials: 'include',
headers: { 'cache-control': 'no-store' },
})
if (!res.ok) throw new Error('Failed to generate socket token')
const body = await res.json().catch(() => ({}))
const token = body?.token
if (!token || typeof token !== 'string') throw new Error('Invalid socket token')
return token
try {
// Avoid overlapping token requests
const res = await fetch('/api/auth/socket-token', {
method: 'POST',
credentials: 'include',
headers: { 'cache-control': 'no-store' },
})
if (!res.ok) {
logger.warn('Failed to generate socket token, status:', res.status)
return '' // Return empty string to allow connection attempt without token
}
const body = await res.json().catch(() => ({}))
const token = body?.token
if (!token || typeof token !== 'string') {
logger.warn('Invalid socket token received')
return '' // Return empty string to allow connection attempt without token
}
return token
} catch (error) {
logger.warn('Error generating socket token:', error)
return '' // Return empty string to allow connection attempt without token
}
}

// Initialize socket when user is available - only once per session
Expand All @@ -164,10 +175,20 @@ 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'
// Determine the socket URL - if not explicitly set, use relative path in production
let socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL')

if (!socketUrl) {
// In production, use relative path to connect to the same domain
if (typeof window !== 'undefined' && window.location.hostname !== 'localhost') {
socketUrl = '' // Empty string means same origin
} else {
socketUrl = 'http://localhost:3002'
}
}

logger.info('Attempting to connect to Socket.IO server', {
url: socketUrl,
url: socketUrl || 'same-origin',
userId: user?.id || 'no-user',
hasToken: !!token,
timestamp: new Date().toISOString(),
Expand Down Expand Up @@ -227,13 +248,22 @@ export function SocketProvider({ children, user }: SocketProviderProps) {

socketInstance.on('connect_error', (error: any) => {
setIsConnecting(false)
logger.error('Socket connection error:', {
message: error.message,
stack: error.stack,
description: error.description,
type: error.type,
transport: error.transport,
})

// Log connection errors but don't spam the console if socket server is not available
if (error.type === 'TransportError' || error.message?.includes('websocket error')) {
logger.debug('Socket connection error (server may not be available):', {
message: error.message,
type: error.type,
})
} else {
logger.error('Socket connection error:', {
message: error.message,
stack: error.stack,
description: error.description,
type: error.type,
transport: error.transport,
})
}

// Authentication errors now indicate either session expiry or token generation issues
if (
Expand Down