|
@@ -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');
|
|
|
+ public static getInstance(): OneDriveConfig {
|
|
|
+ if (!OneDriveConfig.instance) {
|
|
|
+ OneDriveConfig.instance = new OneDriveConfig();
|
|
|
+ }
|
|
|
+ return OneDriveConfig.instance;
|
|
|
}
|
|
|
- const config = await response.json();
|
|
|
- CLIENT_ID = config.onedrive?.client_id;
|
|
|
- if (!CLIENT_ID) {
|
|
|
- throw new Error('OneDrive client ID not configured');
|
|
|
+
|
|
|
+ public async initialize(authorityType?: 'personal' | 'organizations'): Promise<void> {
|
|
|
+ if (authorityType && this.currentAuthorityType !== authorityType) {
|
|
|
+ this.currentAuthorityType = authorityType;
|
|
|
+ this.msalInstance = null;
|
|
|
+ }
|
|
|
+ await this.getCredentials();
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-let msalInstance: PublicClientApplication | null = null;
|
|
|
+ public async ensureInitialized(authorityType?: 'personal' | 'organizations'): Promise<void> {
|
|
|
+ await this.initialize(authorityType);
|
|
|
+ }
|
|
|
|
|
|
-// Initialize MSAL authentication
|
|
|
-async function initializeMsal() {
|
|
|
- try {
|
|
|
- if (!CLIENT_ID) {
|
|
|
- await getCredentials();
|
|
|
+ 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');
|
|
|
}
|
|
|
|
|
|
- const msalParams = {
|
|
|
- auth: {
|
|
|
- authority: 'https://login.microsoftonline.com/consumers',
|
|
|
- clientId: CLIENT_ID
|
|
|
- }
|
|
|
- };
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- if (!msalInstance) {
|
|
|
- msalInstance = new PublicClientApplication(msalParams);
|
|
|
- if (msalInstance.initialize) {
|
|
|
- await msalInstance.initialize();
|
|
|
+ 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<string> {
|
|
|
- const authParams: PopupRequest = { scopes: ['OneDrive.ReadWrite'] };
|
|
|
+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 {
|
|
|
- msalInstance = await initializeMsal();
|
|
|
- if (!msalInstance) {
|
|
|
- throw new Error('MSAL not initialized');
|
|
|
- }
|
|
|
|
|
|
+ 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<string> {
|
|
|
return accessToken;
|
|
|
}
|
|
|
|
|
|
-const baseUrl = 'https://onedrive.live.com/picker';
|
|
|
-const params = {
|
|
|
- sdk: '8.0',
|
|
|
+interface PickerParams {
|
|
|
+ sdk: string;
|
|
|
entry: {
|
|
|
- oneDrive: {
|
|
|
- files: {}
|
|
|
- }
|
|
|
- },
|
|
|
- authentication: {},
|
|
|
+ oneDrive: Record<string, unknown>;
|
|
|
+ };
|
|
|
+ authentication: Record<string, unknown>;
|
|
|
messaging: {
|
|
|
- origin: window?.location?.origin,
|
|
|
- channelId: uuidv4()
|
|
|
- },
|
|
|
+ origin: string;
|
|
|
+ channelId: string;
|
|
|
+ };
|
|
|
typesAndSources: {
|
|
|
- mode: 'files',
|
|
|
- pivots: {
|
|
|
- oneDrive: true,
|
|
|
- recent: true
|
|
|
+ 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: any): Promise<Blob> {
|
|
|
- const accessToken = await getToken();
|
|
|
+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.');
|
|
|
+ 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.');
|
|
|
+ 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<any | null> {
|
|
|
+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;
|
|
@@ -166,7 +285,9 @@ export async function openOneDrivePicker(): Promise<any | null> {
|
|
|
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<any | null> {
|
|
|
}
|
|
|
} 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<any | null> {
|
|
|
|
|
|
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<any | null> {
|
|
|
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<any | null> {
|
|
|
}
|
|
|
|
|
|
// 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 };
|