Skip to content

Commit 620ce97

Browse files
improvement(selectors): consolidate all integration selectors to use the combobox (#2020)
* improvement(selectors): consolidate all integration selectors to use the combobox * improved credential selector and file-upload styling to use emcn combobox * update mcp subblocks to use emcn components, delete unused mcp server modal * fix filterOptions change * fix project selector * attempted jira fix * fix gdrive inf calls * rewrite credential selector * fix docs * fix onedrive folder * fix * fix * fix excel cred fetch * fix excel part 2 --------- Co-authored-by: waleed <[email protected]>
1 parent 25ac917 commit 620ce97

File tree

41 files changed

+2311
-9577
lines changed

Some content is hidden

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

41 files changed

+2311
-9577
lines changed

apps/sim/app/api/auth/oauth/microsoft/file/route.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import { db } from '@sim/db'
2-
import { account } from '@sim/db/schema'
3-
import { eq } from 'drizzle-orm'
41
import { type NextRequest, NextResponse } from 'next/server'
5-
import { getSession } from '@/lib/auth'
2+
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
63
import { createLogger } from '@/lib/logs/console/logger'
74
import { validateMicrosoftGraphId } from '@/lib/security/input-validation'
85
import { generateRequestId } from '@/lib/utils'
9-
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
6+
import { getCredential, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
107

118
export const dynamic = 'force-dynamic'
129

@@ -15,15 +12,10 @@ const logger = createLogger('MicrosoftFileAPI')
1512
export async function GET(request: NextRequest) {
1613
const requestId = generateRequestId()
1714
try {
18-
const session = await getSession()
19-
20-
if (!session?.user?.id) {
21-
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
22-
}
23-
2415
const { searchParams } = new URL(request.url)
2516
const credentialId = searchParams.get('credentialId')
2617
const fileId = searchParams.get('fileId')
18+
const workflowId = searchParams.get('workflowId') || undefined
2719

2820
if (!credentialId || !fileId) {
2921
return NextResponse.json({ error: 'Credential ID and File ID are required' }, { status: 400 })
@@ -35,19 +27,27 @@ export async function GET(request: NextRequest) {
3527
return NextResponse.json({ error: fileIdValidation.error }, { status: 400 })
3628
}
3729

38-
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
30+
const authz = await authorizeCredentialUse(request, {
31+
credentialId,
32+
workflowId,
33+
requireWorkflowIdForInternal: false,
34+
})
3935

40-
if (!credentials.length) {
41-
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
36+
if (!authz.ok || !authz.credentialOwnerUserId) {
37+
const status = authz.error === 'Credential not found' ? 404 : 403
38+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status })
4239
}
4340

44-
const credential = credentials[0]
45-
46-
if (credential.userId !== session.user.id) {
47-
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
41+
const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId)
42+
if (!credential) {
43+
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
4844
}
4945

50-
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
46+
const accessToken = await refreshAccessTokenIfNeeded(
47+
credentialId,
48+
authz.credentialOwnerUserId,
49+
requestId
50+
)
5151

5252
if (!accessToken) {
5353
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })

apps/sim/app/api/auth/oauth/microsoft/files/route.ts

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import { db } from '@sim/db'
2-
import { account } from '@sim/db/schema'
3-
import { eq } from 'drizzle-orm'
41
import { type NextRequest, NextResponse } from 'next/server'
5-
import { getSession } from '@/lib/auth'
2+
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
63
import { createLogger } from '@/lib/logs/console/logger'
74
import { generateRequestId } from '@/lib/utils'
8-
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
5+
import { getCredential, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
96

107
export const dynamic = 'force-dynamic'
118

@@ -18,46 +15,39 @@ export async function GET(request: NextRequest) {
1815
const requestId = generateRequestId()
1916

2017
try {
21-
// Get the session
22-
const session = await getSession()
23-
24-
// Check if the user is authenticated
25-
if (!session?.user?.id) {
26-
logger.warn(`[${requestId}] Unauthenticated request rejected`)
27-
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
28-
}
29-
3018
// Get the credential ID from the query params
3119
const { searchParams } = new URL(request.url)
3220
const credentialId = searchParams.get('credentialId')
3321
const query = searchParams.get('query') || ''
22+
const workflowId = searchParams.get('workflowId') || undefined
3423

3524
if (!credentialId) {
3625
logger.warn(`[${requestId}] Missing credential ID`)
3726
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
3827
}
3928

40-
// Get the credential from the database
41-
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
29+
const authz = await authorizeCredentialUse(request, {
30+
credentialId,
31+
workflowId,
32+
requireWorkflowIdForInternal: false,
33+
})
4234

43-
if (!credentials.length) {
44-
logger.warn(`[${requestId}] Credential not found`, { credentialId })
45-
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
35+
if (!authz.ok || !authz.credentialOwnerUserId) {
36+
const status = authz.error === 'Credential not found' ? 404 : 403
37+
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status })
4638
}
4739

48-
const credential = credentials[0]
49-
50-
// Check if the credential belongs to the user
51-
if (credential.userId !== session.user.id) {
52-
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
53-
credentialUserId: credential.userId,
54-
requestUserId: session.user.id,
55-
})
56-
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
40+
const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId)
41+
if (!credential) {
42+
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
5743
}
5844

5945
// Refresh access token if needed using the utility function
60-
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
46+
const accessToken = await refreshAccessTokenIfNeeded(
47+
credentialId,
48+
authz.credentialOwnerUserId,
49+
requestId
50+
)
6151

6252
if (!accessToken) {
6353
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
'use client'
22

3-
import { useEffect, useRef, useState } from 'react'
3+
import { useEffect, useMemo, useState } from 'react'
44
import { useParams } from 'next/navigation'
55
import { Tooltip } from '@/components/emcn'
6-
import {
7-
type SlackChannelInfo,
8-
SlackChannelSelector,
9-
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/channel-selector/components/slack-channel-selector'
6+
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
107
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-depends-on-gate'
118
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-foreign-credential'
129
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value'
1310
import type { SubBlockConfig } from '@/blocks/types'
11+
import type { SelectorContext } from '@/hooks/selectors/types'
1412

1513
interface ChannelSelectorInputProps {
1614
blockId: string
@@ -41,14 +39,12 @@ export function ChannelSelectorInput({
4139
const effectiveAuthMethod = previewContextValues?.authMethod ?? authMethod
4240
const effectiveBotToken = previewContextValues?.botToken ?? botToken
4341
const effectiveCredential = previewContextValues?.credential ?? connectedCredential
44-
const [selectedChannelId, setSelectedChannelId] = useState<string>('')
45-
const [_channelInfo, setChannelInfo] = useState<SlackChannelInfo | null>(null)
42+
const [_channelInfo, setChannelInfo] = useState<string | null>(null)
4643

47-
// Get provider-specific values
4844
const provider = subBlock.provider || 'slack'
4945
const isSlack = provider === 'slack'
5046
// Central dependsOn gating
51-
const { finalDisabled, dependsOn, dependencyValues } = useDependsOnGate(blockId, subBlock, {
47+
const { finalDisabled, dependsOn } = useDependsOnGate(blockId, subBlock, {
5248
disabled,
5349
isPreview,
5450
previewContextValues,
@@ -69,70 +65,60 @@ export function ChannelSelectorInput({
6965
// Get the current value from the store or prop value if in preview mode (same pattern as file-selector)
7066
useEffect(() => {
7167
const val = isPreview && previewValue !== undefined ? previewValue : storeValue
72-
if (val && typeof val === 'string') {
73-
setSelectedChannelId(val)
68+
if (typeof val === 'string') {
69+
setChannelInfo(val)
7470
}
7571
}, [isPreview, previewValue, storeValue])
7672

77-
// Clear channel when any declared dependency changes (e.g., authMethod/credential)
78-
const prevDepsSigRef = useRef<string>('')
79-
useEffect(() => {
80-
if (dependsOn.length === 0) return
81-
const currentSig = JSON.stringify(dependencyValues)
82-
if (prevDepsSigRef.current && prevDepsSigRef.current !== currentSig) {
83-
if (!isPreview) {
84-
setSelectedChannelId('')
85-
setChannelInfo(null)
86-
setStoreValue('')
87-
}
88-
}
89-
prevDepsSigRef.current = currentSig
90-
}, [dependsOn, dependencyValues, isPreview, setStoreValue])
73+
const requiresCredential = dependsOn.includes('credential')
74+
const missingCredential = !credential || credential.trim().length === 0
75+
const shouldForceDisable = requiresCredential && (missingCredential || isForeignCredential)
9176

92-
// Handle channel selection (same pattern as file-selector)
93-
const handleChannelChange = (channelId: string, info?: SlackChannelInfo) => {
94-
setSelectedChannelId(channelId)
95-
setChannelInfo(info || null)
96-
if (!isPreview) {
97-
setStoreValue(channelId)
98-
}
99-
onChannelSelect?.(channelId)
100-
}
77+
const context: SelectorContext = useMemo(
78+
() => ({
79+
credentialId: credential,
80+
workflowId: workflowIdFromUrl,
81+
}),
82+
[credential, workflowIdFromUrl]
83+
)
10184

102-
// Render Slack channel selector
103-
if (isSlack) {
85+
if (!isSlack) {
10486
return (
10587
<Tooltip.Root>
10688
<Tooltip.Trigger asChild>
107-
<div className='w-full'>
108-
<SlackChannelSelector
109-
value={selectedChannelId}
110-
onChange={(channelId: string, channelInfo?: SlackChannelInfo) => {
111-
handleChannelChange(channelId, channelInfo)
112-
}}
113-
credential={credential}
114-
label={subBlock.placeholder || 'Select Slack channel'}
115-
disabled={finalDisabled}
116-
workflowId={workflowIdFromUrl}
117-
isForeignCredential={isForeignCredential}
118-
/>
89+
<div className='w-full rounded border border-dashed p-4 text-center text-muted-foreground text-sm'>
90+
Channel selector not supported for provider: {provider}
11991
</div>
12092
</Tooltip.Trigger>
93+
<Tooltip.Content side='top'>
94+
<p>This channel selector is not yet implemented for {provider}</p>
95+
</Tooltip.Content>
12196
</Tooltip.Root>
12297
)
12398
}
12499

125-
// Default fallback for unsupported providers
126100
return (
127101
<Tooltip.Root>
128102
<Tooltip.Trigger asChild>
129-
<div className='w-full rounded border border-dashed p-4 text-center text-muted-foreground text-sm'>
130-
Channel selector not supported for provider: {provider}
103+
<div className='w-full'>
104+
<SelectorCombobox
105+
blockId={blockId}
106+
subBlock={subBlock}
107+
selectorKey='slack.channels'
108+
selectorContext={context}
109+
disabled={finalDisabled || shouldForceDisable || isForeignCredential}
110+
isPreview={isPreview}
111+
previewValue={previewValue ?? null}
112+
placeholder={subBlock.placeholder || 'Select Slack channel'}
113+
onOptionChange={(value) => {
114+
setChannelInfo(value)
115+
if (!isPreview) {
116+
onChannelSelect?.(value)
117+
}
118+
}}
119+
/>
131120
</div>
132121
</Tooltip.Trigger>
133-
<Tooltip.Content side='top'>
134-
<p>This channel selector is not yet implemented for {provider}</p>
135-
</Tooltip.Content>
136122
</Tooltip.Root>
137123
)
138124
}

0 commit comments

Comments
 (0)