feat(oauth): added google drive credentials, fixed some oauth bugs

This commit is contained in:
Waleed Latif
2025-03-06 23:51:40 -08:00
parent fdbc01345f
commit 26bcdd7b34
12 changed files with 570 additions and 9 deletions

View File

@@ -40,9 +40,7 @@ export async function GET(request: NextRequest) {
const accounts = await db
.select()
.from(account)
.where(
and(eq(account.userId, session.user.id), like(account.providerId, `${baseProvider}-%`))
)
.where(and(eq(account.userId, session.user.id), eq(account.providerId, provider)))
// Transform accounts into credentials
const credentials = await Promise.all(

View File

@@ -55,7 +55,9 @@ export function CredentialSelector({
if (!open) return
setIsLoading(true)
try {
const response = await fetch(`/api/auth/oauth/credentials?provider=${provider}`)
const providerId = getProviderId()
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
if (response.ok) {
const data = await response.json()
setCredentials(data.credentials)
@@ -173,11 +175,10 @@ export function CredentialSelector({
// Get provider icon
const getProviderIcon = (provider: OAuthProvider) => {
switch (provider) {
const baseProvider = provider.split('-')[0] as OAuthProvider
switch (baseProvider) {
case 'google':
return <GoogleIcon className="h-4 w-4" />
case 'google-email':
return <GmailIcon className="h-4 w-4" />
case 'github':
return <GithubIcon className="h-4 w-4" />
case 'twitter':

165
blocks/blocks/drive.ts Normal file
View File

@@ -0,0 +1,165 @@
import { GoogleDriveIcon } from '@/components/icons'
import {
GoogleDriveDownloadResponse,
GoogleDriveListResponse,
GoogleDriveUploadResponse,
} from '@/tools/drive/types'
import { BlockConfig } from '../types'
type GoogleDriveResponse =
| GoogleDriveUploadResponse
| GoogleDriveDownloadResponse
| GoogleDriveListResponse
export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
type: 'google_drive',
name: 'Google Drive',
description: 'Upload, download, and list files in Google Drive',
longDescription:
'Integrate Google Drive functionality to manage files and folders. Upload new files, download existing ones, and list contents of folders using OAuth authentication. Supports file operations with custom MIME types and folder organization.',
category: 'tools',
bgColor: '#1EA362',
icon: GoogleDriveIcon,
subBlocks: [
// Operation selector
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
// { label: 'Upload File', id: 'upload' },
// { label: 'Download File', id: 'download' },
{ label: 'List Files', id: 'list' },
],
},
// Google Drive Credentials
{
id: 'credential',
title: 'Google Drive Account',
type: 'oauth-input',
layout: 'full',
provider: 'google-drive',
serviceId: 'google-drive',
requiredScopes: ['https://www.googleapis.com/auth/drive'],
placeholder: 'Select Google Drive account',
},
// Upload Fields
{
id: 'fileName',
title: 'File Name',
type: 'short-input',
layout: 'full',
placeholder: 'Name for the uploaded file (e.g., document.txt)',
condition: { field: 'operation', value: 'upload' },
},
{
id: 'content',
title: 'Content',
type: 'long-input',
layout: 'full',
placeholder: 'Content to upload to the file',
condition: { field: 'operation', value: 'upload' },
},
{
id: 'mimeType',
title: 'MIME Type',
type: 'short-input',
layout: 'full',
placeholder:
'File MIME type (default: text/plain, e.g., text/plain, application/json, text/csv)',
condition: { field: 'operation', value: 'upload' },
},
{
id: 'folderId',
title: 'Parent Folder ID',
type: 'short-input',
layout: 'full',
placeholder: 'ID of the parent folder (optional, leave empty for root folder)',
condition: { field: 'operation', value: 'upload' },
},
// Download Fields
{
id: 'fileId',
title: 'File ID',
type: 'short-input',
layout: 'full',
placeholder: 'ID of the file to download (find in file URL or by listing files)',
condition: { field: 'operation', value: 'download' },
},
// List Fields
{
id: 'folderId',
title: 'Folder ID',
type: 'short-input',
layout: 'full',
placeholder: 'ID of the folder to list (optional, leave empty for root folder)',
condition: { field: 'operation', value: 'list' },
},
{
id: 'query',
title: 'Search Query',
type: 'short-input',
layout: 'full',
placeholder: 'Search for specific files (e.g., name contains "report")',
condition: { field: 'operation', value: 'list' },
},
{
id: 'pageSize',
title: 'Results Per Page',
type: 'short-input',
layout: 'full',
placeholder: 'Number of results (default: 100, max: 1000)',
condition: { field: 'operation', value: 'list' },
},
],
tools: {
access: ['google_drive_upload', 'google_drive_download', 'google_drive_list'],
config: {
tool: (params) => {
switch (params.operation) {
case 'upload':
return 'google_drive_upload'
case 'download':
return 'google_drive_download'
case 'list':
return 'google_drive_list'
default:
throw new Error(`Invalid Google Drive operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, ...rest } = params
// Convert pageSize to number if it exists
const pageSize = rest.pageSize ? parseInt(rest.pageSize as string, 10) : undefined
return {
...rest,
pageSize,
credential,
}
},
},
},
inputs: {
operation: { type: 'string', required: true },
credential: { type: 'string', required: true },
// Upload operation inputs
fileName: { type: 'string', required: false },
content: { type: 'string', required: false },
mimeType: { type: 'string', required: false },
// Download operation inputs
fileId: { type: 'string', required: false },
// List operation inputs
folderId: { type: 'string', required: false },
query: { type: 'string', required: false },
pageSize: { type: 'number', required: false },
},
outputs: {
response: {
type: {
content: 'string',
metadata: 'json',
},
},
},
}

View File

@@ -3,6 +3,7 @@ import { AgentBlock } from './blocks/agent'
import { ApiBlock } from './blocks/api'
import { ConditionBlock } from './blocks/condition'
import { CrewAIVisionBlock } from './blocks/crewai'
import { GoogleDriveBlock } from './blocks/drive'
import { EvaluatorBlock } from './blocks/evaluator'
import { ExaBlock } from './blocks/exa'
import { FirecrawlBlock } from './blocks/firecrawl'
@@ -49,6 +50,7 @@ export {
OpenAIBlock,
ExaBlock,
RedditBlock,
GoogleDriveBlock,
}
// Registry of all block configurations, alphabetically sorted
@@ -57,6 +59,7 @@ const blocks: Record<string, BlockConfig> = {
api: ApiBlock,
condition: ConditionBlock,
crewai_vision: CrewAIVisionBlock,
google_drive: GoogleDriveBlock,
evaluator: EvaluatorBlock,
exa: ExaBlock,
firecrawl: FirecrawlBlock,

View File

@@ -1,7 +1,13 @@
'use client'
import { Check } from 'lucide-react'
import { GithubIcon, GmailIcon, GoogleIcon, xIcon as XIcon } from '@/components/icons'
import {
GithubIcon,
GmailIcon,
GoogleDriveIcon,
GoogleIcon,
xIcon as XIcon,
} from '@/components/icons'
import { Button } from '@/components/ui/button'
import {
Dialog,
@@ -28,6 +34,7 @@ const PROVIDER_NAMES: Record<OAuthProvider, string> = {
github: 'GitHub',
google: 'Google',
'google-email': 'Gmail',
'google-drive': 'Google Drive',
twitter: 'X (Twitter)',
}
@@ -36,6 +43,7 @@ const PROVIDER_ICONS: Record<OAuthProvider, React.FC<React.SVGProps<SVGSVGElemen
github: GithubIcon,
google: GoogleIcon,
'google-email': GmailIcon,
'google-drive': GoogleDriveIcon,
twitter: XIcon,
}
@@ -44,6 +52,7 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'https://www.googleapis.com/auth/gmail.send': 'Send emails on your behalf',
'https://www.googleapis.com/auth/gmail.readonly': 'View and read your email messages',
'https://www.googleapis.com/auth/drive': 'View and manage your Google Drive files',
'https://www.googleapis.com/auth/drive.file': 'View and manage your Google Drive files',
'https://www.googleapis.com/auth/calendar': 'View and manage your calendar',
'https://www.googleapis.com/auth/userinfo.email': 'View your email address',
'https://www.googleapis.com/auth/userinfo.profile': 'View your basic profile info',

65
tools/drive/download.ts Normal file
View File

@@ -0,0 +1,65 @@
import { ToolConfig } from '../types'
import { GoogleDriveDownloadResponse, GoogleDriveToolParams } from './types'
export const downloadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveDownloadResponse> = {
id: 'google_drive_download',
name: 'Download from Google Drive',
description: 'Download a file from Google Drive',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
additionalScopes: ['https://www.googleapis.com/auth/drive'],
},
params: {
accessToken: { type: 'string', required: true },
fileId: { type: 'string', required: true },
},
request: {
url: (params) => `https://www.googleapis.com/drive/v3/files/${params.fileId}?alt=media`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const error = await response.json()
throw new Error(error.error?.message || 'Failed to download file from Google Drive')
}
// Get file metadata
const metadataResponse = await fetch(
`https://www.googleapis.com/drive/v3/files/${response.url.split('files/')[1].split('?')[0]}`,
{
headers: {
Authorization: response.headers.get('Authorization') || '',
},
}
)
const metadata = await metadataResponse.json()
const content = await response.text()
return {
success: true,
output: {
content,
metadata: {
id: metadata.id,
name: metadata.name,
mimeType: metadata.mimeType,
webViewLink: metadata.webViewLink,
webContentLink: metadata.webContentLink,
size: metadata.size,
createdTime: metadata.createdTime,
modifiedTime: metadata.modifiedTime,
parents: metadata.parents,
},
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while downloading from Google Drive'
},
}

113
tools/drive/export.ts Normal file
View File

@@ -0,0 +1,113 @@
import { ToolConfig } from '../types'
import { GoogleDriveDownloadResponse } from './types'
import { GoogleDriveToolParams } from './types'
export const exportTool: ToolConfig<
GoogleDriveToolParams & { mimeType?: string },
GoogleDriveDownloadResponse
> = {
id: 'google_drive_export',
name: 'Export from Google Drive',
description: 'Export a Google Workspace file (Docs, Sheets, Slides) from Google Drive',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
additionalScopes: ['https://www.googleapis.com/auth/drive'],
},
params: {
accessToken: { type: 'string', required: true },
fileId: { type: 'string', required: true },
mimeType: { type: 'string', required: false },
},
request: {
url: (params) => {
const exportMimeType = params.mimeType || 'application/pdf'
return `https://www.googleapis.com/drive/v3/files/${params.fileId}/export?mimeType=${encodeURIComponent(exportMimeType)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const error = await response.json()
console.error('Google Drive export error:', {
status: response.status,
statusText: response.statusText,
error,
fileId: response.url.split('files/')[1]?.split('?')[0],
})
throw new Error(
`Failed to export file from Google Drive: ${response.status} ${response.statusText} - ${error.error?.message || 'Unknown error'}`
)
}
// Get file metadata
const fileId = response.url.split('files/')[1]?.split('?')[0]
const metadataResponse = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}`, {
headers: {
Authorization: response.headers.get('Authorization') || '',
},
})
if (!metadataResponse.ok) {
const metadataError = await metadataResponse.json()
console.error('Google Drive metadata error:', {
status: metadataResponse.status,
statusText: metadataResponse.statusText,
error: metadataError,
})
throw new Error(
`Failed to get file metadata: ${metadataResponse.status} ${metadataResponse.statusText} - ${metadataError.error?.message || 'Unknown error'}`
)
}
const metadata = await metadataResponse.json()
let content
try {
content = await response.text()
} catch (error: any) {
console.error('Error reading response content:', {
message: error.message,
stack: error.stack,
error: JSON.stringify(error),
})
throw new Error(`Failed to read file content: ${error?.message || 'Unknown error'}`)
}
return {
success: true,
output: {
content,
metadata: {
id: metadata.id,
name: metadata.name,
mimeType: metadata.mimeType,
webViewLink: metadata.webViewLink,
webContentLink: metadata.webContentLink,
size: metadata.size,
createdTime: metadata.createdTime,
modifiedTime: metadata.modifiedTime,
parents: metadata.parents,
},
},
}
},
transformError: (error: any) => {
console.error('Export tool error:', {
message: error.message,
stack: error.stack,
error: JSON.stringify(error, null, 2),
})
if (typeof error === 'string') {
return error
}
return (
error.message ||
JSON.stringify(error, null, 2) ||
'An error occurred while exporting from Google Drive'
)
},
}

79
tools/drive/list.ts Normal file
View File

@@ -0,0 +1,79 @@
import { ToolConfig } from '../types'
import { GoogleDriveListResponse, GoogleDriveToolParams } from './types'
export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse> = {
id: 'google_drive_list',
name: 'List Google Drive Files',
description: 'List files and folders in Google Drive',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
additionalScopes: ['https://www.googleapis.com/auth/drive'],
},
params: {
accessToken: { type: 'string', required: true },
folderId: { type: 'string', required: false },
query: { type: 'string', required: false },
pageSize: { type: 'number', required: false },
pageToken: { type: 'string', required: false },
},
request: {
url: (params) => {
const url = new URL('https://www.googleapis.com/drive/v3/files')
url.searchParams.append(
'fields',
'files(id,name,mimeType,webViewLink,webContentLink,size,createdTime,modifiedTime,parents),nextPageToken'
)
if (params.folderId) {
url.searchParams.append('q', `'${params.folderId}' in parents`)
}
if (params.query) {
const existingQ = url.searchParams.get('q')
const queryPart = `name contains '${params.query}'`
url.searchParams.set('q', existingQ ? `${existingQ} and ${queryPart}` : queryPart)
}
if (params.pageSize) {
url.searchParams.append('pageSize', params.pageSize.toString())
}
if (params.pageToken) {
url.searchParams.append('pageToken', params.pageToken)
}
return url.toString()
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to list Google Drive files')
}
return {
success: true,
output: {
files: data.files.map((file: any) => ({
id: file.id,
name: file.name,
mimeType: file.mimeType,
webViewLink: file.webViewLink,
webContentLink: file.webContentLink,
size: file.size,
createdTime: file.createdTime,
modifiedTime: file.modifiedTime,
parents: file.parents,
})),
nextPageToken: data.nextPageToken,
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while listing Google Drive files'
},
}

45
tools/drive/types.ts Normal file
View File

@@ -0,0 +1,45 @@
import { ToolResponse } from '../types'
export interface GoogleDriveFile {
id: string
name: string
mimeType: string
webViewLink?: string
webContentLink?: string
size?: string
createdTime?: string
modifiedTime?: string
parents?: string[]
}
export interface GoogleDriveListResponse extends ToolResponse {
output: {
files: GoogleDriveFile[]
nextPageToken?: string
}
}
export interface GoogleDriveUploadResponse extends ToolResponse {
output: {
file: GoogleDriveFile
}
}
export interface GoogleDriveDownloadResponse extends ToolResponse {
output: {
content: string
metadata: GoogleDriveFile
}
}
export interface GoogleDriveToolParams {
accessToken: string
folderId?: string
fileId?: string
fileName?: string
content?: string
mimeType?: string
query?: string
pageSize?: number
pageToken?: string
}

77
tools/drive/upload.ts Normal file
View File

@@ -0,0 +1,77 @@
import { ToolConfig } from '../types'
import { GoogleDriveToolParams, GoogleDriveUploadResponse } from './types'
export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResponse> = {
id: 'google_drive_upload',
name: 'Upload to Google Drive',
description: 'Upload a file to Google Drive',
version: '1.0',
oauth: {
required: true,
provider: 'google-drive',
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
},
params: {
accessToken: { type: 'string', required: true },
fileName: { type: 'string', required: true },
content: { type: 'string', required: true },
mimeType: { type: 'string', required: false },
folderId: { type: 'string', required: false },
},
request: {
url: 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'multipart/related; boundary=boundary',
}),
body: (params) => {
const metadata = {
name: params.fileName,
...(params.folderId ? { parents: [params.folderId] } : {}),
}
const mimeType = params.mimeType || 'text/plain'
const body = `--boundary
Content-Type: application/json; charset=UTF-8
${JSON.stringify(metadata)}
--boundary
Content-Type: ${mimeType}
${params.content}
--boundary--`
return { body }
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to upload file to Google Drive')
}
return {
success: true,
output: {
file: {
id: data.id,
name: data.name,
mimeType: data.mimeType,
webViewLink: data.webViewLink,
webContentLink: data.webContentLink,
size: data.size,
createdTime: data.createdTime,
modifiedTime: data.modifiedTime,
parents: data.parents,
},
},
}
},
transformError: (error) => {
return error.message || 'An error occurred while uploading to Google Drive'
},
}

View File

@@ -1,6 +1,9 @@
import { useCustomToolsStore } from '@/stores/custom-tools/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { visionTool as crewAIVision } from './crewai/vision'
import { downloadTool as driveDownloadTool } from './drive/download'
import { listTool as driveListTool } from './drive/list'
import { uploadTool as driveUploadTool } from './drive/upload'
import { answerTool as exaAnswer } from './exa/answer'
import { findSimilarLinksTool as exaFindSimilarLinks } from './exa/findSimilarLinks'
import { getContentsTool as exaGetContents } from './exa/getContents'
@@ -77,6 +80,9 @@ export const tools: Record<string, ToolConfig> = {
exa_find_similar_links: exaFindSimilarLinks,
exa_answer: exaAnswer,
reddit_hot_posts: redditHotPosts,
google_drive_download: driveDownloadTool,
google_drive_list: driveListTool,
google_drive_upload: driveUploadTool,
}
// Get a tool by its ID

View File

@@ -1,5 +1,5 @@
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
export type OAuthProvider = 'google' | 'google-email' | 'github' | 'twitter'
export type OAuthProvider = 'google' | 'google-email' | 'google-drive' | 'github' | 'twitter'
export interface ToolResponse {
success: boolean // Whether the tool execution was successful