Browse Source

add onedrive sub menu

hurxxxx 3 months ago
parent
commit
5fd794612e
2 changed files with 272 additions and 156 deletions
  1. 59 87
      src/lib/components/chat/MessageInput/InputMenu.svelte
  2. 213 69
      src/lib/utils/onedrive-file-picker.ts

+ 59 - 87
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -229,94 +229,66 @@
 			{/if}
 
 			{#if $config?.features?.enable_onedrive_integration}
-				<DropdownMenu.Item
-					class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
-					on:click={() => {
-						uploadOneDriveHandler();
-					}}
-				>
-					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
-						<mask
-							id="mask0_87_7796"
-							style="mask-type:alpha"
-							maskUnits="userSpaceOnUse"
-							x="0"
-							y="6"
-							width="32"
-							height="20"
+				<DropdownMenu.Sub>
+					<DropdownMenu.SubTrigger class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full">
+						<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
+							<mask id="mask0_87_7796" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="6" width="32" height="20">
+								<path d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z" fill="#C4C4C4"/>
+							</mask>
+							<g mask="url(#mask0_87_7796)">
+								<path d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z" fill="url(#paint0_linear_87_7796)"/>
+								<path d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z" fill="url(#paint1_linear_87_7796)"/>
+								<path d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z" fill="url(#paint2_linear_87_7796)"/>
+								<path d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z" fill="url(#paint3_linear_87_7796)"/>
+							</g>
+							<defs>
+								<linearGradient id="paint0_linear_87_7796" x1="4.42591" y1="24.6668" x2="27.2309" y2="23.2764" gradientUnits="userSpaceOnUse">
+									<stop stop-color="#2086B8"/>
+									<stop offset="1" stop-color="#46D3F6"/>
+								</linearGradient>
+								<linearGradient id="paint1_linear_87_7796" x1="23.8302" y1="19.6668" x2="30.2108" y2="15.2082" gradientUnits="userSpaceOnUse">
+									<stop stop-color="#1694DB"/>
+									<stop offset="1" stop-color="#62C3FE"/>
+								</linearGradient>
+								<linearGradient id="paint2_linear_87_7796" x1="8.51037" y1="7.33333" x2="23.3335" y2="15.9348" gradientUnits="userSpaceOnUse">
+									<stop stop-color="#0D3D78"/>
+									<stop offset="1" stop-color="#063B83"/>
+								</linearGradient>
+								<linearGradient id="paint3_linear_87_7796" x1="-0.340429" y1="19.9998" x2="14.5634" y2="14.4649" gradientUnits="userSpaceOnUse">
+									<stop stop-color="#16589B"/>
+									<stop offset="1" stop-color="#1464B7"/>
+								</linearGradient>
+							</defs>
+						</svg>
+						<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div>
+					</DropdownMenu.SubTrigger>
+					<DropdownMenu.SubContent 
+						class="w-[calc(100vw-2rem)] max-w-[280px] rounded-xl px-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
+						side={$mobile ? "bottom" : "right"}
+						sideOffset={$mobile ? 5 : 0}
+						alignOffset={$mobile ? 0 : -8}
+					>
+						<DropdownMenu.Item
+							class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
+							on:click={() => {
+								uploadOneDriveHandler('personal');
+							}}
 						>
-							<path
-								d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
-								fill="#C4C4C4"
-							/>
-						</mask>
-						<g mask="url(#mask0_87_7796)">
-							<path
-								d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
-								fill="url(#paint0_linear_87_7796)"
-							/>
-							<path
-								d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
-								fill="url(#paint1_linear_87_7796)"
-							/>
-							<path
-								d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
-								fill="url(#paint2_linear_87_7796)"
-							/>
-							<path
-								d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
-								fill="url(#paint3_linear_87_7796)"
-							/>
-						</g>
-						<defs>
-							<linearGradient
-								id="paint0_linear_87_7796"
-								x1="4.42591"
-								y1="24.6668"
-								x2="27.2309"
-								y2="23.2764"
-								gradientUnits="userSpaceOnUse"
-							>
-								<stop stop-color="#2086B8" />
-								<stop offset="1" stop-color="#46D3F6" />
-							</linearGradient>
-							<linearGradient
-								id="paint1_linear_87_7796"
-								x1="23.8302"
-								y1="19.6668"
-								x2="30.2108"
-								y2="15.2082"
-								gradientUnits="userSpaceOnUse"
-							>
-								<stop stop-color="#1694DB" />
-								<stop offset="1" stop-color="#62C3FE" />
-							</linearGradient>
-							<linearGradient
-								id="paint2_linear_87_7796"
-								x1="8.51037"
-								y1="7.33333"
-								x2="23.3335"
-								y2="15.9348"
-								gradientUnits="userSpaceOnUse"
-							>
-								<stop stop-color="#0D3D78" />
-								<stop offset="1" stop-color="#063B83" />
-							</linearGradient>
-							<linearGradient
-								id="paint3_linear_87_7796"
-								x1="-0.340429"
-								y1="19.9998"
-								x2="14.5634"
-								y2="14.4649"
-								gradientUnits="userSpaceOnUse"
-							>
-								<stop stop-color="#16589B" />
-								<stop offset="1" stop-color="#1464B7" />
-							</linearGradient>
-						</defs>
-					</svg>
-					<div class="line-clamp-1">{$i18n.t('OneDrive')}</div>
-				</DropdownMenu.Item>
+							<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
+						</DropdownMenu.Item>
+						<DropdownMenu.Item
+							class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
+							on:click={() => {
+								uploadOneDriveHandler('organizations');
+							}}
+						>
+							<div class="flex flex-col">
+								<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (work/school)')}</div>
+								<div class="text-xs text-gray-500">Includes SharePoint</div>
+							</div>
+						</DropdownMenu.Item>
+					</DropdownMenu.SubContent>
+				</DropdownMenu.Sub>
 			{/if}
 		</DropdownMenu.Content>
 	</div>

+ 213 - 69
src/lib/utils/onedrive-file-picker.ts

@@ -2,70 +2,130 @@ 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 authorityType: 'personal' | 'organizations' = 'personal';
+	private sharepointUrl: string = '';
+	private msalInstance: PublicClientApplication | null = null;
 
-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(selectedAuthorityType?: 'personal' | 'organizations'): Promise<void> {
+		await this.getCredentials(selectedAuthorityType);
 	}
-}
 
-let msalInstance: PublicClientApplication | null = null;
+	public async ensureInitialized(selectedAuthorityType?: 'personal' | 'organizations'): Promise<void> {
+		await this.initialize(selectedAuthorityType);
+	}
 
-// Initialize MSAL authentication
-async function initializeMsal() {
-	try {
-		if (!CLIENT_ID) {
-			await getCredentials();
+	private async getCredentials(selectedAuthorityType?: 'personal' | 'organizations'): Promise<void> {
+		let response;
+		if(window.location.hostname === 'localhost') {
+			response = await fetch('http://localhost:8080/api/config');
+		} else {
+			response = await fetch('/api/config');
+		}
+		
+		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
-			}
-		};
+		// Reset MSAL instance if config changes
+		if (this.clientId && 
+			(this.clientId !== newClientId || 
+			 this.authorityType !== selectedAuthorityType || 
+			 this.sharepointUrl !== newSharepointUrl)) {
+			this.msalInstance = null;
+		}
+
+		this.clientId = newClientId;
+		this.authorityType = selectedAuthorityType || 'personal';
+		this.sharepointUrl = newSharepointUrl;
+	}
+
+	public async getMsalInstance(): Promise<PublicClientApplication> {
+		await this.ensureInitialized();
+		
+		if (!this.msalInstance) {
+			const authorityEndpoint = this.authorityType === '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.authorityType;
+	}
+
+	public getSharepointUrl(): string {
+		return this.sharepointUrl;
+	}
+
+	public getBaseUrl(): string {
+		if (this.authorityType === '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): Promise<string> {
+	const config = OneDriveConfig.getInstance();
+	await config.ensureInitialized();
+	
+	const authorityType = config.getAuthorityType();
+
+	const scopes = authorityType === '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();
 		const resp = await msalInstance.acquireTokenSilent(authParams);
 		accessToken = resp.accessToken;
 	} catch (err) {
-		if (!msalInstance) {
-			throw new Error('MSAL not initialized');
-		}
-
+		const msalInstance = await config.getMsalInstance();
 		try {
 			const resp = await msalInstance.loginPopup(authParams);
 			msalInstance.setActiveAccount(resp.account);
@@ -88,60 +148,129 @@ async function getToken(): Promise<string> {
 	return accessToken;
 }
 
-const baseUrl = 'https://onedrive.live.com/picker';
-const params = {
-	sdk: '8.0',
+// Get picker parameters based on account type
+function getPickerParams(): {
+	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>;
+	};
+} {
+	const channelId = uuidv4();
+	
+	if (OneDriveConfig.getInstance().getAuthorityType() === 'organizations') {
+		// Parameters for OneDrive for Business
+		return {
+			sdk: '8.0',
+			entry: {
+				oneDrive: {}
+			},
+			authentication: {},
+			messaging: {
+				origin: window?.location?.origin || '',
+				channelId
+			},
+			typesAndSources: {
+				mode: 'files',
+				pivots: {
+					oneDrive: true,
+					recent: true
+				}
+			}
+		};
+	} else {
+		// Parameters for personal OneDrive
+		return {
+			sdk: '8.0',
+			entry: {
+				oneDrive: {
+					files: {}
+				}
+			},
+			authentication: {},
+			messaging: {
+				origin: window?.location?.origin || '',
+				channelId
+			},
+			typesAndSources: {
+				mode: 'files',
+				pivots: {
+					oneDrive: true,
+					recent: true
+				}
+			}
+		};
 	}
-};
+}
 
 // Download file from OneDrive
-async function downloadOneDriveFile(fileInfo: any): Promise<Blob> {
+async function downloadOneDriveFile(fileInfo: Record<string, any>): Promise<Blob> {
 	const accessToken = await getToken();
 	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.');
 	}
+	
 	const fileData = await response.json();
 	const downloadUrl = fileData['@content.downloadUrl'];
 	const downloadResponse = await fetch(downloadUrl);
+	
 	if (!downloadResponse.ok) {
 		throw new Error('Failed to download file.');
 	}
+	
 	return await downloadResponse.blob();
 }
 
+interface PickerResult {
+	items?: Array<{
+		id: string;
+		name: string;
+		parentReference: {
+			driveId: string;
+		};
+		'@sharePoint.endpoint': string;
+		[key: string]: any;
+	}>;
+	command?: string;
+	[key: string]: any;
+}
+
 // Open OneDrive file picker and return selected file metadata
-export async function openOneDrivePicker(): Promise<any | null> {
+export async function openOneDrivePicker(): Promise<PickerResult | null> {
 	if (typeof window === 'undefined') {
 		throw new Error('Not in browser environment');
 	}
+
+	// Force reinitialization of OneDrive config
+	const config = OneDriveConfig.getInstance();
+	await config.initialize();
+	
 	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 +295,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 = OneDriveConfig.getInstance().getAuthorityType() === 'organizations' ? command.resource : undefined;
+								const newToken = await getToken(resource);
 								if (newToken) {
 									channelPort?.postMessage({
 										type: 'result',
@@ -178,9 +309,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;
@@ -240,7 +374,14 @@ export async function openOneDrivePicker(): Promise<any | null> {
 				const queryString = new URLSearchParams({
 					filePicker: JSON.stringify(params)
 				});
-				const url = `${baseUrl}?${queryString.toString()}`;
+
+				let url = '';
+				if(OneDriveConfig.getInstance().getAuthorityType() === 'organizations') {
+					url = baseUrl + `/_layouts/15/FilePicker.aspx?${queryString}`;
+				}else{
+					url = baseUrl + `?${queryString}`;
+				}
+			
 
 				const form = pickerWindow.document.createElement('form');
 				form.setAttribute('action', url);
@@ -268,7 +409,10 @@ export async function openOneDrivePicker(): Promise<any | null> {
 }
 
 // Pick and download file from OneDrive
-export async function pickAndDownloadFile(): Promise<{ blob: Blob; name: string } | null> {
+export async function pickAndDownloadFile(authorityType: 'personal' | 'organizations' = 'personal'): Promise<{ blob: Blob; name: string } | null> {
+	const config = OneDriveConfig.getInstance();
+	await config.initialize(authorityType);
+	
 	const pickerResult = await openOneDrivePicker();
 
 	if (!pickerResult || !pickerResult.items || pickerResult.items.length === 0) {
@@ -281,4 +425,4 @@ export async function pickAndDownloadFile(): Promise<{ blob: Blob; name: string
 	return { blob, name: selectedFile.name };
 }
 
-export { downloadOneDriveFile };
+export { downloadOneDriveFile };