(feat:drive) oauth for drive.file scope, picker

This commit is contained in:
ManishMadan2882
2025-09-17 19:37:01 +05:30
parent ec0c4c3b84
commit da2f8477e6
8 changed files with 344 additions and 14 deletions

View File

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

View File

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

View File

@@ -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",

View File

@@ -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",

View File

@@ -358,7 +358,7 @@ export const FilePicker: React.FC<CloudFilePickerProps> = ({
</div>
</div>
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A] mt-3">
<div className="border border-[#D7D7D7] rounded-lg dark:border-[#6A6A6A] mt-3">
<div className=" border-[#EEE6FF78] dark:border-[#6A6A6A] rounded-t-lg">
{/* Breadcrumb navigation */}
<div className="px-4 pt-4 bg-[#EEE6FF78] dark:bg-[#2A262E] rounded-t-lg">

View File

@@ -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<GoogleDrivePickerProps> = ({
token,
onSelectionChange,
initialSelectedFiles = [],
initialSelectedFolders = [],
}) => {
const [selectedFiles, setSelectedFiles] = useState<PickerFile[]>([]);
const [selectedFolders, setSelectedFolders] = useState<PickerFile[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [userEmail, setUserEmail] = useState<string>('');
const [isConnected, setIsConnected] = useState(false);
const [authError, setAuthError] = useState<string>('');
const [accessToken, setAccessToken] = useState<string | null>(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 (
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A] p-6">
{authError && (
<div className="text-red-500 text-sm mb-4 text-center">{authError}</div>
)}
<ConnectorAuth
provider="google_drive"
onSuccess={(data) => {
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);
}}
/>
</div>
);
}
return (
<div className="border border-[#EEE6FF78] rounded-lg dark:border-[#6A6A6A]">
<div className="p-3">
<div className="w-full flex items-center justify-between rounded-[10px] bg-[#8FDD51] px-4 py-2 text-[#212121] font-medium text-sm">
<div className="flex items-center gap-2">
<svg className="h-4 w-4" viewBox="0 0 24 24">
<path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
<span>Connected as {userEmail}</span>
</div>
<button
onClick={handleDisconnect}
className="text-[#212121] hover:text-gray-700 font-medium text-xs underline"
>
Disconnect
</button>
</div>
</div>
<div className="p-4">
<div className="flex justify-between items-center mb-4">
<h3 className="text-sm font-medium">Selected Files</h3>
<button
onClick={() => handleOpenPicker()}
className="bg-[#A076F6] hover:bg-[#8A5FD4] text-white text-sm py-1 px-3 rounded-md"
disabled={isLoading}
>
{isLoading ? 'Loading...' : 'Select Files'}
</button>
</div>
{selectedFiles.length === 0 && selectedFolders.length === 0 ? (
<p className="text-gray-600 dark:text-gray-400 text-sm">No files or folders selected</p>
) : (
<div className="max-h-60 overflow-y-auto">
{/* Display folders */}
{selectedFolders.length > 0 && (
<div className="mb-2">
<h4 className="text-xs font-medium text-gray-500 mb-1">Folders</h4>
{selectedFolders.map((folder) => (
<div key={folder.id} className="flex items-center p-2 border-b border-gray-200 dark:border-gray-700">
<img src={folder.iconUrl} alt="Folder" className="w-5 h-5 mr-2" />
<span className="text-sm truncate flex-1">{folder.name}</span>
<button
onClick={() => {
const newSelectedFolders = selectedFolders.filter(f => f.id !== folder.id);
setSelectedFolders(newSelectedFolders);
onSelectionChange(
selectedFiles.map(f => f.id),
newSelectedFolders.map(f => f.id)
);
}}
className="text-red-500 hover:text-red-700 text-sm ml-2"
>
Remove
</button>
</div>
))}
</div>
)}
{/* Display files */}
{selectedFiles.length > 0 && (
<div>
<h4 className="text-xs font-medium text-gray-500 mb-1">Files</h4>
{selectedFiles.map((file) => (
<div key={file.id} className="flex items-center p-2 border-b border-gray-200 dark:border-gray-700">
<img src={file.iconUrl} alt="File" className="w-5 h-5 mr-2" />
<span className="text-sm truncate flex-1">{file.name}</span>
<button
onClick={() => {
const newSelectedFiles = selectedFiles.filter(f => f.id !== file.id);
setSelectedFiles(newSelectedFiles);
onSelectionChange(
newSelectedFiles.map(f => f.id),
selectedFolders.map(f => f.id)
);
}}
className="text-red-500 hover:text-red-700 text-sm ml-2"
>
Remove
</button>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
);
};
export default GoogleDrivePicker;

View File

@@ -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 (
<GoogleDrivePicker
key={field.name}
onSelectionChange={(selectedFileIds: string[], selectedFolderIds: string[] = []) => {
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;
}

View File

@@ -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<IngestorType, FormField[]> = {
],
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: [