| | import type { PopupRequest, PublicClientApplication } from '@azure/msal-browser'; |
| | import { v4 as uuidv4 } from 'uuid'; |
| |
|
| | class OneDriveConfig { |
| | private static instance: OneDriveConfig; |
| | private clientIdPersonal: string = ''; |
| | private clientIdBusiness: string = ''; |
| | private sharepointUrl: string = ''; |
| | private sharepointTenantId: 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 response = await fetch('/api/config', { |
| | headers: { |
| | 'Content-Type': 'application/json' |
| | }, |
| | credentials: 'include' |
| | }); |
| |
|
| | if (!response.ok) { |
| | throw new Error('Failed to fetch OneDrive credentials'); |
| | } |
| |
|
| | const config = await response.json(); |
| |
|
| | this.clientIdPersonal = config.onedrive?.client_id_personal; |
| | this.clientIdBusiness = config.onedrive?.client_id_business; |
| | this.sharepointUrl = config.onedrive?.sharepoint_url; |
| | this.sharepointTenantId = config.onedrive?.sharepoint_tenant_id; |
| |
|
| | if (!this.clientIdPersonal && !this.clientIdBusiness) { |
| | throw new Error('OneDrive personal or business client ID not configured'); |
| | } |
| | } |
| |
|
| | public async getMsalInstance( |
| | authorityType?: 'personal' | 'organizations' |
| | ): Promise<PublicClientApplication> { |
| | await this.ensureInitialized(authorityType); |
| |
|
| | if (!this.msalInstance) { |
| | const authorityEndpoint = |
| | this.currentAuthorityType === 'organizations' |
| | ? this.sharepointTenantId || 'common' |
| | : 'consumers'; |
| |
|
| | const clientId = |
| | this.currentAuthorityType === 'organizations' |
| | ? this.clientIdBusiness |
| | : this.clientIdPersonal; |
| |
|
| | if (!clientId) { |
| | throw new Error('OneDrive client ID not configured'); |
| | } |
| |
|
| | const msalParams = { |
| | auth: { |
| | authority: `https://login.microsoftonline.com/${authorityEndpoint}`, |
| | clientId: clientId |
| | } |
| | }; |
| |
|
| | const { PublicClientApplication } = await import('@azure/msal-browser'); |
| | 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 getSharepointTenantId(): string { |
| | return this.sharepointTenantId; |
| | } |
| |
|
| | 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'; |
| | } |
| | } |
| | } |
| |
|
| | |
| | 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 { |
| | 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; |
| | }; |
| | search: { |
| | enabled: boolean; |
| | }; |
| | typesAndSources: { |
| | mode: string; |
| | pivots: Record<string, boolean>; |
| | }; |
| | } |
| |
|
| | interface PickerResult { |
| | command?: string; |
| | items?: OneDriveFileInfo[]; |
| | |
| | [key: string]: any; |
| | } |
| |
|
| | |
| | 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 |
| | }, |
| | search: { |
| | enabled: true |
| | }, |
| | typesAndSources: { |
| | mode: 'files', |
| | pivots: { |
| | oneDrive: true, |
| | recent: true, |
| | myOrganization: config.getAuthorityType() === 'organizations' |
| | } |
| | } |
| | }; |
| |
|
| | |
| | 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; |
| | } |
| |
|
| | |
| | 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.'); |
| | } |
| |
|
| | |
| | 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(); |
| | } |
| |
|
| | |
| | export async function openOneDrivePicker( |
| | authorityType?: 'personal' | 'organizations' |
| | ): Promise<PickerResult | null> { |
| | if (typeof window === 'undefined') { |
| | throw new Error('Not in browser environment'); |
| | } |
| |
|
| | |
| | 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 { |
| | |
| | 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 { |
| | 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(); |
| | }); |
| | } |
| |
|
| | |
| | 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 }; |
| |
|