add onedrive sub menu

This commit is contained in:
hurxxxx 2025-04-14 22:57:32 +09:00
parent e4c7417522
commit 5fd794612e
2 changed files with 273 additions and 157 deletions

View File

@ -229,94 +229,66 @@
{/if} {/if}
{#if $config?.features?.enable_onedrive_integration} {#if $config?.features?.enable_onedrive_integration}
<DropdownMenu.Item <DropdownMenu.Sub>
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" <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">
on:click={() => {
uploadOneDriveHandler();
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
<mask <mask id="mask0_87_7796" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="6" width="32" height="20">
id="mask0_87_7796" <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"/>
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> </mask>
<g mask="url(#mask0_87_7796)"> <g mask="url(#mask0_87_7796)">
<path <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)"/>
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" <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)"/>
fill="url(#paint0_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)"/>
<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> </g>
<defs> <defs>
<linearGradient <linearGradient id="paint0_linear_87_7796" x1="4.42591" y1="24.6668" x2="27.2309" y2="23.2764" gradientUnits="userSpaceOnUse">
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8"/> <stop stop-color="#2086B8"/>
<stop offset="1" stop-color="#46D3F6"/> <stop offset="1" stop-color="#46D3F6"/>
</linearGradient> </linearGradient>
<linearGradient <linearGradient id="paint1_linear_87_7796" x1="23.8302" y1="19.6668" x2="30.2108" y2="15.2082" gradientUnits="userSpaceOnUse">
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB"/> <stop stop-color="#1694DB"/>
<stop offset="1" stop-color="#62C3FE"/> <stop offset="1" stop-color="#62C3FE"/>
</linearGradient> </linearGradient>
<linearGradient <linearGradient id="paint2_linear_87_7796" x1="8.51037" y1="7.33333" x2="23.3335" y2="15.9348" gradientUnits="userSpaceOnUse">
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78"/> <stop stop-color="#0D3D78"/>
<stop offset="1" stop-color="#063B83"/> <stop offset="1" stop-color="#063B83"/>
</linearGradient> </linearGradient>
<linearGradient <linearGradient id="paint3_linear_87_7796" x1="-0.340429" y1="19.9998" x2="14.5634" y2="14.4649" gradientUnits="userSpaceOnUse">
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B"/> <stop stop-color="#16589B"/>
<stop offset="1" stop-color="#1464B7"/> <stop offset="1" stop-color="#1464B7"/>
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
<div class="line-clamp-1">{$i18n.t('OneDrive')}</div> <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');
}}
>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
</DropdownMenu.Item> </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} {/if}
</DropdownMenu.Content> </DropdownMenu.Content>
</div> </div>

View File

