import { Injectable, BadRequestException } from '@nestjs/common'; import { google } from 'googleapis'; import { Client } from '@microsoft/microsoft-graph-client'; @Injectable() export class OAuthService { private readonly gmailOAuth2Client = new google.auth.OAuth2( process.env.GMAIL_CLIENT_ID, process.env.GMAIL_CLIENT_SECRET, process.env.GMAIL_REDIRECT_URI || 'http://localhost:3000/api/integration/oauth/callback/gmail', ); private readonly outlookScopes = [ 'https://graph.microsoft.com/mail.send', 'https://graph.microsoft.com/user.read', ]; private readonly gmailScopes = [ 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/userinfo.email', ]; generateGmailAuthUrl(state: string): string { return this.gmailOAuth2Client.generateAuthUrl({ access_type: 'offline', scope: this.gmailScopes, state: state, prompt: 'consent', }); } generateOutlookAuthUrl(state: string): string { const clientId = process.env.OUTLOOK_CLIENT_ID; const redirectUri = encodeURIComponent( process.env.OUTLOOK_REDIRECT_URI || 'http://localhost:3000/api/communication/oauth/callback/outlook', ); const scope = encodeURIComponent(this.outlookScopes.join(' ')); return ( `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + `client_id=${clientId}&` + `response_type=code&` + `redirect_uri=${redirectUri}&` + `scope=${scope}&` + `response_mode=query&` + `state=${state}` ); } async exchangeGmailCode(code: string): Promise { try { const { tokens } = await this.gmailOAuth2Client.getToken(code); this.gmailOAuth2Client.setCredentials(tokens); // Get user info const oauth2 = google.oauth2({ auth: this.gmailOAuth2Client, version: 'v2', }); const userInfo = await oauth2.userinfo.get(); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresIn: tokens.expiry_date, email: userInfo.data.email, tokenType: tokens.token_type, scope: tokens.scope, }; } catch (error) { throw new BadRequestException( 'Failed to exchange Gmail authorization code', ); } } async exchangeOutlookCode(code: string): Promise { try { const clientId = process.env.OUTLOOK_CLIENT_ID; const clientSecret = process.env.OUTLOOK_CLIENT_SECRET; const redirectUri = process.env.OUTLOOK_REDIRECT_URI || 'http://localhost:3000/api/communication/oauth/callback/outlook'; const tokenUrl = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: clientId!, client_secret: clientSecret!, code: code, redirect_uri: redirectUri, grant_type: 'authorization_code', scope: this.outlookScopes.join(' '), }), }); const tokenData = await response.json(); if (!response.ok) { throw new Error(tokenData.error_description || 'Token exchange failed'); } // Get user info using the access token const graphClient = Client.init({ authProvider: { getAccessToken: async () => tokenData.access_token, } as any, }); const userInfo = await graphClient.api('/me').get(); return { accessToken: tokenData.access_token, refreshToken: tokenData.refresh_token, expiresIn: tokenData.expires_in, email: userInfo.mail || userInfo.userPrincipalName, tokenType: tokenData.token_type, scope: tokenData.scope, }; } catch (error) { throw new BadRequestException( 'Failed to exchange Outlook authorization code', ); } } async refreshGmailToken(refreshToken: string): Promise { try { this.gmailOAuth2Client.setCredentials({ refresh_token: refreshToken, }); const { credentials } = await this.gmailOAuth2Client.refreshAccessToken(); return { accessToken: credentials.access_token, refreshToken: credentials.refresh_token || refreshToken, expiresIn: credentials.expiry_date, tokenType: credentials.token_type, }; } catch (error) { throw new BadRequestException('Failed to refresh Gmail token'); } } async refreshOutlookToken(refreshToken: string): Promise { try { const clientId = process.env.OUTLOOK_CLIENT_ID; const clientSecret = process.env.OUTLOOK_CLIENT_SECRET; const tokenUrl = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: clientId!, client_secret: clientSecret!, refresh_token: refreshToken, grant_type: 'refresh_token', scope: this.outlookScopes.join(' '), }), }); const tokenData = await response.json(); if (!response.ok) { throw new Error(tokenData.error_description || 'Token refresh failed'); } return { accessToken: tokenData.access_token, refreshToken: tokenData.refresh_token || refreshToken, expiresIn: tokenData.expires_in, tokenType: tokenData.token_type, }; } catch (error) { throw new BadRequestException('Failed to refresh Outlook token'); } } generateState(prefix: string, additionalData?: any): string { const timestamp = Date.now(); const random = Math.random().toString(36).substring(7); const data = additionalData ? JSON.stringify(additionalData) : ''; return `${prefix}:${timestamp}:${random}:${Buffer.from(data).toString('base64')}`; } parseState(state: string): { prefix: string; timestamp: number; random: string; data: any; } { const parts = state.split(':'); if (parts.length !== 4) { throw new BadRequestException('Invalid state parameter'); } const [prefix, timestamp, random, encodedData] = parts; const data = encodedData ? JSON.parse(Buffer.from(encodedData, 'base64').toString()) : null; return { prefix, timestamp: parseInt(timestamp), random, data, }; } }