UNPKG

@snehal96/unimail

Version:

Unified email fetching & document extraction layer for modern web apps

169 lines (168 loc) 7.06 kB
import express from 'express'; import open from 'open'; import { GoogleOAuthProvider } from './providers/GoogleOAuthProvider.js'; import { MemoryTokenStorage } from './storage/MemoryTokenStorage.js'; /** * Service to manage OAuth flows for different email providers */ export class OAuthService { /** * Create a new OAuthService * @param provider - The OAuth provider to use (Google, Outlook, etc.) * @param tokenStorage - Optional storage mechanism for tokens */ constructor(provider = new GoogleOAuthProvider(), tokenStorage = new MemoryTokenStorage()) { this.server = null; this.pendingCallbacks = new Map(); this.oauthProvider = provider; this.tokenStorage = tokenStorage; } /** * Start the OAuth flow by opening a browser window to the authorization URL * Returns the authorization URL that was opened */ async startOAuthFlow(options, userId, callbackPath = '/oauth/oauth2callback', port = 3000) { const { authUrl, state } = await this.oauthProvider.initializeOAuthFlow(options); // Start a local server to handle the OAuth callback await this.startCallbackServer(options, callbackPath, port, userId); // Open the authorization URL in the user's browser try { await open(authUrl); } catch (error) { console.warn(`Could not open browser automatically: ${error.message}`); console.log('Please open this URL in your browser:', authUrl); } return authUrl; } /** * Handle the OAuth callback manually (without running a local server) * This is useful for server-side applications or when the callback is handled externally */ async handleCallback(code, options, userId) { const tokenData = await this.oauthProvider.handleCallback(code, options); // Store tokens if a userId is provided if (userId) { await this.tokenStorage.saveTokens(userId, tokenData); } return tokenData; } /** * Refresh an access token using a refresh token */ async refreshToken(refreshToken, options, userId) { const tokenData = await this.oauthProvider.refreshToken(refreshToken, options); // Update stored tokens if a userId is provided if (userId) { await this.tokenStorage.updateTokens(userId, tokenData); } return tokenData; } /** * Get tokens for a user from storage */ async getTokens(userId) { return await this.tokenStorage.getTokens(userId); } /** * Revoke a token */ async revokeToken(token, options, userId) { const success = await this.oauthProvider.revokeToken(token, options); // Remove stored tokens if revocation was successful and a userId is provided if (success && userId) { await this.tokenStorage.deleteTokens(userId); } return success; } /** * Register a callback function to be called when the OAuth flow completes * Can be used instead of running a local server */ registerCallback(state, callback) { this.pendingCallbacks.set(state, callback); } /** * Start a local server to handle the OAuth callback * @private */ async startCallbackServer(options, callbackPath, port, userId) { // Stop any existing server await this.stopCallbackServer(); const app = express(); // Handle the OAuth callback app.get(callbackPath, async (req, res) => { try { const { code, error, state } = req.query; if (error) { const errorMsg = `Authorization error: ${error}`; res.send(`<html><body><h2>Authentication Failed</h2><p>${errorMsg}</p><p>Please close this window and try again.</p></body></html>`); // Call any registered callback for this state if (state && typeof state === 'string' && this.pendingCallbacks.has(state)) { const callback = this.pendingCallbacks.get(state); if (callback) { callback(null, new Error(errorMsg)); this.pendingCallbacks.delete(state); } } return; } if (!code || typeof code !== 'string') { const errorMsg = 'No authorization code received'; res.send(`<html><body><h2>Authentication Failed</h2><p>${errorMsg}</p><p>Please close this window and try again.</p></body></html>`); return; } // Exchange the code for tokens const tokenData = await this.oauthProvider.handleCallback(code, options); // Store the tokens if a userId is provided if (userId) { await this.tokenStorage.saveTokens(userId, tokenData); } // Call any registered callback for this state if (state && typeof state === 'string' && this.pendingCallbacks.has(state)) { const callback = this.pendingCallbacks.get(state); if (callback) { callback(tokenData); this.pendingCallbacks.delete(state); } } console.log(tokenData); res.send(`<html><body> <h2>Authentication Successful!</h2> <p>You have successfully authenticated with the email provider.</p> <p>You may close this window and return to the application.</p> ${tokenData.refreshToken ? `<p><strong>Refresh Token:</strong> ${tokenData.refreshToken}</p>` : ''} <script>window.close();</script> </body></html>`); // Optionally, close the server if we don't expect more callbacks setTimeout(() => this.stopCallbackServer(), 2000); } catch (error) { const errorMsg = `Error during OAuth callback: ${error.message}`; console.error(errorMsg); res.status(500).send(`<html><body><h2>Authentication Error</h2><p>${errorMsg}</p><p>Please close this window and try again.</p></body></html>`); } }); // Start the server return new Promise((resolve) => { this.server = app.listen(port, () => { console.log(`OAuth callback server listening on port ${port}`); resolve(); }); }); } /** * Stop the callback server if it's running * @private */ async stopCallbackServer() { if (this.server) { return new Promise((resolve) => { this.server.close(() => { this.server = null; resolve(); }); }); } } }