Skip to content

Commit f0f31d8

Browse files
committed
make trigger creds collab supported
1 parent 4cf3f51 commit f0f31d8

5 files changed

Lines changed: 142 additions & 57 deletions

File tree

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,8 @@ 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
333-
const success = await configureGmailPolling(workflowRecord.userId, savedWebhook, requestId)
332+
// Strict: utils will resolve credential ownership; do not fall back to workflow owner
333+
const success = await configureGmailPolling('', savedWebhook, requestId)
334334

335335
if (!success) {
336336
logger.error(`[${requestId}] Failed to configure Gmail polling`)
@@ -364,12 +364,8 @@ 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
368-
const success = await configureOutlookPolling(
369-
workflowRecord.userId,
370-
savedWebhook,
371-
requestId
372-
)
367+
// Strict: utils will resolve credential ownership; do not fall back to workflow owner
368+
const success = await configureOutlookPolling('', savedWebhook, requestId)
373369

374370
if (!success) {
375371
logger.error(`[${requestId}] Failed to configure Outlook polling`)

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ export function TriggerConfig({
172172
// Map trigger ID to webhook provider name
173173
const webhookProvider = effectiveTriggerId.replace(/_webhook|_poller$/, '') // e.g., 'slack_webhook' -> 'slack', 'gmail_poller' -> 'gmail'
174174

175+
// Include selected credential from the modal (if any)
176+
const selectedCredentialId =
177+
(useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as string | null) ||
178+
null
179+
175180
// For credential-based triggers (like Gmail), create webhook entry for polling service but no webhook URL
176181
if (triggerDef.requiresCredentials && !triggerDef.webhook) {
177182
// Gmail polling service requires a webhook database entry to find the configuration
@@ -185,7 +190,10 @@ export function TriggerConfig({
185190
blockId,
186191
path: '', // Empty path - API will generate dummy path for Gmail
187192
provider: webhookProvider,
188-
providerConfig: config,
193+
providerConfig: {
194+
...config,
195+
...(selectedCredentialId ? { credentialId: selectedCredentialId } : {}),
196+
},
189197
}),
190198
})
191199

@@ -225,7 +233,10 @@ export function TriggerConfig({
225233
blockId,
226234
path,
227235
provider: webhookProvider,
228-
providerConfig: config,
236+
providerConfig: {
237+
...config,
238+
...(selectedCredentialId ? { credentialId: selectedCredentialId } : {}),
239+
},
229240
}),
230241
})
231242

apps/sim/lib/webhooks/gmail-polling-service.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { nanoid } from 'nanoid'
33
import { Logger } from '@/lib/logs/console/logger'
44
import { hasProcessedMessage, markMessageAsProcessed } from '@/lib/redis'
55
import { getBaseUrl } from '@/lib/urls/utils'
6-
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
6+
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
77
import { db } from '@/db'
8-
import { webhook } from '@/db/schema'
8+
import { account, webhook } from '@/db/schema'
99

1010
const logger = new Logger('GmailPollingService')
1111

@@ -81,20 +81,30 @@ export async function pollGmailWebhooks() {
8181
const requestId = nanoid()
8282

8383
try {
84-
// Extract user ID from webhook metadata if available
84+
// Extract metadata
8585
const metadata = webhookData.providerConfig as any
86-
const userId = metadata?.userId
86+
const credentialId: string | undefined = metadata?.credentialId
8787

88-
if (!userId) {
89-
logger.error(`[${requestId}] No user ID found for webhook ${webhookId}`)
90-
return { success: false, webhookId, error: 'No user ID' }
88+
if (!credentialId) {
89+
logger.error(`[${requestId}] Missing credentialId for webhook ${webhookId}`)
90+
return { success: false, webhookId, error: 'Missing credentialId' }
9191
}
9292

93-
// Get OAuth token for Gmail API
94-
const accessToken = await getOAuthToken(userId, 'google-email')
93+
// Resolve owner and token strictly via credentialId
94+
const rows = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
95+
if (rows.length === 0) {
96+
logger.error(
97+
`[${requestId}] Credential ${credentialId} not found for webhook ${webhookId}`
98+
)
99+
return { success: false, webhookId, error: 'Credential not found' }
100+
}
101+
const ownerUserId = rows[0].userId
95102

103+
const accessToken = await refreshAccessTokenIfNeeded(credentialId, ownerUserId, requestId)
96104
if (!accessToken) {
97-
logger.error(`[${requestId}] Failed to get Gmail access token for webhook ${webhookId}`)
105+
logger.error(
106+
`[${requestId}] Failed to get Gmail access token for webhook ${webhookId} via credential ${credentialId}`
107+
)
98108
return { success: false, webhookId, error: 'No access token' }
99109
}
100110

apps/sim/lib/webhooks/outlook-polling-service.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { nanoid } from 'nanoid'
33
import { Logger } from '@/lib/logs/console/logger'
44
import { hasProcessedMessage, markMessageAsProcessed } from '@/lib/redis'
55
import { getBaseUrl } from '@/lib/urls/utils'
6-
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
6+
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
77
import { db } from '@/db'
8-
import { webhook } from '@/db/schema'
8+
import { account, webhook } from '@/db/schema'
99

1010
const logger = new Logger('OutlookPollingService')
1111

@@ -108,28 +108,30 @@ export async function pollOutlookWebhooks() {
108108
try {
109109
logger.info(`[${requestId}] Processing Outlook webhook: ${webhookId}`)
110110

111-
// Extract user ID from webhook metadata if available
111+
// Extract credentialId from providerConfig (strict)
112112
const metadata = webhookData.providerConfig as any
113-
const userId = metadata?.userId
113+
const credentialId: string | undefined = metadata?.credentialId
114114

115-
// Debug: Webhook metadata extraction
116-
logger.debug(
117-
`[${requestId}] Webhook ${webhookId} providerConfig:`,
118-
JSON.stringify(metadata, null, 2)
119-
)
120-
logger.debug(`[${requestId}] Extracted userId:`, userId)
121-
122-
if (!userId) {
123-
logger.error(`[${requestId}] No user ID found for webhook ${webhookId}`)
124-
logger.debug(`[${requestId}] No userId found in providerConfig for webhook ${webhookId}`)
125-
return { success: false, webhookId, error: 'No user ID' }
115+
if (!credentialId) {
116+
logger.error(`[${requestId}] Missing credentialId for webhook ${webhookId}`)
117+
return { success: false, webhookId, error: 'Missing credentialId' }
126118
}
127119

128-
// Get OAuth token for Outlook API
129-
const accessToken = await getOAuthToken(userId, 'outlook')
120+
// Resolve owner and token via credentialId
121+
const rows = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
122+
if (!rows.length) {
123+
logger.error(
124+
`[${requestId}] Credential ${credentialId} not found for webhook ${webhookId}`
125+
)
126+
return { success: false, webhookId, error: 'Credential not found' }
127+
}
128+
const ownerUserId = rows[0].userId
130129

130+
const accessToken = await refreshAccessTokenIfNeeded(credentialId, ownerUserId, requestId)
131131
if (!accessToken) {
132-
logger.error(`[${requestId}] Failed to get Outlook access token for webhook ${webhookId}`)
132+
logger.error(
133+
`[${requestId}] Failed to get Outlook access token for webhook ${webhookId} via credential ${credentialId}`
134+
)
133135
return { success: false, webhookId, error: 'No access token' }
134136
}
135137

apps/sim/lib/webhooks/utils.ts

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { and, eq } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { createLogger } from '@/lib/logs/console/logger'
4-
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
4+
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
55
import { db } from '@/db'
6-
import { webhook } from '@/db/schema'
6+
import { account, webhook } from '@/db/schema'
77

88
const logger = createLogger('WebhookUtils')
99

@@ -860,6 +860,31 @@ export async function fetchAndProcessAirtablePayloads(
860860
return // Exit early
861861
}
862862

863+
// Require credentialId
864+
const credentialId: string | undefined = localProviderConfig.credentialId
865+
if (!credentialId) {
866+
logger.error(
867+
`[${requestId}] Missing credentialId in providerConfig for Airtable webhook ${webhookData.id}.`
868+
)
869+
return
870+
}
871+
872+
// Resolve owner and access token strictly via credentialId (no fallback)
873+
let ownerUserId: string | null = null
874+
try {
875+
const rows = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
876+
ownerUserId = rows.length ? rows[0].userId : null
877+
} catch (_e) {
878+
ownerUserId = null
879+
}
880+
881+
if (!ownerUserId) {
882+
logger.error(
883+
`[${requestId}] Could not resolve owner for Airtable credential ${credentialId} on webhook ${webhookData.id}`
884+
)
885+
return
886+
}
887+
863888
// --- Retrieve Stored Cursor from localProviderConfig ---
864889
const storedCursor = localProviderConfig.externalWebhookCursor
865890

@@ -908,26 +933,25 @@ export async function fetchAndProcessAirtablePayloads(
908933
)
909934
}
910935

911-
// --- Get OAuth Token ---
936+
// --- Get OAuth Token (strict via credentialId) ---
912937
let accessToken: string | null = null
913938
try {
914-
accessToken = await getOAuthToken(workflowData.userId, 'airtable')
939+
accessToken = await refreshAccessTokenIfNeeded(credentialId, ownerUserId, requestId)
915940
if (!accessToken) {
916941
logger.error(
917-
`[${requestId}] Failed to obtain valid Airtable access token. Cannot proceed.`,
918-
{ userId: workflowData.userId }
942+
`[${requestId}] Failed to obtain valid Airtable access token via credential ${credentialId}.`
919943
)
920944
throw new Error('Airtable access token not found.')
921945
}
922946

923947
logger.info(`[${requestId}] Successfully obtained Airtable access token`)
924948
} catch (tokenError: any) {
925949
logger.error(
926-
`[${requestId}] Failed to get Airtable OAuth token for user ${workflowData.userId}`,
950+
`[${requestId}] Failed to get Airtable OAuth token for credential ${credentialId}`,
927951
{
928952
error: tokenError.message,
929953
stack: tokenError.stack,
930-
userId: workflowData.userId,
954+
credentialId,
931955
}
932956
)
933957
// Error logging handled by logging session
@@ -1102,7 +1126,7 @@ export async function fetchAndProcessAirtablePayloads(
11021126

11031127
// DEBUG: Log totals for this batch
11041128
logger.debug(
1105-
`[${requestId}] TRACE: Processed ${changeCount} changes in API call ${apiCallCount}`,
1129+
`[${requestId}] TRACE: Processed ${changeCount} changes in API call ${apiCallCount})`,
11061130
{
11071131
currentMapSize: consolidatedChangesMap.size,
11081132
}
@@ -1257,13 +1281,33 @@ export async function configureGmailPolling(
12571281
logger.info(`[${requestId}] Setting up Gmail polling for webhook ${webhookData.id}`)
12581282

12591283
try {
1260-
const accessToken = await getOAuthToken(userId, 'google-email')
1261-
if (!accessToken) {
1262-
logger.error(`[${requestId}] Failed to retrieve Gmail access token for user ${userId}`)
1284+
const providerConfig = (webhookData.providerConfig as Record<string, any>) || {}
1285+
1286+
const credentialId: string | undefined = providerConfig.credentialId
1287+
if (!credentialId) {
1288+
logger.error(
1289+
`[${requestId}] Missing credentialId for Gmail webhook ${webhookData.id}. Refusing to proceed.`
1290+
)
12631291
return false
12641292
}
12651293

1266-
const providerConfig = (webhookData.providerConfig as Record<string, any>) || {}
1294+
// Resolve owner user ID from the credential and refresh token if needed
1295+
const rows = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
1296+
if (rows.length === 0) {
1297+
logger.error(
1298+
`[${requestId}] Credential ${credentialId} not found for Gmail webhook ${webhookData.id}`
1299+
)
1300+
return false
1301+
}
1302+
const ownerUserId = rows[0].userId
1303+
1304+
const accessToken = await refreshAccessTokenIfNeeded(credentialId, ownerUserId, requestId)
1305+
if (!accessToken) {
1306+
logger.error(
1307+
`[${requestId}] Failed to refresh/access Gmail token for credential ${credentialId}`
1308+
)
1309+
return false
1310+
}
12671311

12681312
const maxEmailsPerPoll =
12691313
typeof providerConfig.maxEmailsPerPoll === 'string'
@@ -1282,7 +1326,8 @@ export async function configureGmailPolling(
12821326
.set({
12831327
providerConfig: {
12841328
...providerConfig,
1285-
userId, // Store user ID for OAuth access during polling
1329+
userId: ownerUserId,
1330+
credentialId,
12861331
maxEmailsPerPoll,
12871332
pollingInterval,
12881333
markAsRead: providerConfig.markAsRead || false,
@@ -1323,13 +1368,33 @@ export async function configureOutlookPolling(
13231368
logger.info(`[${requestId}] Setting up Outlook polling for webhook ${webhookData.id}`)
13241369

13251370
try {
1326-
const accessToken = await getOAuthToken(userId, 'outlook')
1327-
if (!accessToken) {
1328-
logger.error(`[${requestId}] Failed to retrieve Outlook access token for user ${userId}`)
1371+
const providerConfig = (webhookData.providerConfig as Record<string, any>) || {}
1372+
1373+
const credentialId: string | undefined = providerConfig.credentialId
1374+
if (!credentialId) {
1375+
logger.error(
1376+
`[${requestId}] Missing credentialId for Outlook webhook ${webhookData.id}. Refusing to proceed.`
1377+
)
13291378
return false
13301379
}
13311380

1332-
const providerConfig = (webhookData.providerConfig as Record<string, any>) || {}
1381+
// Resolve owner user ID from the credential and refresh token if needed
1382+
const rows = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
1383+
if (rows.length === 0) {
1384+
logger.error(
1385+
`[${requestId}] Credential ${credentialId} not found for Outlook webhook ${webhookData.id}`
1386+
)
1387+
return false
1388+
}
1389+
const ownerUserId = rows[0].userId
1390+
1391+
const accessToken = await refreshAccessTokenIfNeeded(credentialId, ownerUserId, requestId)
1392+
if (!accessToken) {
1393+
logger.error(
1394+
`[${requestId}] Failed to refresh/access Outlook token for credential ${credentialId}`
1395+
)
1396+
return false
1397+
}
13331398

13341399
const maxEmailsPerPoll =
13351400
typeof providerConfig.maxEmailsPerPoll === 'string'
@@ -1348,7 +1413,8 @@ export async function configureOutlookPolling(
13481413
.set({
13491414
providerConfig: {
13501415
...providerConfig,
1351-
userId, // Store user ID for OAuth access during polling
1416+
userId: ownerUserId,
1417+
credentialId,
13521418
maxEmailsPerPoll,
13531419
pollingInterval,
13541420
markAsRead: providerConfig.markAsRead || false,

0 commit comments

Comments
 (0)