Update dependencies and refactor Graph API integration

- Added isomorphic-fetch to package.json and package-lock.json for improved fetch capabilities.
- Removed the deprecated graphApi.js file and refactored the renderer to utilize a new graph service structure.
- Updated renderer.js to enhance folder content retrieval and authentication flow, ensuring better management of access tokens.
- Improved logging for better visibility during the sync process and error handling.
This commit is contained in:
Tiger Ren 2025-01-22 17:26:01 +08:00
parent f09966ecd5
commit 0760fb2c2a
8 changed files with 341 additions and 13 deletions

53
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@electron/remote": "^2.1.2",
"@microsoft/microsoft-graph-client": "^3.0.2",
"electron-store": "^10.0.0",
"isomorphic-fetch": "^3.0.0",
"node-fetch": "^3.3.0",
"uuid": "^11.0.4"
},
@ -787,6 +788,34 @@
"node": ">=10.19.0"
}
},
"node_modules/isomorphic-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
"dependencies": {
"node-fetch": "^2.6.1",
"whatwg-fetch": "^3.4.1"
}
},
"node_modules/isomorphic-fetch/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@ -1174,6 +1203,11 @@
"node": ">= 8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -1235,6 +1269,25 @@
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/when-exit": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.3.tgz",

View File

@ -14,11 +14,12 @@
"electron": "^33.2.1"
},
"dependencies": {
"@electron/remote": "^2.1.2",
"electron-store": "^10.0.0",
"node-fetch": "^3.3.0",
"uuid": "^11.0.4",
"@azure/msal-node": "^1.18.4",
"@microsoft/microsoft-graph-client": "^3.0.2"
"@electron/remote": "^2.1.2",
"@microsoft/microsoft-graph-client": "^3.0.2",
"electron-store": "^10.0.0",
"isomorphic-fetch": "^3.0.0",
"node-fetch": "^3.3.0",
"uuid": "^11.0.4"
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const { PublicClientApplication, InteractionRequiredAuthError } = require('@azure/msal-node');
const { shell } = require('electron');
class AuthProvider {
msalConfig
clientApplication;
account;
cache;
constructor(msalConfig) {
/**
* Initialize a public client application. For more information, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/initialize-public-client-application.md
*/
this.msalConfig = msalConfig;
this.clientApplication = new PublicClientApplication(this.msalConfig);
this.cache = this.clientApplication.getTokenCache();
this.account = null;
}
async login() {
const authResponse = await this.getToken({
// If there are scopes that you would like users to consent up front, add them below
// by default, MSAL will add the OIDC scopes to every token request, so we omit those here
scopes: [],
});
return this.handleResponse(authResponse);
}
async logout() {
if (!this.account) return;
try {
/**
* If you would like to end the session with AAD, use the logout endpoint. You'll need to enable
* the optional token claim 'login_hint' for this to work as expected. For more information, visit:
* https://learn.microsoft.com/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
*/
if (this.account.idTokenClaims.hasOwnProperty('login_hint')) {
await shell.openExternal(`${this.msalConfig.auth.authority}/oauth2/v2.0/logout?logout_hint=${encodeURIComponent(this.account.idTokenClaims.login_hint)}`);
}
await this.cache.removeAccount(this.account);
this.account = null;
} catch (error) {
console.log(error);
}
}
async getToken(tokenRequest) {
let authResponse;
const account = this.account || (await this.getAccount());
if (account) {
tokenRequest.account = account;
authResponse = await this.getTokenSilent(tokenRequest);
} else {
authResponse = await this.getTokenInteractive(tokenRequest);
}
return authResponse || null;
}
async getTokenSilent(tokenRequest) {
try {
return await this.clientApplication.acquireTokenSilent(tokenRequest);
} catch (error) {
if (error instanceof InteractionRequiredAuthError) {
console.log('Silent token acquisition failed, acquiring token interactive');
return await this.getTokenInteractive(tokenRequest);
}
console.log(error);
}
}
async getTokenInteractive(tokenRequest) {
try {
const openBrowser = async (url) => {
await shell.openExternal(url);
};
const authResponse = await this.clientApplication.acquireTokenInteractive({
...tokenRequest,
openBrowser,
successTemplate: '<h1>Successfully signed in!</h1> <p>You can close this window now.</p>',
errorTemplate: '<h1>Oops! Something went wrong</h1> <p>Check the console for more information.</p>',
});
return authResponse;
} catch (error) {
throw error;
}
}
/**
* Handles the response from a popup or redirect. If response is null, will check if we have any accounts and attempt to sign in.
* @param response
*/
async handleResponse(response) {
if (response !== null) {
this.account = response.account;
} else {
this.account = await this.getAccount();
}
return this.account;
}
/**
* Calls getAllAccounts and determines the correct account to sign into, currently defaults to first account found in cache.
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
*/
async getAccount() {
const currentAccounts = await this.cache.getAllAccounts();
if (!currentAccounts) {
console.log('No accounts detected');
return null;
}
if (currentAccounts.length > 1) {
// Add choose account code here
console.log('Multiple accounts detected, need to add choose account code.');
return currentAccounts[0];
} else if (currentAccounts.length === 1) {
return currentAccounts[0];
} else {
return null;
}
}
}
module.exports = AuthProvider;

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const { LogLevel } = require("@azure/msal-node");
/**
* Configuration object to be passed to MSAL instance on creation.
* For a full list of MSAL.js configuration parameters, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
*/
const AAD_ENDPOINT_HOST = "https://login.microsoftonline.com/"; // include the trailing slash
const msalConfig = {
auth: {
clientId: "d581ab07-3a21-44d3-84c4-16b06bef6266",
authority: `${AAD_ENDPOINT_HOST}common`,
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: LogLevel.Verbose,
},
},
};
/**
* Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
*/
const GRAPH_ENDPOINT_HOST = "https://graph.microsoft.com/"; // include the trailing slash
const protectedResources = {
graphMe: {
endpoint: `${GRAPH_ENDPOINT_HOST}v1.0/me`,
scopes: ["User.Read"],
}
};
module.exports = {
msalConfig: msalConfig,
protectedResources: protectedResources,
};

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const IPC_MESSAGES = {
SHOW_WELCOME_MESSAGE: 'SHOW_WELCOME_MESSAGE',
LOGIN: 'LOGIN',
LOGOUT: 'LOGOUT',
GET_PROFILE: 'GET_PROFILE',
SET_PROFILE: 'SET_PROFILE',
}
module.exports = {
IPC_MESSAGES: IPC_MESSAGES,
}

