Compare commits

...

8 Commits

Author SHA1 Message Date
Manish Madan
7c46d8a094 Merge pull request #2029 from abfeb8/main
feat: add Microsoft Entra ID integration
2025-10-15 13:38:48 +05:30
Abhishek Malviya
065939302b Merge pull request #1 from abfeb8/feature/auth-fe-impl
feat: add SharePoint integration with session validation and UI components
2025-10-10 15:51:44 +05:30
Abhishek Malviya
5fa87db9e7 Merge branch 'arc53:main' into main 2025-10-10 15:44:40 +05:30
Abhishek Malviya
cc54cea783 feat: add SharePoint integration with session validation and UI components 2025-10-10 15:15:38 +05:30
Abhishek Malviya
d9f0072112 refactor: remove MICROSOFT_REDIRECT_URI and update SharePointAuth to use CONNECTOR_REDIRECT_BASE_URI 2025-10-09 10:36:12 +05:30
Abhishek Malviya
2b73c0c9a0 feat: add init for Share Point connector module 2025-10-08 10:34:38 +05:30
Abhishek Malviya
da62133d21 Merge branch 'main' into main 2025-10-08 09:40:34 +05:30
Abhishek Malviya
8edb6dcf2a 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.
2025-10-07 15:23:32 +05:30
23 changed files with 586 additions and 47 deletions

View File

@@ -6,4 +6,17 @@ VITE_API_STREAMING=true
OPENAI_API_BASE=
OPENAI_API_VERSION=
AZURE_DEPLOYMENT_NAME=
AZURE_EMBEDDINGS_DEPLOYMENT_NAME=
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
#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}

View File

@@ -113,10 +113,14 @@ class ConnectorsCallback(Resource):
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'

View File

@@ -55,6 +55,11 @@ 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_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

View File

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

View File

@@ -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']

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 48 48" id="b" xmlns="http://www.w3.org/2000/svg" fill="#000000" stroke="#000000" stroke-width="3.312">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier">
<defs>
<style>.c{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style>
</defs>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,13 @@
const ConnectedStateSkeleton = () => (
<div className="mb-4">
<div className="flex w-full animate-pulse items-center justify-between rounded-[10px] bg-gray-200 px-4 py-2 dark:bg-gray-700">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-4 w-32 rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
<div className="h-4 w-16 rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
</div>
);
export default ConnectedStateSkeleton;

View File

@@ -150,7 +150,7 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
{isConnected ? (
<div className="mb-4">
<div className="flex w-full items-center justify-between rounded-[10px] bg-[#8FDD51] px-4 py-2 text-sm font-medium text-[#212121]">
<div className="flex items-center gap-2">
<div className="flex max-w-[500px] items-center gap-2">
<svg className="h-4 w-4" viewBox="0 0 24 24">
<path
fill="currentColor"

View File

@@ -0,0 +1,13 @@
const FilesSectionSkeleton = () => (
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
<div className="p-4">
<div className="mb-4 flex items-center justify-between">
<div className="h-5 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
<div className="h-8 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
<div className="h-4 w-40 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
</div>
);
export default FilesSectionSkeleton;

View File

@@ -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<GoogleDrivePickerProps> = ({
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<GoogleDrivePickerProps> = ({
onSelectionChange([], []);
};
const ConnectedStateSkeleton = () => (
<div className="mb-4">
<div className="flex w-full animate-pulse items-center justify-between rounded-[10px] bg-gray-200 px-4 py-2 dark:bg-gray-700">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded bg-gray-300 dark:bg-gray-600"></div>
<div className="h-4 w-32 rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
<div className="h-4 w-16 rounded bg-gray-300 dark:bg-gray-600"></div>
</div>
</div>
);
const FilesSectionSkeleton = () => (
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
<div className="p-4">
<div className="mb-4 flex items-center justify-between">
<div className="h-5 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
<div className="h-8 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
<div className="h-4 w-40 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
</div>
</div>
);
return (
<div>
{isValidating ? (

View File

@@ -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<SharePointPickerProps> = ({ token }) => {
const { t } = useTranslation();
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 [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 (
<div>
{isValidating ? (
<>
<ConnectedStateSkeleton />
<FilesSectionSkeleton />
</>
) : (
<>
<ConnectorAuth
provider="share_point"
label={t('modals.uploadDoc.connectors.sharePoint.connect')}
onSuccess={(data) => {
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 && (
<div className="rounded-lg border border-[#EEE6FF78] dark:border-[#6A6A6A]">
<div className="p-4">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-medium">
{t('modals.uploadDoc.connectors.sharePoint.selectedFiles')}
</h3>
<button
onClick={() => handleOpenPicker()}
className="rounded-md bg-[#A076F6] px-3 py-1 text-sm text-white hover:bg-[#8A5FD4]"
disabled={isLoading}
>
{isLoading
? t('modals.uploadDoc.connectors.sharePoint.loading')
: t('modals.uploadDoc.connectors.sharePoint.selectFiles')}
</button>
</div>
</div>
</div>
)}
</>
)}
</div>
);
};
export default SharePointPicker;

View File

@@ -298,6 +298,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "Upload from Google Drive"
},
"share_point": {
"label": "SharePoint",
"heading": "Upload from SharePoint"
}
},
"connectors": {
@@ -327,6 +331,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"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "Subir desde Google Drive"
},
"share_point": {
"label": "SharePoint",
"heading": "Subir desde SharePoint"
}
},
"connectors": {
@@ -290,6 +294,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"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "Google Driveからアップロード"
},
"share_point": {
"label": "SharePoint",
"heading": "SharePointからアップロード"
}
},
"connectors": {
@@ -290,6 +294,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": "ファイル"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "Загрузить из Google Drive"
},
"share_point": {
"label": "SharePoint",
"heading": "Загрузить из SharePoint"
}
},
"connectors": {
@@ -290,6 +294,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": "Файл"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "從Google Drive上傳"
},
"share_point": {
"label": "SharePoint",
"heading": "從SharePoint上傳"
}
},
"connectors": {
@@ -290,6 +294,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": "檔案"
}
}
},

View File

@@ -261,6 +261,10 @@
"google_drive": {
"label": "Google Drive",
"heading": "从Google Drive上传"
},
"share_point": {
"label": "SharePoint",
"heading": "从SharePoint上传"
}
},
"connectors": {
@@ -290,6 +294,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": "文件"
}
}
},

View File

@@ -32,6 +32,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';
@@ -252,6 +253,8 @@ function Upload({
token={token}
/>
);
case 'share_point_picker':
return <SharePointPicker key={field.name} token={token} />;
default:
return null;
}

View File

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

View File

@@ -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),
}),
});
};