Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
7 changes: 6 additions & 1 deletion apps/sim/app/api/auth/oauth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ 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`.
* Otherwise assumes `credentialId` is already a raw `account.id` (legacy).
*/
export async function resolveOAuthAccountId(
Comment thread
icecrasher321 marked this conversation as resolved.
credentialId: string
): Promise<{ accountId: string; usedCredentialTable: boolean } | null> {
const [credentialRow] = await db
Expand Down
19 changes: 16 additions & 3 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,7 +57,16 @@ 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 })
}

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 })
Expand All @@ -74,7 +83,11 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
Comment thread
icecrasher321 marked this conversation as resolved.
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
session.user.id,
requestId
)

if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
Expand Down
22 changes: 16 additions & 6 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,8 +47,16 @@ 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 })
}

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 })
Expand All @@ -57,7 +65,6 @@ export async function GET(request: NextRequest) {

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,
Expand All @@ -66,8 +73,11 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}

// Refresh access token if needed
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
const accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
session.user.id,
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
21 changes: 15 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,33 @@ 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 })
}

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
)
Comment thread
icecrasher321 marked this conversation as resolved.
Comment thread
icecrasher321 marked this conversation as resolved.

if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
Expand Down
30 changes: 18 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,33 @@ 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 })
}

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
)
Comment thread
icecrasher321 marked this conversation as resolved.

if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
Expand Down
29 changes: 17 additions & 12 deletions apps/sim/app/api/tools/microsoft_planner/tasks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
import type { PlannerTask } from '@/tools/microsoft_planner/types'

const logger = createLogger('MicrosoftPlannerTasksAPI')
Expand Down Expand Up @@ -42,24 +42,29 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: planIdValidation.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 })
}

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]

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]

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
28 changes: 17 additions & 11 deletions apps/sim/app/api/tools/onedrive/files/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
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 @@ -45,22 +45,28 @@ export async function GET(request: NextRequest) {

logger.info(`[${requestId}] Fetching credential`, { credentialId })

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

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

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`)
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
Expand Down
Loading