fix sidebar issue

This commit is contained in:
Vikhyath Mondreti
2025-06-27 19:07:23 -07:00
parent f8cfdba5ee
commit 244c086ef2
5 changed files with 23 additions and 112 deletions

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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 })