Skip to content

Commit 9ad36c0

Browse files
icecrasher321Sg312
authored andcommitted
fix(oauth-block): race condition for rendering credential selectors and other subblocks + gdrive fixes (#1029)
* fix(oauth-block): race condition for rendering credential selectors and other subblocks * fix import * add dependsOn field to track cros-subblock deps * remove redundant check * remove redundant checks * remove misleading comment * fix * fix jira * fix * fix * confluence * fix triggers * fix * fix * make trigger creds collab supported * fix for backwards compat * fix trigger modal
1 parent 2771c68 commit 9ad36c0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+655
-485
lines changed

apps/sim/app/api/tools/drive/file/route.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export async function GET(request: NextRequest) {
4545
// Fetch the file from Google Drive API
4646
logger.info(`[${requestId}] Fetching file ${fileId} from Google Drive API`)
4747
const response = await fetch(
48-
`https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,exportLinks`,
48+
`https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,exportLinks,shortcutDetails&supportsAllDrives=true`,
4949
{
5050
headers: {
5151
Authorization: `Bearer ${accessToken}`,
@@ -77,6 +77,34 @@ export async function GET(request: NextRequest) {
7777
'application/vnd.google-apps.presentation': 'application/pdf', // Google Slides to PDF
7878
}
7979

80+
// Resolve shortcuts transparently for UI stability
81+
if (
82+
file.mimeType === 'application/vnd.google-apps.shortcut' &&
83+
file.shortcutDetails?.targetId
84+
) {
85+
const targetId = file.shortcutDetails.targetId
86+
const shortcutResp = await fetch(
87+
`https://www.googleapis.com/drive/v3/files/${targetId}?fields=id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,exportLinks&supportsAllDrives=true`,
88+
{
89+
headers: { Authorization: `Bearer ${accessToken}` },
90+
}
91+
)
92+
if (shortcutResp.ok) {
93+
const targetFile = await shortcutResp.json()
94+
file.id = targetFile.id
95+
file.name = targetFile.name
96+
file.mimeType = targetFile.mimeType
97+
file.iconLink = targetFile.iconLink
98+
file.webViewLink = targetFile.webViewLink
99+
file.thumbnailLink = targetFile.thumbnailLink
100+
file.createdTime = targetFile.createdTime
101+
file.modifiedTime = targetFile.modifiedTime
102+
file.size = targetFile.size
103+
file.owners = targetFile.owners
104+
file.exportLinks = targetFile.exportLinks
105+
}
106+
}
107+
80108
// If the file is a Google Docs, Sheets, or Slides file, we need to provide the export link
81109
if (file.mimeType.startsWith('application/vnd.google-apps.')) {
82110
const format = exportFormats[file.mimeType] || 'application/pdf'

apps/sim/app/api/tools/drive/files/route.ts

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { eq } from 'drizzle-orm'
21
import { type NextRequest, NextResponse } from 'next/server'
32
import { getSession } from '@/lib/auth'
3+
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
44
import { createLogger } from '@/lib/logs/console/logger'
55
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
6-
import { db } from '@/db'
7-
import { account } from '@/db/schema'
86

97
export const dynamic = 'force-dynamic'
108

@@ -32,64 +30,48 @@ export async function GET(request: NextRequest) {
3230
const credentialId = searchParams.get('credentialId')
3331
const mimeType = searchParams.get('mimeType')
3432
const query = searchParams.get('query') || ''
33+
const folderId = searchParams.get('folderId') || searchParams.get('parentId') || ''
34+
const workflowId = searchParams.get('workflowId') || undefined
3535

3636
if (!credentialId) {
3737
logger.warn(`[${requestId}] Missing credential ID`)
3838
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
3939
}
4040

41-
// Get the credential from the database
42-
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
43-
44-
if (!credentials.length) {
45-
logger.warn(`[${requestId}] Credential not found`, { credentialId })
46-
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
47-
}
48-
49-
const credential = credentials[0]
50-
51-
// Check if the credential belongs to the user
52-
if (credential.userId !== session.user.id) {
53-
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
54-
credentialUserId: credential.userId,
55-
requestUserId: session.user.id,
56-
})
57-
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
41+
// Authorize use of the credential (supports collaborator credentials via workflow)
42+
const authz = await authorizeCredentialUse(request, { credentialId: credentialId!, workflowId })
43+
if (!authz.ok || !authz.credentialOwnerUserId) {
44+
logger.warn(`[${requestId}] Unauthorized credential access attempt`, authz)
45+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
5846
}
5947

6048
// Refresh access token if needed using the utility function
61-
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
49+
const accessToken = await refreshAccessTokenIfNeeded(
50+
credentialId!,
51+
authz.credentialOwnerUserId,
52+
requestId
53+
)
6254

6355
if (!accessToken) {
6456
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
6557
}
6658

67-
// Build the query parameters for Google Drive API
68-
let queryParams = 'trashed=false'
69-
70-
// Add mimeType filter if provided
59+
// Build Drive 'q' expression safely
60+
const qParts: string[] = ['trashed = false']
61+
if (folderId) {
62+
qParts.push(`'${folderId.replace(/'/g, "\\'")}' in parents`)
63+
}
7164
if (mimeType) {
72-
// For Google Drive API, we need to use 'q' parameter for mimeType filtering
73-
// Instead of using the mimeType parameter directly, we'll add it to the query
74-
if (queryParams.includes('q=')) {
75-
queryParams += ` and mimeType='${mimeType}'`
76-
} else {
77-
queryParams += `&q=mimeType='${mimeType}'`
78-
}
65+
qParts.push(`mimeType = '${mimeType.replace(/'/g, "\\'")}'`)
7966
}
80-
81-
// Add search query if provided
8267
if (query) {
83-
if (queryParams.includes('q=')) {
84-
queryParams += ` and name contains '${query}'`
85-
} else {
86-
queryParams += `&q=name contains '${query}'`
87-
}
68+
qParts.push(`name contains '${query.replace(/'/g, "\\'")}'`)
8869
}
70+
const q = encodeURIComponent(qParts.join(' and '))
8971

90-
// Fetch files from Google Drive API
72+
// Fetch files from Google Drive API with shared drives support
9173
const response = await fetch(
92-
`https://www.googleapis.com/drive/v3/files?${queryParams}&fields=files(id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners)`,
74+
`https://www.googleapis.com/drive/v3/files?q=${q}&supportsAllDrives=true&includeItemsFromAllDrives=true&spaces=drive&fields=files(id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,parents)`,
9375
{
9476
headers: {
9577
Authorization: `Bearer ${accessToken}`,

apps/sim/app/api/webhooks/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ export async function POST(request: NextRequest) {
329329
logger.info(`[${requestId}] Gmail provider detected. Setting up Gmail webhook configuration.`)
330330
try {
331331
const { configureGmailPolling } = await import('@/lib/webhooks/utils')
332-
// Use workflow owner for OAuth lookups to support collaborator-saved credentials
332+
// Pass workflow owner for backward-compat fallback (utils prefers credentialId if present)
333333
const success = await configureGmailPolling(workflowRecord.userId, savedWebhook, requestId)
334334

335335
if (!success) {
@@ -364,7 +364,7 @@ export async function POST(request: NextRequest) {
364364
)
365365
try {
366366
const { configureOutlookPolling } = await import('@/lib/webhooks/utils')
367-
// Use workflow owner for OAuth lookups to support collaborator-saved credentials
367+
// Pass workflow owner for backward-compat fallback (utils prefers credentialId if present)
368368
const success = await configureOutlookPolling(
369369
workflowRecord.userId,
370370
savedWebhook,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/channel-selector-input.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
type SlackChannelInfo,
77
SlackChannelSelector,
88
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/channel-selector/components/slack-channel-selector'
9+
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-depends-on-gate'
910
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
1011
import type { SubBlockConfig } from '@/blocks/types'
11-
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
1212

1313
interface ChannelSelectorInputProps {
1414
blockId: string
@@ -29,8 +29,6 @@ export function ChannelSelectorInput({
2929
isPreview = false,
3030
previewValue,
3131
}: ChannelSelectorInputProps) {
32-
const { getValue } = useSubBlockStore()
33-
3432
// Use the proper hook to get the current value and setter (same as file-selector)
3533
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
3634
// Reactive upstream fields
@@ -43,6 +41,8 @@ export function ChannelSelectorInput({
4341
// Get provider-specific values
4442
const provider = subBlock.provider || 'slack'
4543
const isSlack = provider === 'slack'
44+
// Central dependsOn gating
45+
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
4646

4747
// Get the credential for the provider - use provided credential or fall back to reactive values
4848
let credential: string
@@ -89,15 +89,10 @@ export function ChannelSelectorInput({
8989
}}
9090
credential={credential}
9191
label={subBlock.placeholder || 'Select Slack channel'}
92-
disabled={disabled || !credential}
92+
disabled={finalDisabled}
9393
/>
9494
</div>
9595
</TooltipTrigger>
96-
{!credential && (
97-
<TooltipContent side='top'>
98-
<p>Please select a Slack account or enter a bot token first</p>
99-
</TooltipContent>
100-
)}
10196
</Tooltip>
10297
</TooltipProvider>
10398
)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
2626
import type { SubBlockConfig } from '@/blocks/types'
2727
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
2828
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
29-
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
3029

3130
const logger = createLogger('CredentialSelector')
3231

@@ -217,17 +216,6 @@ export function CredentialSelector({
217216
setSelectedId(credentialId)
218217
if (!isPreview) {
219218
setStoreValue(credentialId)
220-
// If credential changed, clear other sub-block fields for a clean state
221-
if (previousId && previousId !== credentialId) {
222-
const wfId = (activeWorkflowId as string) || ''
223-
const workflowValues = useSubBlockStore.getState().workflowValues[wfId] || {}
224-
const blockValues = workflowValues[blockId] || {}
225-
Object.keys(blockValues).forEach((key) => {
226-
if (key !== subBlock.id) {
227-
collaborativeSetSubblockValue(blockId, key, '')
228-
}
229-
})
230-
}
231219
}
232220
setOpen(false)
233221
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
CommandList,
1313
} from '@/components/ui/command'
1414
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
15+
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-depends-on-gate'
1516
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
1617
import type { SubBlockConfig } from '@/blocks/types'
1718

@@ -65,6 +66,9 @@ export function DocumentSelector({
6566
// Use preview value when in preview mode, otherwise use store value
6667
const value = isPreview ? previewValue : storeValue
6768

69+
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
70+
const isDisabled = finalDisabled
71+
6872
// Fetch documents for the selected knowledge base
6973
const fetchDocuments = useCallback(async () => {
7074
if (!knowledgeBaseId) {
@@ -103,6 +107,7 @@ export function DocumentSelector({
103107
// Handle dropdown open/close - fetch documents when opening
104108
const handleOpenChange = (isOpen: boolean) => {
105109
if (isPreview) return
110+
if (isDisabled) return
106111

107112
setOpen(isOpen)
108113

@@ -124,13 +129,14 @@ export function DocumentSelector({
124129

125130
// Sync selected document with value prop
126131
useEffect(() => {
132+
if (isDisabled) return
127133
if (value && documents.length > 0) {
128134
const docInfo = documents.find((doc) => doc.id === value)
129135
setSelectedDocument(docInfo || null)
130136
} else {
131137
setSelectedDocument(null)
132138
}
133-
}, [value, documents])
139+
}, [value, documents, isDisabled])
134140

135141
// Reset documents when knowledge base changes
136142
useEffect(() => {
@@ -141,10 +147,10 @@ export function DocumentSelector({
141147

142148
// Fetch documents when knowledge base is available
143149
useEffect(() => {
144-
if (knowledgeBaseId && !isPreview) {
150+
if (knowledgeBaseId && !isPreview && !isDisabled) {
145151
fetchDocuments()
146152
}
147-
}, [knowledgeBaseId, isPreview, fetchDocuments])
153+
}, [knowledgeBaseId, isPreview, isDisabled, fetchDocuments])
148154

149155
const formatDocumentName = (document: DocumentData) => {
150156
return document.filename
@@ -166,9 +172,6 @@ export function DocumentSelector({
166172

167173
const label = subBlock.placeholder || 'Select document'
168174

169-
// Show disabled state if no knowledge base is selected
170-
const isDisabled = disabled || isPreview || !knowledgeBaseId
171-
172175
return (
173176
<div className='w-full'>
174177
<Popover open={open} onOpenChange={handleOpenChange}>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,14 @@ export function ConfluenceFileSelector({
376376
}
377377
}, [value])
378378

379+
// Clear preview when value is cleared (e.g., collaborator cleared or domain change cascade)
380+
useEffect(() => {
381+
if (!value) {
382+
setSelectedFile(null)
383+
onFileInfoChange?.(null)
384+
}
385+
}, [value, onFileInfoChange])
386+
379387
// Handle file selection
380388
const handleSelectFile = (file: ConfluenceFileInfo) => {
381389
setSelectedFileId(file.id)
@@ -547,7 +555,7 @@ export function ConfluenceFileSelector({
547555
</Popover>
548556

549557
{/* File preview */}
550-
{showPreview && selectedFile && (
558+
{showPreview && selectedFile && selectedFileId && selectedFile.id === selectedFileId && (
551559
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
552560
<div className='absolute top-2 right-2'>
553561
<Button

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,13 @@ export function GoogleDrivePicker({
414414
return <FileIcon className={`${iconSize} text-muted-foreground`} />
415415
}
416416

417+
const canShowPreview = !!(
418+
showPreview &&
419+
selectedFile &&
420+
selectedFileId &&
421+
selectedFile.id === selectedFileId
422+
)
423+
417424
return (
418425
<>
419426
<div className='space-y-2'>
@@ -440,7 +447,7 @@ export function GoogleDrivePicker({
440447
}}
441448
>
442449
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
443-
{selectedFile ? (
450+
{canShowPreview ? (
444451
<>
445452
{getFileIcon(selectedFile, 'sm')}
446453
<span className='truncate font-normal'>{selectedFile.name}</span>
@@ -460,7 +467,7 @@ export function GoogleDrivePicker({
460467
</Button>
461468

462469
{/* File preview */}
463-
{showPreview && selectedFile && (
470+
{canShowPreview && (
464471
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
465472
<div className='absolute top-2 right-2'>
466473
<Button

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/microsoft-file-selector.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,13 @@ export function MicrosoftFileSelector({
727727
})
728728
: availableFiles
729729

730+
const canShowPreview = !!(
731+
showPreview &&
732+
selectedFile &&
733+
selectedFileId &&
734+
selectedFile.id === selectedFileId
735+
)
736+
730737
return (
731738
<>
732739
<div className='space-y-2'>
@@ -750,7 +757,7 @@ export function MicrosoftFileSelector({
750757
}
751758
>
752759
<div className='flex min-w-0 items-center gap-2 overflow-hidden'>
753-
{selectedFile ? (
760+
{canShowPreview ? (
754761
<>
755762
{getFileIcon(selectedFile, 'sm')}
756763
<span className='truncate font-normal'>{selectedFile.name}</span>
@@ -925,7 +932,7 @@ export function MicrosoftFileSelector({
925932
</Popover>
926933

927934
{/* File preview */}
928-
{showPreview && selectedFile && (
935+
{canShowPreview && (
929936
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
930937
<div className='absolute top-2 right-2'>
931938
<Button

0 commit comments

Comments
 (0)