mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix sidebar issue
This commit is contained in:
@@ -4,93 +4,29 @@ import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { workflow, workspace, workspaceMember } from '@/db/schema'
|
||||
import { workflow, workspace } from '@/db/schema'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
|
||||
const logger = createLogger('WorkflowAPI')
|
||||
|
||||
// Cache for workspace membership to reduce DB queries
|
||||
const workspaceMembershipCache = new Map<string, { role: string; expires: number }>()
|
||||
const CACHE_TTL = 60000 // 1 minute cache expiration
|
||||
const MAX_CACHE_SIZE = 1000 // Maximum number of entries to prevent unbounded growth
|
||||
|
||||
/**
|
||||
* Cleans up expired entries from the workspace membership cache
|
||||
*/
|
||||
function cleanupExpiredCacheEntries(): void {
|
||||
const now = Date.now()
|
||||
let expiredCount = 0
|
||||
|
||||
// Remove expired entries
|
||||
for (const [key, value] of workspaceMembershipCache.entries()) {
|
||||
if (value.expires <= now) {
|
||||
workspaceMembershipCache.delete(key)
|
||||
expiredCount++
|
||||
}
|
||||
}
|
||||
|
||||
// If we're still over the limit after removing expired entries,
|
||||
// remove the oldest entries (those that will expire soonest)
|
||||
if (workspaceMembershipCache.size > MAX_CACHE_SIZE) {
|
||||
const entries = Array.from(workspaceMembershipCache.entries()).sort(
|
||||
(a, b) => a[1].expires - b[1].expires
|
||||
)
|
||||
|
||||
const toRemove = entries.slice(0, workspaceMembershipCache.size - MAX_CACHE_SIZE)
|
||||
toRemove.forEach(([key]) => workspaceMembershipCache.delete(key))
|
||||
|
||||
logger.debug(
|
||||
`Cache cleanup: removed ${expiredCount} expired entries and ${toRemove.length} additional entries due to size limit`
|
||||
)
|
||||
} else if (expiredCount > 0) {
|
||||
logger.debug(`Cache cleanup: removed ${expiredCount} expired entries`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently verifies user's membership and role in a workspace with caching
|
||||
* Verifies user's workspace permissions using the permissions table
|
||||
* @param userId User ID to check
|
||||
* @param workspaceId Workspace ID to check
|
||||
* @returns Role if user is a member, null otherwise
|
||||
* @returns Permission type if user has access, null otherwise
|
||||
*/
|
||||
async function verifyWorkspaceMembership(
|
||||
userId: string,
|
||||
workspaceId: string
|
||||
): Promise<string | null> {
|
||||
// Opportunistic cleanup of expired cache entries
|
||||
if (workspaceMembershipCache.size > MAX_CACHE_SIZE / 2) {
|
||||
cleanupExpiredCacheEntries()
|
||||
}
|
||||
|
||||
// Create cache key from userId and workspaceId
|
||||
const cacheKey = `${userId}:${workspaceId}`
|
||||
|
||||
// Check cache first
|
||||
const cached = workspaceMembershipCache.get(cacheKey)
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
return cached.role
|
||||
}
|
||||
|
||||
// If not in cache or expired, query the database
|
||||
try {
|
||||
const membership = await db
|
||||
.select({ role: workspaceMember.role })
|
||||
.from(workspaceMember)
|
||||
.where(and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, userId)))
|
||||
.then((rows) => rows[0])
|
||||
const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
|
||||
|
||||
if (!membership) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
workspaceMembershipCache.set(cacheKey, {
|
||||
role: membership.role,
|
||||
expires: Date.now() + CACHE_TTL,
|
||||
})
|
||||
|
||||
return membership.role
|
||||
return permission
|
||||
} catch (error) {
|
||||
logger.error(`Error verifying workspace membership for ${userId} in ${workspaceId}:`, error)
|
||||
logger.error(`Error verifying workspace permissions for ${userId} in ${workspaceId}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ const IS_DEV = process.env.NODE_ENV === 'development'
|
||||
export function Sidebar() {
|
||||
useGlobalShortcuts()
|
||||
|
||||
const { workflows, createWorkflow, isLoading: workflowsLoading } = useWorkflowRegistry()
|
||||
const { workflows, createWorkflow, isLoading: workflowsLoading, loadWorkflows } = useWorkflowRegistry()
|
||||
const { isPending: sessionLoading } = useSession()
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
const isLoading = workflowsLoading || sessionLoading
|
||||
@@ -41,6 +41,14 @@ export function Sidebar() {
|
||||
const workspaceId = params.workspaceId as string
|
||||
const pathname = usePathname()
|
||||
|
||||
// Load workflows for the current workspace when workspaceId changes
|
||||
// This is the single source of truth for workflow loading
|
||||
useEffect(() => {
|
||||
if (workspaceId) {
|
||||
loadWorkflows(workspaceId)
|
||||
}
|
||||
}, [workspaceId, loadWorkflows])
|
||||
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [showHelp, setShowHelp] = useState(false)
|
||||
const [showInviteMembers, setShowInviteMembers] = useState(false)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { db } from '../../db'
|
||||
import { workflow, workspaceMember } from '../../db/schema'
|
||||
import { workflow } from '../../db/schema'
|
||||
import { createLogger } from '../../lib/logs/console-logger'
|
||||
import { getUserEntityPermissions } from '../../lib/permissions/utils'
|
||||
|
||||
const logger = createLogger('SocketPermissions')
|
||||
|
||||
@@ -10,15 +11,10 @@ export async function verifyWorkspaceMembership(
|
||||
workspaceId: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const membership = await db
|
||||
.select({ role: workspaceMember.role })
|
||||
.from(workspaceMember)
|
||||
.where(and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, userId)))
|
||||
.limit(1)
|
||||
|
||||
return membership.length > 0 ? membership[0].role : null
|
||||
const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
|
||||
return permission
|
||||
} catch (error) {
|
||||
logger.error(`Error verifying workspace membership for ${userId} in ${workspaceId}:`, error)
|
||||
logger.error(`Error verifying workspace permissions for ${userId} in ${workspaceId}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@ async function initializeApplication(): Promise<void> {
|
||||
// Load custom tools from server
|
||||
await useCustomToolsStore.getState().loadCustomTools()
|
||||
|
||||
// Load workflows from database (replaced sync system)
|
||||
await useWorkflowRegistry.getState().loadWorkflows()
|
||||
|
||||
// Mark data as initialized only after sync managers have loaded data from DB
|
||||
dataInitialized = true
|
||||
|
||||
@@ -66,30 +63,7 @@ async function initializeApplication(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract workflow ID from current URL
|
||||
* @returns workflow ID if found in URL, null otherwise
|
||||
*/
|
||||
function extractWorkflowIdFromUrl(): string | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
try {
|
||||
const pathSegments = window.location.pathname.split('/')
|
||||
// Check if URL matches pattern /w/{workflowId}
|
||||
if (pathSegments.length >= 3 && pathSegments[1] === 'w') {
|
||||
const workflowId = pathSegments[2]
|
||||
// Basic UUID validation (36 characters, contains hyphens)
|
||||
if (workflowId && workflowId.length === 36 && workflowId.includes('-')) {
|
||||
logger.info(`Extracted workflow ID from URL: ${workflowId}`)
|
||||
return workflowId
|
||||
}
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
logger.warn('Failed to extract workflow ID from URL:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if application is fully initialized
|
||||
|
||||
@@ -267,7 +267,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
await fetchWorkflowsFromDB(workspaceId)
|
||||
},
|
||||
|
||||
// Switch to workspace with comprehensive error handling and loading states
|
||||
// Switch to workspace - just clear state, let sidebar handle workflow loading
|
||||
switchToWorkspace: async (workspaceId: string) => {
|
||||
// Prevent multiple simultaneous transitions
|
||||
if (isWorkspaceTransitioning) {
|
||||
@@ -286,7 +286,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
// Clear current workspace state
|
||||
resetWorkflowStores()
|
||||
|
||||
// Update state
|
||||
// Update state - sidebar will load workflows when URL changes
|
||||
set({
|
||||
activeWorkflowId: null,
|
||||
workflows: {},
|
||||
@@ -294,9 +294,6 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
|
||||
error: null,
|
||||
})
|
||||
|
||||
// Fetch workflows for the new workspace
|
||||
await fetchWorkflowsFromDB(workspaceId)
|
||||
|
||||
logger.info(`Successfully switched to workspace: ${workspaceId}`)
|
||||
} catch (error) {
|
||||
logger.error(`Error switching to workspace ${workspaceId}:`, { error })
|
||||
|
||||
Reference in New Issue
Block a user