mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Refresh creds on typing in impersonated email
This commit is contained in:
@@ -308,17 +308,34 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes an OAuth token if needed based on credential information
|
||||
* Refreshes an OAuth token if needed based on credential information.
|
||||
* Also handles service account credentials by generating a JWT-based token.
|
||||
* @param credentialId The ID of the credential to check and potentially refresh
|
||||
* @param userId The user ID who owns the credential (for security verification)
|
||||
* @param requestId Request ID for log correlation
|
||||
* @param scopes Optional scopes for service account token generation
|
||||
* @returns The valid access token or null if refresh fails
|
||||
*/
|
||||
export async function refreshAccessTokenIfNeeded(
|
||||
credentialId: string,
|
||||
userId: string,
|
||||
requestId: string
|
||||
requestId: string,
|
||||
scopes?: string[],
|
||||
impersonateEmail?: string
|
||||
): Promise<string | null> {
|
||||
const resolved = await resolveOAuthAccountId(credentialId)
|
||||
if (!resolved) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
|
||||
const effectiveScopes = scopes?.length
|
||||
? scopes
|
||||
: ['https://www.googleapis.com/auth/cloud-platform']
|
||||
logger.info(`[${requestId}] Using service account token for credential`)
|
||||
return getServiceAccountToken(resolved.credentialId, effectiveScopes, impersonateEmail)
|
||||
}
|
||||
|
||||
// Get the credential directly using the getCredential helper
|
||||
const credential = await getCredential(requestId, credentialId, userId)
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ 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, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
import {
|
||||
getServiceAccountToken,
|
||||
refreshTokenIfNeeded,
|
||||
resolveOAuthAccountId,
|
||||
} from '@/app/api/auth/oauth/utils'
|
||||
import type { StreamingExecution } from '@/executor/types'
|
||||
import { executeProviderRequest } from '@/providers'
|
||||
|
||||
@@ -365,6 +369,14 @@ async function resolveVertexCredential(requestId: string, credentialId: string):
|
||||
throw new Error(`Vertex AI credential not found: ${credentialId}`)
|
||||
}
|
||||
|
||||
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
|
||||
const accessToken = await getServiceAccountToken(resolved.credentialId, [
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
])
|
||||
logger.info(`[${requestId}] Successfully resolved Vertex AI service account credential`)
|
||||
return accessToken
|
||||
}
|
||||
|
||||
const credential = await db.query.account.findFirst({
|
||||
where: eq(account.id, resolved.accountId),
|
||||
})
|
||||
|
||||
@@ -26,6 +26,7 @@ export async function GET(request: NextRequest) {
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const fileId = searchParams.get('fileId')
|
||||
const workflowId = searchParams.get('workflowId') || undefined
|
||||
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
|
||||
|
||||
if (!credentialId || !fileId) {
|
||||
logger.warn(`[${requestId}] Missing required parameters`)
|
||||
@@ -46,7 +47,9 @@ export async function GET(request: NextRequest) {
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
requestId,
|
||||
['https://www.googleapis.com/auth/drive'],
|
||||
impersonateEmail
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -85,6 +85,7 @@ export async function GET(request: NextRequest) {
|
||||
const query = searchParams.get('query') || ''
|
||||
const folderId = searchParams.get('folderId') || searchParams.get('parentId') || ''
|
||||
const workflowId = searchParams.get('workflowId') || undefined
|
||||
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
|
||||
|
||||
if (!credentialId) {
|
||||
logger.warn(`[${requestId}] Missing credential ID`)
|
||||
@@ -100,7 +101,9 @@ export async function GET(request: NextRequest) {
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId!,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
requestId,
|
||||
['https://www.googleapis.com/auth/drive'],
|
||||
impersonateEmail
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -6,7 +6,11 @@ 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, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
import {
|
||||
getServiceAccountToken,
|
||||
refreshAccessTokenIfNeeded,
|
||||
resolveOAuthAccountId,
|
||||
} from '@/app/api/auth/oauth/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -26,6 +30,7 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const labelId = searchParams.get('labelId')
|
||||
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
|
||||
|
||||
if (!credentialId || !labelId) {
|
||||
logger.warn(`[${requestId}] Missing required parameters`)
|
||||
@@ -58,29 +63,40 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(eq(account.id, resolved.accountId))
|
||||
.limit(1)
|
||||
let accessToken: string | null = null
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`)
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
|
||||
accessToken = await getServiceAccountToken(
|
||||
resolved.credentialId,
|
||||
['https://www.googleapis.com/auth/gmail.labels'],
|
||||
impersonateEmail
|
||||
)
|
||||
} else {
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.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 accountRow = credentials[0]
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
|
||||
)
|
||||
|
||||
accessToken = await refreshAccessTokenIfNeeded(
|
||||
resolved.accountId,
|
||||
accountRow.userId,
|
||||
requestId,
|
||||
['https://www.googleapis.com/auth/gmail.labels']
|
||||
)
|
||||
}
|
||||
|
||||
const accountRow = credentials[0]
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
|
||||
)
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
resolved.accountId,
|
||||
accountRow.userId,
|
||||
requestId
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ 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, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
import {
|
||||
getServiceAccountToken,
|
||||
refreshAccessTokenIfNeeded,
|
||||
resolveOAuthAccountId,
|
||||
} from '@/app/api/auth/oauth/utils'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('GmailLabelsAPI')
|
||||
@@ -33,6 +37,7 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const query = searchParams.get('query')
|
||||
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
|
||||
|
||||
if (!credentialId) {
|
||||
logger.warn(`[${requestId}] Missing credentialId parameter`)
|
||||
@@ -62,29 +67,40 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(eq(account.id, resolved.accountId))
|
||||
.limit(1)
|
||||
let accessToken: string | null = null
|
||||
|
||||
if (!credentials.length) {
|
||||
logger.warn(`[${requestId}] Credential not found`)
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
|
||||
accessToken = await getServiceAccountToken(
|
||||
resolved.credentialId,
|
||||
['https://www.googleapis.com/auth/gmail.labels'],
|
||||
impersonateEmail
|
||||
)
|
||||
} else {
|
||||
const credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.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 accountRow = credentials[0]
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
|
||||
)
|
||||
|
||||
accessToken = await refreshAccessTokenIfNeeded(
|
||||
resolved.accountId,
|
||||
accountRow.userId,
|
||||
requestId,
|
||||
['https://www.googleapis.com/auth/gmail.labels']
|
||||
)
|
||||
}
|
||||
|
||||
const accountRow = credentials[0]
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Using credential: ${accountRow.id}, provider: ${accountRow.providerId}`
|
||||
)
|
||||
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
resolved.accountId,
|
||||
accountRow.userId,
|
||||
requestId
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, projectId } = body
|
||||
const { credential, workflowId, projectId, impersonateEmail } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
@@ -43,7 +43,9 @@ export async function POST(request: Request) {
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
requestId,
|
||||
['https://www.googleapis.com/auth/bigquery'],
|
||||
impersonateEmail
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId, projectId, datasetId } = body
|
||||
const { credential, workflowId, projectId, datasetId, impersonateEmail } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
@@ -40,7 +40,9 @@ export async function POST(request: Request) {
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
requestId,
|
||||
['https://www.googleapis.com/auth/bigquery'],
|
||||
impersonateEmail
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
|
||||
@@ -28,6 +28,7 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const workflowId = searchParams.get('workflowId') || undefined
|
||||
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
|
||||
|
||||
if (!credentialId) {
|
||||
logger.warn(`[${requestId}] Missing credentialId parameter`)
|
||||
@@ -41,7 +42,9 @@ export async function GET(request: NextRequest) {
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
requestId,
|
||||
['https://www.googleapis.com/auth/calendar'],
|
||||
impersonateEmail
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -40,6 +40,7 @@ export async function GET(request: NextRequest) {
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const spreadsheetId = searchParams.get('spreadsheetId')
|
||||
const workflowId = searchParams.get('workflowId') || undefined
|
||||
const impersonateEmail = searchParams.get('impersonateEmail') || undefined
|
||||
|
||||
if (!credentialId) {
|
||||
logger.warn(`[${requestId}] Missing credentialId parameter`)
|
||||
@@ -59,7 +60,9 @@ export async function GET(request: NextRequest) {
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
requestId,
|
||||
['https://www.googleapis.com/auth/drive'],
|
||||
impersonateEmail
|
||||
)
|
||||
|
||||
if (!accessToken) {
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function POST(request: Request) {
|
||||
const requestId = generateRequestId()
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
const { credential, workflowId, impersonateEmail } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
@@ -30,7 +30,9 @@ export async function POST(request: Request) {
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
requestId,
|
||||
['https://www.googleapis.com/auth/tasks'],
|
||||
impersonateEmail
|
||||
)
|
||||
if (!accessToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { Input as UiInput } from '@/components/ui'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import {
|
||||
clearPendingCredentialCreateRequest,
|
||||
@@ -96,6 +97,7 @@ export function IntegrationsManager() {
|
||||
const [saDescription, setSaDescription] = useState('')
|
||||
const [saError, setSaError] = useState<string | null>(null)
|
||||
const [saIsSubmitting, setSaIsSubmitting] = useState(false)
|
||||
const [saDragActive, setSaDragActive] = useState(false)
|
||||
|
||||
const { data: session } = useSession()
|
||||
const currentUserId = session?.user?.id || ''
|
||||
@@ -689,29 +691,63 @@ export function IntegrationsManager() {
|
||||
}
|
||||
}
|
||||
|
||||
const readSaJsonFile = useCallback(
|
||||
(file: File) => {
|
||||
if (!file.name.endsWith('.json')) {
|
||||
setSaError('Only .json files are supported')
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const text = e.target?.result
|
||||
if (typeof text === 'string') {
|
||||
setSaJsonInput(text)
|
||||
setSaError(null)
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
if (parsed.client_email && !saDisplayName.trim()) {
|
||||
setSaDisplayName(parsed.client_email)
|
||||
}
|
||||
} catch {
|
||||
// validation will catch this on submit
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
},
|
||||
[saDisplayName]
|
||||
)
|
||||
|
||||
const handleSaFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const text = e.target?.result
|
||||
if (typeof text === 'string') {
|
||||
setSaJsonInput(text)
|
||||
setSaError(null)
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
if (parsed.client_email && !saDisplayName.trim()) {
|
||||
setSaDisplayName(parsed.client_email)
|
||||
}
|
||||
} catch {
|
||||
// validation will catch this on submit
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
readSaJsonFile(file)
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
const handleSaDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setSaDragActive(true)
|
||||
}, [])
|
||||
|
||||
const handleSaDragLeave = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setSaDragActive(false)
|
||||
}, [])
|
||||
|
||||
const handleSaDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setSaDragActive(false)
|
||||
const file = event.dataTransfer.files[0]
|
||||
if (file) readSaJsonFile(file)
|
||||
},
|
||||
[readSaJsonFile]
|
||||
)
|
||||
|
||||
const filteredServices = useMemo(() => {
|
||||
if (!serviceSearch.trim()) return oauthServiceOptions
|
||||
const q = serviceSearch.toLowerCase()
|
||||
@@ -963,26 +999,48 @@ export function IntegrationsManager() {
|
||||
<Label>
|
||||
JSON Key<span className='ml-1'>*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
value={saJsonInput}
|
||||
onChange={(event) => {
|
||||
setSaJsonInput(event.target.value)
|
||||
setSaError(null)
|
||||
if (!saDisplayName.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(event.target.value)
|
||||
if (parsed.client_email) setSaDisplayName(parsed.client_email)
|
||||
} catch {
|
||||
// not valid yet
|
||||
<div
|
||||
onDragOver={handleSaDragOver}
|
||||
onDragLeave={handleSaDragLeave}
|
||||
onDrop={handleSaDrop}
|
||||
className={cn(
|
||||
'relative mt-1.5 rounded-md border-2 border-dashed transition-colors',
|
||||
saDragActive
|
||||
? 'border-[var(--accent)] bg-[var(--accent)]/5'
|
||||
: 'border-transparent'
|
||||
)}
|
||||
>
|
||||
{saDragActive && (
|
||||
<div className='pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-md bg-[var(--accent)]/5'>
|
||||
<p className='font-medium text-[13px] text-[var(--accent)]'>
|
||||
Drop JSON key file here
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Textarea
|
||||
value={saJsonInput}
|
||||
onChange={(event) => {
|
||||
setSaJsonInput(event.target.value)
|
||||
setSaError(null)
|
||||
if (!saDisplayName.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(event.target.value)
|
||||
if (parsed.client_email) setSaDisplayName(parsed.client_email)
|
||||
} catch {
|
||||
// not valid yet
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder='Paste your service account JSON key here...'
|
||||
autoComplete='off'
|
||||
data-lpignore='true'
|
||||
className='mt-1.5 min-h-[120px] resize-none font-mono text-[12px]'
|
||||
autoFocus
|
||||
/>
|
||||
}}
|
||||
placeholder='Paste your service account JSON key here or drag & drop a .json file...'
|
||||
autoComplete='off'
|
||||
data-lpignore='true'
|
||||
className={cn(
|
||||
'min-h-[120px] resize-none border-0 font-mono text-[12px]',
|
||||
saDragActive && 'opacity-30'
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-1.5'>
|
||||
<label className='inline-flex cursor-pointer items-center gap-1.5 text-[12px] text-[var(--text-muted)] hover:text-[var(--text-secondary)]'>
|
||||
<input
|
||||
|
||||
@@ -377,7 +377,7 @@ export function CredentialSelector({
|
||||
className={overlayContent ? 'pl-7' : ''}
|
||||
/>
|
||||
|
||||
{supportsServiceAccount && !isPreview && (
|
||||
{((supportsServiceAccount && subBlock.mode === 'advanced') || isServiceAccount) && !isPreview && (
|
||||
<div className='mt-2.5 flex flex-col gap-2.5'>
|
||||
<div className='flex items-center gap-1.5 pl-0.5'>
|
||||
<Label>
|
||||
|
||||
@@ -2,7 +2,11 @@ import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { refreshTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
import {
|
||||
getServiceAccountToken,
|
||||
refreshTokenIfNeeded,
|
||||
resolveOAuthAccountId,
|
||||
} from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('VertexCredential')
|
||||
|
||||
@@ -23,6 +27,14 @@ export async function resolveVertexCredential(
|
||||
throw new Error(`Vertex AI credential is not a valid OAuth credential: ${credentialId}`)
|
||||
}
|
||||
|
||||
if (resolved.credentialType === 'service_account' && resolved.credentialId) {
|
||||
const accessToken = await getServiceAccountToken(resolved.credentialId, [
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
])
|
||||
logger.info(`[${requestId}] Successfully resolved Vertex AI service account credential`)
|
||||
return accessToken
|
||||
}
|
||||
|
||||
const credential = await db.query.account.findFirst({
|
||||
where: eq(account.id, resolved.accountId),
|
||||
})
|
||||
|
||||
@@ -252,6 +252,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
'bigquery.datasets',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.projectId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
@@ -261,6 +262,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
projectId: context.projectId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
})
|
||||
const data = await fetchJson<{ datasets: BigQueryDataset[] }>(
|
||||
'/api/tools/google_bigquery/datasets',
|
||||
@@ -278,6 +280,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
projectId: context.projectId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
})
|
||||
const data = await fetchJson<{ datasets: BigQueryDataset[] }>(
|
||||
'/api/tools/google_bigquery/datasets',
|
||||
@@ -301,6 +304,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
context.oauthCredential ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
context.datasetId ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(context.oauthCredential && context.projectId && context.datasetId),
|
||||
@@ -313,6 +317,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
workflowId: context.workflowId,
|
||||
projectId: context.projectId,
|
||||
datasetId: context.datasetId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
})
|
||||
const data = await fetchJson<{ tables: BigQueryTable[] }>(
|
||||
'/api/tools/google_bigquery/tables',
|
||||
@@ -331,6 +336,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
workflowId: context.workflowId,
|
||||
projectId: context.projectId,
|
||||
datasetId: context.datasetId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
})
|
||||
const data = await fetchJson<{ tables: BigQueryTable[] }>(
|
||||
'/api/tools/google_bigquery/tables',
|
||||
@@ -557,11 +563,16 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
'selectors',
|
||||
'google.tasks.lists',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.tasks.lists')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
const body = JSON.stringify({
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
})
|
||||
const data = await fetchJson<{ taskLists: GoogleTaskList[] }>(
|
||||
'/api/tools/google_tasks/task-lists',
|
||||
{ method: 'POST', body }
|
||||
@@ -571,7 +582,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
fetchById: async ({ context, detailId }: SelectorQueryArgs) => {
|
||||
if (!detailId) return null
|
||||
const credentialId = ensureCredential(context, 'google.tasks.lists')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
const body = JSON.stringify({
|
||||
credential: credentialId,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
})
|
||||
const data = await fetchJson<{ taskLists: GoogleTaskList[] }>(
|
||||
'/api/tools/google_tasks/task-lists',
|
||||
{ method: 'POST', body }
|
||||
@@ -877,11 +892,15 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
'selectors',
|
||||
'gmail.labels',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const data = await fetchJson<{ labels: FolderResponse[] }>('/api/tools/gmail/labels', {
|
||||
searchParams: { credentialId: context.oauthCredential },
|
||||
searchParams: {
|
||||
credentialId: context.oauthCredential,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
})
|
||||
return (data.labels || []).map((label) => ({
|
||||
id: label.id,
|
||||
@@ -915,12 +934,18 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
'selectors',
|
||||
'google.calendar',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const data = await fetchJson<{ calendars: { id: string; summary: string }[] }>(
|
||||
'/api/tools/google_calendar/calendars',
|
||||
{ searchParams: { credentialId: context.oauthCredential } }
|
||||
{
|
||||
searchParams: {
|
||||
credentialId: context.oauthCredential,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
}
|
||||
)
|
||||
return (data.calendars || []).map((calendar) => ({
|
||||
id: calendar.id,
|
||||
@@ -1393,6 +1418,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
context.mimeType ?? 'any',
|
||||
context.fileId ?? 'root',
|
||||
search ?? '',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
@@ -1406,6 +1432,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
parentId: context.fileId,
|
||||
query: search,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1424,6 +1451,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
credentialId,
|
||||
fileId: detailId,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1440,6 +1468,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
'google.sheets',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.spreadsheetId ?? 'none',
|
||||
context.impersonateUserEmail ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
@@ -1454,6 +1483,7 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
credentialId,
|
||||
spreadsheetId: context.spreadsheetId,
|
||||
workflowId: context.workflowId,
|
||||
impersonateEmail: context.impersonateUserEmail,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface SelectorContext {
|
||||
baseId?: string
|
||||
datasetId?: string
|
||||
serviceDeskId?: string
|
||||
impersonateUserEmail?: string
|
||||
}
|
||||
|
||||
export interface SelectorQueryArgs {
|
||||
|
||||
@@ -86,6 +86,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-docs': {
|
||||
name: 'Google Docs',
|
||||
@@ -99,6 +100,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-sheets': {
|
||||
name: 'Google Sheets',
|
||||
@@ -112,6 +114,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-forms': {
|
||||
name: 'Google Forms',
|
||||
@@ -126,6 +129,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/forms.body',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-calendar': {
|
||||
name: 'Google Calendar',
|
||||
@@ -138,6 +142,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/calendar',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-contacts': {
|
||||
name: 'Google Contacts',
|
||||
@@ -150,6 +155,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/contacts',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-ads': {
|
||||
name: 'Google Ads',
|
||||
@@ -162,6 +168,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/adwords',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-bigquery': {
|
||||
name: 'Google BigQuery',
|
||||
@@ -174,6 +181,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/bigquery',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-tasks': {
|
||||
name: 'Google Tasks',
|
||||
@@ -186,6 +194,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/tasks',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-vault': {
|
||||
name: 'Google Vault',
|
||||
@@ -199,6 +208,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/ediscovery',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-groups': {
|
||||
name: 'Google Groups',
|
||||
@@ -212,6 +222,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/admin.directory.group',
|
||||
'https://www.googleapis.com/auth/admin.directory.group.member',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-meet': {
|
||||
name: 'Google Meet',
|
||||
@@ -225,6 +236,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/meetings.space.created',
|
||||
'https://www.googleapis.com/auth/meetings.space.readonly',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
'google-service-account': {
|
||||
name: 'Google Service Account',
|
||||
@@ -246,6 +258,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
],
|
||||
serviceAccountProviderId: 'google-service-account',
|
||||
},
|
||||
},
|
||||
defaultService: 'gmail',
|
||||
|
||||
@@ -21,6 +21,7 @@ export const SELECTOR_CONTEXT_FIELDS = new Set<keyof SelectorContext>([
|
||||
'baseId',
|
||||
'datasetId',
|
||||
'serviceDeskId',
|
||||
'impersonateUserEmail',
|
||||
])
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user