open-webui/src/lib/utils/onedrive-file-picker.ts

421 lines
11 KiB
TypeScript
Raw Normal View History

import { PublicClientApplication } from '@azure/msal-browser';
import type { PopupRequest } from '@azure/msal-browser';
2025-02-27 19:37:44 +00:00
import { v4 as uuidv4 } from 'uuid';
2025-04-14 13:57:32 +00:00
class OneDriveConfig {
private static instance: OneDriveConfig;
private clientId: string = '';
private sharepointUrl: string = '';
private msalInstance: PublicClientApplication | null = null;
2025-04-14 15:27:59 +00:00
private currentAuthorityType: 'personal' | 'organizations' = 'personal';
2025-02-24 08:27:37 +00:00
2025-04-14 13:57:32 +00:00
private constructor() {}
2025-03-05 06:10:47 +00:00
2025-04-14 13:57:32 +00:00
public static getInstance(): OneDriveConfig {
if (!OneDriveConfig.instance) {
OneDriveConfig.instance = new OneDriveConfig();
}
return OneDriveConfig.instance;
2025-02-25 09:46:08 +00:00
}
2025-04-14 13:57:32 +00:00
2025-04-14 15:27:59 +00:00
public async initialize(authorityType?: 'personal' | 'organizations'): Promise<void> {
if (authorityType && this.currentAuthorityType !== authorityType) {
this.currentAuthorityType = authorityType;
this.msalInstance = null;
}
await this.getCredentials();
2025-02-25 09:46:08 +00:00
}
2025-02-24 08:27:37 +00:00
2025-04-14 15:27:59 +00:00
public async ensureInitialized(authorityType?: 'personal' | 'organizations'): Promise<void> {
await this.initialize(authorityType);
2025-04-14 13:57:32 +00:00
}
2025-02-24 08:27:37 +00:00
2025-04-14 15:35:18 +00:00
private async getCredentials(): Promise<void> {
2025-04-14 13:57:32 +00:00
let response;
2025-04-14 15:27:59 +00:00
const headers: HeadersInit = {
'Content-Type': 'application/json'
};
2025-04-14 13:57:32 +00:00
if(window.location.hostname === 'localhost') {
2025-04-14 15:27:59 +00:00
response = await fetch('http://localhost:8080/api/config', {
headers,
credentials: 'include'
});
2025-04-14 13:57:32 +00:00
} else {
2025-04-14 15:27:59 +00:00
response = await fetch('/api/config', {
headers,
credentials: 'include'
});
2025-04-14 13:57:32 +00:00
}
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');
}
2025-04-14 15:35:18 +00:00
2025-04-14 13:57:32 +00:00
this.clientId = newClientId;
this.sharepointUrl = newSharepointUrl;
}
2025-04-14 15:27:59 +00:00
public async getMsalInstance(authorityType?: 'personal' | 'organizations'): Promise<PublicClientApplication> {
await this.ensureInitialized(authorityType);
2025-04-14 13:57:32 +00:00
if (!this.msalInstance) {
2025-04-14 15:27:59 +00:00
const authorityEndpoint = this.currentAuthorityType === 'organizations' ? 'common' : 'consumers';
2025-04-14 13:57:32 +00:00
const msalParams = {
auth: {
authority: `https://login.microsoftonline.com/${authorityEndpoint}`,
clientId: this.clientId
}
};
2025-03-05 06:10:47 +00:00
2025-04-14 13:57:32 +00:00
this.msalInstance = new PublicClientApplication(msalParams);
if (this.msalInstance.initialize) {
await this.msalInstance.initialize();
}
2025-02-25 09:46:08 +00:00
}
2025-03-05 06:10:47 +00:00
2025-04-14 13:57:32 +00:00
return this.msalInstance;
}
public getAuthorityType(): 'personal' | 'organizations' {
2025-04-14 15:27:59 +00:00
return this.currentAuthorityType;
2025-04-14 13:57:32 +00:00
}
public getSharepointUrl(): string {
return this.sharepointUrl;
}
public getBaseUrl(): string {
2025-04-14 15:27:59 +00:00
if (this.currentAuthorityType === 'organizations') {
2025-04-14 13:57:32 +00:00
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';
}
2025-02-25 09:46:08 +00:00
}
2025-02-24 14:14:10 +00:00
}
2025-02-24 08:27:37 +00:00
2025-04-14 13:57:32 +00:00
2025-02-24 14:14:10 +00:00
// Retrieve OneDrive access token
2025-04-14 15:27:59 +00:00
async function getToken(resource?: string, authorityType?: 'personal' | 'organizations'): Promise<string> {
2025-04-14 13:57:32 +00:00
const config = OneDriveConfig.getInstance();
2025-04-14 15:27:59 +00:00
await config.ensureInitialized(authorityType);
2025-04-14 13:57:32 +00:00
2025-04-14 15:27:59 +00:00
const currentAuthorityType = config.getAuthorityType();
2025-04-14 13:57:32 +00:00
2025-04-14 15:27:59 +00:00
const scopes = currentAuthorityType === 'organizations'
2025-04-14 13:57:32 +00:00
? [`${resource || config.getBaseUrl()}/.default`]
: ['OneDrive.ReadWrite'];
const authParams: PopupRequest = { scopes };
2025-02-25 09:46:08 +00:00
let accessToken = '';
2025-03-05 06:10:47 +00:00
2025-04-14 13:57:32 +00:00
try {
2025-04-14 15:27:59 +00:00
const msalInstance = await config.getMsalInstance(authorityType);
2025-02-25 09:46:08 +00:00
const resp = await msalInstance.acquireTokenSilent(authParams);
accessToken = resp.accessToken;
} catch (err) {
2025-04-14 15:27:59 +00:00
const msalInstance = await config.getMsalInstance(authorityType);
try {
const resp = await msalInstance.loginPopup(authParams);
msalInstance.setActiveAccount(resp.account);
if (resp.idToken) {
const resp2 = await msalInstance.acquireTokenSilent(authParams);
accessToken = resp2.accessToken;
}
} catch (popupError) {
2025-03-05 06:10:47 +00:00
throw new Error(
'Failed to login: ' +
(popupError instanceof Error ? popupError.message : String(popupError))
);
2025-02-25 09:46:08 +00:00
}
}
2025-03-05 06:10:47 +00:00
if (!accessToken) {
throw new Error('Failed to acquire access token');
}
2025-03-05 06:10:47 +00:00
2025-02-25 09:46:08 +00:00
return accessToken;
2025-02-24 14:14:10 +00:00
}
2025-02-24 08:27:37 +00:00
2025-04-14 15:35:18 +00:00
interface PickerParams {
2025-04-14 13:57:32 +00:00
sdk: string;
2025-02-25 09:46:08 +00:00
entry: {
2025-04-14 13:57:32 +00:00
oneDrive: Record<string, unknown>;
};
authentication: Record<string, unknown>;
2025-02-25 09:46:08 +00:00
messaging: {
2025-04-14 13:57:32 +00:00
origin: string;
channelId: string;
};
2025-02-25 09:46:08 +00:00
typesAndSources: {
2025-04-14 13:57:32 +00:00
mode: string;
pivots: Record<string, boolean>;
};
2025-04-14 15:35:18 +00:00
}
interface PickerResult {
command?: string;
items?: OneDriveFileInfo[];
[key: string]: any;
}
// Get picker parameters based on account type
function getPickerParams(): PickerParams {
2025-04-14 13:57:32 +00:00
const channelId = uuidv4();
2025-04-14 15:35:18 +00:00
const config = OneDriveConfig.getInstance();
2025-04-14 13:57:32 +00:00
2025-04-14 15:35:18 +00:00
const params: PickerParams = {
sdk: '8.0',
entry: {
oneDrive: {}
},
authentication: {},
messaging: {
origin: window?.location?.origin || '',
channelId
},
typesAndSources: {
mode: 'files',
pivots: {
oneDrive: true,
recent: true
2025-04-14 13:57:32 +00:00
}
2025-04-14 15:35:18 +00:00
}
};
// For personal accounts, set files object in oneDrive
if (config.getAuthorityType() !== 'organizations') {
params.entry.oneDrive = { files: {} };
2025-02-25 09:46:08 +00:00
}
2025-04-14 15:35:18 +00:00
return params;
}
interface OneDriveFileInfo {
id: string;
name: string;
parentReference: {
driveId: string;
};
'@sharePoint.endpoint': string;
[key: string]: any;
2025-04-14 13:57:32 +00:00
}
2025-02-24 08:27:37 +00:00
2025-02-24 14:14:10 +00:00
// Download file from OneDrive
2025-04-14 15:35:18 +00:00
async function downloadOneDriveFile(fileInfo: OneDriveFileInfo, authorityType?: 'personal' | 'organizations'): Promise<Blob> {
2025-04-14 15:27:59 +00:00
const accessToken = await getToken(undefined, authorityType);
2025-02-25 09:46:08 +00:00
if (!accessToken) {
throw new Error('Unable to retrieve OneDrive access token.');
}
2025-04-14 13:57:32 +00:00
// The endpoint URL is provided in the file info
2025-02-25 09:46:08 +00:00
const fileInfoUrl = `${fileInfo['@sharePoint.endpoint']}/drives/${fileInfo.parentReference.driveId}/items/${fileInfo.id}`;
2025-04-14 13:57:32 +00:00
2025-02-25 09:46:08 +00:00
const response = await fetch(fileInfoUrl, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
2025-04-14 13:57:32 +00:00
2025-02-25 09:46:08 +00:00
if (!response.ok) {
2025-04-14 15:35:18 +00:00
throw new Error(`Failed to fetch file information: ${response.status} ${response.statusText}`);
2025-02-25 09:46:08 +00:00
}
2025-04-14 13:57:32 +00:00
2025-02-25 09:46:08 +00:00
const fileData = await response.json();
const downloadUrl = fileData['@content.downloadUrl'];
2025-04-14 15:35:18 +00:00
if (!downloadUrl) {
throw new Error('Download URL not found in file data');
}
2025-02-25 09:46:08 +00:00
const downloadResponse = await fetch(downloadUrl);
2025-04-14 13:57:32 +00:00
2025-02-25 09:46:08 +00:00
if (!downloadResponse.ok) {
2025-04-14 15:35:18 +00:00
throw new Error(`Failed to download file: ${downloadResponse.status} ${downloadResponse.statusText}`);
2025-02-25 09:46:08 +00:00
}
2025-04-14 13:57:32 +00:00
2025-02-25 09:46:08 +00:00
return await downloadResponse.blob();
2025-02-24 14:14:10 +00:00
}
2025-02-24 08:27:37 +00:00
2025-02-24 14:14:10 +00:00
// Open OneDrive file picker and return selected file metadata
2025-04-14 15:35:18 +00:00
export async function openOneDrivePicker(authorityType?: 'personal' | 'organizations'): Promise<PickerResult | null> {
2025-02-25 09:46:08 +00:00
if (typeof window === 'undefined') {
throw new Error('Not in browser environment');
}
2025-04-14 13:57:32 +00:00
2025-04-14 15:35:18 +00:00
// Initialize OneDrive config with the specified authority type
2025-04-14 13:57:32 +00:00
const config = OneDriveConfig.getInstance();
2025-04-14 15:35:18 +00:00
await config.initialize(authorityType);
2025-04-14 13:57:32 +00:00
2025-02-25 09:46:08 +00:00
return new Promise((resolve, reject) => {
let pickerWindow: Window | null = null;
let channelPort: MessagePort | null = null;
2025-04-14 13:57:32 +00:00
const params = getPickerParams();
const baseUrl = config.getBaseUrl();
2025-02-24 08:27:37 +00:00
2025-02-25 09:46:08 +00:00
const handleWindowMessage = (event: MessageEvent) => {
if (event.source !== pickerWindow) return;
const message = event.data;
if (message?.type === 'initialize' && message?.channelId === params.messaging.channelId) {
channelPort = event.ports?.[0];
if (!channelPort) return;
channelPort.addEventListener('message', handlePortMessage);
channelPort.start();
channelPort.postMessage({ type: 'activate' });
}
};
2025-02-24 08:27:37 +00:00
2025-02-25 09:46:08 +00:00
const handlePortMessage = async (portEvent: MessageEvent) => {
const portData = portEvent.data;
switch (portData.type) {
case 'notification':
break;
case 'command': {
channelPort?.postMessage({ type: 'acknowledge', id: portData.id });
const command = portData.data;
switch (command.command) {
case 'authenticate': {
try {
2025-04-14 13:57:32 +00:00
// Pass the resource from the command for org accounts
2025-04-14 15:35:18 +00:00
const resource = config.getAuthorityType() === 'organizations' ? command.resource : undefined;
const newToken = await getToken(resource, authorityType);
2025-02-25 09:46:08 +00:00
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) {
channelPort?.postMessage({
2025-04-14 13:57:32 +00:00
type: 'result',
id: portData.id,
data: {
result: 'error',
error: { code: 'tokenError', message: 'Failed to get token' }
}
2025-02-25 09:46:08 +00:00
});
}
break;
}
case 'close': {
cleanup();
resolve(null);
break;
}
case 'pick': {
channelPort?.postMessage({
type: 'result',
id: portData.id,
data: { result: 'success' }
});
cleanup();
resolve(command);
break;
}
default: {
channelPort?.postMessage({
result: 'error',
error: { code: 'unsupportedCommand', message: command.command },
isExpected: true
});
break;
}
}
break;
}
}
};
2025-02-24 08:27:37 +00:00
2025-02-25 09:46:08 +00:00
function cleanup() {
window.removeEventListener('message', handleWindowMessage);
if (channelPort) {
channelPort.removeEventListener('message', handlePortMessage);
}
if (pickerWindow) {
pickerWindow.close();
pickerWindow = null;
}
}
2025-02-24 08:27:37 +00:00
2025-02-25 09:46:08 +00:00
const initializePicker = async () => {
try {
2025-04-14 15:35:18 +00:00
const authToken = await getToken(undefined, authorityType);
2025-02-25 09:46:08 +00:00
if (!authToken) {
return reject(new Error('Failed to acquire access token'));
}
2025-03-05 06:10:47 +00:00
2025-02-25 09:46:08 +00:00
pickerWindow = window.open('', 'OneDrivePicker', 'width=800,height=600');
if (!pickerWindow) {
return reject(new Error('Failed to open OneDrive picker window'));
}
2025-03-05 06:10:47 +00:00
2025-02-25 09:46:08 +00:00
const queryString = new URLSearchParams({
filePicker: JSON.stringify(params)
});
2025-04-14 13:57:32 +00:00
let url = '';
2025-04-14 15:35:18 +00:00
if(config.getAuthorityType() === 'organizations') {
2025-04-14 13:57:32 +00:00
url = baseUrl + `/_layouts/15/FilePicker.aspx?${queryString}`;
2025-04-14 15:35:18 +00:00
} else {
2025-04-14 13:57:32 +00:00
url = baseUrl + `?${queryString}`;
}
2025-02-25 09:46:08 +00:00
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);
2025-03-05 06:10:47 +00:00
2025-02-25 09:46:08 +00:00
pickerWindow.document.body.appendChild(form);
form.submit();
2025-03-05 06:10:47 +00:00
2025-02-25 09:46:08 +00:00
window.addEventListener('message', handleWindowMessage);
} catch (err) {
if (pickerWindow) {
pickerWindow.close();
}
2025-02-25 09:46:08 +00:00
reject(err);
}
};
2025-02-24 08:27:37 +00:00
2025-02-25 09:46:08 +00:00
initializePicker();
});
2025-02-24 14:14:10 +00:00
}
2025-02-24 08:27:37 +00:00
2025-02-24 14:14:10 +00:00
// Pick and download file from OneDrive
2025-04-14 15:27:59 +00:00
export async function pickAndDownloadFile(authorityType?: 'personal' | 'organizations'): Promise<{ blob: Blob; name: string } | null> {
2025-04-14 15:35:18 +00:00
const pickerResult = await openOneDrivePicker(authorityType);
2025-03-05 06:10:47 +00:00
if (!pickerResult || !pickerResult.items || pickerResult.items.length === 0) {
return null;
2025-02-25 09:46:08 +00:00
}
2025-03-05 06:10:47 +00:00
const selectedFile = pickerResult.items[0];
2025-04-14 15:27:59 +00:00
const blob = await downloadOneDriveFile(selectedFile, authorityType);
2025-03-05 06:10:47 +00:00
return { blob, name: selectedFile.name };
2025-02-24 08:27:37 +00:00
}
2025-02-24 14:14:10 +00:00
2025-04-14 13:57:32 +00:00
export { downloadOneDriveFile };