From c2bebbaefaf48e77a4f4f9a8574a709a58e3e78b Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Fri, 22 Aug 2025 03:29:23 +0530 Subject: [PATCH] (feat:oauth/drive) raw fe integrate --- frontend/public/google-drive-callback.html | 117 ++++++ frontend/src/upload/Upload.tsx | 442 ++++++++++++++++++++- frontend/src/upload/types/ingestor.ts | 28 +- 3 files changed, 563 insertions(+), 24 deletions(-) create mode 100644 frontend/public/google-drive-callback.html diff --git a/frontend/public/google-drive-callback.html b/frontend/public/google-drive-callback.html new file mode 100644 index 00000000..0272af9a --- /dev/null +++ b/frontend/public/google-drive-callback.html @@ -0,0 +1,117 @@ + + + + Google Drive Authentication + + + +
+

Google Drive Authentication

+
Processing authentication...
+
+ + + + diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 420427d4..75749f29 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -25,6 +25,8 @@ import { IngestorFormSchemas, IngestorType, } from './types/ingestor'; +import FileIcon from '../assets/file.svg'; +import FolderIcon from '../assets/folder.svg'; function Upload({ receivedFile = [], @@ -48,6 +50,15 @@ function Upload({ const [activeTab, setActiveTab] = useState(renderTab); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + // Google Drive state + const [isGoogleDriveConnected, setIsGoogleDriveConnected] = useState(false); + const [googleDriveFiles, setGoogleDriveFiles] = useState([]); + const [selectedFiles, setSelectedFiles] = useState([]); + const [isLoadingFiles, setIsLoadingFiles] = useState(false); + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [userEmail, setUserEmail] = useState(''); + const [authError, setAuthError] = useState(''); + const renderFormFields = () => { const schema = IngestorFormSchemas[ingestor.type]; if (!schema) return null; @@ -204,6 +215,7 @@ function Upload({ { label: 'Link', value: 'url' }, { label: 'GitHub', value: 'github' }, { label: 'Reddit', value: 'reddit' }, + { label: 'Google Drive', value: 'google_drive' }, ]; const sourceDocs = useSelector(selectSourceDocs); @@ -428,29 +440,40 @@ function Upload({ formData.append('user', 'local'); formData.append('source', ingestor.type); - const defaultConfig = IngestorDefaultConfigs[ingestor.type].config; + let configData; - const mergedConfig = { ...defaultConfig, ...ingestor.config }; - const filteredConfig = Object.entries(mergedConfig).reduce( - (acc, [key, value]) => { - const field = IngestorFormSchemas[ingestor.type].find( - (f) => f.name === key, - ); - // Include the field if: - // 1. It's required, or - // 2. It's optional and has a non-empty value - if ( - field?.required || - (value !== undefined && value !== null && value !== '') - ) { - acc[key] = value; - } - return acc; - }, - {} as Record, - ); + if (ingestor.type === 'google_drive') { + const sessionToken = localStorage.getItem('google_drive_session_token'); + + configData = { + file_ids: selectedFiles, + recursive: ingestor.config.recursive, + session_token: sessionToken || null + }; + } else { + const defaultConfig = IngestorDefaultConfigs[ingestor.type].config; + const mergedConfig = { ...defaultConfig, ...ingestor.config }; + configData = Object.entries(mergedConfig).reduce( + (acc, [key, value]) => { + const field = IngestorFormSchemas[ingestor.type].find( + (f) => f.name === key, + ); + // Include the field if: + // 1. It's required, or + // 2. It's optional and has a non-empty value + if ( + field?.required || + (value !== undefined && value !== null && value !== '') + ) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + } - formData.append('data', JSON.stringify(filteredConfig)); + formData.append('data', JSON.stringify(configData)); const apiHost: string = import.meta.env.VITE_API_HOST; const xhr = new XMLHttpRequest(); @@ -477,6 +500,233 @@ function Upload({ xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.send(formData); }; + + useEffect(() => { + if (ingestor.type === 'google_drive') { + const sessionToken = localStorage.getItem('google_drive_session_token'); + + if (sessionToken) { + // Auto-authenticate if session token exists + setIsGoogleDriveConnected(true); + setAuthError(''); + + // Fetch user email and files using the existing session token + fetchUserEmailAndLoadFiles(sessionToken); + } + } + }, [ingestor.type]); + + const fetchUserEmailAndLoadFiles = async (sessionToken: string) => { + try { + const apiHost = import.meta.env.VITE_API_HOST; + + const validateResponse = await fetch(`${apiHost}/api/google-drive/validate-session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ session_token: sessionToken }) + }); + + if (!validateResponse.ok) { + localStorage.removeItem('google_drive_session_token'); + setIsGoogleDriveConnected(false); + setAuthError('Session expired. Please reconnect to Google Drive.'); + return; + } + + const validateData = await validateResponse.json(); + + if (validateData.success) { + setUserEmail(validateData.user_email || 'Connected User'); + loadGoogleDriveFiles(sessionToken); + } else { + localStorage.removeItem('google_drive_session_token'); + setIsGoogleDriveConnected(false); + setAuthError(validateData.error || 'Session expired. Please reconnect your Google Drive account and make sure to grant offline access.'); + } + } catch (error) { + console.error('Error validating Google Drive session:', error); + setAuthError('Failed to validate session. Please reconnect.'); + setIsGoogleDriveConnected(false); + } + }; + + const handleGoogleDriveConnect = async () => { + console.log('Google Drive connect button clicked'); + setIsAuthenticating(true); + setAuthError(''); + + const existingToken = localStorage.getItem('google_drive_session_token'); + if (existingToken) { + fetchUserEmailAndLoadFiles(existingToken); + setIsAuthenticating(false); + return; + } + + try { + const apiHost = import.meta.env.VITE_API_HOST; + + const authResponse = await fetch(`${apiHost}/api/google-drive/auth`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!authResponse.ok) { + throw new Error(`Failed to get authorization URL: ${authResponse.status}`); + } + + const authData = await authResponse.json(); + + if (!authData.success || !authData.authorization_url) { + throw new Error(authData.error || 'Failed to get authorization URL'); + } + + console.log('Opening Google OAuth window...'); + + const authWindow = window.open( + authData.authorization_url, + 'google-drive-auth', + 'width=500,height=600,scrollbars=yes,resizable=yes' + ); + + if (!authWindow) { + throw new Error('Failed to open authentication window. Please allow popups.'); + } + + const handleAuthMessage = (event: MessageEvent) => { + console.log('Received message event:', event.data); + + if (event.data.type === 'google_drive_auth_success') { + console.log('OAuth success received:', event.data); + setUserEmail(event.data.user_email || 'Connected User'); + setIsGoogleDriveConnected(true); + setIsAuthenticating(false); + setAuthError(''); + + if (event.data.session_token) { + localStorage.setItem('google_drive_session_token', event.data.session_token); + } + + window.removeEventListener('message', handleAuthMessage); + + loadGoogleDriveFiles(event.data.session_token); + } else if (event.data.type === 'google_drive_auth_error') { + console.error('OAuth error received:', event.data); + setAuthError(event.data.error || 'Authentication failed. Please make sure to grant all requested permissions, including offline access. You may need to revoke previous access and re-authorize.'); + setIsAuthenticating(false); + setIsGoogleDriveConnected(false); + window.removeEventListener('message', handleAuthMessage); + } + }; + + window.addEventListener('message', handleAuthMessage); + const checkClosed = setInterval(() => { + if (authWindow.closed) { + clearInterval(checkClosed); + window.removeEventListener('message', handleAuthMessage); + + if (!isGoogleDriveConnected && !isAuthenticating) { + setAuthError('Authentication was cancelled'); + } + } + }, 1000); + + } catch (error) { + console.error('Error during Google Drive authentication:', error); + setAuthError(error instanceof Error ? error.message : 'Authentication failed'); + setIsAuthenticating(false); + } + }; + + const loadGoogleDriveFiles = async (sessionToken: string) => { + setIsLoadingFiles(true); + + try { + const apiHost = import.meta.env.VITE_API_HOST; + const filesResponse = await fetch(`${apiHost}/api/google-drive/files`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + session_token: sessionToken, + limit: 50 + }) + }); + + if (!filesResponse.ok) { + throw new Error(`Failed to load files: ${filesResponse.status}`); + } + + const filesData = await filesResponse.json(); + + if (filesData.success && filesData.files) { + setGoogleDriveFiles(filesData.files); + } else { + throw new Error(filesData.error || 'Failed to load files'); + } + + } catch (error) { + console.error('Error loading Google Drive files:', error); + setAuthError(error instanceof Error ? error.message : 'Failed to load files. Please make sure your Google Drive account is properly connected and you granted offline access during authorization.'); + + // Fallback to mock data for demo purposes + console.log('Using mock data as fallback...'); + const mockFiles = [ + { + id: '1', + name: 'Project Documentation.pdf', + type: 'application/pdf', + size: '2.5 MB', + modifiedTime: '2024-01-15', + iconUrl: '�' + }, + { + id: '2', + name: 'Meeting Notes.docx', + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + size: '1.2 MB', + modifiedTime: '2024-01-14', + iconUrl: '�' + }, + { + id: '3', + name: 'Presentation.pptx', + type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + size: '5.8 MB', + modifiedTime: '2024-01-13', + iconUrl: '�' + } + ]; + setGoogleDriveFiles(mockFiles); + } finally { + setIsLoadingFiles(false); + } + }; + + // Handle file selection + const handleFileSelect = (fileId: string) => { + setSelectedFiles(prev => { + if (prev.includes(fileId)) { + return prev.filter(id => id !== fileId); + } else { + return [...prev, fileId]; + } + }); + }; + + const handleSelectAll = () => { + if (selectedFiles.length === googleDriveFiles.length) { + setSelectedFiles([]); + } else { + setSelectedFiles(googleDriveFiles.map(file => file.id)); + } + }; + const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: true, @@ -515,6 +765,10 @@ function Upload({ if (!remoteName?.trim()) { return true; } + if (ingestor.type === 'google_drive') { + return !isGoogleDriveConnected || selectedFiles.length === 0; + } + const formFields: FormField[] = IngestorFormSchemas[ingestor.type]; for (const field of formFields) { if (field.required) { @@ -679,6 +933,147 @@ function Upload({ required={true} labelBgClassName="bg-white dark:bg-charleston-green-2" /> + {ingestor.type === 'google_drive' && ( +
+ {authError && ( +
+

+ ⚠️ {authError} +

+
+ )} + + {!isGoogleDriveConnected ? ( + + ) : ( +
+ {/* Connection Status */} +
+
+ + + + Connected as {userEmail} +
+ +
+ + {/* File Browser */} +
+
+
+

+ Select Files from Google Drive +

+ {googleDriveFiles.length > 0 && ( + + )} +
+ {selectedFiles.length > 0 && ( +

+ {selectedFiles.length} file{selectedFiles.length !== 1 ? 's' : ''} selected +

+ )} +
+ +
+ {isLoadingFiles ? ( +
+
+
+ Loading files... +
+
+ ) : googleDriveFiles.length === 0 ? ( +
+ No files found in your Google Drive +
+ ) : ( +
+ {googleDriveFiles.map((file) => ( +
handleFileSelect(file.id)} + > +
+
+ handleFileSelect(file.id)} + className="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500" + /> +
+
{file.iconUrl}
+
+

+ {file.name} +

+

+ {file.size} • Modified {file.modifiedTime} +

+
+
+
+ ))} +
+ )} +
+
+
+ )} +
+ )} + {renderFormFields()} {IngestorFormSchemas[ingestor.type].some( (field) => field.advanced, @@ -719,7 +1114,10 @@ function Upload({ : 'bg-purple-30 hover:bg-violets-are-blue cursor-pointer text-white' }`} > - {t('modals.uploadDoc.train')} + {ingestor.type === 'google_drive' && selectedFiles.length > 0 + ? `Train with ${selectedFiles.length} file${selectedFiles.length !== 1 ? 's' : ''}` + : t('modals.uploadDoc.train') + } )} diff --git a/frontend/src/upload/types/ingestor.ts b/frontend/src/upload/types/ingestor.ts index cd709847..f5c29dee 100644 --- a/frontend/src/upload/types/ingestor.ts +++ b/frontend/src/upload/types/ingestor.ts @@ -22,7 +22,14 @@ export interface UrlIngestorConfig extends BaseIngestorConfig { url: string; } -export type IngestorType = 'crawler' | 'github' | 'reddit' | 'url'; +export interface GoogleDriveIngestorConfig extends BaseIngestorConfig { + folder_id?: string; + file_ids?: string; + recursive?: boolean; + token_info?: any; +} + +export type IngestorType = 'crawler' | 'github' | 'reddit' | 'url' | 'google_drive'; export interface IngestorConfig { type: IngestorType; @@ -31,7 +38,8 @@ export interface IngestorConfig { | RedditIngestorConfig | GithubIngestorConfig | CrawlerIngestorConfig - | UrlIngestorConfig; + | UrlIngestorConfig + | GoogleDriveIngestorConfig; } export type IngestorFormData = { @@ -109,6 +117,14 @@ export const IngestorFormSchemas: Record = { required: true, }, ], + google_drive: [ + { + name: 'recursive', + label: 'Include subfolders', + type: 'boolean', + required: false, + }, + ], }; export const IngestorDefaultConfigs: Record< @@ -143,4 +159,12 @@ export const IngestorDefaultConfigs: Record< repo_url: '', } as GithubIngestorConfig, }, + google_drive: { + name: '', + config: { + folder_id: '', + file_ids: '', + recursive: true, + } as GoogleDriveIngestorConfig, + }, };