diff --git a/packages/api/src/@core/connections/connections.controller.spec.ts b/packages/api/src/@core/connections/connections.controller.spec.ts deleted file mode 100644 index b3fdb3a29..000000000 --- a/packages/api/src/@core/connections/connections.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ConnectionsController } from './connections.controller'; -import { ConnectionsService } from './services/connections.service'; - -describe('ConnectionsController', () => { - let controller: ConnectionsController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [ConnectionsController], - providers: [ConnectionsService], - }).compile(); - - controller = module.get(ConnectionsController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/packages/api/src/@core/connections/connections.module.ts b/packages/api/src/@core/connections/connections.module.ts index 3ce9d156b..f3072162d 100644 --- a/packages/api/src/@core/connections/connections.module.ts +++ b/packages/api/src/@core/connections/connections.module.ts @@ -1,11 +1,7 @@ import { Module } from '@nestjs/common'; -import { ConnectionsService } from './services/connections.service'; -import { ConnectionsController } from './connections.controller'; -import { CrmConnectionsService } from './services/crm/crm-connection.service'; -import { PrismaService } from '../prisma/prisma.service'; +import { CrmConnectionModule } from './crm/crm-connection.module'; @Module({ - controllers: [ConnectionsController], - providers: [ConnectionsService, CrmConnectionsService, PrismaService], + imports: [CrmConnectionModule], }) export class ConnectionsModule {} diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/crm/crm-connection.controller.ts similarity index 68% rename from packages/api/src/@core/connections/connections.controller.ts rename to packages/api/src/@core/connections/crm/crm-connection.controller.ts index 9b2a920e6..1d1255d54 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/crm/crm-connection.controller.ts @@ -1,12 +1,12 @@ import { Controller, Get, Query, Res } from '@nestjs/common'; -import { ConnectionsService } from './services/connections.service'; import { Response } from 'express'; // Importing the Express Response type for type checking +import { CrmConnectionsService } from './services/crm-connection.service'; -@Controller('connections') -export class ConnectionsController { - constructor(private readonly connectionsService: ConnectionsService) {} +@Controller('connections/crm') +export class CrmConnectionsController { + constructor(private readonly crmConnectionsService: CrmConnectionsService) {} - @Get('oauth/crm/callback') + @Get('oauth/callback') handleCRMCallback( @Res() res: Response, @Query('projectId') projectId: string, @@ -18,7 +18,7 @@ export class ConnectionsController { ) { //TODO; ADD VERIFICATION OF PARAMS - this.connectionsService.handleCRMCallBack( + this.crmConnectionsService.handleCRMCallBack( projectId, linkedUserId, providerName, diff --git a/packages/api/src/@core/connections/crm/crm-connection.module.ts b/packages/api/src/@core/connections/crm/crm-connection.module.ts new file mode 100644 index 000000000..885df4a56 --- /dev/null +++ b/packages/api/src/@core/connections/crm/crm-connection.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { CrmConnectionsController } from './crm-connection.controller'; +import { CrmConnectionsService } from './services/crm-connection.service'; +import { PrismaService } from 'src/@core/prisma/prisma.service'; +import { FreshsalesConnectionService } from './services/freshsales/freshsales.service'; +import { HubspotConnectionService } from './services/hubspot/hubspot.service'; +import { PipedriveConnectionService } from './services/pipedrive/pipedrive.service'; +import { ZendeskConnectionService } from './services/zendesk/zendesk.service'; +import { ZohoConnectionService } from './services/zoho/zoho.service'; + +@Module({ + controllers: [CrmConnectionsController], + providers: [ + CrmConnectionsService, + PrismaService, + FreshsalesConnectionService, + HubspotConnectionService, + PipedriveConnectionService, + ZendeskConnectionService, + ZohoConnectionService, + ], + exports: [CrmConnectionsService], +}) +export class CrmConnectionModule {} diff --git a/packages/api/src/@core/connections/dto/create-connection.dto.ts b/packages/api/src/@core/connections/crm/dto/create-connection.dto.ts similarity index 100% rename from packages/api/src/@core/connections/dto/create-connection.dto.ts rename to packages/api/src/@core/connections/crm/dto/create-connection.dto.ts diff --git a/packages/api/src/@core/connections/services/connections.service.ts b/packages/api/src/@core/connections/crm/services/crm-connection.service.ts similarity index 68% rename from packages/api/src/@core/connections/services/connections.service.ts rename to packages/api/src/@core/connections/crm/services/crm-connection.service.ts index 7262297a5..1cec76f4f 100644 --- a/packages/api/src/@core/connections/services/connections.service.ts +++ b/packages/api/src/@core/connections/crm/services/crm-connection.service.ts @@ -1,9 +1,21 @@ import { Injectable } from '@nestjs/common'; -import { CrmConnectionsService } from './crm/crm-connection.service'; +import { PrismaService } from 'src/@core/prisma/prisma.service'; +import { ZohoConnectionService } from './zoho/zoho.service'; import { NotFoundError } from 'src/@core/utils/errors'; +import { HubspotConnectionService } from './hubspot/hubspot.service'; +import { PipedriveConnectionService } from './pipedrive/pipedrive.service'; +import { ZendeskConnectionService } from './zendesk/zendesk.service'; +import { FreshsalesConnectionService } from './freshsales/freshsales.service'; @Injectable() -export class ConnectionsService { +export class CrmConnectionsService { + constructor( + private zohoConnectionService: ZohoConnectionService, + private hubspotConnectionService: HubspotConnectionService, + private pipedriveConnectionService: PipedriveConnectionService, + private zendeskConnectionService: ZendeskConnectionService, + private freshsalesConnectionService: FreshsalesConnectionService, + ) {} //STEP 1:[FRONTEND STEP] //create a frontend SDK snippet in which an authorization embedded link is set up so when users click // on it to grant access => they grant US the access and then when confirmed @@ -18,8 +30,6 @@ export class ConnectionsService { // we catch the tmp token and swap it against oauth2 server for access/refresh tokens // to perform actions on his behalf // this call pass 1. integrationID 2. CustomerId 3. Panora Api Key - constructor(private crmConnectionService: CrmConnectionsService) {} - async handleCRMCallBack( projectId: string, linkedUserId: string, @@ -33,7 +43,7 @@ export class ConnectionsService { if (!code) { throw new NotFoundError('no hubspot code found'); } - return this.crmConnectionService.handleHubspotCallback( + return this.hubspotConnectionService.handleHubspotCallback( linkedUserId, projectId, code, @@ -42,7 +52,7 @@ export class ConnectionsService { if (!code || !zohoAccountURL) { throw new NotFoundError('no zoho code/ zoho AccountURL found'); } - return this.crmConnectionService.handleZohoCallback( + return this.zohoConnectionService.handleZohoCallback( linkedUserId, projectId, code, @@ -52,7 +62,7 @@ export class ConnectionsService { if (!code) { throw new NotFoundError('no pipedrive code found'); } - return this.crmConnectionService.handlePipedriveCallback( + return this.pipedriveConnectionService.handlePipedriveCallback( linkedUserId, projectId, code, @@ -64,7 +74,7 @@ export class ConnectionsService { if (!code) { throw new NotFoundError('no zendesk code found'); } - return this.crmConnectionService.handleZendeskCallback( + return this.zendeskConnectionService.handleZendeskCallback( linkedUserId, projectId, code, @@ -89,18 +99,18 @@ export class ConnectionsService { try { switch (providerId) { case 'hubspot': - return this.crmConnectionService.handleHubspotTokenRefresh( + return this.hubspotConnectionService.handleHubspotTokenRefresh( connectionId, refresh_token, ); case 'zoho': - return this.crmConnectionService.handleZohoTokenRefresh( + return this.zohoConnectionService.handleZohoTokenRefresh( connectionId, refresh_token, account_url, ); case 'pipedrive': - return this.crmConnectionService.handlePipedriveTokenRefresh( + return this.pipedriveConnectionService.handlePipedriveTokenRefresh( connectionId, refresh_token, ); @@ -108,7 +118,7 @@ export class ConnectionsService { //todo: LATER break; case 'zendesk': - return this.crmConnectionService.handleZendeskTokenRefresh( + return this.zendeskConnectionService.handleZendeskTokenRefresh( connectionId, refresh_token, ); diff --git a/packages/api/src/@core/connections/crm/services/freshsales/freshsales.service.ts b/packages/api/src/@core/connections/crm/services/freshsales/freshsales.service.ts new file mode 100644 index 000000000..a617d19a3 --- /dev/null +++ b/packages/api/src/@core/connections/crm/services/freshsales/freshsales.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FreshsalesConnectionService { + //TODO: later + async handleFreshsalesCallback() { + return; + } + async handleFreshsalesTokenRefresh() { + return; + } +} diff --git a/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts b/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts new file mode 100644 index 000000000..0d32b4cea --- /dev/null +++ b/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from 'src/@core/prisma/prisma.service'; +import axios from 'axios'; +import config from 'src/@core/utils/config'; +import { Prisma } from '@prisma/client'; +import { HubspotOAuthResponse } from '../../types'; + +@Injectable() +export class HubspotConnectionService { + constructor(private prisma: PrismaService) {} + + async handleHubspotCallback( + linkedUserId: string, + projectId: string, + code: string, + ) { + try { + //first create a linked_user + //reconstruct the redirect URI that was passed in the frontend it must be the same + const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; //tocheck + + const formData = { + grant_type: 'authorization_code', + client_id: config.HUBSPOT_CLIENT_ID, + client_secret: config.HUBSPOT_CLIENT_SECRET, + redirect_uri: REDIRECT_URI, + code: code, + }; + const res = await axios.post( + 'https://api.hubapi.com/oauth/v1/token', + formData, + ); + const data: HubspotOAuthResponse = res.data; + console.log('OAuth credentials : hubspot ', data); + // save tokens for this customer inside our db + //TODO: encrypt the access token and refresh tokens + const db_res = await this.prisma.connections.create({ + data: { + provider_slug: 'hubspot', + token_type: 'oauth', + access_token: data.access_token, + refresh_token: data.refresh_token, + expiration_timestamp: new Date( + new Date().getTime() + data.expires_in * 1000, + ), + created_at: new Date(), + projects: { + connect: { id_project: BigInt(projectId) }, + }, + linked_users: { + connect: { id_linked_user: BigInt(linkedUserId) }, + }, + //id of the end-customer defined in the company application, this is how requests could be made on behlaf of the user + // without it, we cant retrieve the right row in our db + }, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + // Handle Axios-specific errors + console.error('Error with Axios request:', error.response?.data); + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error('Error with Prisma request:', error); + } + console.log(error); + } + } + async handleHubspotTokenRefresh(connectionId: bigint, refresh_token: string) { + try { + const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; + + const formData = { + grant_type: 'refresh_token', + client_id: config.HUBSPOT_CLIENT_ID, + client_secret: config.HUBSPOT_CLIENT_SECRET, + redirect_uri: REDIRECT_URI, + refresh_token: refresh_token, + }; + const res = await axios.post( + 'https://api.hubapi.com/oauth/v1/token', + formData, + ); + const data: HubspotOAuthResponse = res.data; + await this.prisma.connections.update({ + where: { + id_connection: connectionId, + }, + data: { + access_token: data.access_token, + refresh_token: data.refresh_token, + expiration_timestamp: new Date( + new Date().getTime() + data.expires_in * 1000, + ), + }, + }); + console.log('OAuth credentials updated : hubspot ', data); + } catch (error) { + if (axios.isAxiosError(error)) { + // Handle Axios-specific errors + console.error('Error with Axios request:', error.response?.data); + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error('Error with Prisma request:', error); + } + console.log(error); + } + } +} diff --git a/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts b/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts new file mode 100644 index 000000000..faf613469 --- /dev/null +++ b/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from 'src/@core/prisma/prisma.service'; +import axios from 'axios'; +import config from 'src/@core/utils/config'; +import { PipeDriveOAuthResponse } from '../../types'; +import { Prisma } from '@prisma/client'; + +@Injectable() +export class PipedriveConnectionService { + constructor(private prisma: PrismaService) {} + + async handlePipedriveCallback( + linkedUserId: string, + projectId: string, + code: string, + ) { + try { + //reconstruct the redirect URI that was passed in the frontend it must be the same + const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; // TODO; + + const formData = { + grant_type: 'authorization_code', + redirect_uri: REDIRECT_URI, + code: code, + }; + const res = await axios.post( + 'https://oauth.pipedrive.com/oauth/token', + formData, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Basic ${Buffer.from( + `${config.PIPEDRIVE_CLIENT_ID}:${config.PIPEDRIVE_CLIENT_SECRET}`, + ).toString('base64')}`, + }, + }, + ); + //TODO: handle if res throws an error + const data: PipeDriveOAuthResponse = res.data; + console.log('OAuth credentials : pipedrive ', data); + // save tokens for this customer inside our db + //TODO: encrypt the access token and refresh tokens + const db_res = await this.prisma.connections.create({ + data: { + provider_slug: 'pipedrive', + token_type: 'oauth', + access_token: data.access_token, + refresh_token: data.refresh_token, + expiration_timestamp: new Date( + new Date().getTime() + data.expires_in * 1000, + ), + created_at: new Date(), + projects: { + connect: { id_project: BigInt(projectId) }, + }, + linked_users: { + connect: { id_linked_user: BigInt(linkedUserId) }, + }, + }, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + // Handle Axios-specific errors + console.error('Error with Axios request:', error.response?.data); + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error('Error with Prisma request:', error); + } + console.log(error); + } + } + + async handlePipedriveTokenRefresh( + connectionId: bigint, + refresh_token: string, + ) { + try { + const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; + + const formData = { + grant_type: 'refresh_token', + redirect_uri: REDIRECT_URI, + refresh_token: refresh_token, + }; + const res = await axios.post( + 'https://oauth.pipedrive.com/oauth/token', + formData, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Basic ${Buffer.from( + `${config.PIPEDRIVE_CLIENT_ID}:${config.PIPEDRIVE_CLIENT_SECRET}`, + ).toString('base64')}`, + }, + }, + ); + const data: PipeDriveOAuthResponse = res.data; + await this.prisma.connections.update({ + where: { + id_connection: connectionId, + }, + data: { + access_token: data.access_token, + refresh_token: data.refresh_token, + expiration_timestamp: new Date( + new Date().getTime() + data.expires_in * 1000, + ), + }, + }); + console.log('OAuth credentials updated : pipedrive ', data); + } catch (error) { + if (axios.isAxiosError(error)) { + // Handle Axios-specific errors + console.error('Error with Axios request:', error.response?.data); + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error('Error with Prisma request:', error); + } + console.log(error); + } + } +} diff --git a/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts b/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts new file mode 100644 index 000000000..43453156b --- /dev/null +++ b/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import config from 'src/@core/utils/config'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from 'src/@core/prisma/prisma.service'; +import { ZendeskOAuthResponse } from '../../types'; +@Injectable() +export class ZendeskConnectionService { + constructor(private prisma: PrismaService) {} + + async handleZendeskCallback( + linkedUserId: string, + projectId: string, + code: string, + ) { + try { + //reconstruct the redirect URI that was passed in the frontend it must be the same + const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; // TODO; + + const formData = { + grant_type: 'authorization_code', + redirect_uri: REDIRECT_URI, + code: code, + }; + const res = await axios.post( + 'https://api.getbase.com/oauth2/token', + formData, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Basic ${Buffer.from( + `${config.ZENDESK_CLIENT_ID}:${config.ZENDESK_CLIENT_SECRET}`, + ).toString('base64')}`, + }, + }, + ); + //TODO: handle if res throws an error + const data: ZendeskOAuthResponse = res.data; + console.log('OAuth credentials : zendesk ', data); + // save tokens for this customer inside our db + //TODO: encrypt the access token and refresh tokens + const db_res = await this.prisma.connections.create({ + data: { + provider_slug: 'zendesk', + token_type: 'oauth', + access_token: data.access_token, + refresh_token: data.refresh_token, + expiration_timestamp: new Date( + new Date().getTime() + data.expires_in * 1000, + ), + created_at: new Date(), + projects: { + connect: { id_project: BigInt(projectId) }, + }, + linked_users: { + connect: { id_linked_user: BigInt(linkedUserId) }, + }, + }, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + // Handle Axios-specific errors + console.error('Error with Axios request:', error.response?.data); + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error('Error with Prisma request:', error); + } + console.log(error); + } + } + async handleZendeskTokenRefresh(connectionId: bigint, refresh_token: string) { + try { + const formData = { + grant_type: 'refresh_token', + refresh_token: refresh_token, + }; + const res = await axios.post( + 'https://api.getbase.com/oauth2/token', + formData, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Basic ${Buffer.from( + `${config.ZENDESK_CLIENT_ID}:${config.ZENDESK_CLIENT_SECRET}`, + ).toString('base64')}`, + }, + }, + ); + const data: ZendeskOAuthResponse = res.data; + await this.prisma.connections.update({ + where: { + id_connection: connectionId, + }, + data: { + access_token: data.access_token, + refresh_token: data.refresh_token, + expiration_timestamp: new Date( + new Date().getTime() + data.expires_in * 1000, + ), + }, + }); + console.log('OAuth credentials updated : zendesk ', data); + } catch (error) { + if (axios.isAxiosError(error)) { + // Handle Axios-specific errors + console.error('Error with Axios request:', error.response?.data); + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error('Error with Prisma request:', error); + } + console.log(error); + } + } +} diff --git a/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts b/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts new file mode 100644 index 000000000..9def7388f --- /dev/null +++ b/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts @@ -0,0 +1,106 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import config from 'src/@core/utils/config'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from 'src/@core/prisma/prisma.service'; +import { ZohoOAuthResponse } from '../../types'; +@Injectable() +export class ZohoConnectionService { + constructor(private prisma: PrismaService) {} + + async handleZohoCallback( + linkedUserId: string, + projectId: string, + code: string, + accountURL: string, + ) { + try { + //reconstruct the redirect URI that was passed in the frontend it must be the same + const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; // TODO; + + const formData = { + grant_type: 'authorization_code', + client_id: config.ZOHOCRM_CLIENT_ID, + client_secret: config.ZOHOCRM_CLIENT_SECRET, + redirect_uri: REDIRECT_URI, + code: code, + }; + const res = await axios.post( + `https://${accountURL}/oauth/v2/token`, + formData, + ); + const data: ZohoOAuthResponse = res.data; + console.log('OAuth credentials : zoho ', data); + //TODO: encrypt the access token and refresh tokens + const db_res = await this.prisma.connections.create({ + data: { + provider_slug: 'zoho', + token_type: 'oauth', + access_token: data.access_token, + refresh_token: data.refresh_token, + expiration_timestamp: new Date( + new Date().getTime() + data.expires_in * 1000, + ), + created_at: new Date(), + projects: { + connect: { id_project: BigInt(projectId) }, + }, + linked_users: { + connect: { id_linked_user: BigInt(linkedUserId) }, + }, + account_url: accountURL, + }, + }); + } catch (error) { + if (axios.isAxiosError(error)) { + // Handle Axios-specific errors + console.error('Error with Axios request:', error.response?.data); + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error('Error with Prisma request:', error); + } + console.log(error); + } + } + async handleZohoTokenRefresh( + connectionId: bigint, + refresh_token: string, + account_url: string, + ) { + try { + const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; + + const formData = { + grant_type: 'refresh_token', + client_id: config.HUBSPOT_CLIENT_ID, + client_secret: config.HUBSPOT_CLIENT_SECRET, + redirect_uri: REDIRECT_URI, + refresh_token: refresh_token, + }; + const res = await axios.post(`${account_url}/oauth/v2/token`, formData); + const data: ZohoOAuthResponse = res.data; + await this.prisma.connections.update({ + where: { + id_connection: connectionId, + }, + data: { + access_token: data.access_token, + refresh_token: data.refresh_token, + expiration_timestamp: new Date( + new Date().getTime() + data.expires_in * 1000, + ), + }, + }); + console.log('OAuth credentials updated : zoho ', data); + } catch (error) { + if (axios.isAxiosError(error)) { + // Handle Axios-specific errors + console.error('Error with Axios request:', error.response?.data); + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + console.error('Error with Prisma request:', error); + } + console.log(error); + } + } +} diff --git a/packages/api/src/@core/connections/services/crm/types.ts b/packages/api/src/@core/connections/crm/types/index.ts similarity index 100% rename from packages/api/src/@core/connections/services/crm/types.ts rename to packages/api/src/@core/connections/crm/types/index.ts diff --git a/packages/api/src/@core/connections/entities/connection.entity.ts b/packages/api/src/@core/connections/entities/connection.entity.ts deleted file mode 100644 index 31d07a5b4..000000000 --- a/packages/api/src/@core/connections/entities/connection.entity.ts +++ /dev/null @@ -1 +0,0 @@ -export class Connection {} diff --git a/packages/api/src/@core/connections/services/connections.service.spec.ts b/packages/api/src/@core/connections/services/connections.service.spec.ts deleted file mode 100644 index bf464983c..000000000 --- a/packages/api/src/@core/connections/services/connections.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ConnectionsService } from './connections.service'; - -describe('ConnectionsService', () => { - let service: ConnectionsService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ConnectionsService], - }).compile(); - - service = module.get(ConnectionsService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/packages/api/src/@core/connections/services/crm/crm-connection.service.ts b/packages/api/src/@core/connections/services/crm/crm-connection.service.ts deleted file mode 100644 index cee6644b1..000000000 --- a/packages/api/src/@core/connections/services/crm/crm-connection.service.ts +++ /dev/null @@ -1,432 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import axios from 'axios'; -import { - HubspotOAuthResponse, - PipeDriveOAuthResponse, - ZendeskOAuthResponse, - ZohoOAuthResponse, -} from './types'; -import { PrismaService } from 'src/@core/prisma/prisma.service'; -import config from 'src/@core/utils/config'; -import { Prisma } from '@prisma/client'; - -@Injectable() -export class CrmConnectionsService { - constructor(private prisma: PrismaService) {} - - async handleHubspotCallback( - linkedUserId: string, - projectId: string, - code: string, - ) { - try { - //first create a linked_user - //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; //tocheck - - const formData = { - grant_type: 'authorization_code', - client_id: config.HUBSPOT_CLIENT_ID, - client_secret: config.HUBSPOT_CLIENT_SECRET, - redirect_uri: REDIRECT_URI, - code: code, - }; - const res = await axios.post( - 'https://api.hubapi.com/oauth/v1/token', - formData, - ); - const data: HubspotOAuthResponse = res.data; - console.log('OAuth credentials : hubspot ', data); - // save tokens for this customer inside our db - //TODO: encrypt the access token and refresh tokens - const db_res = await this.prisma.connections.create({ - data: { - provider_slug: 'hubspot', - token_type: 'oauth', - access_token: data.access_token, - refresh_token: data.refresh_token, - expiration_timestamp: new Date( - new Date().getTime() + data.expires_in * 1000, - ), - created_at: new Date(), - projects: { - connect: { id_project: BigInt(projectId) }, - }, - linked_users: { - connect: { id_linked_user: BigInt(linkedUserId) }, - }, - //id of the end-customer defined in the company application, this is how requests could be made on behlaf of the user - // without it, we cant retrieve the right row in our db - }, - }); - } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); - } - } - - async handleZohoCallback( - linkedUserId: string, - projectId: string, - code: string, - accountURL: string, - ) { - try { - //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; // TODO; - - const formData = { - grant_type: 'authorization_code', - client_id: config.ZOHOCRM_CLIENT_ID, - client_secret: config.ZOHOCRM_CLIENT_SECRET, - redirect_uri: REDIRECT_URI, - code: code, - }; - const res = await axios.post( - `https://${accountURL}/oauth/v2/token`, - formData, - ); - const data: ZohoOAuthResponse = res.data; - console.log('OAuth credentials : zoho ', data); - //TODO: encrypt the access token and refresh tokens - const db_res = await this.prisma.connections.create({ - data: { - provider_slug: 'zoho', - token_type: 'oauth', - access_token: data.access_token, - refresh_token: data.refresh_token, - expiration_timestamp: new Date( - new Date().getTime() + data.expires_in * 1000, - ), - created_at: new Date(), - projects: { - connect: { id_project: BigInt(projectId) }, - }, - linked_users: { - connect: { id_linked_user: BigInt(linkedUserId) }, - }, - account_url: accountURL, - }, - }); - } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); - } - } - - async handlePipedriveCallback( - linkedUserId: string, - projectId: string, - code: string, - ) { - try { - //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; // TODO; - - const formData = { - grant_type: 'authorization_code', - redirect_uri: REDIRECT_URI, - code: code, - }; - const res = await axios.post( - 'https://oauth.pipedrive.com/oauth/token', - formData, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - Authorization: `Basic ${Buffer.from( - `${config.PIPEDRIVE_CLIENT_ID}:${config.PIPEDRIVE_CLIENT_SECRET}`, - ).toString('base64')}`, - }, - }, - ); - //TODO: handle if res throws an error - const data: PipeDriveOAuthResponse = res.data; - console.log('OAuth credentials : pipedrive ', data); - // save tokens for this customer inside our db - //TODO: encrypt the access token and refresh tokens - const db_res = await this.prisma.connections.create({ - data: { - provider_slug: 'pipedrive', - token_type: 'oauth', - access_token: data.access_token, - refresh_token: data.refresh_token, - expiration_timestamp: new Date( - new Date().getTime() + data.expires_in * 1000, - ), - created_at: new Date(), - projects: { - connect: { id_project: BigInt(projectId) }, - }, - linked_users: { - connect: { id_linked_user: BigInt(linkedUserId) }, - }, - }, - }); - } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); - } - } - - //TODO: later - async handleFreshsalesCallback() { - return; - } - - async handleZendeskCallback( - linkedUserId: string, - projectId: string, - code: string, - ) { - try { - //reconstruct the redirect URI that was passed in the frontend it must be the same - const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; // TODO; - - const formData = { - grant_type: 'authorization_code', - redirect_uri: REDIRECT_URI, - code: code, - }; - const res = await axios.post( - 'https://api.getbase.com/oauth2/token', - formData, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - Authorization: `Basic ${Buffer.from( - `${config.ZENDESK_CLIENT_ID}:${config.ZENDESK_CLIENT_SECRET}`, - ).toString('base64')}`, - }, - }, - ); - //TODO: handle if res throws an error - const data: ZendeskOAuthResponse = res.data; - console.log('OAuth credentials : zendesk ', data); - // save tokens for this customer inside our db - //TODO: encrypt the access token and refresh tokens - const db_res = await this.prisma.connections.create({ - data: { - provider_slug: 'zendesk', - token_type: 'oauth', - access_token: data.access_token, - refresh_token: data.refresh_token, - expiration_timestamp: new Date( - new Date().getTime() + data.expires_in * 1000, - ), - created_at: new Date(), - projects: { - connect: { id_project: BigInt(projectId) }, - }, - linked_users: { - connect: { id_linked_user: BigInt(linkedUserId) }, - }, - }, - }); - } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); - } - } - - async handleHubspotTokenRefresh(connectionId: bigint, refresh_token: string) { - try { - const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; - - const formData = { - grant_type: 'refresh_token', - client_id: config.HUBSPOT_CLIENT_ID, - client_secret: config.HUBSPOT_CLIENT_SECRET, - redirect_uri: REDIRECT_URI, - refresh_token: refresh_token, - }; - const res = await axios.post( - 'https://api.hubapi.com/oauth/v1/token', - formData, - ); - const data: HubspotOAuthResponse = res.data; - await this.prisma.connections.update({ - where: { - id_connection: connectionId, - }, - data: { - access_token: data.access_token, - refresh_token: data.refresh_token, - expiration_timestamp: new Date( - new Date().getTime() + data.expires_in * 1000, - ), - }, - }); - console.log('OAuth credentials updated : hubspot ', data); - } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); - } - } - - async handlePipedriveTokenRefresh( - connectionId: bigint, - refresh_token: string, - ) { - try { - const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; - - const formData = { - grant_type: 'refresh_token', - redirect_uri: REDIRECT_URI, - refresh_token: refresh_token, - }; - const res = await axios.post( - 'https://oauth.pipedrive.com/oauth/token', - formData, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - Authorization: `Basic ${Buffer.from( - `${config.PIPEDRIVE_CLIENT_ID}:${config.PIPEDRIVE_CLIENT_SECRET}`, - ).toString('base64')}`, - }, - }, - ); - const data: HubspotOAuthResponse = res.data; - await this.prisma.connections.update({ - where: { - id_connection: connectionId, - }, - data: { - access_token: data.access_token, - refresh_token: data.refresh_token, - expiration_timestamp: new Date( - new Date().getTime() + data.expires_in * 1000, - ), - }, - }); - console.log('OAuth credentials updated : pipedrive ', data); - } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); - } - } - - async handleZohoTokenRefresh( - connectionId: bigint, - refresh_token: string, - account_url: string, - ) { - try { - const REDIRECT_URI = `${config.OAUTH_REDIRECT_BASE}/oauth/crm/callback`; - - const formData = { - grant_type: 'refresh_token', - client_id: config.HUBSPOT_CLIENT_ID, - client_secret: config.HUBSPOT_CLIENT_SECRET, - redirect_uri: REDIRECT_URI, - refresh_token: refresh_token, - }; - const res = await axios.post(`${account_url}/oauth/v2/token`, formData); - const data: ZohoOAuthResponse = res.data; - await this.prisma.connections.update({ - where: { - id_connection: connectionId, - }, - data: { - access_token: data.access_token, - refresh_token: data.refresh_token, - expiration_timestamp: new Date( - new Date().getTime() + data.expires_in * 1000, - ), - }, - }); - console.log('OAuth credentials updated : zoho ', data); - } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); - } - } - - async handleZendeskTokenRefresh(connectionId: bigint, refresh_token: string) { - try { - const formData = { - grant_type: 'refresh_token', - refresh_token: refresh_token, - }; - const res = await axios.post( - 'https://api.getbase.com/oauth2/token', - formData, - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - Authorization: `Basic ${Buffer.from( - `${config.ZENDESK_CLIENT_ID}:${config.ZENDESK_CLIENT_SECRET}`, - ).toString('base64')}`, - }, - }, - ); - const data: ZendeskOAuthResponse = res.data; - await this.prisma.connections.update({ - where: { - id_connection: connectionId, - }, - data: { - access_token: data.access_token, - refresh_token: data.refresh_token, - expiration_timestamp: new Date( - new Date().getTime() + data.expires_in * 1000, - ), - }, - }); - console.log('OAuth credentials updated : zendesk ', data); - } catch (error) { - if (axios.isAxiosError(error)) { - // Handle Axios-specific errors - console.error('Error with Axios request:', error.response?.data); - } - if (error instanceof Prisma.PrismaClientKnownRequestError) { - console.error('Error with Prisma request:', error); - } - console.log(error); - } - } -} diff --git a/packages/api/src/@core/tasks/tasks.service.ts b/packages/api/src/@core/tasks/tasks.service.ts index ef3c6b71b..8374ccb68 100644 --- a/packages/api/src/@core/tasks/tasks.service.ts +++ b/packages/api/src/@core/tasks/tasks.service.ts @@ -2,13 +2,13 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { PrismaService } from '../prisma/prisma.service'; -import { ConnectionsService } from '../connections/services/connections.service'; +import { CrmConnectionsService } from '../connections/crm/services/crm-connection.service'; @Injectable() export class TasksService implements OnModuleInit { constructor( private prisma: PrismaService, - private connectionsService: ConnectionsService, + private crmConnectionsService: CrmConnectionsService, ) {} onModuleInit() { @@ -33,7 +33,7 @@ export class TasksService implements OnModuleInit { if (connection.refresh_token) { const account_url = connection.provider_slug == 'zoho' ? connection.account_url : ''; - await this.connectionsService.handleCRMTokensRefresh( + await this.crmConnectionsService.handleCRMTokensRefresh( connection.id_connection, connection.provider_slug, connection.refresh_token, diff --git a/packages/api/src/app.module.ts b/packages/api/src/app.module.ts index aeb9563b8..ef11ac21d 100644 --- a/packages/api/src/app.module.ts +++ b/packages/api/src/app.module.ts @@ -9,8 +9,7 @@ import { ConnectionsModule } from './@core/connections/connections.module'; import { ScheduleModule } from '@nestjs/schedule'; import { TasksService } from './@core/tasks/tasks.service'; import { SentryModule } from './@core/sentry/sentry.module'; -import { ConnectionsService } from './@core/connections/services/connections.service'; -import { CrmConnectionsService } from './@core/connections/services/crm/crm-connection.service'; +import { CrmConnectionModule } from './@core/connections/crm/crm-connection.module'; @Module({ imports: [ @@ -18,16 +17,11 @@ import { CrmConnectionsService } from './@core/connections/services/crm/crm-conn AuthModule, ConfigModule.forRoot({ isGlobal: true }), ConnectionsModule, + CrmConnectionModule, ScheduleModule.forRoot(), SentryModule.forRoot(), ], controllers: [AppController], - providers: [ - AppService, - AuthService, - TasksService, - ConnectionsService, - CrmConnectionsService, - ], + providers: [AppService, AuthService, TasksService], }) export class AppModule {}