61
renderer/graph/graph.js Normal file
View File

@ -0,0 +1,61 @@
const { Client } = require('@microsoft/microsoft-graph-client');
const { msalConfig } = require('./authConfig');
const AuthProvider = require('./AuthProvider');
require('isomorphic-fetch');
class GraphService {
constructor() {
this.authProvider = new AuthProvider(msalConfig);
this.graphClient = null;
}
async initialize() {
try {
// Get account
await this.authProvider.login();
// Get token with OneDrive scopes
const tokenRequest = {
scopes: ['Files.Read', 'Files.Read.All', 'Sites.Read.All']
};
const authResponse = await this.authProvider.getToken(tokenRequest);
// Initialize Graph client
this.graphClient = Client.init({
authProvider: (done) => {
done(null, authResponse.accessToken);
}
});
return authResponse.accessToken;
} catch (error) {
console.error('Error initializing Graph service:', error);
throw error;
}
}
async listFolderContents(folderPath) {
if (!this.graphClient) {
await this.initialize();
}
try {
// Clean up the path
const cleanPath = folderPath.replace(/^\/+|\/+$/g, '');
const endpoint = `/me/drive/root:/${cleanPath}:/children`;
// Get items from the folder
const response = await this.graphClient.api(endpoint)
.select('id,name,size,file,@microsoft.graph.downloadUrl')
.get();
return response.value;
} catch (error) {
console.error('Error listing folder contents:', error);
throw error;
}
}
}
module.exports = new GraphService();

View File

@ -1,5 +1,8 @@
const { ipcRenderer, session } = require('electron');
const graphApi = require('./graphApi');
const graphApi = require('./graph/graphApi');
// Wait for webview to load
const webview = document.getElementById('main-content');
@ -82,17 +85,23 @@ document.getElementById('sync-button').addEventListener('click', async () => {
syncButton.textContent = 'Syncing...';
syncButton.disabled = true;
// Get folder contents using GraphApiClient
console.log('renderer: Getting folder contents:', onedriveSource);
const items = await graphApi.listFolderContents(onedriveSource);
console.log('renderer: Found items:', items.length);
// Get folder contents using GraphService
console.log('Getting folder contents:', onedriveSource);
const graphService = require('./graph/graph');
// This will handle authentication and get folder contents
const items = await graphService.listFolderContents(onedriveSource);
console.log('Found items:', items.length);
// Process the items
// TODO: Handle the items as needed
showStatus(`Found ${items.length} items`);
// Send the items to the main process
ipcRenderer.send('start-sync', { destFolder, onedriveSource , items});
ipcRenderer.send('start-sync', {
destFolder,
onedriveSource,
items,
accessToken: await graphService.initialize() // Get fresh token for download
});
} catch (error) {
console.error('Sync error:', error);