Spaces:
Building
Building
import { PublicClientApplication } from '@azure/msal-browser'; | |
import type { PopupRequest } from '@azure/msal-browser'; | |
import { v4 as uuidv4 } from 'uuid'; | |
class OneDriveConfig { | |
private static instance: OneDriveConfig; | |
private clientId: string = ''; | |
private sharepointUrl: string = ''; | |
private msalInstance: PublicClientApplication | null = null; | |
private currentAuthorityType: 'personal' | 'organizations' = 'personal'; | |
private constructor() {} | |
public static getInstance(): OneDriveConfig { | |
if (!OneDriveConfig.instance) { | |
OneDriveConfig.instance = new OneDriveConfig(); | |
} | |
return OneDriveConfig.instance; | |
} | |
public async initialize(authorityType?: 'personal' | 'organizations'): Promise<void> { | |
if (authorityType && this.currentAuthorityType !== authorityType) { | |
this.currentAuthorityType = authorityType; | |
this.msalInstance = null; | |
} | |
await this.getCredentials(); | |
} | |
public async ensureInitialized(authorityType?: 'personal' | 'organizations'): Promise<void> { | |
await this.initialize(authorityType); | |
} | |
private async getCredentials(): Promise<void> { | |
const headers: HeadersInit = { | |
'Content-Type': 'application/json' | |
}; | |
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<PublicClientApplication> { | |
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 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( | |
resource?: string, | |
authorityType?: 'personal' | 'organizations' | |
): Promise<string> { | |
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) { | |
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) { | |
throw new Error( | |
'Failed to login: ' + | |
(popupError instanceof Error ? popupError.message : String(popupError)) | |
); | |
} | |
} | |
if (!accessToken) { | |
throw new Error('Failed to acquire access token'); | |
} | |
return accessToken; | |
} | |
interface PickerParams { | |
sdk: string; | |
entry: { | |
oneDrive: Record<string, unknown>; | |
}; | |
authentication: Record<string, unknown>; | |
messaging: { | |
origin: string; | |
channelId: string; | |
}; | |
typesAndSources: { | |
mode: string; | |
pivots: Record<string, boolean>; | |
}; | |
} | |
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: OneDriveFileInfo, | |
authorityType?: 'personal' | 'organizations' | |
): Promise<Blob> { | |
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: ${response.status} ${response.statusText}`); | |
} | |
const fileData = await response.json(); | |
const downloadUrl = fileData['@content.downloadUrl']; | |
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( | |
authorityType?: 'personal' | 'organizations' | |
): Promise<PickerResult | null> { | |
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; | |
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' }); | |
} | |
}; | |
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 { | |
// 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', | |
id: portData.id, | |
data: { result: 'token', token: newToken } | |
}); | |
} else { | |
throw new Error('Could not retrieve auth token'); | |
} | |
} catch (err) { | |
channelPort?.postMessage({ | |
type: 'result', | |
id: portData.id, | |
data: { | |
result: 'error', | |
error: { code: 'tokenError', message: 'Failed to get token' } | |
} | |
}); | |
} | |
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; | |
} | |
} | |
}; | |
function cleanup() { | |
window.removeEventListener('message', handleWindowMessage); | |
if (channelPort) { | |
channelPort.removeEventListener('message', handlePortMessage); | |
} | |
if (pickerWindow) { | |
pickerWindow.close(); | |
pickerWindow = null; | |
} | |
} | |
const initializePicker = async () => { | |
try { | |
const authToken = await getToken(undefined, authorityType); | |
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) | |
}); | |
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'); | |
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(); | |
window.addEventListener('message', handleWindowMessage); | |
} catch (err) { | |
if (pickerWindow) { | |
pickerWindow.close(); | |
} | |
reject(err); | |
} | |
}; | |
initializePicker(); | |
}); | |
} | |
// Pick and download file from OneDrive | |
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, authorityType); | |
return { blob, name: selectedFile.name }; | |
} | |
export { downloadOneDriveFile }; | |