From 7e2cbdd88c51c352c84499ee1b8e47d9d2169956 Mon Sep 17 00:00:00 2001 From: ManishMadan2882 Date: Wed, 3 Sep 2025 09:57:13 +0530 Subject: [PATCH] (feat:connector) redirect url as backend overhead --- application/api/connector/routes.py | 203 ++++++++---------- application/core/settings.py | 4 +- .../parser/connectors/google_drive/auth.py | 2 +- frontend/public/google-drive-callback.html | 114 ---------- .../src/components/ConnectorTreeComponent.tsx | 165 +++++++------- 5 files changed, 190 insertions(+), 298 deletions(-) delete mode 100644 frontend/public/google-drive-callback.html diff --git a/application/api/connector/routes.py b/application/api/connector/routes.py index 014fc71f..821b2d91 100644 --- a/application/api/connector/routes.py +++ b/application/api/connector/routes.py @@ -255,136 +255,64 @@ class ConnectorsCallback(Resource): """Handle OAuth callback for external connectors""" try: from application.parser.connectors.connector_creator import ConnectorCreator - from flask import request + from flask import request, redirect import uuid + provider = request.args.get('provider', 'google_drive') authorization_code = request.args.get('code') _ = request.args.get('state') error = request.args.get('error') if error: - return make_response( - jsonify({"success": False, "error": f"OAuth error: {error}. Please try again and make sure to grant all requested permissions, including offline access."}), 400 - ) + return redirect(f"/api/connectors/callback-status?status=error&message=OAuth+error:+{error}.+Please+try+again+and+make+sure+to+grant+all+requested+permissions,+including+offline+access.&provider={provider}") if not authorization_code: - return make_response( - jsonify({"success": False, "error": "Authorization code not provided. Please complete the authorization process and make sure to grant offline access."}), 400 - ) + return redirect(f"/api/connectors/callback-status?status=error&message=Authorization+code+not+provided.+Please+complete+the+authorization+process+and+make+sure+to+grant+offline+access.&provider={provider}") try: - auth = ConnectorCreator.create_auth("google_drive") + auth = ConnectorCreator.create_auth(provider) token_info = auth.exchange_code_for_tokens(authorization_code) - current_app.logger.info(f"Token info received from OAuth callback - has refresh_token: {bool(token_info.get('refresh_token'))}, " - f"has access_token: {bool(token_info.get('access_token'))}, " - f"expiry: {token_info.get('expiry')}") - safe_token_info = {k: v for k, v in token_info.items() if k not in ['access_token', 'refresh_token', 'client_secret']} - current_app.logger.info(f"Full token info structure: {safe_token_info}") + session_token = str(uuid.uuid4()) + - # Validate that we got token info - if not token_info: - current_app.logger.error("exchange_code_for_tokens returned None or empty result") - return make_response( - jsonify({"success": False, "error": "Failed to exchange authorization code for tokens. Please try again and make sure to grant all requested permissions, including offline access."}), 400 - ) + try: + credentials = auth.create_credentials_from_token_info(token_info) + service = auth.build_drive_service(credentials) + user_info = service.about().get(fields="user").execute() + user_email = user_info.get('user', {}).get('emailAddress', 'Connected User') + except Exception as e: + current_app.logger.warning(f"Could not get user info: {e}") + user_email = 'Connected User' - # Validate required fields in token_info - required_fields = ['access_token', 'token_uri', 'client_id', 'client_secret'] - missing_fields = [field for field in required_fields if not token_info.get(field)] - if missing_fields: - current_app.logger.error(f"Token info missing required fields: {missing_fields}") - return make_response( - jsonify({"success": False, "error": f"Token information incomplete. Missing fields: {missing_fields}. Please try again and make sure to grant all requested permissions."}), 400 - ) + sanitized_token_info = { + "access_token": token_info.get("access_token"), + "refresh_token": token_info.get("refresh_token"), + "token_uri": token_info.get("token_uri"), + "expiry": token_info.get("expiry"), + "scopes": token_info.get("scopes") + } - if not token_info.get('refresh_token'): - current_app.logger.warning("OAuth flow did not return a refresh token - user will need to re-authenticate when token expires") - - required_fields = ['access_token', 'token_uri', 'client_id', 'client_secret'] - missing_fields = [field for field in required_fields if not token_info.get(field)] - if missing_fields: - current_app.logger.error(f"Token info missing required fields: {missing_fields}") - return make_response( - jsonify({"success": False, "error": f"Token info missing required fields: {missing_fields}"}), 400 - ) + user_id = request.decoded_token.get("sub") if getattr(request, "decoded_token", None) else None + sessions_collection.insert_one({ + "session_token": session_token, + "user": user_id, + "token_info": sanitized_token_info, + "created_at": datetime.datetime.now(datetime.timezone.utc), + "user_email": user_email, + "provider": provider + }) + + # Redirect to success page with session token and user email + return redirect(f"/api/connectors/callback-status?status=success&message=Authentication+successful&provider={provider}&session_token={session_token}&user_email={user_email}") except Exception as e: current_app.logger.error(f"Error exchanging code for tokens: {str(e)}", exc_info=True) - - if 'refresh' in str(e).lower(): - current_app.logger.warning(f"Missing refresh token but continuing: {str(e)}") - - else: - return make_response( - jsonify({"success": False, "error": f"Failed to exchange authorization code for tokens: {str(e)}"}), 400 - ) - - try: - credentials = auth.create_credentials_from_token_info(token_info) - service = auth.build_drive_service(credentials) - user_info = service.about().get(fields="user").execute() - user_email = user_info.get('user', {}).get('emailAddress', 'Connected User') - except Exception as e: - current_app.logger.warning(f"Could not get user info: {e}") - if token_info.get('access_token'): - try: - import requests - headers = {'Authorization': f'Bearer {token_info["access_token"]}'} - response = requests.get( - 'https://www.googleapis.com/drive/v3/about?fields=user', - headers=headers - ) - if response.status_code == 200: - user_info = response.json() - user_email = user_info.get('user', {}).get('emailAddress', 'Connected User') - else: - user_email = 'Connected User' - except Exception as request_error: - current_app.logger.warning(f"Could not get user info via direct request: {request_error}") - user_email = 'Connected User' - else: - user_email = 'Connected User' - - session_token = str(uuid.uuid4()) - - - - sanitized_token_info = { - "access_token": token_info.get("access_token"), - "refresh_token": token_info.get("refresh_token"), - "token_uri": token_info.get("token_uri"), - "expiry": token_info.get("expiry"), - "scopes": token_info.get("scopes") - } - - user_id = request.decoded_token.get("sub") if getattr(request, "decoded_token", None) else None - sessions_collection.insert_one({ - "session_token": session_token, - "user": user_id, - "token_info": sanitized_token_info, - "created_at": datetime.datetime.now(datetime.timezone.utc), - "user_email": user_email - }) - - return make_response( - jsonify({ - "success": True, - "message": "Google Drive authentication successful", - "session_token": session_token, - "user_email": user_email - }), - 200 - ) + return redirect(f"/api/connectors/callback-status?status=error&message=Failed+to+exchange+authorization+code+for+tokens:+{str(e)}&provider={provider}") except Exception as e: current_app.logger.error(f"Error handling connector callback: {e}") - return make_response( - jsonify({ - "success": False, - "error": f"Failed to complete connector authentication: {str(e)}. Please try again and make sure to grant all requested permissions, including offline access." - }), 500 - ) + return redirect(f"/api/connectors/callback-status?status=error&message=Failed+to+complete+connector+authentication:+{str(e)}.+Please+try+again+and+make+sure+to+grant+all+requested+permissions,+including+offline+access.") @connectors_ns.route("/api/connectors/refresh") @@ -623,4 +551,63 @@ class ConnectorSync(Resource): ) +@connectors_ns.route("/api/connectors/callback-status") +class ConnectorCallbackStatus(Resource): + @api.doc(description="Return HTML page with connector authentication status") + def get(self): + """Return HTML page with connector authentication status""" + try: + status = request.args.get('status', 'error') + message = request.args.get('message', '') + provider = request.args.get('provider', 'connector') + session_token = request.args.get('session_token', '') + user_email = request.args.get('user_email', '') + + html_content = f""" + + + + {provider.replace('_', ' ').title()} Authentication + + + + +
+

