diff --git a/package-lock.json b/package-lock.json
index c65870772..066cf2be5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "open-webui",
"version": "0.5.16",
"dependencies": {
+ "@azure/msal-browser": "^4.4.0",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language-data": "^6.5.1",
@@ -134,6 +135,27 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@azure/msal-browser": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.4.0.tgz",
+ "integrity": "sha512-rU6juYXk67CKQmpgi6fDgZoPQ9InZ1760z1BSAH7RbeIc4lHZM/Tu+H0CyRk7cnrfvTkexyYE4pjYhMghpzheA==",
+ "license": "MIT",
+ "dependencies": {
+ "@azure/msal-common": "15.2.0"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@azure/msal-common": {
+ "version": "15.2.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz",
+ "integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
diff --git a/package.json b/package.json
index 86568869f..0e8fe2bc6 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
},
"type": "module",
"dependencies": {
+ "@azure/msal-browser": "^4.4.0",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language-data": "^6.5.1",
diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte
index 38cb91cc0..a81139d2f 100644
--- a/src/lib/components/chat/MessageInput.svelte
+++ b/src/lib/components/chat/MessageInput.svelte
@@ -2,6 +2,7 @@
import { toast } from 'svelte-sonner';
import { v4 as uuidv4 } from 'uuid';
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
+ import { openOneDrivePicker } from '$lib/utils/onedrive-file-picker';
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
const dispatch = createEventDispatcher();
@@ -1108,6 +1109,26 @@
);
}
}}
+ uploadOneDriveHandler={async () => {
+ try {
+ const fileData = await openOneDrivePicker();
+ if (fileData) {
+ const file = new File([fileData.blob], fileData.name, {
+ type: fileData.blob.type
+ });
+ await uploadFileHandler(file);
+ } else {
+ console.log('No file was selected from OneDrive');
+ }
+ } catch (error) {
+ console.error('OneDrive Error:', error);
+ toast.error(
+ $i18n.t('Error accessing OneDrive: {{error}}', {
+ error: error.message
+ })
+ );
+ }
+ }}
onClose={async () => {
await tick();
diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte
index 801093d8f..91f9cf81b 100644
--- a/src/lib/components/chat/MessageInput/InputMenu.svelte
+++ b/src/lib/components/chat/MessageInput/InputMenu.svelte
@@ -5,6 +5,7 @@
import { config, user, tools as _tools, mobile } from '$lib/stores';
import { createPicker } from '$lib/utils/google-drive-picker';
+
import { getTools } from '$lib/apis/tools';
import Dropdown from '$lib/components/common/Dropdown.svelte';
@@ -24,6 +25,7 @@
export let inputFilesHandler: Function;
export let uploadGoogleDriveHandler: Function;
+ export let uploadOneDriveHandler: Function;
export let selectedToolIds: string[] = [];
@@ -225,6 +227,35 @@
{$i18n.t('Google Drive')}
{/if}
+
+ {#if $config?.features?.enable_onedrive_integration || true}
+ {
+ uploadOneDriveHandler();
+ }}
+ >
+
+ {$i18n.t('OneDrive')}
+
+ {/if}
diff --git a/src/lib/utils/onedrive-auth.ts b/src/lib/utils/onedrive-auth.ts
new file mode 100644
index 000000000..be2de44a0
--- /dev/null
+++ b/src/lib/utils/onedrive-auth.ts
@@ -0,0 +1,42 @@
+import { PublicClientApplication } from '@azure/msal-browser';
+
+const msalParams = {
+ auth: {
+ authority: 'https://login.microsoftonline.com/consumers',
+ clientId: '2ab80a1e-7300-4cb1-beac-c38c730e8b7f'
+ }
+};
+
+// MSAL 초기화
+const app = new PublicClientApplication(msalParams);
+
+export async function initializeMsal() {
+ try {
+ await app.initialize();
+ console.log('MSAL initialized successfully');
+ } catch (error) {
+ console.error('MSAL initialization error:', error);
+ }
+ }
+
+ export async function getToken(): Promise {
+ const authParams = { scopes: ['OneDrive.ReadWrite'] };
+ let accessToken = '';
+
+ try {
+ // Ensure initialization happens early
+ await initializeMsal();
+ const resp = await app.acquireTokenSilent(authParams);
+ accessToken = resp.accessToken;
+ } catch (err) {
+ const resp = await app.loginPopup(authParams);
+ app.setActiveAccount(resp.account);
+
+ if (resp.idToken) {
+ const resp2 = await app.acquireTokenSilent(authParams);
+ accessToken = resp2.accessToken;
+ }
+ }
+
+ return accessToken;
+ }
diff --git a/src/lib/utils/onedrive-file-picker.ts b/src/lib/utils/onedrive-file-picker.ts
new file mode 100644
index 000000000..d003e38ec
--- /dev/null
+++ b/src/lib/utils/onedrive-file-picker.ts
@@ -0,0 +1,211 @@
+// src/lib/utils/onedrive-file-picker.ts
+import { getToken } from './onedrive-auth';
+
+
+const baseUrl = "https://onedrive.live.com/picker";
+const params = {
+ sdk: '8.0',
+ entry: {
+ oneDrive: {
+ files: {}
+ }
+ },
+ authentication: {},
+ messaging: {
+ origin: 'http://localhost:3000', // 현재 부모 페이지의 origin
+ channelId: '27' // 메시징 채널용 임의의 ID
+ },
+ typesAndSources: {
+ mode: 'files',
+ pivots: {
+ oneDrive: true,
+ recent: true
+ }
+ }
+};
+
+/**
+ * OneDrive 파일 피커 창을 열고, 사용자가 선택한 파일 메타데이터를 받아오는 함수
+ */
+export async function openOneDrivePicker(): Promise {
+ // SSR 환경(SvelteKit)에서 window 객체가 없을 수 있으므로 가드
+ if (typeof window === 'undefined') {
+ throw new Error('Not in browser environment');
+ }
+
+ return new Promise(async (resolve, reject) => {
+ let pickerWindow: Window | null = null;
+ let channelPort: MessagePort | null = null;
+
+ try {
+ const authToken = await getToken();
+ if (!authToken) {
+ return reject(new Error('Failed to acquire access token'));
+ }
+
+ // 팝업 창 오픈
+ pickerWindow = window.open('', 'OneDrivePicker', 'width=800,height=600');
+ if (!pickerWindow) {
+ return reject(new Error('Failed to open OneDrive picker window'));
+ }
+
+ // 쿼리스트링 구성
+ const queryString = new URLSearchParams({
+ filePicker: JSON.stringify(params)
+ });
+ const url = `${baseUrl}?${queryString.toString()}`;
+
+ // 새로 연 window에 form을 동적으로 추가하여 POST
+ const form = pickerWindow.document.createElement('form');
+ form.setAttribute('action', url);
+ form.setAttribute('method', 'POST');
+
+ const input = pickerWindow.document.createElement('input');
+ input.setAttribute('type', 'hidden');
+ input.setAttribute('name', 'access_token');
+ input.setAttribute('value', authToken);
+
+ form.appendChild(input);
+ pickerWindow.document.body.appendChild(form);
+ form.submit();
+
+ // 부모 창에서 message 이벤트 수신
+ const handleWindowMessage = (event: MessageEvent) => {
+ // pickerWindow가 아닌 다른 window에서 온 메시지는 무시
+ if (event.source !== pickerWindow) return;
+
+ const message = event.data;
+
+ // 초기화 메시지 => SharedWorker(MessageChannel) 식으로 포트 받기
+ if (
+ message?.type === 'initialize' &&
+ message?.channelId === params.messaging.channelId
+ ) {
+ channelPort = event.ports?.[0];
+ if (!channelPort) return;
+
+ channelPort.addEventListener('message', handlePortMessage);
+ channelPort.start();
+
+ // picker iframe에 'activate' 전달
+ channelPort.postMessage({
+ type: 'activate'
+ });
+ }
+ };
+
+ // 포트 메시지 핸들러
+ const handlePortMessage = async (portEvent: MessageEvent) => {
+ const portData = portEvent.data;
+ switch (portData.type) {
+ case 'notification':
+ console.log('notification:', portData);
+ break;
+
+ case 'command': {
+ // picker에 응답
+ channelPort?.postMessage({
+ type: 'acknowledge',
+ id: portData.id
+ });
+
+ const command = portData.data;
+
+ switch (command.command) {
+ case 'authenticate': {
+ // 재인증
+ try {
+ const newToken = await getToken();
+ if (newToken) {
+ channelPort?.postMessage({
+ type: 'result',
+ id: portData.id,
+ data: {
+ result: 'token',
+ token: newToken
+ }
+ });
+ } else {
+ throw new Error('Could not retrieve auth token');
+ }
+ } catch (err) {
+ console.error(err);
+ channelPort?.postMessage({
+ result: 'error',
+ error: {
+ code: 'tokenError',
+ message: 'Failed to get token'
+ },
+ isExpected: true
+ });
+ }
+ break;
+ }
+
+ case 'close': {
+ // 사용자가 취소하거나 닫았을 경우
+ cleanup();
+ resolve(null);
+ break;
+ }
+
+ case 'pick': {
+ // 사용자가 파일 선택 완료
+ console.log('Picked:', command);
+ /**
+ * command 안에는 사용자가 선택한 파일들의 메타데이터 정보가 들어있습니다.
+ * 필요하다면 Microsoft Graph API 등을 통해 Blob(실제 파일 데이터)을 받아와야 할 수 있습니다.
+ */
+
+ // picker에 응답
+ channelPort?.postMessage({
+ type: 'result',
+ id: portData.id,
+ data: {
+ result: 'success'
+ }
+ });
+
+ // 선택한 파일들(메타정보)을 resolve
+ cleanup();
+ resolve(command);
+ break;
+ }
+
+ default: {
+ console.warn('Unsupported command:', command);
+ channelPort?.postMessage({
+ result: 'error',
+ error: {
+ code: 'unsupportedCommand',
+ message: command.command
+ },
+ isExpected: true
+ });
+ break;
+ }
+ }
+ break;
+ }
+ }
+ };
+
+ function cleanup() {
+ window.removeEventListener('message', handleWindowMessage);
+ if (channelPort) {
+ channelPort.removeEventListener('message', handlePortMessage);
+ }
+ if (pickerWindow) {
+ pickerWindow.close();
+ pickerWindow = null;
+ }
+ }
+
+ // 메시지 이벤트 등록
+ window.addEventListener('message', handleWindowMessage);
+ } catch (err) {
+ if (pickerWindow) pickerWindow.close();
+ reject(err);
+ }
+ });
+}