diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 1bc880bbfa..4d31b5e940 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1763,6 +1763,13 @@ ONEDRIVE_CLIENT_ID = PersistentConfig( os.environ.get("ONEDRIVE_CLIENT_ID", ""), ) +ONEDRIVE_SHAREPOINT_URL = PersistentConfig( + "ONEDRIVE_SHAREPOINT_URL", + "onedrive.sharepoint_url", + os.environ.get("ONEDRIVE_SHAREPOINT_URL", ""), +) + + # RAG Content Extraction CONTENT_EXTRACTION_ENGINE = PersistentConfig( "CONTENT_EXTRACTION_ENGINE", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index fce3d463be..89222ecce8 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -100,6 +100,7 @@ from open_webui.config import ( # OpenAI ENABLE_OPENAI_API, ONEDRIVE_CLIENT_ID, + ONEDRIVE_SHAREPOINT_URL, OPENAI_API_BASE_URLS, OPENAI_API_KEYS, OPENAI_API_CONFIGS, @@ -240,6 +241,7 @@ from open_webui.config import ( GOOGLE_DRIVE_CLIENT_ID, GOOGLE_DRIVE_API_KEY, ONEDRIVE_CLIENT_ID, + ONEDRIVE_SHAREPOINT_URL, ENABLE_RAG_HYBRID_SEARCH, ENABLE_RAG_LOCAL_WEB_FETCH, ENABLE_WEB_LOADER_SSL_VERIFICATION, @@ -1327,7 +1329,10 @@ async def get_app_config(request: Request): "client_id": GOOGLE_DRIVE_CLIENT_ID.value, "api_key": GOOGLE_DRIVE_API_KEY.value, }, - "onedrive": {"client_id": ONEDRIVE_CLIENT_ID.value}, + "onedrive": { + "client_id": ONEDRIVE_CLIENT_ID.value, + "sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value, + }, "license_metadata": app.state.LICENSE_METADATA, **( { diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index ca6487cf58..b17cabeadd 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -1063,9 +1063,9 @@ ); } }} - uploadOneDriveHandler={async () => { + uploadOneDriveHandler={async (authorityType) => { try { - const fileData = await pickAndDownloadFile(); + const fileData = await pickAndDownloadFile(authorityType); if (fileData) { const file = new File([fileData.blob], fileData.name, { type: fileData.blob.type || 'application/octet-stream' diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte index 27fe2cde29..ab455ddeba 100644 --- a/src/lib/components/chat/MessageInput/InputMenu.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -229,94 +229,66 @@ {/if} {#if $config?.features?.enable_onedrive_integration} - { - uploadOneDriveHandler(); - }} - > - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{$i18n.t('Microsoft OneDrive')}
+ + + { + uploadOneDriveHandler('personal'); + }} > - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{$i18n.t('OneDrive')}
-
+
{$i18n.t('Microsoft OneDrive (personal)')}
+
+ { + uploadOneDriveHandler('organizations'); + }} + > +
+
{$i18n.t('Microsoft OneDrive (work/school)')}
+
Includes SharePoint
+
+
+ + {/if} diff --git a/src/lib/utils/onedrive-file-picker.ts b/src/lib/utils/onedrive-file-picker.ts index 60d2bb13c9..47b4e96273 100644 --- a/src/lib/utils/onedrive-file-picker.ts +++ b/src/lib/utils/onedrive-file-picker.ts @@ -2,70 +2,128 @@ import { PublicClientApplication } from '@azure/msal-browser'; import type { PopupRequest } from '@azure/msal-browser'; import { v4 as uuidv4 } from 'uuid'; -let CLIENT_ID = ''; +class OneDriveConfig { + private static instance: OneDriveConfig; + private clientId: string = ''; + private sharepointUrl: string = ''; + private msalInstance: PublicClientApplication | null = null; + private currentAuthorityType: 'personal' | 'organizations' = 'personal'; -async function getCredentials() { - if (CLIENT_ID) return; + private constructor() {} - const response = await fetch('/api/config'); - if (!response.ok) { - throw new Error('Failed to fetch OneDrive credentials'); - } - const config = await response.json(); - CLIENT_ID = config.onedrive?.client_id; - if (!CLIENT_ID) { - throw new Error('OneDrive client ID not configured'); - } -} - -let msalInstance: PublicClientApplication | null = null; - -// Initialize MSAL authentication -async function initializeMsal() { - try { - if (!CLIENT_ID) { - await getCredentials(); + public static getInstance(): OneDriveConfig { + if (!OneDriveConfig.instance) { + OneDriveConfig.instance = new OneDriveConfig(); } + return OneDriveConfig.instance; + } - const msalParams = { - auth: { - authority: 'https://login.microsoftonline.com/consumers', - clientId: CLIENT_ID - } + public async initialize(authorityType?: 'personal' | 'organizations'): Promise { + if (authorityType && this.currentAuthorityType !== authorityType) { + this.currentAuthorityType = authorityType; + this.msalInstance = null; + } + await this.getCredentials(); + } + + public async ensureInitialized(authorityType?: 'personal' | 'organizations'): Promise { + await this.initialize(authorityType); + } + + private async getCredentials(): Promise { + + const headers: HeadersInit = { + 'Content-Type': 'application/json' }; - if (!msalInstance) { - msalInstance = new PublicClientApplication(msalParams); - if (msalInstance.initialize) { - await msalInstance.initialize(); + const response = await fetch('/api/config', { + headers, + credentials: 'include' + }); + + if (!response.ok) { + throw new Error('Failed to fetch OneDrive credentials'); + } + + const config = await response.json(); + + const newClientId = config.onedrive?.client_id; + const newSharepointUrl = config.onedrive?.sharepoint_url; + + if (!newClientId) { + throw new Error('OneDrive configuration is incomplete'); + } + + this.clientId = newClientId; + this.sharepointUrl = newSharepointUrl; + } + + public async getMsalInstance(authorityType?: 'personal' | 'organizations'): Promise { + await this.ensureInitialized(authorityType); + + if (!this.msalInstance) { + const authorityEndpoint = this.currentAuthorityType === 'organizations' ? 'common' : 'consumers'; + const msalParams = { + auth: { + authority: `https://login.microsoftonline.com/${authorityEndpoint}`, + clientId: this.clientId + } + }; + + this.msalInstance = new PublicClientApplication(msalParams); + if (this.msalInstance.initialize) { + await this.msalInstance.initialize(); } } - return msalInstance; - } catch (error) { - throw new Error( - 'MSAL initialization failed: ' + (error instanceof Error ? error.message : String(error)) - ); + return this.msalInstance; + } + + public getAuthorityType(): 'personal' | 'organizations' { + return this.currentAuthorityType; + } + + public getSharepointUrl(): string { + return this.sharepointUrl; + } + + public getBaseUrl(): string { + if (this.currentAuthorityType === 'organizations') { + if (!this.sharepointUrl || this.sharepointUrl === '') { + throw new Error('Sharepoint URL not configured'); + } + + let sharePointBaseUrl = this.sharepointUrl.replace(/^https?:\/\//, ''); + sharePointBaseUrl = sharePointBaseUrl.replace(/\/$/, ''); + + return `https://${sharePointBaseUrl}`; + } else { + return 'https://onedrive.live.com/picker'; + } } } -// Retrieve OneDrive access token -async function getToken(): Promise { - const authParams: PopupRequest = { scopes: ['OneDrive.ReadWrite'] }; - let accessToken = ''; - try { - msalInstance = await initializeMsal(); - if (!msalInstance) { - throw new Error('MSAL not initialized'); - } +// Retrieve OneDrive access token +async function getToken(resource?: string, authorityType?: 'personal' | 'organizations'): Promise { + const config = OneDriveConfig.getInstance(); + await config.ensureInitialized(authorityType); + + const currentAuthorityType = config.getAuthorityType(); + + const scopes = currentAuthorityType === 'organizations' + ? [`${resource || config.getBaseUrl()}/.default`] + : ['OneDrive.ReadWrite']; + + const authParams: PopupRequest = { scopes }; + let accessToken = ''; + + try { + const msalInstance = await config.getMsalInstance(authorityType); const resp = await msalInstance.acquireTokenSilent(authParams); accessToken = resp.accessToken; } catch (err) { - if (!msalInstance) { - throw new Error('MSAL not initialized'); - } - + const msalInstance = await config.getMsalInstance(authorityType); try { const resp = await msalInstance.loginPopup(authParams); msalInstance.setActiveAccount(resp.account); @@ -88,60 +146,121 @@ async function getToken(): Promise { return accessToken; } -const baseUrl = 'https://onedrive.live.com/picker'; -const params = { - sdk: '8.0', +interface PickerParams { + sdk: string; entry: { - oneDrive: { - files: {} - } - }, - authentication: {}, + oneDrive: Record; + }; + authentication: Record; messaging: { - origin: window?.location?.origin, - channelId: uuidv4() - }, + origin: string; + channelId: string; + }; typesAndSources: { - mode: 'files', - pivots: { - oneDrive: true, - recent: true + mode: string; + pivots: Record; + }; +} + +interface PickerResult { + command?: string; + items?: OneDriveFileInfo[]; + [key: string]: any; +} + +// Get picker parameters based on account type +function getPickerParams(): PickerParams { + const channelId = uuidv4(); + const config = OneDriveConfig.getInstance(); + + const params: PickerParams = { + sdk: '8.0', + entry: { + oneDrive: {} + }, + authentication: {}, + messaging: { + origin: window?.location?.origin || '', + channelId + }, + typesAndSources: { + mode: 'files', + pivots: { + oneDrive: true, + recent: true + } } + }; + + // For personal accounts, set files object in oneDrive + if (config.getAuthorityType() !== 'organizations') { + params.entry.oneDrive = { files: {} }; } -}; + + return params; +} + +interface OneDriveFileInfo { + id: string; + name: string; + parentReference: { + driveId: string; + }; + '@sharePoint.endpoint': string; + [key: string]: any; +} // Download file from OneDrive -async function downloadOneDriveFile(fileInfo: any): Promise { - const accessToken = await getToken(); +async function downloadOneDriveFile(fileInfo: OneDriveFileInfo, authorityType?: 'personal' | 'organizations'): Promise { + const accessToken = await getToken(undefined, authorityType); if (!accessToken) { throw new Error('Unable to retrieve OneDrive access token.'); } + + // The endpoint URL is provided in the file info const fileInfoUrl = `${fileInfo['@sharePoint.endpoint']}/drives/${fileInfo.parentReference.driveId}/items/${fileInfo.id}`; + const response = await fetch(fileInfoUrl, { headers: { Authorization: `Bearer ${accessToken}` } }); + if (!response.ok) { - throw new Error('Failed to fetch file information.'); + throw new Error(`Failed to fetch file information: ${response.status} ${response.statusText}`); } + const fileData = await response.json(); const downloadUrl = fileData['@content.downloadUrl']; - const downloadResponse = await fetch(downloadUrl); - if (!downloadResponse.ok) { - throw new Error('Failed to download file.'); + + if (!downloadUrl) { + throw new Error('Download URL not found in file data'); } + + const downloadResponse = await fetch(downloadUrl); + + if (!downloadResponse.ok) { + throw new Error(`Failed to download file: ${downloadResponse.status} ${downloadResponse.statusText}`); + } + return await downloadResponse.blob(); } // Open OneDrive file picker and return selected file metadata -export async function openOneDrivePicker(): Promise { +export async function openOneDrivePicker(authorityType?: 'personal' | 'organizations'): Promise { if (typeof window === 'undefined') { throw new Error('Not in browser environment'); } + + // Initialize OneDrive config with the specified authority type + const config = OneDriveConfig.getInstance(); + await config.initialize(authorityType); + return new Promise((resolve, reject) => { let pickerWindow: Window | null = null; let channelPort: MessagePort | null = null; + const params = getPickerParams(); + const baseUrl = config.getBaseUrl(); const handleWindowMessage = (event: MessageEvent) => { if (event.source !== pickerWindow) return; @@ -166,7 +285,9 @@ export async function openOneDrivePicker(): Promise { switch (command.command) { case 'authenticate': { try { - const newToken = await getToken(); + // Pass the resource from the command for org accounts + const resource = config.getAuthorityType() === 'organizations' ? command.resource : undefined; + const newToken = await getToken(resource, authorityType); if (newToken) { channelPort?.postMessage({ type: 'result', @@ -178,9 +299,12 @@ export async function openOneDrivePicker(): Promise { } } catch (err) { channelPort?.postMessage({ - result: 'error', - error: { code: 'tokenError', message: 'Failed to get token' }, - isExpected: true + type: 'result', + id: portData.id, + data: { + result: 'error', + error: { code: 'tokenError', message: 'Failed to get token' } + } }); } break; @@ -227,7 +351,7 @@ export async function openOneDrivePicker(): Promise { const initializePicker = async () => { try { - const authToken = await getToken(); + const authToken = await getToken(undefined, authorityType); if (!authToken) { return reject(new Error('Failed to acquire access token')); } @@ -240,8 +364,14 @@ export async function openOneDrivePicker(): Promise { const queryString = new URLSearchParams({ filePicker: JSON.stringify(params) }); - const url = `${baseUrl}?${queryString.toString()}`; + let url = ''; + if(config.getAuthorityType() === 'organizations') { + url = baseUrl + `/_layouts/15/FilePicker.aspx?${queryString}`; + } else { + url = baseUrl + `?${queryString}`; + } + const form = pickerWindow.document.createElement('form'); form.setAttribute('action', url); form.setAttribute('method', 'POST'); @@ -268,17 +398,17 @@ export async function openOneDrivePicker(): Promise { } // Pick and download file from OneDrive -export async function pickAndDownloadFile(): Promise<{ blob: Blob; name: string } | null> { - const pickerResult = await openOneDrivePicker(); +export async function pickAndDownloadFile(authorityType?: 'personal' | 'organizations'): Promise<{ blob: Blob; name: string } | null> { + const pickerResult = await openOneDrivePicker(authorityType); if (!pickerResult || !pickerResult.items || pickerResult.items.length === 0) { return null; } const selectedFile = pickerResult.items[0]; - const blob = await downloadOneDriveFile(selectedFile); + const blob = await downloadOneDriveFile(selectedFile, authorityType); return { blob, name: selectedFile.name }; } -export { downloadOneDriveFile }; +export { downloadOneDriveFile }; \ No newline at end of file