{provider.replace('_', ' ').title()} Authentication

+
+

{message}

+ {f'

Connected as: {user_email}

' if status == 'success' else ''} +
+

You can close this window. {f"Your {provider.replace('_', ' ').title()} is now connected and ready to use." if status == 'success' else ''}

+
+ + + """ + + return make_response(html_content, 200, {'Content-Type': 'text/html'}) + except Exception as e: + current_app.logger.error(f"Error rendering callback status page: {e}") + return make_response("Authentication error occurred", 500, {'Content-Type': 'text/html'}) + diff --git a/application/core/settings.py b/application/core/settings.py index f1000056..cb7d75e3 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -43,7 +43,9 @@ class Settings(BaseSettings): # Google Drive integration GOOGLE_CLIENT_ID: Optional[str] = None # Replace with your actual Google OAuth client ID GOOGLE_CLIENT_SECRET: Optional[str] = None# Replace with your actual Google OAuth client secret - GOOGLE_REDIRECT_URI: Optional[str] = None + CONNECTOR_REDIRECT_BASE_URI: Optional[str] = "http://127.0.0.1:7091/api/connectors/callback" + ##append ?provider={provider_name} in your Provider console like http://127.0.0.1:7091/api/connectors/callback?provider=google_drive + # LLM Cache CACHE_REDIS_URL: str = "redis://localhost:6379/2" diff --git a/application/parser/connectors/google_drive/auth.py b/application/parser/connectors/google_drive/auth.py index 5a903653..37d55dcc 100644 --- a/application/parser/connectors/google_drive/auth.py +++ b/application/parser/connectors/google_drive/auth.py @@ -24,7 +24,7 @@ class GoogleDriveAuth(BaseConnectorAuth): def __init__(self): self.client_id = settings.GOOGLE_CLIENT_ID self.client_secret = settings.GOOGLE_CLIENT_SECRET - self.redirect_uri = settings.GOOGLE_REDIRECT_URI or "http://localhost:7091/api/google-drive/callback" + self.redirect_uri = f"{settings.CONNECTOR_REDIRECT_BASE_URI}?provider=google_drive" if not self.client_id or not self.client_secret: raise ValueError("Google OAuth credentials not configured. Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in settings.") diff --git a/frontend/public/google-drive-callback.html b/frontend/public/google-drive-callback.html deleted file mode 100644 index d2113624..00000000 --- a/frontend/public/google-drive-callback.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - Google Drive Authentication - - - -
-

Google Drive Authentication

-
Processing authentication...
-
- - - - diff --git a/frontend/src/components/ConnectorTreeComponent.tsx b/frontend/src/components/ConnectorTreeComponent.tsx index 972c55f7..dfd868b3 100644 --- a/frontend/src/components/ConnectorTreeComponent.tsx +++ b/frontend/src/components/ConnectorTreeComponent.tsx @@ -64,7 +64,6 @@ const ConnectorTreeComponent: React.FC = ({ const [sourceProvider, setSourceProvider] = useState(''); const [syncDone, setSyncDone] = useState(false); - useOutsideAlerter( searchDropdownRef, () => { @@ -84,7 +83,6 @@ const ConnectorTreeComponent: React.FC = ({ }; const handleSync = async () => { - if (isSyncing) return; const provider = sourceProvider; @@ -106,10 +104,16 @@ const ConnectorTreeComponent: React.FC = ({ for (let attempt = 0; attempt < maxAttempts; attempt++) { try { - const statusResponse = await userService.getTaskStatus(data.task_id, token); + const statusResponse = await userService.getTaskStatus( + data.task_id, + token, + ); const statusData = await statusResponse.json(); - console.log(`Task status (attempt ${attempt + 1}):`, statusData.status); + console.log( + `Task status (attempt ${attempt + 1}):`, + statusData.status, + ); if (statusData.status === 'SUCCESS') { setSyncProgress(100); @@ -117,7 +121,10 @@ const ConnectorTreeComponent: React.FC = ({ // Refresh directory structure try { - const refreshResponse = await userService.getDirectoryStructure(docId, token); + const refreshResponse = await userService.getDirectoryStructure( + docId, + token, + ); const refreshData = await refreshResponse.json(); if (refreshData && refreshData.directory_structure) { setDirectoryStructure(refreshData.directory_structure); @@ -137,12 +144,13 @@ const ConnectorTreeComponent: React.FC = ({ console.error('Sync task failed:', statusData.result); break; } else if (statusData.status === 'PROGRESS') { - - const progress = Number((statusData.result && statusData.result.current != null) - ? statusData.result.current - : (statusData.meta && statusData.meta.current != null) - ? statusData.meta.current - : 0); + const progress = Number( + statusData.result && statusData.result.current != null + ? statusData.result.current + : statusData.meta && statusData.meta.current != null + ? statusData.meta.current + : 0, + ); setSyncProgress(Math.max(10, progress)); } @@ -168,7 +176,10 @@ const ConnectorTreeComponent: React.FC = ({ try { setLoading(true); - const directoryResponse = await userService.getDirectoryStructure(docId, token); + const directoryResponse = await userService.getDirectoryStructure( + docId, + token, + ); const directoryData = await directoryResponse.json(); if (directoryData && directoryData.directory_structure) { @@ -261,7 +272,6 @@ const ConnectorTreeComponent: React.FC = ({ variant: 'primary', }); - return options; }; @@ -301,18 +311,18 @@ const ConnectorTreeComponent: React.FC = ({ const renderPathNavigation = () => { return ( -
+
{/* Left side with path navigation */}
- + {sourceName} {currentPath.length > 0 && ( @@ -324,7 +334,9 @@ const ConnectorTreeComponent: React.FC = ({ {dir} {index < currentPath.length - 1 && ( - / + + / + )} ))} @@ -333,29 +345,36 @@ const ConnectorTreeComponent: React.FC = ({
-
- +
{renderFileSearch()} {/* Sync button */}
@@ -369,46 +388,47 @@ const ConnectorTreeComponent: React.FC = ({ const parentRow = currentPath.length > 0 ? [ - - -
- {t('settings.sources.parentFolderAlt')} - - .. - -
- - - - - - - - - - - , - ] + + +
+ {t('settings.sources.parentFolderAlt')} + + .. + +
+ + + - + + + - + + + , + ] : []; // Sort entries: directories first, then files, both alphabetically - const sortedEntries = Object.entries(directory).sort(([nameA, nodeA], [nameB, nodeB]) => { - const isFileA = !!nodeA.type; - const isFileB = !!nodeB.type; + const sortedEntries = Object.entries(directory).sort( + ([nameA, nodeA], [nameB, nodeB]) => { + const isFileA = !!nodeA.type; + const isFileB = !!nodeB.type; - if (isFileA !== isFileB) { - return isFileA ? 1 : -1; // Directories first - } - - return nameA.localeCompare(nameB); // Alphabetical within each group - }); + if (isFileA !== isFileB) { + return isFileA ? 1 : -1; // Directories first + } + return nameA.localeCompare(nameB); // Alphabetical within each group + }, + ); // Process directories const directoryRows = sortedEntries @@ -450,7 +470,7 @@ const ConnectorTreeComponent: React.FC = ({