@ -2,70 +2,130 @@ import { PublicClientApplication } from '@azure/msal-browser';
import type { PopupRequest } from '@azure/msal-browser'; import type { PopupRequest } from '@azure/msal-browser';
import { v4 as uuidv4 } from 'uuid'; 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() { private constructor() {}
if (CLIENT_ID) return;
public static getInstance(): OneDriveConfig {
if (!OneDriveConfig.instance) {
OneDriveConfig.instance = new OneDriveConfig();
}
return OneDriveConfig.instance;
}
public async initialize(selectedAuthorityType?: 'personal' | 'organizations'): Promise<void> {
await this.getCredentials(selectedAuthorityType);
}
public async ensureInitialized(selectedAuthorityType?: 'personal' | 'organizations'): Promise<void> {
await this.initialize(selectedAuthorityType);
}
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');
}
const response = await fetch('/api/config');
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch OneDrive credentials'); throw new Error('Failed to fetch OneDrive credentials');
} }
const config = await response.json(); const config = await response.json();
CLIENT_ID = config.onedrive?.client_id;
if (!CLIENT_ID) { const newClientId = config.onedrive?.client_id;
throw new Error('OneDrive client ID not configured'); const newSharepointUrl = config.onedrive?.sharepoint_url;
}
if (!newClientId) {
throw new Error('OneDrive configuration is incomplete');
} }
let msalInstance: PublicClientApplication | null = null; // Reset MSAL instance if config changes
if (this.clientId &&
// Initialize MSAL authentication (this.clientId !== newClientId ||
async function initializeMsal() { this.authorityType !== selectedAuthorityType ||
try { this.sharepointUrl !== newSharepointUrl)) {
if (!CLIENT_ID) { this.msalInstance = null;
await getCredentials();
} }
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 = { const msalParams = {
auth: { auth: {
authority: 'https://login.microsoftonline.com/consumers', authority: `https://login.microsoftonline.com/${authorityEndpoint}`,
clientId: CLIENT_ID clientId: this.clientId
} }
}; };
if (!msalInstance) { this.msalInstance = new PublicClientApplication(msalParams);
msalInstance = new PublicClientApplication(msalParams); if (this.msalInstance.initialize) {
if (msalInstance.initialize) { await this.msalInstance.initialize();
await msalInstance.initialize();
} }
} }
return msalInstance; return this.msalInstance;
} catch (error) { }
throw new Error(
'MSAL initialization failed: ' + (error instanceof Error ? error.message : String(error)) 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 // Retrieve OneDrive access token
async function getToken(): Promise<string> { async function getToken(resource?: string): Promise<string> {
const authParams: PopupRequest = { scopes: ['OneDrive.ReadWrite'] }; const config = OneDriveConfig.getInstance();
let accessToken = ''; await config.ensureInitialized();
try {
msalInstance = await initializeMsal();
if (!msalInstance) {
throw new Error('MSAL not initialized');
}
const authorityType = config.getAuthorityType();
const scopes = authorityType === 'organizations'
? [`${resource || config.getBaseUrl()}/.default`]
: ['OneDrive.ReadWrite'];
const authParams: PopupRequest = { scopes };
let accessToken = '';
try {
const msalInstance = await config.getMsalInstance();
const resp = await msalInstance.acquireTokenSilent(authParams); const resp = await msalInstance.acquireTokenSilent(authParams);
accessToken = resp.accessToken; accessToken = resp.accessToken;
} catch (err) { } catch (err) {
if (!msalInstance) { const msalInstance = await config.getMsalInstance();
throw new Error('MSAL not initialized');
}
try { try {
const resp = await msalInstance.loginPopup(authParams); const resp = await msalInstance.loginPopup(authParams);
msalInstance.setActiveAccount(resp.account); msalInstance.setActiveAccount(resp.account);
@ -88,18 +148,35 @@ async function getToken(): Promise<string> {
return accessToken; return accessToken;
} }
const baseUrl = 'https://onedrive.live.com/picker'; // Get picker parameters based on account type
const params = { function getPickerParams(): {
sdk: string;
entry: {
oneDrive: Record<string, unknown>;
};
authentication: Record<string, unknown>;
messaging: {
origin: string;
channelId: string;
};
typesAndSources: {
mode: string;
pivots: Record<string, boolean>;
};
} {
const channelId = uuidv4();
if (OneDriveConfig.getInstance().getAuthorityType() === 'organizations') {
// Parameters for OneDrive for Business
return {
sdk: '8.0', sdk: '8.0',
entry: { entry: {
oneDrive: { oneDrive: {}
files: {}
}
}, },
authentication: {}, authentication: {},
messaging: { messaging: {
origin: window?.location?.origin, origin: window?.location?.origin || '',
channelId: uuidv4() channelId
}, },
typesAndSources: { typesAndSources: {
mode: 'files', mode: 'files',
@ -109,39 +186,91 @@ const params = {
} }
} }
}; };
} 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 // Download file from OneDrive
async function downloadOneDriveFile(fileInfo: any): Promise<Blob> { async function downloadOneDriveFile(fileInfo: Record<string, any>): Promise<Blob> {
const accessToken = await getToken(); const accessToken = await getToken();
if (!accessToken) { if (!accessToken) {
throw new Error('Unable to retrieve OneDrive access token.'); 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 fileInfoUrl = `${fileInfo['@sharePoint.endpoint']}/drives/${fileInfo.parentReference.driveId}/items/${fileInfo.id}`;
const response = await fetch(fileInfoUrl, { const response = await fetch(fileInfoUrl, {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
} }
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch file information.'); throw new Error('Failed to fetch file information.');
} }
const fileData = await response.json(); const fileData = await response.json();
const downloadUrl = fileData['@content.downloadUrl']; const downloadUrl = fileData['@content.downloadUrl'];
const downloadResponse = await fetch(downloadUrl); const downloadResponse = await fetch(downloadUrl);
if (!downloadResponse.ok) { if (!downloadResponse.ok) {
throw new Error('Failed to download file.'); throw new Error('Failed to download file.');
} }
return await downloadResponse.blob(); 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 // 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') { if (typeof window === 'undefined') {
throw new Error('Not in browser environment'); throw new Error('Not in browser environment');
} }
// Force reinitialization of OneDrive config
const config = OneDriveConfig.getInstance();
await config.initialize();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let pickerWindow: Window | null = null; let pickerWindow: Window | null = null;
let channelPort: MessagePort | null = null; let channelPort: MessagePort | null = null;
const params = getPickerParams();
const baseUrl = config.getBaseUrl();
const handleWindowMessage = (event: MessageEvent) => { const handleWindowMessage = (event: MessageEvent) => {
if (event.source !== pickerWindow) return; if (event.source !== pickerWindow) return;
@ -166,7 +295,9 @@ export async function openOneDrivePicker(): Promise<any | null> {
switch (command.command) { switch (command.command) {
case 'authenticate': { case 'authenticate': {
try { 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) { if (newToken) {
channelPort?.postMessage({ channelPort?.postMessage({
type: 'result', type: 'result',
@ -178,9 +309,12 @@ export async function openOneDrivePicker(): Promise<any | null> {
} }
} catch (err) { } catch (err) {
channelPort?.postMessage({ channelPort?.postMessage({
type: 'result',
id: portData.id,
data: {
result: 'error', result: 'error',
error: { code: 'tokenError', message: 'Failed to get token' }, error: { code: 'tokenError', message: 'Failed to get token' }
isExpected: true }
}); });
} }
break; break;
@ -240,7 +374,14 @@ export async function openOneDrivePicker(): Promise<any | null> {
const queryString = new URLSearchParams({ const queryString = new URLSearchParams({
filePicker: JSON.stringify(params) 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'); const form = pickerWindow.document.createElement('form');
form.setAttribute('action', url); form.setAttribute('action', url);
@ -268,7 +409,10 @@ export async function openOneDrivePicker(): Promise<any | null> {
} }
// Pick and download file from OneDrive // 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(); const pickerResult = await openOneDrivePicker();
if (!pickerResult || !pickerResult.items || pickerResult.items.length === 0) { if (!pickerResult || !pickerResult.items || pickerResult.items.length === 0) {