diff --git a/application/api/connector/routes.py b/application/api/connector/routes.py index aca55b42..96f02625 100644 --- a/application/api/connector/routes.py +++ b/application/api/connector/routes.py @@ -419,7 +419,7 @@ class ConnectorFiles(Resource): @connectors_ns.route("/api/connectors/validate-session") class ConnectorValidateSession(Resource): @api.expect(api.model("ConnectorValidateSessionModel", {"provider": fields.String(required=True), "session_token": fields.String(required=True)})) - @api.doc(description="Validate connector session token and return user info") + @api.doc(description="Validate connector session token and return user info and access token") def post(self): try: data = request.get_json() @@ -428,7 +428,6 @@ class ConnectorValidateSession(Resource): if not provider or not session_token: return make_response(jsonify({"success": False, "error": "provider and session_token are required"}), 400) - decoded_token = request.decoded_token if not decoded_token: return make_response(jsonify({"success": False, "error": "Unauthorized"}), 401) @@ -445,7 +444,8 @@ class ConnectorValidateSession(Resource): return make_response(jsonify({ "success": True, "expired": is_expired, - "user_email": session.get('user_email', 'Connected User') + "user_email": session.get('user_email', 'Connected User'), + "access_token": token_info.get('access_token') }), 200) except Exception as e: current_app.logger.error(f"Error validating connector session: {e}") diff --git a/application/parser/connectors/google_drive/auth.py b/application/parser/connectors/google_drive/auth.py index 37d55dcc..c282279e 100644 --- a/application/parser/connectors/google_drive/auth.py +++ b/application/parser/connectors/google_drive/auth.py @@ -17,8 +17,7 @@ class GoogleDriveAuth(BaseConnectorAuth): """ SCOPES = [ - 'https://www.googleapis.com/auth/drive.readonly', - 'https://www.googleapis.com/auth/drive.metadata.readonly' + 'https://www.googleapis.com/auth/drive.file' ] def __init__(self): @@ -50,7 +49,7 @@ class GoogleDriveAuth(BaseConnectorAuth): authorization_url, _ = flow.authorization_url( access_type='offline', prompt='consent', - include_granted_scopes='true', + include_granted_scopes='false', state=state ) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 934f9e57..5a746dcb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-dropzone": "^14.3.8", + "react-google-drive-picker": "^1.2.2", "react-i18next": "^15.4.0", "react-markdown": "^9.0.1", "react-redux": "^9.2.0", @@ -9382,6 +9383,16 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-google-drive-picker": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/react-google-drive-picker/-/react-google-drive-picker-1.2.2.tgz", + "integrity": "sha512-x30mYkt9MIwPCgL+fyK75HZ8E6G5L/WGW0bfMG6kbD4NG2kmdlmV9oH5lPa6P6d46y9hj5Y3btAMrZd4JRRkSA==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/react-i18next": { "version": "15.4.0", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3b869b2d..fe6ce59f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-dropzone": "^14.3.8", + "react-google-drive-picker": "^1.2.2", "react-i18next": "^15.4.0", "react-markdown": "^9.0.1", "react-redux": "^9.2.0", diff --git a/frontend/src/components/FilePicker.tsx b/frontend/src/components/FilePicker.tsx index c31c4e62..fb27b5c3 100644 --- a/frontend/src/components/FilePicker.tsx +++ b/frontend/src/components/FilePicker.tsx @@ -358,7 +358,7 @@ export const FilePicker: React.FC = ({ -
+
{/* Breadcrumb navigation */}
diff --git a/frontend/src/components/GoogleDrivePicker.tsx b/frontend/src/components/GoogleDrivePicker.tsx new file mode 100644 index 00000000..9bda9798 --- /dev/null +++ b/frontend/src/components/GoogleDrivePicker.tsx @@ -0,0 +1,299 @@ +import React, { useState, useEffect } from 'react'; +import useDrivePicker from 'react-google-drive-picker'; + +import ConnectorAuth from './ConnectorAuth'; +import { getSessionToken, setSessionToken, removeSessionToken } from '../utils/providerUtils'; + + +interface PickerFile { + id: string; + name: string; + mimeType: string; + iconUrl: string; + description?: string; + sizeBytes?: string; +} + +interface GoogleDrivePickerProps { + token: string | null; + onSelectionChange: (fileIds: string[], folderIds?: string[]) => void; + initialSelectedFiles?: string[]; + initialSelectedFolders?: string[]; +} + +const GoogleDrivePicker: React.FC = ({ + token, + onSelectionChange, + initialSelectedFiles = [], + initialSelectedFolders = [], +}) => { + const [selectedFiles, setSelectedFiles] = useState([]); + const [selectedFolders, setSelectedFolders] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [userEmail, setUserEmail] = useState(''); + const [isConnected, setIsConnected] = useState(false); + const [authError, setAuthError] = useState(''); + const [accessToken, setAccessToken] = useState(null); + + const [openPicker] = useDrivePicker(); + + useEffect(() => { + const sessionToken = getSessionToken('google_drive'); + if (sessionToken) { + validateSession(sessionToken); + } + }, [token]); + + const validateSession = async (sessionToken: string) => { + try { + const apiHost = import.meta.env.VITE_API_HOST; + const validateResponse = await fetch(`${apiHost}/api/connectors/validate-session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ provider: 'google_drive', session_token: sessionToken }) + }); + + if (!validateResponse.ok) { + setIsConnected(false); + setAuthError('Session expired. Please reconnect to Google Drive.'); + return; + } + + const validateData = await validateResponse.json(); + if (validateData.success) { + setUserEmail(validateData.user_email || 'Connected User'); + setIsConnected(true); + setAuthError(''); + setAccessToken(validateData.access_token || null); + } else { + setIsConnected(false); + setAuthError(validateData.error || 'Session expired. Please reconnect your account.'); + } + } catch (error) { + console.error('Error validating session:', error); + setAuthError('Failed to validate session. Please reconnect.'); + setIsConnected(false); + } + }; + + const handleOpenPicker = async () => { + setIsLoading(true); + + const sessionToken = getSessionToken('google_drive'); + + if (!sessionToken) { + setAuthError('No valid session found. Please reconnect to Google Drive.'); + setIsLoading(false); + return; + } + + if (!accessToken) { + setAuthError('No access token available. Please reconnect to Google Drive.'); + setIsLoading(false); + return; + } + + try { + openPicker({ + clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID, + developerKey: import.meta.env.VITE_GOOGLE_API_KEY, + viewId: "DOCS_IMAGES_AND_VIDEOS", + showUploadView: false, + showUploadFolders: false, + supportDrives: true, + multiselect: true, + token: accessToken, + viewMimeTypes: 'application/vnd.google-apps.folder,application/vnd.google-apps.document,application/pdf', + callbackFunction: (data:any) => { + setIsLoading(false); + if (data.action === 'picked') { + const docs = data.docs; + + const files: PickerFile[] = []; + const folders: PickerFile[] = []; + + docs.forEach((doc: any) => { + const item = { + id: doc.id, + name: doc.name, + mimeType: doc.mimeType, + iconUrl: doc.iconUrl || '', + description: doc.description, + sizeBytes: doc.sizeBytes + }; + + if (doc.mimeType === 'application/vnd.google-apps.folder') { + folders.push(item); + } else { + files.push(item); + } + }); + + setSelectedFiles(files); + setSelectedFolders(folders); + + const fileIds = files.map(file => file.id); + const folderIds = folders.map(folder => folder.id); + + console.log('Selected file IDs:', fileIds); + console.log('Selected folder IDs:', folderIds); + + onSelectionChange(fileIds, folderIds); + } + }, + }); + } catch (error) { + console.error('Error opening picker:', error); + setAuthError('Failed to open file picker. Please try again.'); + setIsLoading(false); + } + }; + + const handleDisconnect = async () => { + const sessionToken = getSessionToken('google_drive'); + if (sessionToken) { + try { + const apiHost = import.meta.env.VITE_API_HOST; + await fetch(`${apiHost}/api/connectors/disconnect`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ provider: 'google_drive', session_token: sessionToken }) + }); + } catch (err) { + console.error('Error disconnecting from Google Drive:', err); + } + } + + removeSessionToken('google_drive'); + setIsConnected(false); + setSelectedFiles([]); + setSelectedFolders([]); + onSelectionChange([], []); + }; + + if (!isConnected) { + return ( +
+ {authError && ( +
{authError}
+ )} + { + setUserEmail(data.user_email || 'Connected User'); + setIsConnected(true); + setAuthError(''); + + if (data.session_token) { + setSessionToken('google_drive', data.session_token); + } + }} + onError={(error) => { + setAuthError(error); + setIsConnected(false); + }} + /> +
+ ); + } + + return ( +
+
+
+
+ + + + Connected as {userEmail} +
+ +
+
+ +
+
+

Selected Files

+ +
+ + {selectedFiles.length === 0 && selectedFolders.length === 0 ? ( +

No files or folders selected

+ ) : ( +
+ {/* Display folders */} + {selectedFolders.length > 0 && ( +
+

Folders

+ {selectedFolders.map((folder) => ( +
+ Folder + {folder.name} + +
+ ))} +
+ )} + + {/* Display files */} + {selectedFiles.length > 0 && ( +
+

Files

+ {selectedFiles.map((file) => ( +
+ File + {file.name} + +
+ ))} +
+ )} +
+ )} +
+
+ ); +}; + +export default GoogleDrivePicker; diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index a6db7e56..dd34ace5 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -35,6 +35,7 @@ import { } from './types/ingestor'; import {FilePicker} from '../components/FilePicker'; +import GoogleDrivePicker from '../components/GoogleDrivePicker'; import CrawlerIcon from '../assets/crawler.svg'; import FileUploadIcon from '../assets/file_upload.svg'; @@ -244,6 +245,19 @@ function Upload({ initialSelectedFolders={selectedFolders} /> ); + case 'google_drive_picker': + return ( + { + setSelectedFiles(selectedFileIds); + setSelectedFolders(selectedFolderIds); + }} + token={token} + initialSelectedFiles={selectedFiles} + initialSelectedFolders={selectedFolders} + /> + ); default: return null; } @@ -384,8 +398,7 @@ function Upload({ data?.find( (d: Doc) => d.type?.toLowerCase() === 'local', ), - ), - ); + )); }); setProgress( (progress) => @@ -509,18 +522,19 @@ function Upload({ formData.append('user', 'local'); formData.append('source', ingestor.type as string); - let configData; + let configData: any = {}; const schema: FormField[] = IngestorFormSchemas[ingestor.type as IngestorType]; const hasLocalFilePicker = schema.some((field: FormField) => field.type === 'local_file_picker'); const hasRemoteFilePicker = schema.some((field: FormField) => field.type === 'remote_file_picker'); + const hasGoogleDrivePicker = schema.some((field: FormField) => field.type === 'google_drive_picker'); if (hasLocalFilePicker) { files.forEach((file) => { formData.append('file', file); }); configData = { ...ingestor.config }; - } else if (hasRemoteFilePicker) { + } else if (hasRemoteFilePicker || hasGoogleDrivePicker) { const sessionToken = getSessionToken(ingestor.type as string); configData = { provider: ingestor.type as string, @@ -606,12 +620,13 @@ function Upload({ const schema: FormField[] = IngestorFormSchemas[ingestor.type as IngestorType]; const hasLocalFilePicker = schema.some((field: FormField) => field.type === 'local_file_picker'); const hasRemoteFilePicker = schema.some((field: FormField) => field.type === 'remote_file_picker'); + const hasGoogleDrivePicker = schema.some((field: FormField) => field.type === 'google_drive_picker'); if (hasLocalFilePicker) { if (files.length === 0) { return true; } - } else if (hasRemoteFilePicker) { + } else if (hasRemoteFilePicker || hasGoogleDrivePicker) { if (selectedFiles.length === 0 && selectedFolders.length === 0) { return true; } diff --git a/frontend/src/upload/types/ingestor.ts b/frontend/src/upload/types/ingestor.ts index ed53bd26..08686796 100644 --- a/frontend/src/upload/types/ingestor.ts +++ b/frontend/src/upload/types/ingestor.ts @@ -13,7 +13,7 @@ export type IngestorFormData = { data: string; }; -export type FieldType = 'string' | 'number' | 'enum' | 'boolean' | 'local_file_picker' | 'remote_file_picker'; +export type FieldType = 'string' | 'number' | 'enum' | 'boolean' | 'local_file_picker' | 'remote_file_picker' | 'google_drive_picker'; export interface FormField { name: string; @@ -36,7 +36,12 @@ export const IngestorFormSchemas: Record = { ], github: [{ name: 'repo_url', label: 'Repository URL', type: 'string', required: true }], google_drive: [ - { name: 'file_picker', label: 'Select files', type: 'remote_file_picker', required: true }, + { + name: 'files', + label: 'Select Files from Google Drive', + type: 'google_drive_picker', + required: true, + }, { name: 'recursive', label: 'Include subfolders', type: 'boolean', required: false }, ], local_file: [