Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions apps/sim/app/api/auth/oauth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ interface AccountInsertData {
accessTokenExpiresAt?: Date
}

async function resolveOAuthAccountId(
/**
* Resolves a credential ID to its underlying account ID.
* If `credentialId` matches a `credential` row, returns its `accountId` and `workspaceId`.
* Otherwise assumes `credentialId` is already a raw `account.id` (legacy).
*/
export async function resolveOAuthAccountId(
credentialId: string
): Promise<{ accountId: string; usedCredentialTable: boolean } | null> {
): Promise<{ accountId: string; workspaceId?: string; usedCredentialTable: boolean } | null> {
const [credentialRow] = await db
.select({
type: credential.type,
accountId: credential.accountId,
workspaceId: credential.workspaceId,
})
.from(credential)
.where(eq(credential.id, credentialId))
Expand All @@ -41,7 +47,11 @@ async function resolveOAuthAccountId(
if (credentialRow.type !== 'oauth' || !credentialRow.accountId) {
return null
}
return { accountId: credentialRow.accountId, usedCredentialTable: true }
return {
accountId: credentialRow.accountId,
workspaceId: credentialRow.workspaceId,
usedCredentialTable: true,
}
}

return { accountId: credentialId, usedCredentialTable: false }
Expand Down
41 changes: 29 additions & 12 deletions apps/sim/app/api/auth/oauth/wealthbox/item/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateEnum, validatePathSegment } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -57,24 +57,41 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: itemIdValidation.error }, { status: 400 })
}

const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}

if (resolved.workspaceId) {
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
const perm = await getUserEntityPermissions(
session.user.id,
'workspace',
resolved.workspaceId
)
if (perm === null) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}

const credentials = await db
.select()
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)

if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}

const credential = credentials[0]
const accountRow = credentials[0]

if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
requestId
)

if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
Expand Down
44 changes: 29 additions & 15 deletions apps/sim/app/api/auth/oauth/wealthbox/items/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -47,27 +47,41 @@ export async function GET(request: NextRequest) {
)
}

// Get the credential from the database
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}

if (resolved.workspaceId) {
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
const perm = await getUserEntityPermissions(
session.user.id,
'workspace',
resolved.workspaceId
)
if (perm === null) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}

const credentials = await db
.select()
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)

if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}

const credential = credentials[0]

// Check if the credential belongs to the user
if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
const accountRow = credentials[0]

// Refresh access token if needed
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
requestId
)

if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
Expand Down
25 changes: 17 additions & 8 deletions apps/sim/app/api/cron/renew-subscriptions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@ import { createLogger } from '@sim/logger'
import { and, eq, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'

const logger = createLogger('TeamsSubscriptionRenewal')

async function getCredentialOwnerUserId(credentialId: string): Promise<string | null> {
async function getCredentialOwner(
credentialId: string
): Promise<{ userId: string; accountId: string } | null> {
const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
logger.error(`Failed to resolve OAuth account for credential ${credentialId}`)
return null
}
const [credentialRecord] = await db
.select({ userId: account.userId })
.from(account)
.where(eq(account.id, credentialId))
.where(eq(account.id, resolved.accountId))
.limit(1)

return credentialRecord?.userId ?? null
return credentialRecord
? { userId: credentialRecord.userId, accountId: resolved.accountId }
: null
}

/**
Expand Down Expand Up @@ -88,17 +97,17 @@ export async function GET(request: NextRequest) {
continue
}

const credentialOwnerUserId = await getCredentialOwnerUserId(credentialId)
if (!credentialOwnerUserId) {
const credentialOwner = await getCredentialOwner(credentialId)
if (!credentialOwner) {
logger.error(`Credential owner not found for credential ${credentialId}`)
totalFailed++
continue
}

// Get fresh access token
const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
credentialOwnerUserId,
credentialOwner.accountId,
credentialOwner.userId,
`renewal-${webhook.id}`
)

Expand Down
11 changes: 8 additions & 3 deletions apps/sim/app/api/providers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
import type { StreamingExecution } from '@/executor/types'
import { executeProviderRequest } from '@/providers'

Expand Down Expand Up @@ -360,15 +360,20 @@ function sanitizeObject(obj: any): any {
async function resolveVertexCredential(requestId: string, credentialId: string): Promise<string> {
logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`)

const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
throw new Error(`Vertex AI credential not found: ${credentialId}`)
}

const credential = await db.query.account.findFirst({
where: eq(account.id, credentialId),
where: eq(account.id, resolved.accountId),
})

if (!credential) {
throw new Error(`Vertex AI credential not found: ${credentialId}`)
}

const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, resolved.accountId)

if (!accessToken) {
throw new Error('Failed to get Vertex AI access token')
Expand Down
33 changes: 27 additions & 6 deletions apps/sim/app/api/tools/gmail/label/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -41,24 +41,45 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: labelIdValidation.error }, { status: 400 })
}

const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}

if (resolved.workspaceId) {
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
const perm = await getUserEntityPermissions(
session.user.id,
'workspace',
resolved.workspaceId
)
if (perm === null) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}

const credentials = await db
.select()
.from(account)
.where(and(eq(account.id, credentialId), eq(account.userId, session.user.id)))
.where(eq(account.id, resolved.accountId))
.limit(1)

if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}

const credential = credentials[0]
const accountRow = credentials[0]

logger.info(
`[${requestId}] Using credential: ${credential.id}, provider: ${credential.providerId}`
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
)

const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
requestId
)

if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
Expand Down
42 changes: 30 additions & 12 deletions apps/sim/app/api/tools/gmail/labels/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'

const logger = createLogger('GmailLabelsAPI')
Expand Down Expand Up @@ -45,27 +45,45 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
}

let credentials = await db
const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}

if (resolved.workspaceId) {
const { getUserEntityPermissions } = await import('@/lib/workspaces/permissions/utils')
const perm = await getUserEntityPermissions(
session.user.id,
'workspace',
resolved.workspaceId
)
if (perm === null) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}

const credentials = await db
.select()
.from(account)
.where(and(eq(account.id, credentialId), eq(account.userId, session.user.id)))
.where(eq(account.id, resolved.accountId))
.limit(1)

if (!credentials.length) {
credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
logger.warn(`[${requestId}] Credential not found`)
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}

const credential = credentials[0]
const accountRow = credentials[0]

logger.info(
`[${requestId}] Using credential: ${credential.id}, provider: ${credential.providerId}`
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
)

const accessToken = await refreshAccessTokenIfNeeded(credentialId, credential.userId, requestId)
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
accountRow.userId,
requestId
)

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