From 8edb6dcf2abbd189c87a871f88363f327633bc1a Mon Sep 17 00:00:00 2001 From: Abhishek Malviya Date: Tue, 7 Oct 2025 15:23:32 +0530 Subject: [PATCH 1/4] feat: add Microsoft Entra ID integration - Updated .env-template and settings.py for Microsoft Entra ID configuration. - Enhanced ConnectorsCallback to support SharePoint authentication. - Introduced SharePointAuth and SharePointLoader classes. - Added required dependencies in requirements.txt. --- .env-template | 18 +++- application/api/connector/routes.py | 12 ++- application/core/settings.py | 7 ++ .../parser/connectors/connector_creator.py | 4 + .../parser/connectors/share_point/auth.py | 91 +++++++++++++++++++ .../parser/connectors/share_point/loader.py | 44 +++++++++ application/requirements.txt | 3 +- 7 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 application/parser/connectors/share_point/auth.py create mode 100644 application/parser/connectors/share_point/loader.py diff --git a/.env-template b/.env-template index e93f03635..b733bdf96 100644 --- a/.env-template +++ b/.env-template @@ -6,4 +6,20 @@ VITE_API_STREAMING=true OPENAI_API_BASE= OPENAI_API_VERSION= AZURE_DEPLOYMENT_NAME= -AZURE_EMBEDDINGS_DEPLOYMENT_NAME= \ No newline at end of file +AZURE_EMBEDDINGS_DEPLOYMENT_NAME= + +#Azure AD Application (client) ID +MICROSOFT_CLIENT_ID=your-azure-ad-client-id +#Azure AD Application client secret +MICROSOFT_CLIENT_SECRET=your-azure-ad-client-secret +#Azure AD Tenant ID (or 'common' for multi-tenant) +MICROSOFT_TENANT_ID=your-azure-ad-tenant-id +#Your project's redirect URI that you registered in Azure Portal. +#For example: http://localhost:5000/redirect +MICROSOFT_REDIRECT_URI=http://localhost:7091/api/connectors/callback/ms_entra_id +#If you are using a Microsoft Entra ID tenant, +#configure the AUTHORITY variable as +#"https://login.microsoftonline.com/TENANT_GUID" +#or "https://login.microsoftonline.com/contoso.onmicrosoft.com". +#Alternatively, use "https://login.microsoftonline.com/common" for multi-tenant app. +MICROSOFT_AUTHORITY=https://{tenentId}.ciamlogin.com/{tenentId} diff --git a/application/api/connector/routes.py b/application/api/connector/routes.py index 49307058e..8b9cee382 100644 --- a/application/api/connector/routes.py +++ b/application/api/connector/routes.py @@ -298,10 +298,14 @@ def get(self): session_token = str(uuid.uuid4()) 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') + if provider == "google_drive": + 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') + else: + user_email = token_info.get('user_info', {}).get('email', 'Connected User') + except Exception as e: current_app.logger.warning(f"Could not get user info: {e}") user_email = 'Connected User' diff --git a/application/core/settings.py b/application/core/settings.py index 4475c4439..674b2f9c2 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -51,6 +51,13 @@ class Settings(BaseSettings): "http://127.0.0.1:7091/api/connectors/callback" ##add redirect url as it is to your provider's console(gcp) ) + # Microsoft Entra ID (Azure AD) integration + MICROSOFT_CLIENT_ID: Optional[str] = None # Azure AD Application (client) ID + MICROSOFT_CLIENT_SECRET: Optional[str] = None # Azure AD Application client secret + MICROSOFT_TENANT_ID: Optional[str] = "common" # Azure AD Tenant ID (or 'common' for multi-tenant) + MICROSOFT_REDIRECT_URI: Optional[str] = "http://localhost:7091/api/connectors/callback" # Your project's redirect URI that you registered in Azure Portal. + MICROSOFT_AUTHORITY: Optional[str] = None # e.g., "https://login.microsoftonline.com/{tenant_id}" + # LLM Cache CACHE_REDIS_URL: str = "redis://localhost:6379/2" diff --git a/application/parser/connectors/connector_creator.py b/application/parser/connectors/connector_creator.py index bf4456caa..609e6407c 100644 --- a/application/parser/connectors/connector_creator.py +++ b/application/parser/connectors/connector_creator.py @@ -1,5 +1,7 @@ from application.parser.connectors.google_drive.loader import GoogleDriveLoader from application.parser.connectors.google_drive.auth import GoogleDriveAuth +from application.parser.connectors.share_point.auth import SharePointAuth +from application.parser.connectors.share_point.loader import SharePointLoader class ConnectorCreator: @@ -12,10 +14,12 @@ class ConnectorCreator: connectors = { "google_drive": GoogleDriveLoader, + "share_point": SharePointLoader, } auth_providers = { "google_drive": GoogleDriveAuth, + "share_point": SharePointAuth, } @classmethod diff --git a/application/parser/connectors/share_point/auth.py b/application/parser/connectors/share_point/auth.py new file mode 100644 index 000000000..504732cc8 --- /dev/null +++ b/application/parser/connectors/share_point/auth.py @@ -0,0 +1,91 @@ +import logging +import datetime +from typing import Optional, Dict, Any + +from msal import ConfidentialClientApplication + +from application.core.settings import settings +from application.parser.connectors.base import BaseConnectorAuth + + +class SharePointAuth(BaseConnectorAuth): + """ + Handles Microsoft OAuth 2.0 authentication. + + # Documentation: + - https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow + - https://learn.microsoft.com/en-gb/entra/msal/python/ + """ + + # Microsoft Graph scopes for SharePoint access + SCOPES = [ + "User.Read", + ] + + def __init__(self): + self.client_id = settings.MICROSOFT_CLIENT_ID + self.client_secret = settings.MICROSOFT_CLIENT_SECRET + + if not self.client_id or not self.client_secret: + raise ValueError( + "Microsoft OAuth credentials not configured. Please set MICROSOFT_CLIENT_ID and MICROSOFT_CLIENT_SECRET in settings." + ) + + self.redirect_uri = settings.MICROSOFT_REDIRECT_URI + self.tenant_id = settings.MICROSOFT_TENANT_ID + self.authority = getattr(settings, "MICROSOFT_AUTHORITY", f"https://{self.tenant_id}.ciamlogin.com/{self.tenant_id}") + + self.auth_app = ConfidentialClientApplication( + client_id=self.client_id, client_credential=self.client_secret, authority=self.authority + ) + + def get_authorization_url(self, state: Optional[str] = None) -> str: + return self.auth_app.get_authorization_request_url( + scopes=self.SCOPES, state=state, redirect_uri=self.redirect_uri + ) + + def exchange_code_for_tokens(self, authorization_code: str) -> Dict[str, Any]: + result = self.auth_app.acquire_token_by_authorization_code( + code=authorization_code, scopes=self.SCOPES, redirect_uri=self.redirect_uri + ) + + if "error" in result: + logging.error(f"Error acquiring token: {result.get('error_description')}") + raise ValueError(f"Error acquiring token: {result.get('error_description')}") + + return self.map_token_response(result) + + def refresh_access_token(self, refresh_token: str) -> Dict[str, Any]: + result = self.auth_app.acquire_token_by_refresh_token(refresh_token=refresh_token, scopes=self.SCOPES) + + if "error" in result: + logging.error(f"Error acquiring token: {result.get('error_description')}") + raise ValueError(f"Error acquiring token: {result.get('error_description')}") + + return self.map_token_response(result) + + def is_token_expired(self, token_info: Dict[str, Any]) -> bool: + if not token_info or "expiry" not in token_info: + # If no expiry info, consider token expired to be safe + return True + + # Get expiry timestamp and current time + expiry_timestamp = token_info["expiry"] + current_timestamp = int(datetime.datetime.now().timestamp()) + + # Token is expired if current time is greater than or equal to expiry time + return current_timestamp >= expiry_timestamp + + def map_token_response(self, result) -> Dict[str, Any]: + return { + "access_token": result.get("access_token"), + "refresh_token": result.get("refresh_token"), + "token_uri": result.get("id_token_claims", {}).get("iss"), + "scopes": result.get("scope"), + "expiry": result.get("id_token_claims", {}).get("exp"), + "user_info": { + "name": result.get("id_token_claims", {}).get("name"), + "email": result.get("id_token_claims", {}).get("preferred_username"), + }, + "raw_token": result, + } diff --git a/application/parser/connectors/share_point/loader.py b/application/parser/connectors/share_point/loader.py new file mode 100644 index 000000000..ea081afea --- /dev/null +++ b/application/parser/connectors/share_point/loader.py @@ -0,0 +1,44 @@ +from typing import List, Dict, Any +from application.parser.connectors.base import BaseConnectorLoader +from application.parser.schema.base import Document + + +class SharePointLoader(BaseConnectorLoader): + def __init__(self, session_token: str): + pass + + def load_data(self, inputs: Dict[str, Any]) -> List[Document]: + """ + Load documents from the external knowledge base. + + Args: + inputs: Configuration dictionary containing: + - file_ids: Optional list of specific file IDs to load + - folder_ids: Optional list of folder IDs to browse/download + - limit: Maximum number of items to return + - list_only: If True, return metadata without content + - recursive: Whether to recursively process folders + + Returns: + List of Document objects + """ + pass + + def download_to_directory(self, local_dir: str, source_config: Dict[str, Any] = None) -> Dict[str, Any]: + """ + Download files/folders to a local directory. + + Args: + local_dir: Local directory path to download files to + source_config: Configuration for what to download + + Returns: + Dictionary containing download results: + - files_downloaded: Number of files downloaded + - directory_path: Path where files were downloaded + - empty_result: Whether no files were downloaded + - source_type: Type of connector + - config_used: Configuration that was used + - error: Error message if download failed (optional) + """ + pass diff --git a/application/requirements.txt b/application/requirements.txt index 3882bd6dc..396ea358b 100644 --- a/application/requirements.txt +++ b/application/requirements.txt @@ -40,6 +40,7 @@ markupsafe==3.0.2 marshmallow==3.26.1 mpmath==1.3.0 multidict==6.4.3 +msal==1.34.0 mypy-extensions==1.0.0 networkx==3.4.2 numpy==2.2.1 @@ -87,4 +88,4 @@ werkzeug>=3.1.0,<3.1.2 yarl==1.20.0 markdownify==1.1.0 tldextract==5.1.3 -websockets==14.1 +websockets==14.1 \ No newline at end of file From 2b73c0c9a0f8d5372937955ccd177b4d4eaa3f31 Mon Sep 17 00:00:00 2001 From: Abhishek Malviya Date: Wed, 8 Oct 2025 10:34:38 +0530 Subject: [PATCH 2/4] feat: add init for Share Point connector module --- application/parser/connectors/share_point/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 application/parser/connectors/share_point/__init__.py diff --git a/application/parser/connectors/share_point/__init__.py b/application/parser/connectors/share_point/__init__.py new file mode 100644 index 000000000..b83bb56f4 --- /dev/null +++ b/application/parser/connectors/share_point/__init__.py @@ -0,0 +1,10 @@ +""" +Share Point connector package for DocsGPT. + +This module provides authentication and document loading capabilities for Share Point. +""" + +from .auth import SharePointAuth +from .loader import SharePointLoader + +__all__ = ['SharePointAuth', 'SharePointLoader'] \ No newline at end of file From d9f00721120ea138485cffe8708715071c935f1a Mon Sep 17 00:00:00 2001 From: Abhishek Malviya Date: Thu, 9 Oct 2025 10:36:12 +0530 Subject: [PATCH 3/4] refactor: remove MICROSOFT_REDIRECT_URI and update SharePointAuth to use CONNECTOR_REDIRECT_BASE_URI --- .env-template | 3 --- application/core/settings.py | 1 - application/parser/connectors/share_point/auth.py | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.env-template b/.env-template index b733bdf96..30158b77d 100644 --- a/.env-template +++ b/.env-template @@ -14,9 +14,6 @@ MICROSOFT_CLIENT_ID=your-azure-ad-client-id MICROSOFT_CLIENT_SECRET=your-azure-ad-client-secret #Azure AD Tenant ID (or 'common' for multi-tenant) MICROSOFT_TENANT_ID=your-azure-ad-tenant-id -#Your project's redirect URI that you registered in Azure Portal. -#For example: http://localhost:5000/redirect -MICROSOFT_REDIRECT_URI=http://localhost:7091/api/connectors/callback/ms_entra_id #If you are using a Microsoft Entra ID tenant, #configure the AUTHORITY variable as #"https://login.microsoftonline.com/TENANT_GUID" diff --git a/application/core/settings.py b/application/core/settings.py index 03ea24689..490717fc5 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -55,7 +55,6 @@ class Settings(BaseSettings): MICROSOFT_CLIENT_ID: Optional[str] = None # Azure AD Application (client) ID MICROSOFT_CLIENT_SECRET: Optional[str] = None # Azure AD Application client secret MICROSOFT_TENANT_ID: Optional[str] = "common" # Azure AD Tenant ID (or 'common' for multi-tenant) - MICROSOFT_REDIRECT_URI: Optional[str] = "http://localhost:7091/api/connectors/callback" # Your project's redirect URI that you registered in Azure Portal. MICROSOFT_AUTHORITY: Optional[str] = None # e.g., "https://login.microsoftonline.com/{tenant_id}" # GitHub source GITHUB_ACCESS_TOKEN: Optional[str] = None # PAT token with read repo access diff --git a/application/parser/connectors/share_point/auth.py b/application/parser/connectors/share_point/auth.py index 504732cc8..85120666c 100644 --- a/application/parser/connectors/share_point/auth.py +++ b/application/parser/connectors/share_point/auth.py @@ -31,7 +31,7 @@ def __init__(self): "Microsoft OAuth credentials not configured. Please set MICROSOFT_CLIENT_ID and MICROSOFT_CLIENT_SECRET in settings." ) - self.redirect_uri = settings.MICROSOFT_REDIRECT_URI + self.redirect_uri = settings.CONNECTOR_REDIRECT_BASE_URI self.tenant_id = settings.MICROSOFT_TENANT_ID self.authority = getattr(settings, "MICROSOFT_AUTHORITY", f"https://{self.tenant_id}.ciamlogin.com/{self.tenant_id}") From cc54cea783c2eaaf238399fc474f889fece63c6e Mon Sep 17 00:00:00 2001 From: Abhishek Malviya Date: Fri, 10 Oct 2025 15:15:38 +0530 Subject: [PATCH 4/4] feat: add SharePoint integration with session validation and UI components --- frontend/src/assets/sharepoint.svg | 16 ++ .../src/components/ConnectedStateSkeleton.tsx | 13 ++ frontend/src/components/ConnectorAuth.tsx | 2 +- .../src/components/FileSelectionSkeleton.tsx | 13 ++ frontend/src/components/GoogleDrivePicker.tsx | 44 +---- frontend/src/components/SharePointPicker.tsx | 175 ++++++++++++++++++ frontend/src/locale/en.json | 22 +++ frontend/src/locale/es.json | 22 +++ frontend/src/locale/jp.json | 22 +++ frontend/src/locale/ru.json | 22 +++ frontend/src/locale/zh-TW.json | 22 +++ frontend/src/locale/zh.json | 22 +++ frontend/src/upload/Upload.tsx | 3 + frontend/src/upload/types/ingestor.ts | 33 +++- frontend/src/utils/providerUtils.ts | 18 ++ 15 files changed, 408 insertions(+), 41 deletions(-) create mode 100644 frontend/src/assets/sharepoint.svg create mode 100644 frontend/src/components/ConnectedStateSkeleton.tsx create mode 100644 frontend/src/components/FileSelectionSkeleton.tsx create mode 100644 frontend/src/components/SharePointPicker.tsx diff --git a/frontend/src/assets/sharepoint.svg b/frontend/src/assets/sharepoint.svg new file mode 100644 index 000000000..9a332f8ed --- /dev/null +++ b/frontend/src/assets/sharepoint.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/ConnectedStateSkeleton.tsx b/frontend/src/components/ConnectedStateSkeleton.tsx new file mode 100644 index 000000000..f871fc9b6 --- /dev/null +++ b/frontend/src/components/ConnectedStateSkeleton.tsx @@ -0,0 +1,13 @@ +const ConnectedStateSkeleton = () => ( +
+
+
+
+
+
+
+
+
+); + +export default ConnectedStateSkeleton; diff --git a/frontend/src/components/ConnectorAuth.tsx b/frontend/src/components/ConnectorAuth.tsx index a60d293c5..6411df90f 100644 --- a/frontend/src/components/ConnectorAuth.tsx +++ b/frontend/src/components/ConnectorAuth.tsx @@ -150,7 +150,7 @@ const ConnectorAuth: React.FC = ({ {isConnected ? (
-
+
( +
+
+
+
+
+
+
+
+
+); + +export default FilesSectionSkeleton; diff --git a/frontend/src/components/GoogleDrivePicker.tsx b/frontend/src/components/GoogleDrivePicker.tsx index be7f178e9..bb3d240b2 100644 --- a/frontend/src/components/GoogleDrivePicker.tsx +++ b/frontend/src/components/GoogleDrivePicker.tsx @@ -7,7 +7,10 @@ import { getSessionToken, setSessionToken, removeSessionToken, + validateProviderSession, } from '../utils/providerUtils'; +import ConnectedStateSkeleton from './ConnectedStateSkeleton'; +import FilesSectionSkeleton from './FileSelectionSkeleton'; interface PickerFile { id: string; @@ -50,20 +53,9 @@ const GoogleDrivePicker: React.FC = ({ 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, - }), - }, + const validateResponse = await validateProviderSession( + token, + 'google_drive', ); if (!validateResponse.ok) { @@ -234,30 +226,6 @@ const GoogleDrivePicker: React.FC = ({ onSelectionChange([], []); }; - const ConnectedStateSkeleton = () => ( -
-
-
-
-
-
-
-
-
- ); - - const FilesSectionSkeleton = () => ( -
-
-
-
-
-
-
-
-
- ); - return (
{isValidating ? ( diff --git a/frontend/src/components/SharePointPicker.tsx b/frontend/src/components/SharePointPicker.tsx new file mode 100644 index 000000000..682fd1cd6 --- /dev/null +++ b/frontend/src/components/SharePointPicker.tsx @@ -0,0 +1,175 @@ +import { useTranslation } from 'react-i18next'; +import ConnectorAuth from './ConnectorAuth'; +import { useEffect, useState } from 'react'; + +import { + getSessionToken, + setSessionToken, + removeSessionToken, + validateProviderSession, +} from '../utils/providerUtils'; +import ConnectedStateSkeleton from './ConnectedStateSkeleton'; +import FilesSectionSkeleton from './FileSelectionSkeleton'; + +interface SharePointPickerProps { + token: string | null; +} + +const SharePointPicker: React.FC = ({ token }) => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [userEmail, setUserEmail] = useState(''); + const [isConnected, setIsConnected] = useState(false); + const [authError, setAuthError] = useState(''); + const [accessToken, setAccessToken] = useState(null); + const [isValidating, setIsValidating] = useState(false); + + useEffect(() => { + const sessionToken = getSessionToken('share_point'); + if (sessionToken) { + setIsValidating(true); + setIsConnected(true); // Optimistically set as connected for skeleton + validateSession(sessionToken); + } + }, [token]); + + const validateSession = async (sessionToken: string) => { + try { + const validateResponse = await validateProviderSession( + token, + 'share_point', + ); + + if (!validateResponse.ok) { + setIsConnected(false); + setAuthError( + t('modals.uploadDoc.connectors.sharePoint.sessionExpired'), + ); + setIsValidating(false); + return false; + } + + const validateData = await validateResponse.json(); + if (validateData.success) { + setUserEmail( + validateData.user_email || + t('modals.uploadDoc.connectors.auth.connectedUser'), + ); + setIsConnected(true); + setAuthError(''); + setAccessToken(validateData.access_token || null); + setIsValidating(false); + + return true; + } else { + setIsConnected(false); + setAuthError( + validateData.error || + t('modals.uploadDoc.connectors.sharePoint.sessionExpiredGeneric'), + ); + setIsValidating(false); + return false; + } + } catch (error) { + console.error('Error validating session:', error); + setAuthError(t('modals.uploadDoc.connectors.sharePoint.validateFailed')); + setIsConnected(false); + setIsValidating(false); + return false; + } + }; + + const handleDisconnect = async () => { + const sessionToken = getSessionToken('share_point'); + 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: 'share_point', + session_token: sessionToken, + }), + }); + } catch (err) { + console.error('Error disconnecting from SharePoint:', err); + } + } + + removeSessionToken('share_point'); + setIsConnected(false); + setAccessToken(null); + setUserEmail(''); + setAuthError(''); + }; + + const handleOpenPicker = async () => { + alert('Feature not supported yet.'); + }; + + return ( +
+ {isValidating ? ( + <> + + + + ) : ( + <> + { + setUserEmail( + data.user_email || + t('modals.uploadDoc.connectors.auth.connectedUser'), + ); + setIsConnected(true); + setAuthError(''); + + if (data.session_token) { + setSessionToken('share_point', data.session_token); + validateSession(data.session_token); + } + }} + onError={(error) => { + setAuthError(error); + setIsConnected(false); + }} + isConnected={isConnected} + userEmail={userEmail} + onDisconnect={handleDisconnect} + errorMessage={authError} + /> + + {isConnected && ( +
+
+
+

+ {t('modals.uploadDoc.connectors.sharePoint.selectedFiles')} +

+ +
+
+
+ )} + + )} +
+ ); +}; + +export default SharePointPicker; diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 25a8fb9e6..9620738bb 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -297,6 +297,10 @@ "google_drive": { "label": "Google Drive", "heading": "Upload from Google Drive" + }, + "share_point": { + "label": "SharePoint", + "heading": "Upload from SharePoint" } }, "connectors": { @@ -326,6 +330,24 @@ "remove": "Remove", "folderAlt": "Folder", "fileAlt": "File" + }, + "sharePoint": { + "connect": "Connect to SharePoint", + "sessionExpired": "Session expired. Please reconnect to SharePoint.", + "sessionExpiredGeneric": "Session expired. Please reconnect your account.", + "validateFailed": "Failed to validate session. Please reconnect.", + "noSession": "No valid session found. Please reconnect to SharePoint.", + "noAccessToken": "No access token available. Please reconnect to SharePoint.", + "pickerFailed": "Failed to open file picker. Please try again.", + "selectedFiles": "Selected Files", + "selectFiles": "Select Files", + "loading": "Loading...", + "noFilesSelected": "No files or folders selected", + "folders": "Folders", + "files": "Files", + "remove": "Remove", + "folderAlt": "Folder", + "fileAlt": "File" } } }, diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json index cedc32326..7f6d06904 100644 --- a/frontend/src/locale/es.json +++ b/frontend/src/locale/es.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "Subir desde Google Drive" + }, + "share_point": { + "label": "SharePoint", + "heading": "Subir desde SharePoint" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "Eliminar", "folderAlt": "Carpeta", "fileAlt": "Archivo" + }, + "sharePoint": { + "connect": "Conectar a SharePoint", + "sessionExpired": "Sesión expirada. Por favor, reconecte a SharePoint.", + "sessionExpiredGeneric": "Sesión expirada. Por favor, reconecte su cuenta.", + "validateFailed": "Error al validar la sesión. Por favor, reconecte.", + "noSession": "No se encontró una sesión válida. Por favor, reconecte a SharePoint.", + "noAccessToken": "No hay token de acceso disponible. Por favor, reconecte a SharePoint.", + "pickerFailed": "Error al abrir el selector de archivos. Por favor, inténtelo de nuevo.", + "selectedFiles": "Archivos Seleccionados", + "selectFiles": "Seleccionar Archivos", + "loading": "Cargando...", + "noFilesSelected": "No hay archivos o carpetas seleccionados", + "folders": "Carpetas", + "files": "Archivos", + "remove": "Eliminar", + "folderAlt": "Carpeta", + "fileAlt": "Archivo" } } }, diff --git a/frontend/src/locale/jp.json b/frontend/src/locale/jp.json index 1072b17c9..0ab898982 100644 --- a/frontend/src/locale/jp.json +++ b/frontend/src/locale/jp.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "Google Driveからアップロード" + }, + "share_point": { + "label": "SharePoint", + "heading": "SharePointからアップロード" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "削除", "folderAlt": "フォルダ", "fileAlt": "ファイル" + }, + "sharePoint": { + "connect": "SharePointに接続", + "sessionExpired": "セッションが期限切れです。SharePointに再接続してください。", + "sessionExpiredGeneric": "セッションが期限切れです。アカウントに再接続してください。", + "validateFailed": "セッションの検証に失敗しました。再接続してください。", + "noSession": "有効なセッションが見つかりません。SharePointに再接続してください。", + "noAccessToken": "アクセストークンが利用できません。SharePointに再接続してください。", + "pickerFailed": "ファイルピッカーを開けませんでした。もう一度お試しください。", + "selectedFiles": "選択されたファイル", + "selectFiles": "ファイルを選択", + "loading": "読み込み中...", + "noFilesSelected": "ファイルまたはフォルダが選択されていません", + "folders": "フォルダ", + "files": "ファイル", + "remove": "削除", + "folderAlt": "フォルダ", + "fileAlt": "ファイル" } } }, diff --git a/frontend/src/locale/ru.json b/frontend/src/locale/ru.json index 0ca63ec5e..97dc18c82 100644 --- a/frontend/src/locale/ru.json +++ b/frontend/src/locale/ru.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "Загрузить из Google Drive" + }, + "share_point": { + "label": "SharePoint", + "heading": "Загрузить из SharePoint" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "Удалить", "folderAlt": "Папка", "fileAlt": "Файл" + }, + "sharePoint": { + "connect": "Подключиться к SharePoint", + "sessionExpired": "Сеанс истек. Пожалуйста, переподключитесь к SharePoint.", + "sessionExpiredGeneric": "Сеанс истек. Пожалуйста, переподключите свою учетную запись.", + "validateFailed": "Не удалось проверить сеанс. Пожалуйста, переподключитесь.", + "noSession": "Действительный сеанс не найден. Пожалуйста, переподключитесь к SharePoint.", + "noAccessToken": "Токен доступа недоступен. Пожалуйста, переподключитесь к SharePoint.", + "pickerFailed": "Не удалось открыть средство выбора файлов. Пожалуйста, попробуйте еще раз.", + "selectedFiles": "Выбранные файлы", + "selectFiles": "Выбрать файлы", + "loading": "Загрузка...", + "noFilesSelected": "Файлы или папки не выбраны", + "folders": "Папки", + "files": "Файлы", + "remove": "Удалить", + "folderAlt": "Папка", + "fileAlt": "Файл" } } }, diff --git a/frontend/src/locale/zh-TW.json b/frontend/src/locale/zh-TW.json index 60a0c91d0..8fbf13bb6 100644 --- a/frontend/src/locale/zh-TW.json +++ b/frontend/src/locale/zh-TW.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "從Google Drive上傳" + }, + "share_point": { + "label": "SharePoint", + "heading": "從SharePoint上傳" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "移除", "folderAlt": "資料夾", "fileAlt": "檔案" + }, + "sharePoint": { + "connect": "連接到 SharePoint", + "sessionExpired": "工作階段已過期。請重新連接到 SharePoint。", + "sessionExpiredGeneric": "工作階段已過期。請重新連接您的帳戶。", + "validateFailed": "驗證工作階段失敗。請重新連接。", + "noSession": "未找到有效工作階段。請重新連接到 SharePoint。", + "noAccessToken": "存取權杖不可用。請重新連接到 SharePoint。", + "pickerFailed": "無法開啟檔案選擇器。請重試。", + "selectedFiles": "已選擇的檔案", + "selectFiles": "選擇檔案", + "loading": "載入中...", + "noFilesSelected": "未選擇檔案或資料夾", + "folders": "資料夾", + "files": "檔案", + "remove": "移除", + "folderAlt": "資料夾", + "fileAlt": "檔案" } } }, diff --git a/frontend/src/locale/zh.json b/frontend/src/locale/zh.json index c3f4ec594..732d235e5 100644 --- a/frontend/src/locale/zh.json +++ b/frontend/src/locale/zh.json @@ -260,6 +260,10 @@ "google_drive": { "label": "Google Drive", "heading": "从Google Drive上传" + }, + "share_point": { + "label": "SharePoint", + "heading": "从SharePoint上传" } }, "connectors": { @@ -289,6 +293,24 @@ "remove": "删除", "folderAlt": "文件夹", "fileAlt": "文件" + }, + "sharePoint": { + "connect": "连接到 SharePoint", + "sessionExpired": "会话已过期。请重新连接到 SharePoint。", + "sessionExpiredGeneric": "会话已过期。请重新连接您的账户。", + "validateFailed": "验证会话失败。请重新连接。", + "noSession": "未找到有效会话。请重新连接到 SharePoint。", + "noAccessToken": "访问令牌不可用。请重新连接到 SharePoint。", + "pickerFailed": "无法打开文件选择器。请重试。", + "selectedFiles": "已选择的文件", + "selectFiles": "选择文件", + "loading": "加载中...", + "noFilesSelected": "未选择文件或文件夹", + "folders": "文件夹", + "files": "文件", + "remove": "删除", + "folderAlt": "文件夹", + "fileAlt": "文件" } } }, diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index 6d2223d10..86b3dc816 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -31,6 +31,7 @@ import { FormField, IngestorConfig, IngestorType } from './types/ingestor'; import { FilePicker } from '../components/FilePicker'; import GoogleDrivePicker from '../components/GoogleDrivePicker'; +import SharePointPicker from '../components/SharePointPicker'; import ChevronRight from '../assets/chevron-right.svg'; @@ -250,6 +251,8 @@ function Upload({ token={token} /> ); + case 'share_point_picker': + return ; default: return null; } diff --git a/frontend/src/upload/types/ingestor.ts b/frontend/src/upload/types/ingestor.ts index c06c90431..c56fa5b37 100644 --- a/frontend/src/upload/types/ingestor.ts +++ b/frontend/src/upload/types/ingestor.ts @@ -4,6 +4,7 @@ import UrlIcon from '../../assets/url.svg'; import GithubIcon from '../../assets/github.svg'; import RedditIcon from '../../assets/reddit.svg'; import DriveIcon from '../../assets/drive.svg'; +import SharePoint from '../../assets/sharepoint.svg'; export type IngestorType = | 'crawler' @@ -11,7 +12,8 @@ export type IngestorType = | 'reddit' | 'url' | 'google_drive' - | 'local_file'; + | 'local_file' + | 'share_point'; export interface IngestorConfig { type: IngestorType | null; @@ -33,7 +35,8 @@ export type FieldType = | 'boolean' | 'local_file_picker' | 'remote_file_picker' - | 'google_drive_picker'; + | 'google_drive_picker' + | 'share_point_picker'; export interface FormField { name: string; @@ -147,6 +150,24 @@ export const IngestorFormSchemas: IngestorSchema[] = [ }, ], }, + { + key: 'share_point', + label: 'Share Point', + icon: SharePoint, + heading: 'Upload from Share Point', + validate: () => { + const sharePointClientId = import.meta.env.VITE_SHARE_POINT_CLIENT_ID; + return !!sharePointClientId; + }, + fields: [ + { + name: 'files', + label: 'Select Files from Share Point', + type: 'share_point_picker', + required: true, + }, + ], + }, ]; export const IngestorDefaultConfigs: Record< @@ -175,6 +196,14 @@ export const IngestorDefaultConfigs: Record< }, }, local_file: { name: '', config: { files: [] } }, + share_point: { + name: '', + config: { + file_ids: '', + folder_ids: '', + recursive: true, + }, + }, }; export interface IngestorOption { diff --git a/frontend/src/utils/providerUtils.ts b/frontend/src/utils/providerUtils.ts index 25236ad2e..01f25c5c9 100644 --- a/frontend/src/utils/providerUtils.ts +++ b/frontend/src/utils/providerUtils.ts @@ -14,3 +14,21 @@ export const setSessionToken = (provider: string, token: string): void => { export const removeSessionToken = (provider: string): void => { localStorage.removeItem(`${provider}_session_token`); }; + +export const validateProviderSession = async ( + token: string | null, + provider: string, +) => { + const apiHost = import.meta.env.VITE_API_HOST; + return await fetch(`${apiHost}/api/connectors/validate-session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + provider: provider, + session_token: getSessionToken(provider), + }), + }); +};