From 16d73c1e8d241a9e5deb8cd24ffc19392a0d14c3 Mon Sep 17 00:00:00 2001 From: nael Date: Wed, 19 Jun 2024 20:01:04 +0200 Subject: [PATCH] :bug: Fix connections code --- apps/magic-link/src/lib/ProviderModal.tsx | 8 +- docker-compose.dev.yml | 35 +-- docker-compose.source.yml | 3 + docker-compose.yml | 3 + ngrok.yml | 2 +- .../connections-strategies.service.ts | 9 +- .../api/src/@core/connections/@utils/index.ts | 4 + .../services/accounting.connection.service.ts | 10 +- .../services/freeagent/freeagent.service.ts | 8 +- .../services/freshbooks/freshbooks.service.ts | 8 +- .../services/moneybird/moneybird.service.ts | 8 +- .../services/pennylane/pennylane.service.ts | 8 +- .../services/quickbooks/quickbooks.service.ts | 8 +- .../accounting/services/sage/sage.service.ts | 6 +- .../wave_financial/wave_financial.service.ts | 8 +- .../accounting/services/xero/xero.service.ts | 21 +- .../connections/ats/ats.connection.module.ts | 8 +- .../ats/services/ashby/ashby.service.ts | 84 +++++++ .../ats/services/ats.connection.service.ts | 2 +- .../ats/services/bamboohr/bamboohr.service.ts | 161 +++++++++++++ .../services/greenhouse/greenhouse.service.ts | 8 +- .../ats/services/jobadder/jobadder.service.ts | 2 +- .../ats/services/lever/lever.service.ts | 14 +- .../ats/{ => services}/registry.service.ts | 2 +- .../ats/services/workday/workday.service.ts | 91 ++++++++ .../connections/connections.controller.ts | 30 +-- .../@core/connections/connections.module.ts | 3 + .../crm/services/accelo/accelo.service.ts | 11 +- .../crm/services/affinity/affinity.service.ts | 6 +- .../crm/services/attio/attio.service.ts | 3 +- .../crm/services/capsule/capsule.service.ts | 6 +- .../crm/services/close/close.service.ts | 5 +- .../crm/services/copper/copper.service.ts | 3 +- .../crm/services/hubspot/hubspot.service.ts | 3 +- .../crm/services/keap/keap.service.ts | 3 +- .../services/pipedrive/pipedrive.service.ts | 3 +- .../services/teamleader/teamleader.service.ts | 6 +- .../crm/services/teamwork/teamwork.service.ts | 24 +- .../crm/services/zendesk/zendesk.service.ts | 8 +- .../crm/services/zoho/zoho.service.ts | 7 +- .../filestorage/services/box/box.service.ts | 6 +- .../services/dropbox/dropbox.service.ts | 8 +- .../google_drive/google_drive.service.ts | 8 +- .../services/onedrive/onedrive.service.ts | 20 +- .../services/sharepoint/sharepoint.service.ts | 8 +- .../hris/hris.connection.module.ts | 4 + .../services/bamboohr/bamboohr.service.ts | 161 +++++++++++++ .../hris/services/deel/deel.service.ts | 6 +- .../services/factorial/factorial.service.ts | 6 +- .../hris/services/gusto/gusto.service.ts | 6 +- .../hris/services/namely/namely.service.ts | 218 +++++++++++++++++ .../hris/services/payfit/payfit.service.ts | 6 +- .../services/rippling/rippling.service.ts | 6 +- .../services/notion/notion.service.ts | 10 +- .../services/slack/slack.service.ts | 78 +++---- .../marketingautomation.connection.module.ts | 3 +- .../services/brevo/brevo.service.ts | 8 +- .../getresponse/getresponse.service.ts | 12 +- .../services/keap/keap.service.ts | 189 +++++++++++++++ .../services/podium/podium.service.ts | 8 +- .../ticketing/services/aha/aha.service.ts | 25 +- .../ticketing/services/asana/asana.service.ts | 6 +- .../services/clickup/clickup.service.ts | 8 +- .../ticketing/services/dixa/dixa.service.ts | 6 +- .../ticketing/services/front/front.service.ts | 6 +- .../services/github/github.service.ts | 6 +- .../services/gitlab/gitlab.service.ts | 3 +- .../services/gorgias/gorgias.service.ts | 12 +- .../services/helpscout/helpscout.service.ts | 8 +- .../services/linear/linear.service.ts | 6 +- .../ticketing/services/wrike/wrike.service.ts | 220 ++++++++++++++++++ .../services/zendesk/zendesk.service.ts | 25 +- .../ticketing/ticketing.connection.module.ts | 2 + .../field-mapping/field-mapping.service.ts | 2 +- .../@core/passthrough/passthrough.service.ts | 8 +- packages/api/swagger/swagger-spec.json | 71 ------ packages/shared/src/authUrl.ts | 66 +++--- packages/shared/src/categories.ts | 1 + packages/shared/src/connectors/metadata.ts | 131 ++++++----- packages/shared/src/envConfig.ts | 27 ++- packages/shared/src/types.ts | 12 +- packages/shared/src/utils.ts | 12 +- 82 files changed, 1619 insertions(+), 457 deletions(-) create mode 100644 packages/api/src/@core/connections/ats/services/ashby/ashby.service.ts create mode 100644 packages/api/src/@core/connections/ats/services/bamboohr/bamboohr.service.ts rename packages/api/src/@core/connections/ats/{ => services}/registry.service.ts (92%) create mode 100644 packages/api/src/@core/connections/ats/services/workday/workday.service.ts create mode 100644 packages/api/src/@core/connections/hris/services/bamboohr/bamboohr.service.ts create mode 100644 packages/api/src/@core/connections/hris/services/namely/namely.service.ts create mode 100644 packages/api/src/@core/connections/marketingautomation/services/keap/keap.service.ts create mode 100644 packages/api/src/@core/connections/ticketing/services/wrike/wrike.service.ts diff --git a/apps/magic-link/src/lib/ProviderModal.tsx b/apps/magic-link/src/lib/ProviderModal.tsx index 0e1c93c93..a1b20071e 100644 --- a/apps/magic-link/src/lib/ProviderModal.tsx +++ b/apps/magic-link/src/lib/ProviderModal.tsx @@ -19,6 +19,7 @@ const ProviderModal = () => { provider: string; category: string; }>(); + const [optionalApiUrlForLocal, setOptionalApiUrlForLocal] = useState(); const [startFlow, setStartFlow] = useState(false); const [preStartFlow, setPreStartFlow] = useState(false); const [projectId, setProjectId] = useState(""); @@ -79,8 +80,8 @@ const ProviderModal = () => { vertical: selectedProvider?.category!, returnUrl: window.location.href, projectId: projectId, + optionalApiUrl: optionalApiUrlForLocal, linkedUserId: magicLink?.id_linked_user as string, - //optionalApiUrl: "https://prepared-wildcat-infinitely.ngrok-free.app", //CONNECTORS_METADATA[selectedProvider?.category!][selectedProvider?.provider!].options?.local_redirect_uri_in_https == true ? "https://prepared-wildcat-infinitely.ngrok-free.app": undefined, onSuccess: () => { console.log('OAuth successful'); setOpenSuccessDialog(true); @@ -130,10 +131,15 @@ const ProviderModal = () => { const handleWalletClick = (walletName: string, category: string) => { setSelectedProvider({provider: walletName.toLowerCase(), category: category.toLowerCase()}); + const options = CONNECTORS_METADATA[selectedProvider?.category!][selectedProvider?.provider!].options; + if(options && options.local_redirect_uri_in_https) { + setOptionalApiUrlForLocal('https://prepared-wildcat-infinitely.ngrok-free.app') + } const logoPath = CONNECTORS_METADATA[category.toLowerCase()][walletName.toLowerCase()].logoPath; setCurrentProviderLogoURL(logoPath); setCurrentProvider(walletName.toLowerCase()) setPreStartFlow(true); + setOptionalApiUrlForLocal('') }; const handleStartFlow = () => { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 41d7bd422..b16a21e26 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -150,6 +150,9 @@ services: NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET: ${NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET} SLACK_MANAGEMENT_CLOUD_CLIENT_ID: ${SLACK_MANAGEMENT_CLOUD_CLIENT_ID} SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET: ${SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET} + NAMELY_HRIS_CLOUD_CLIENT_ID: ${NAMELY_HRIS_CLOUD_CLIENT_ID} + NAMELY_HRIS_CLOUD_CLIENT_SECRET: ${NAMELY_HRIS_CLOUD_CLIENT_SECRET} + NAMELY_HRIS_CLOUD_SUBDOMAIN: ${NAMELY_HRIS_CLOUD_SUBDOMAIN} restart: unless-stopped ports: - 3000:3000 @@ -225,22 +228,22 @@ services: volumes: - .:/app - # ngrok: - # image: ngrok/ngrok:latest - # restart: always - # command: - # - "start" - # - "--all" - # - "--config" - # - "/etc/ngrok.yml" - # volumes: - # - ./ngrok.yml:/etc/ngrok.yml - # ports: - # - 4040:4040 - # depends_on: - # api: - # condition: service_healthy - # network_mode: "host" + ngrok: + image: ngrok/ngrok:latest + restart: always + command: + - "start" + - "--all" + - "--config" + - "/etc/ngrok.yml" + volumes: + - ./ngrok.yml:/etc/ngrok.yml + ports: + - 4040:4040 + depends_on: + api: + condition: service_healthy + network_mode: "host" docs: build: diff --git a/docker-compose.source.yml b/docker-compose.source.yml index ebd4dc210..feaf8932b 100644 --- a/docker-compose.source.yml +++ b/docker-compose.source.yml @@ -150,6 +150,9 @@ services: NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET: ${NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET} SLACK_MANAGEMENT_CLOUD_CLIENT_ID: ${SLACK_MANAGEMENT_CLOUD_CLIENT_ID} SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET: ${SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET} + NAMELY_HRIS_CLOUD_CLIENT_ID: ${NAMELY_HRIS_CLOUD_CLIENT_ID} + NAMELY_HRIS_CLOUD_CLIENT_SECRET: ${NAMELY_HRIS_CLOUD_CLIENT_SECRET} + NAMELY_HRIS_CLOUD_SUBDOMAIN: ${NAMELY_HRIS_CLOUD_SUBDOMAIN} restart: unless-stopped ports: - 3000:3000 diff --git a/docker-compose.yml b/docker-compose.yml index 445a87318..0e4706114 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -144,6 +144,9 @@ services: NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET: ${NOTION_MANAGEMENT_CLOUD_CLIENT_SECRET} SLACK_MANAGEMENT_CLOUD_CLIENT_ID: ${SLACK_MANAGEMENT_CLOUD_CLIENT_ID} SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET: ${SLACK_MANAGEMENT_CLOUD_CLIENT_SECRET} + NAMELY_HRIS_CLOUD_CLIENT_ID: ${NAMELY_HRIS_CLOUD_CLIENT_ID} + NAMELY_HRIS_CLOUD_CLIENT_SECRET: ${NAMELY_HRIS_CLOUD_CLIENT_SECRET} + NAMELY_HRIS_CLOUD_SUBDOMAIN: ${NAMELY_HRIS_CLOUD_SUBDOMAIN} restart: unless-stopped ports: - 3000:3000 diff --git a/ngrok.yml b/ngrok.yml index e0f328e08..88ff12466 100644 --- a/ngrok.yml +++ b/ngrok.yml @@ -1,5 +1,5 @@ version: 2 -authtoken: NGROK_TOKEN +authtoken: NGROK_AUTH_TOKEN log_level: debug log: stdout diff --git a/packages/api/src/@core/connections-strategies/connections-strategies.service.ts b/packages/api/src/@core/connections-strategies/connections-strategies.service.ts index 57472214a..9f750205c 100644 --- a/packages/api/src/@core/connections-strategies/connections-strategies.service.ts +++ b/packages/api/src/@core/connections-strategies/connections-strategies.service.ts @@ -368,14 +368,7 @@ export class ConnectionsStrategiesService { return res; } } catch (error) { - throwTypedError( - new ConnectionStrategiesError({ - name: 'GET_CREDENTIALS_ERROR', - message: 'ConnectionsStrategiesService.getCredentials() call failed', - cause: error, - }), - this.logger, - ); + throw error; } } diff --git a/packages/api/src/@core/connections/@utils/index.ts b/packages/api/src/@core/connections/@utils/index.ts index 700168273..f64b3333b 100644 --- a/packages/api/src/@core/connections/@utils/index.ts +++ b/packages/api/src/@core/connections/@utils/index.ts @@ -65,4 +65,8 @@ export class ConnectionUtils { } return id_linked_user; } + + applyPanoraDelimiter(values: string[]): string { + return values.join('panoradelimiter'); + } } diff --git a/packages/api/src/@core/connections/accounting/services/accounting.connection.service.ts b/packages/api/src/@core/connections/accounting/services/accounting.connection.service.ts index 2554e5f9f..c6f96792d 100644 --- a/packages/api/src/@core/connections/accounting/services/accounting.connection.service.ts +++ b/packages/api/src/@core/connections/accounting/services/accounting.connection.service.ts @@ -67,15 +67,7 @@ export class AccountingConnectionsService { event.id_event, ); } catch (error) { - throwTypedError( - new ConnectionsError({ - name: 'HANDLE_OAUTH_CALLBACK_CRM', - message: - 'AccountingConnectionsService.handleAccountingOAuthCallBack() call failed', - cause: error, - }), - this.logger, - ); + throw error; } } diff --git a/packages/api/src/@core/connections/accounting/services/freeagent/freeagent.service.ts b/packages/api/src/@core/connections/accounting/services/freeagent/freeagent.service.ts index fcea27868..c1571a8a0 100644 --- a/packages/api/src/@core/connections/accounting/services/freeagent/freeagent.service.ts +++ b/packages/api/src/@core/connections/accounting/services/freeagent/freeagent.service.ts @@ -102,8 +102,8 @@ export class FreeagentConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['accounting']['freeagent'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['freeagent'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -119,8 +119,8 @@ export class FreeagentConnectionService provider_slug: 'freeagent', vertical: 'accounting', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['accounting']['freeagent'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['freeagent'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/accounting/services/freshbooks/freshbooks.service.ts b/packages/api/src/@core/connections/accounting/services/freshbooks/freshbooks.service.ts index 88f2ab56d..bdf7ee8c1 100644 --- a/packages/api/src/@core/connections/accounting/services/freshbooks/freshbooks.service.ts +++ b/packages/api/src/@core/connections/accounting/services/freshbooks/freshbooks.service.ts @@ -104,8 +104,8 @@ export class FreshbooksConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['accounting']['freshbooks'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['freshbooks'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -121,8 +121,8 @@ export class FreshbooksConnectionService provider_slug: 'freshbooks', vertical: 'accounting', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['accounting']['freshbooks'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['freshbooks'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/accounting/services/moneybird/moneybird.service.ts b/packages/api/src/@core/connections/accounting/services/moneybird/moneybird.service.ts index 2e5a7a53f..b93423a3d 100644 --- a/packages/api/src/@core/connections/accounting/services/moneybird/moneybird.service.ts +++ b/packages/api/src/@core/connections/accounting/services/moneybird/moneybird.service.ts @@ -102,8 +102,8 @@ export class MoneybirdConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['accounting']['moneybird'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['moneybird'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -116,8 +116,8 @@ export class MoneybirdConnectionService provider_slug: 'moneybird', vertical: 'accounting', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['accounting']['moneybird'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['moneybird'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), status: 'valid', diff --git a/packages/api/src/@core/connections/accounting/services/pennylane/pennylane.service.ts b/packages/api/src/@core/connections/accounting/services/pennylane/pennylane.service.ts index ef8a215b6..2f0a718d9 100644 --- a/packages/api/src/@core/connections/accounting/services/pennylane/pennylane.service.ts +++ b/packages/api/src/@core/connections/accounting/services/pennylane/pennylane.service.ts @@ -103,8 +103,8 @@ export class PennylaneConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['accounting']['pennylane'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['pennylane'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -120,8 +120,8 @@ export class PennylaneConnectionService provider_slug: 'pennylane', vertical: 'accounting', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['accounting']['pennylane'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['pennylane'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/accounting/services/quickbooks/quickbooks.service.ts b/packages/api/src/@core/connections/accounting/services/quickbooks/quickbooks.service.ts index f795c6262..bdab70ea6 100644 --- a/packages/api/src/@core/connections/accounting/services/quickbooks/quickbooks.service.ts +++ b/packages/api/src/@core/connections/accounting/services/quickbooks/quickbooks.service.ts @@ -102,8 +102,8 @@ export class QuickbooksConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['accounting']['quickbooks'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['quickbooks'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -119,8 +119,8 @@ export class QuickbooksConnectionService provider_slug: 'quickbooks', vertical: 'accounting', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['accounting']['quickbooks'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['quickbooks'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/accounting/services/sage/sage.service.ts b/packages/api/src/@core/connections/accounting/services/sage/sage.service.ts index 0c55a38c4..ccd49128f 100644 --- a/packages/api/src/@core/connections/accounting/services/sage/sage.service.ts +++ b/packages/api/src/@core/connections/accounting/services/sage/sage.service.ts @@ -101,7 +101,8 @@ export class SageConnectionService implements IAccountingConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['accounting']['sage'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['sage'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -117,7 +118,8 @@ export class SageConnectionService implements IAccountingConnectionService { provider_slug: 'sage', vertical: 'accounting', token_type: 'oauth', - account_url: CONNECTORS_METADATA['accounting']['sage'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['sage'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/accounting/services/wave_financial/wave_financial.service.ts b/packages/api/src/@core/connections/accounting/services/wave_financial/wave_financial.service.ts index ec599ec42..67c4149be 100644 --- a/packages/api/src/@core/connections/accounting/services/wave_financial/wave_financial.service.ts +++ b/packages/api/src/@core/connections/accounting/services/wave_financial/wave_financial.service.ts @@ -107,8 +107,8 @@ export class WaveFinancialConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['accounting']['wave_financial'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['wave_financial'] + .urls.apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -124,8 +124,8 @@ export class WaveFinancialConnectionService provider_slug: 'wave_financial', vertical: 'accounting', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['accounting']['wave_financial'].urls.apiUrl, + account_url: CONNECTORS_METADATA['accounting']['wave_financial'] + .urls.apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/accounting/services/xero/xero.service.ts b/packages/api/src/@core/connections/accounting/services/xero/xero.service.ts index f37204534..2cff8e8c7 100644 --- a/packages/api/src/@core/connections/accounting/services/xero/xero.service.ts +++ b/packages/api/src/@core/connections/accounting/services/xero/xero.service.ts @@ -99,6 +99,11 @@ export class XeroConnectionService implements IAccountingConnectionService { code: code, redirect_uri: REDIRECT_URI, }); + this.logger.log( + `data 64 is ${Buffer.from( + `${CREDENTIALS.CLIENT_ID}:${CREDENTIALS.CLIENT_SECRET}`, + ).toString('base64')}`, + ); const res = await axios.post( 'https://identity.xero.com/connect/token', formData.toString(), @@ -130,7 +135,9 @@ export class XeroConnectionService implements IAccountingConnectionService { ); //Important Note: Xero asks for a tenantId for which the token is valid for so we append it as a param and it MUST be extracted when making the calls in unified requests - const CUSTOM_ACCOUNT_URL = `${CONNECTORS_METADATA['accounting']['xero'].urls.apiUrl}?xeroTenantId=${res_.data[0].tenantId}`; + const CUSTOM_ACCOUNT_URL = `${ + CONNECTORS_METADATA['accounting']['xero'].urls.apiUrl as string + }?xeroTenantId=${res_.data[0].tenantId}`; let db_res; const connection_token = uuidv4(); @@ -183,18 +190,6 @@ export class XeroConnectionService implements IAccountingConnectionService { } return db_res; } catch (error) { - /*throwTypedError( - new ConnectionsError({ - name: 'HANDLE_OAUTH_CALLBACK_ACCOUNTING', - message: `XeroConnectionService.handleCallback() call failed ---> ${format3rdPartyError( - 'xero', - Action.oauthCallback, - ActionType.POST, - )}`, - cause: error, - }), - this.logger, - );*/ throw error; } } diff --git a/packages/api/src/@core/connections/ats/ats.connection.module.ts b/packages/api/src/@core/connections/ats/ats.connection.module.ts index a30e3f7e2..135f97708 100644 --- a/packages/api/src/@core/connections/ats/ats.connection.module.ts +++ b/packages/api/src/@core/connections/ats/ats.connection.module.ts @@ -8,9 +8,12 @@ import { ConnectionsStrategiesService } from '@@core/connections-strategies/conn import { ConnectionUtils } from '../@utils'; import { GreenhouseConnectionService } from './services/greenhouse/greenhouse.service'; import { AtsConnectionsService } from './services/ats.connection.service'; -import { ServiceRegistry } from './registry.service'; import { LeverConnectionService } from './services/lever/lever.service'; import { JobadderConnectionService } from './services/jobadder/jobadder.service'; +import { WorkdayConnectionService } from './services/workday/workday.service'; +import { AshbyConnectionService } from './services/ashby/ashby.service'; +import { ServiceRegistry } from './services/registry.service'; +import { BamboohrConnectionService } from './services/bamboohr/bamboohr.service'; @Module({ imports: [WebhookModule], @@ -27,6 +30,9 @@ import { JobadderConnectionService } from './services/jobadder/jobadder.service' GreenhouseConnectionService, LeverConnectionService, JobadderConnectionService, + WorkdayConnectionService, + AshbyConnectionService, + BamboohrConnectionService, ], exports: [AtsConnectionsService], }) diff --git a/packages/api/src/@core/connections/ats/services/ashby/ashby.service.ts b/packages/api/src/@core/connections/ats/services/ashby/ashby.service.ts new file mode 100644 index 000000000..7f4321571 --- /dev/null +++ b/packages/api/src/@core/connections/ats/services/ashby/ashby.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { IAtsConnectionService } from '../../types'; +import { CONNECTORS_METADATA } from '@panora/shared'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { APIKeyCallbackParams } from '@@core/connections/@utils/types'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class AshbyConnectionService implements IAtsConnectionService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + ) { + this.logger.setContext(AshbyConnectionService.name); + this.registry.registerService('ashby', this); + } + + async handleCallback(opts: APIKeyCallbackParams) { + try { + const { linkedUserId, projectId } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'ashby', + vertical: 'ats', + }, + }); + + let db_res; + const connection_token = uuidv4(); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(opts.apikey), + account_url: CONNECTORS_METADATA['ats']['ashby'].urls + .apiUrl as string, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'ashby', + vertical: 'ats', + token_type: 'api_key', + account_url: CONNECTORS_METADATA['ats']['ashby'].urls + .apiUrl as string, + access_token: this.cryptoService.encrypt(opts.apikey), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + return db_res; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/@core/connections/ats/services/ats.connection.service.ts b/packages/api/src/@core/connections/ats/services/ats.connection.service.ts index cd9a41b8a..ee30842cc 100644 --- a/packages/api/src/@core/connections/ats/services/ats.connection.service.ts +++ b/packages/api/src/@core/connections/ats/services/ats.connection.service.ts @@ -5,7 +5,7 @@ import { WebhookService } from '@@core/webhook/webhook.service'; import { connections as Connection } from '@prisma/client'; import { PrismaService } from '@@core/prisma/prisma.service'; import { v4 as uuidv4 } from 'uuid'; -import { ServiceRegistry } from '../registry.service'; +import { ServiceRegistry } from './registry.service'; import { CallbackParams, RefreshParams } from '@@core/connections/@utils/types'; @Injectable() diff --git a/packages/api/src/@core/connections/ats/services/bamboohr/bamboohr.service.ts b/packages/api/src/@core/connections/ats/services/bamboohr/bamboohr.service.ts new file mode 100644 index 000000000..b55ea2d01 --- /dev/null +++ b/packages/api/src/@core/connections/ats/services/bamboohr/bamboohr.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { IAtsConnectionService } from '../../types'; +import { ServiceRegistry } from '../registry.service'; +import { + AuthStrategy, + CONNECTORS_METADATA, + OAuth2AuthData, + providerToType, +} from '@panora/shared'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { OAuthCallbackParams } from '@@core/connections/@utils/types'; + +export type BamboohrOAuthResponse = { + access_token: string; + token_type: string; + expires_in: number; + scope: string; + id_token: string; +}; + +@Injectable() +export class BamboohrConnectionService implements IAtsConnectionService { + private readonly type: string; + + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private env: EnvironmentService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + private cService: ConnectionsStrategiesService, + private connectionUtils: ConnectionUtils, + ) { + this.logger.setContext(BamboohrConnectionService.name); + this.registry.registerService('bamboohr', this); + this.type = providerToType('bamboohr', 'ats', AuthStrategy.oauth2); + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'bamboohr', + vertical: 'ats', + }, + }); + + //reconstruct the redirect URI that was passed in the githubend it must be the same + const REDIRECT_URI = `${ + this.env.getDistributionMode() == 'selfhost' + ? this.env.getWebhookIngress() + : this.env.getPanoraBaseUrl() + }/connections/oauth/callback`; + + const CREDENTIALS = (await this.cService.getCredentials( + projectId, + this.type, + )) as OAuth2AuthData; + + const formData = new URLSearchParams({ + redirect_uri: REDIRECT_URI, + code: code, + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + grant_type: 'authorization_code', + scope: CONNECTORS_METADATA['ats']['bamboohr'].scopes, + }); + const res = await axios.post( + `https://.bamboohr.com/token.php?request=token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: BamboohrOAuthResponse = res.data; + this.logger.log( + 'OAuth credentials : bamboohr ats ' + JSON.stringify(data), + ); + + const formData_ = new URLSearchParams({ + id_token: data.id_token, + applicationKey: '', // TODO + }); + //fetch the api key of the user + const res_ = await axios.post( + `https://api.bamboohr.com/api/gateway.php/{company}/v1/oidcLogin`, + formData_.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data_: { + success: boolean; + userId: number; + employeeId: number; + key: string; + } = res_.data; + this.logger.log( + 'OAuth credentials : bamboohr ats apikey res ' + JSON.stringify(data_), + ); + + let db_res; + const connection_token = uuidv4(); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data_.key), + account_url: `https://api.bamboohr.com/api/gateway.php/{company}`, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'bamboohr', + vertical: 'ats', + token_type: 'oauth', + account_url: `https://api.bamboohr.com/api/gateway.php/{company}`, + access_token: this.cryptoService.encrypt(data_.key), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + return db_res; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/@core/connections/ats/services/greenhouse/greenhouse.service.ts b/packages/api/src/@core/connections/ats/services/greenhouse/greenhouse.service.ts index 52bfdadb7..abcc803a5 100644 --- a/packages/api/src/@core/connections/ats/services/greenhouse/greenhouse.service.ts +++ b/packages/api/src/@core/connections/ats/services/greenhouse/greenhouse.service.ts @@ -13,7 +13,7 @@ import { v4 as uuidv4 } from 'uuid'; import { EnvironmentService } from '@@core/environment/environment.service'; import { EncryptionService } from '@@core/encryption/encryption.service'; import { IAtsConnectionService } from '../../types'; -import { ServiceRegistry } from '../../registry.service'; +import { ServiceRegistry } from '../registry.service'; import { AuthStrategy, CONNECTORS_METADATA } from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; @@ -102,7 +102,8 @@ export class GreenhouseConnectionService implements IAtsConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['ats']['greenhouse'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ats']['greenhouse'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -118,7 +119,8 @@ export class GreenhouseConnectionService implements IAtsConnectionService { provider_slug: 'greenhouse', vertical: 'ats', token_type: 'oauth', - account_url: CONNECTORS_METADATA['ats']['greenhouse'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ats']['greenhouse'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/ats/services/jobadder/jobadder.service.ts b/packages/api/src/@core/connections/ats/services/jobadder/jobadder.service.ts index 42720055b..89d4ede9d 100644 --- a/packages/api/src/@core/connections/ats/services/jobadder/jobadder.service.ts +++ b/packages/api/src/@core/connections/ats/services/jobadder/jobadder.service.ts @@ -13,7 +13,7 @@ import { v4 as uuidv4 } from 'uuid'; import { EnvironmentService } from '@@core/environment/environment.service'; import { EncryptionService } from '@@core/encryption/encryption.service'; import { IAtsConnectionService } from '../../types'; -import { ServiceRegistry } from '../../registry.service'; +import { ServiceRegistry } from '../registry.service'; import { AuthStrategy, CONNECTORS_METADATA } from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; diff --git a/packages/api/src/@core/connections/ats/services/lever/lever.service.ts b/packages/api/src/@core/connections/ats/services/lever/lever.service.ts index 5b740aaf9..bbaa54826 100644 --- a/packages/api/src/@core/connections/ats/services/lever/lever.service.ts +++ b/packages/api/src/@core/connections/ats/services/lever/lever.service.ts @@ -13,7 +13,7 @@ import { v4 as uuidv4 } from 'uuid'; import { EnvironmentService } from '@@core/environment/environment.service'; import { EncryptionService } from '@@core/encryption/encryption.service'; import { IAtsConnectionService } from '../../types'; -import { ServiceRegistry } from '../../registry.service'; +import { ServiceRegistry } from '../registry.service'; import { AuthStrategy, CONNECTORS_METADATA } from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; @@ -30,11 +30,9 @@ export type LeverOAuthResponse = { token_type: string; }; -//TODO @Injectable() export class LeverConnectionService implements IAtsConnectionService { private readonly type: string; - constructor( private prisma: PrismaService, private logger: LoggerService, @@ -78,7 +76,7 @@ export class LeverConnectionService implements IAtsConnectionService { grant_type: 'authorization_code', }); const res = await axios.post( - 'https://auth.lever.co/oauth/token', + 'https://sandbox-lever.auth0.com/oauth/token', formData.toString(), { headers: { @@ -100,7 +98,8 @@ export class LeverConnectionService implements IAtsConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['ats']['lever'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ats']['lever'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -116,7 +115,8 @@ export class LeverConnectionService implements IAtsConnectionService { provider_slug: 'lever', vertical: 'ats', token_type: 'oauth', - account_url: CONNECTORS_METADATA['ats']['lever'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ats']['lever'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( @@ -160,7 +160,7 @@ export class LeverConnectionService implements IAtsConnectionService { refresh_token: this.cryptoService.decrypt(refreshToken), }); const res = await axios.post( - 'https://auth.lever.co/oauth/token', + 'https://sandbox-lever.auth0.com/oauth/token', formData.toString(), { headers: { diff --git a/packages/api/src/@core/connections/ats/registry.service.ts b/packages/api/src/@core/connections/ats/services/registry.service.ts similarity index 92% rename from packages/api/src/@core/connections/ats/registry.service.ts rename to packages/api/src/@core/connections/ats/services/registry.service.ts index 89b2c4e8b..b724782ff 100644 --- a/packages/api/src/@core/connections/ats/registry.service.ts +++ b/packages/api/src/@core/connections/ats/services/registry.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { IAtsConnectionService } from './types'; +import { IAtsConnectionService } from '../types'; @Injectable() export class ServiceRegistry { diff --git a/packages/api/src/@core/connections/ats/services/workday/workday.service.ts b/packages/api/src/@core/connections/ats/services/workday/workday.service.ts new file mode 100644 index 000000000..98ee5ce41 --- /dev/null +++ b/packages/api/src/@core/connections/ats/services/workday/workday.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { IAtsConnectionService } from '../../types'; +import { CONNECTORS_METADATA } from '@panora/shared'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { APIKeyCallbackParams } from '@@core/connections/@utils/types'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class WorkdayConnectionService implements IAtsConnectionService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + ) { + this.logger.setContext(WorkdayConnectionService.name); + this.registry.registerService('workday', this); + } + + async handleCallback(opts: APIKeyCallbackParams) { + try { + const { linkedUserId, projectId } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'workday', + vertical: 'ats', + }, + }); + + let db_res; + const connection_token = uuidv4(); + // construct a custom key as workday asks for 3 params (X-Api-Key, X-User-Token, X-User-Email) + // we simply use the string panoradelimiter to separate and encode easily + const data_to_encode = this.connectionUtils.applyPanoraDelimiter([ + opts.apikey, + opts.body_data.userToken, + opts.body_data.userEmail, + ]); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data_to_encode), + account_url: CONNECTORS_METADATA['ats']['workday'].urls + .apiUrl as string, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'workday', + vertical: 'ats', + token_type: 'api_key', + account_url: CONNECTORS_METADATA['ats']['workday'].urls + .apiUrl as string, + access_token: this.cryptoService.encrypt(data_to_encode), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + return db_res; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/@core/connections/connections.controller.ts b/packages/api/src/@core/connections/connections.controller.ts index 1a9743651..a77d635f8 100644 --- a/packages/api/src/@core/connections/connections.controller.ts +++ b/packages/api/src/@core/connections/connections.controller.ts @@ -29,6 +29,7 @@ import { CoreSyncService } from '@@core/sync/sync.service'; import { HrisConnectionsService } from './hris/services/hris.connection.service'; import { FilestorageConnectionsService } from './filestorage/services/filestorage.connection.service'; import { AtsConnectionsService } from './ats/services/ats.connection.service'; +import { ManagementConnectionsService } from './management/services/management.connection.service'; export type StateDataType = { projectId: string; @@ -54,6 +55,7 @@ export class ConnectionsController { private readonly filestorageConnectionsService: FilestorageConnectionsService, private readonly hrisConnectionsService: HrisConnectionsService, private readonly atsConnectionsService: AtsConnectionsService, + private readonly managementConnectionsService: ManagementConnectionsService, private logger: LoggerService, private prisma: PrismaService, private coreSync: CoreSyncService, @@ -141,6 +143,13 @@ export class ConnectionsController { 'oauth', ); break; + case ConnectorCategory.Management: + await this.managementConnectionsService.handleManagementCallBack( + providerName, + { linkedUserId, projectId, code }, + 'oauth', + ); + break; } res.redirect(returnUrl); @@ -159,19 +168,11 @@ export class ConnectionsController { ); }*/ } catch (error) { - /*throwTypedError( - new ConnectionsError({ - name: 'OAUTH_CALLBACK_ERROR', - message: 'ConnectionsController.handleCallback() call failed', - cause: error, - }), - this.logger, - );*/ throw error; } } - @Get('/gorgias/oauth/install') + /*@Get('/gorgias/oauth/install') handleGorgiasAuthUrl( @Res() res: Response, @Query('account') account: string, @@ -183,6 +184,7 @@ export class ConnectionsController { @Query('state') state: string, ) { try { + console.log(client_id) if (!account) throw new ReferenceError('account prop not found'); const params = `?client_id=${client_id}&response_type=${response_type}&redirect_uri=${redirect_uri}&state=${state}&nonce=${nonce}&scope=${scope}`; res.redirect(`https://${account}.gorgias.com/oauth/authorize${params}`); @@ -196,7 +198,7 @@ export class ConnectionsController { this.logger, ); } - } + }*/ @ApiOperation({ operationId: 'handleApiKeyCallback', @@ -327,14 +329,6 @@ export class ConnectionsController { ); }*/ } catch (error) { - /*throwTypedError( - new ConnectionsError({ - name: 'APIKEY_CALLBACK_ERROR', - message: 'ConnectionsController.handleApiKeyCallback() call failed', - cause: error, - }), - this.logger, - );*/ throw error; } } diff --git a/packages/api/src/@core/connections/connections.module.ts b/packages/api/src/@core/connections/connections.module.ts index 9fc992e03..6cc57e606 100644 --- a/packages/api/src/@core/connections/connections.module.ts +++ b/packages/api/src/@core/connections/connections.module.ts @@ -28,11 +28,13 @@ import { FilestorageConnectionModule } from './filestorage/filestorage.connectio import { HrisConnectionModule } from './hris/hris.connection.module'; import { ConnectionUtils } from './@utils'; import { AtsConnectionModule } from './ats/ats.connection.module'; +import { ManagementConnectionsModule } from './management/management.connection.module'; @Module({ controllers: [ConnectionsController], imports: [ CrmConnectionModule, + ManagementConnectionsModule, TicketingConnectionModule, AccountingConnectionModule, AtsConnectionModule, @@ -70,6 +72,7 @@ import { AtsConnectionModule } from './ats/ats.connection.module'; MarketingAutomationConnectionsModule, FilestorageConnectionModule, HrisConnectionModule, + ManagementConnectionsModule, ], }) export class ConnectionsModule {} diff --git a/packages/api/src/@core/connections/crm/services/accelo/accelo.service.ts b/packages/api/src/@core/connections/crm/services/accelo/accelo.service.ts index 102758e2d..e7e3f926b 100644 --- a/packages/api/src/@core/connections/crm/services/accelo/accelo.service.ts +++ b/packages/api/src/@core/connections/crm/services/accelo/accelo.service.ts @@ -18,6 +18,7 @@ import { OAuth2AuthData, CONNECTORS_METADATA, providerToType, + DynamicApiUrl, } from '@panora/shared'; import { AuthStrategy } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; @@ -77,7 +78,7 @@ export class AcceloConnectionService implements ICrmConnectionService { code: code, }); const res = await axios.post( - `${CREDENTIALS.SUBDOMAIN}/oauth2/v0/token`, + `https://${CREDENTIALS.SUBDOMAIN}.api.accelo.com/oauth2/v0/token`, formData.toString(), { headers: { @@ -95,9 +96,9 @@ export class AcceloConnectionService implements ICrmConnectionService { let db_res; const connection_token = uuidv4(); //get the right BASE URL API - const BASE_API_URL = - CREDENTIALS.SUBDOMAIN + - CONNECTORS_METADATA['crm']['accelo'].urls.apiUrl; + const BASE_API_URL = ( + CONNECTORS_METADATA['crm']['accelo'].urls.apiUrl as DynamicApiUrl + )(CREDENTIALS.SUBDOMAIN); if (isNotUnique) { // Update existing connection @@ -178,7 +179,7 @@ export class AcceloConnectionService implements ICrmConnectionService { )) as OAuth2AuthData; const res = await axios.post( - `${CREDENTIALS.SUBDOMAIN}/oauth2/v0/token`, + `https://${CREDENTIALS.SUBDOMAIN}.api.accelo.com/oauth2/v0/token`, formData.toString(), { headers: { diff --git a/packages/api/src/@core/connections/crm/services/affinity/affinity.service.ts b/packages/api/src/@core/connections/crm/services/affinity/affinity.service.ts index 557292a1d..c946f840a 100644 --- a/packages/api/src/@core/connections/crm/services/affinity/affinity.service.ts +++ b/packages/api/src/@core/connections/crm/services/affinity/affinity.service.ts @@ -43,7 +43,8 @@ export class AffinityConnectionService implements ICrmConnectionService { }, data: { access_token: this.cryptoService.encrypt(opts.apikey), - account_url: CONNECTORS_METADATA['crm']['affinity'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['affinity'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -56,7 +57,8 @@ export class AffinityConnectionService implements ICrmConnectionService { provider_slug: 'affinity', vertical: 'crm', token_type: 'api_key', - account_url: CONNECTORS_METADATA['crm']['affinity'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['affinity'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(opts.apikey), status: 'valid', created_at: new Date(), diff --git a/packages/api/src/@core/connections/crm/services/attio/attio.service.ts b/packages/api/src/@core/connections/crm/services/attio/attio.service.ts index 76299a35f..8311ea4f5 100644 --- a/packages/api/src/@core/connections/crm/services/attio/attio.service.ts +++ b/packages/api/src/@core/connections/crm/services/attio/attio.service.ts @@ -115,7 +115,8 @@ export class AttioConnectionService implements ICrmConnectionService { provider_slug: 'attio', vertical: 'crm', token_type: 'oauth', - account_url: CONNECTORS_METADATA['crm']['attio'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['attio'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), status: 'valid', created_at: new Date(), diff --git a/packages/api/src/@core/connections/crm/services/capsule/capsule.service.ts b/packages/api/src/@core/connections/crm/services/capsule/capsule.service.ts index c5f8d9ea9..5f66789fb 100644 --- a/packages/api/src/@core/connections/crm/services/capsule/capsule.service.ts +++ b/packages/api/src/@core/connections/crm/services/capsule/capsule.service.ts @@ -101,7 +101,8 @@ export class CapsuleConnectionService implements ICrmConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['crm']['capsule'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['capsule'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -117,7 +118,8 @@ export class CapsuleConnectionService implements ICrmConnectionService { provider_slug: 'capsule', vertical: 'crm', token_type: 'oauth', - account_url: CONNECTORS_METADATA['crm']['capsule'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['capsule'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/crm/services/close/close.service.ts b/packages/api/src/@core/connections/crm/services/close/close.service.ts index fbab35c6b..3b3daa5d8 100644 --- a/packages/api/src/@core/connections/crm/services/close/close.service.ts +++ b/packages/api/src/@core/connections/crm/services/close/close.service.ts @@ -102,7 +102,8 @@ export class CloseConnectionService implements ICrmConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['crm']['close']?.urls?.apiUrl, + account_url: CONNECTORS_METADATA['crm']['close'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -118,7 +119,7 @@ export class CloseConnectionService implements ICrmConnectionService { provider_slug: 'close', vertical: 'crm', token_type: 'oauth', - account_url: CONNECTORS_METADATA['crm']?.close?.urls?.apiUrl, + account_url: CONNECTORS_METADATA['crm'].close.urls.apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/crm/services/copper/copper.service.ts b/packages/api/src/@core/connections/crm/services/copper/copper.service.ts index e226d964b..dce705827 100644 --- a/packages/api/src/@core/connections/crm/services/copper/copper.service.ts +++ b/packages/api/src/@core/connections/crm/services/copper/copper.service.ts @@ -111,7 +111,8 @@ export class CopperConnectionService implements ICrmConnectionService { provider_slug: 'copper', vertical: 'crm', token_type: 'oauth', - account_url: CONNECTORS_METADATA['crm']['copper'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['copper'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), status: 'valid', created_at: new Date(), 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 index d0eb86f63..c26f2d462 100644 --- a/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts +++ b/packages/api/src/@core/connections/crm/services/hubspot/hubspot.service.ts @@ -116,7 +116,8 @@ export class HubspotConnectionService implements ICrmConnectionService { provider_slug: 'hubspot', vertical: 'crm', token_type: 'oauth', - account_url: CONNECTORS_METADATA['crm']['hubspot'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['hubspot'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/crm/services/keap/keap.service.ts b/packages/api/src/@core/connections/crm/services/keap/keap.service.ts index 7b730ac49..1d2b7f629 100644 --- a/packages/api/src/@core/connections/crm/services/keap/keap.service.ts +++ b/packages/api/src/@core/connections/crm/services/keap/keap.service.ts @@ -116,7 +116,8 @@ export class KeapConnectionService implements ICrmConnectionService { provider_slug: 'keap', vertical: 'crm', token_type: 'oauth', - account_url: CONNECTORS_METADATA['crm']['keap'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['keap'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( 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 index ed6d91c67..97b86f985 100644 --- a/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts +++ b/packages/api/src/@core/connections/crm/services/pipedrive/pipedrive.service.ts @@ -116,7 +116,8 @@ export class PipedriveConnectionService implements ICrmConnectionService { provider_slug: 'pipedrive', vertical: 'crm', token_type: 'oauth', - account_url: CONNECTORS_METADATA['crm']['pipedrive'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['pipedrive'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/crm/services/teamleader/teamleader.service.ts b/packages/api/src/@core/connections/crm/services/teamleader/teamleader.service.ts index 113cf85e4..cd9a70785 100644 --- a/packages/api/src/@core/connections/crm/services/teamleader/teamleader.service.ts +++ b/packages/api/src/@core/connections/crm/services/teamleader/teamleader.service.ts @@ -102,7 +102,8 @@ export class TeamleaderConnectionService implements ICrmConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['crm']['teamleader'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['teamleader'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -118,7 +119,8 @@ export class TeamleaderConnectionService implements ICrmConnectionService { provider_slug: 'teamleader', vertical: 'crm', token_type: 'oauth', - account_url: CONNECTORS_METADATA['crm']['teamleader'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['teamleader'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/crm/services/teamwork/teamwork.service.ts b/packages/api/src/@core/connections/crm/services/teamwork/teamwork.service.ts index ed3d0f486..fdbda235e 100644 --- a/packages/api/src/@core/connections/crm/services/teamwork/teamwork.service.ts +++ b/packages/api/src/@core/connections/crm/services/teamwork/teamwork.service.ts @@ -14,11 +14,7 @@ import { EnvironmentService } from '@@core/environment/environment.service'; import { EncryptionService } from '@@core/encryption/encryption.service'; import { ICrmConnectionService } from '../../types'; import { ServiceRegistry } from '../registry.service'; -import { - OAuth2AuthData, - CONNECTORS_METADATA, - providerToType, -} from '@panora/shared'; +import { OAuth2AuthData, providerToType } from '@panora/shared'; import { AuthStrategy } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; import { ConnectionUtils } from '@@core/connections/@utils'; @@ -29,6 +25,20 @@ import { export type TeamworkOAuthResponse = { access_token: string; + installation: { + apiEndPoint: string; + company: { + id: number; + logo: string; + name: string; + }; + id: number; + logo: string; + name: string; + region: string; + url: string; + }; + status: string; }; @Injectable() @@ -90,9 +100,7 @@ export class TeamworkConnectionService implements ICrmConnectionService { let db_res; const connection_token = uuidv4(); //get the right BASE URL API - const BASE_API_URL = - CREDENTIALS.SUBDOMAIN + - CONNECTORS_METADATA['crm']['teamwork'].urls.apiUrl; + const BASE_API_URL = data.installation.apiEndPoint; if (isNotUnique) { db_res = await this.prisma.connections.update({ 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 index 90a428b21..c92465332 100644 --- a/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts +++ b/packages/api/src/@core/connections/crm/services/zendesk/zendesk.service.ts @@ -63,10 +63,7 @@ export class ZendeskConnectionService implements ICrmConnectionService { }, }); - //reconstruct the redirect URI that was passed in the frontend it must be the same - //const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; - //TODO - const REDIRECT_URI = `http://localhost:3000/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, @@ -120,7 +117,8 @@ export class ZendeskConnectionService implements ICrmConnectionService { provider_slug: 'zendesk', vertical: 'crm', token_type: 'oauth', - account_url: CONNECTORS_METADATA['crm']['zendesk'].urls.apiUrl, + account_url: CONNECTORS_METADATA['crm']['zendesk'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: data.refresh_token ? this.cryptoService.encrypt(data.refresh_token) 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 index ebdaac99e..c78ea0be2 100644 --- a/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts +++ b/packages/api/src/@core/connections/crm/services/zoho/zoho.service.ts @@ -18,6 +18,7 @@ import { OAuth2AuthData, CONNECTORS_METADATA, providerToType, + DynamicApiUrl, } from '@panora/shared'; import { AuthStrategy } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; @@ -126,6 +127,9 @@ export class ZohoConnectionService implements ICrmConnectionService { let db_res; const connection_token = uuidv4(); const apiDomain = ZOHOLocations[location].apiBase; + const BASE_API_URL = ( + CONNECTORS_METADATA['crm']['zoho'].urls.apiUrl as DynamicApiUrl + )(apiDomain); if (isNotUnique) { db_res = await this.prisma.connections.update({ @@ -142,8 +146,7 @@ export class ZohoConnectionService implements ICrmConnectionService { ), status: 'valid', created_at: new Date(), - account_url: - apiDomain + CONNECTORS_METADATA['crm']['zoho'].urls.apiUrl, + account_url: BASE_API_URL, }, }); } else { diff --git a/packages/api/src/@core/connections/filestorage/services/box/box.service.ts b/packages/api/src/@core/connections/filestorage/services/box/box.service.ts index 621c06bc8..325cf5c24 100644 --- a/packages/api/src/@core/connections/filestorage/services/box/box.service.ts +++ b/packages/api/src/@core/connections/filestorage/services/box/box.service.ts @@ -100,7 +100,8 @@ export class BoxConnectionService implements IFilestorageConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['filestorage']['box'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['box'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -116,7 +117,8 @@ export class BoxConnectionService implements IFilestorageConnectionService { provider_slug: 'box', vertical: 'filestorage', token_type: 'oauth', - account_url: CONNECTORS_METADATA['filestorage']['box'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['box'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/filestorage/services/dropbox/dropbox.service.ts b/packages/api/src/@core/connections/filestorage/services/dropbox/dropbox.service.ts index 28220d3be..ea3eac80f 100644 --- a/packages/api/src/@core/connections/filestorage/services/dropbox/dropbox.service.ts +++ b/packages/api/src/@core/connections/filestorage/services/dropbox/dropbox.service.ts @@ -103,8 +103,8 @@ export class DropboxConnectionService implements IFilestorageConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['filestorage']['dropbox'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['dropbox'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -120,8 +120,8 @@ export class DropboxConnectionService implements IFilestorageConnectionService { provider_slug: 'dropbox', vertical: 'filestorage', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['filestorage']['dropbox'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['dropbox'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/filestorage/services/google_drive/google_drive.service.ts b/packages/api/src/@core/connections/filestorage/services/google_drive/google_drive.service.ts index 2caeb50df..66fbf1dab 100644 --- a/packages/api/src/@core/connections/filestorage/services/google_drive/google_drive.service.ts +++ b/packages/api/src/@core/connections/filestorage/services/google_drive/google_drive.service.ts @@ -109,8 +109,8 @@ export class GoogleDriveConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['filestorage']['googledrive'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['googledrive'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -126,8 +126,8 @@ export class GoogleDriveConnectionService provider_slug: 'google_drive', vertical: 'filestorage', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['filestorage']['googledrive'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['googledrive'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/filestorage/services/onedrive/onedrive.service.ts b/packages/api/src/@core/connections/filestorage/services/onedrive/onedrive.service.ts index 490810c92..74c4b8c95 100644 --- a/packages/api/src/@core/connections/filestorage/services/onedrive/onedrive.service.ts +++ b/packages/api/src/@core/connections/filestorage/services/onedrive/onedrive.service.ts @@ -66,11 +66,7 @@ export class OneDriveConnectionService }, }); - const REDIRECT_URI = `${ - this.env.getDistributionMode() == 'selfhost' - ? this.env.getWebhookIngress() - : this.env.getPanoraBaseUrl() - }/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, @@ -109,8 +105,8 @@ export class OneDriveConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['filestorage']['onedrive'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['onedrive'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -126,8 +122,8 @@ export class OneDriveConnectionService provider_slug: 'onedrive', vertical: 'filestorage', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['filestorage']['onedrive'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['onedrive'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( @@ -168,11 +164,7 @@ export class OneDriveConnectionService async handleTokenRefresh(opts: RefreshParams) { try { const { connectionId, refreshToken, projectId } = opts; - const REDIRECT_URI = `${ - this.env.getDistributionMode() == 'selfhost' - ? this.env.getWebhookIngress() - : this.env.getPanoraBaseUrl() - }/connections/oauth/callback`; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; const CREDENTIALS = (await this.cService.getCredentials( projectId, diff --git a/packages/api/src/@core/connections/filestorage/services/sharepoint/sharepoint.service.ts b/packages/api/src/@core/connections/filestorage/services/sharepoint/sharepoint.service.ts index 6a76fb649..17895d361 100644 --- a/packages/api/src/@core/connections/filestorage/services/sharepoint/sharepoint.service.ts +++ b/packages/api/src/@core/connections/filestorage/services/sharepoint/sharepoint.service.ts @@ -115,8 +115,8 @@ export class SharepointConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['filestorage']['sharepoint'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['sharepoint'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -132,8 +132,8 @@ export class SharepointConnectionService provider_slug: 'sharepoint', vertical: 'filestorage', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['filestorage']['sharepoint'].urls.apiUrl, + account_url: CONNECTORS_METADATA['filestorage']['sharepoint'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/hris/hris.connection.module.ts b/packages/api/src/@core/connections/hris/hris.connection.module.ts index 8139271d3..2ac490383 100644 --- a/packages/api/src/@core/connections/hris/hris.connection.module.ts +++ b/packages/api/src/@core/connections/hris/hris.connection.module.ts @@ -13,6 +13,8 @@ import { ConnectionUtils } from '../@utils'; import { GustoConnectionService } from './services/gusto/gusto.service'; import { PayfitConnectionService } from './services/payfit/payfit.service'; import { FactorialConnectionService } from './services/factorial/factorial.service'; +import { NamelyConnectionService } from './services/namely/namely.service'; +import { BamboohrConnectionService } from './services/bamboohr/bamboohr.service'; @Module({ imports: [WebhookModule], @@ -31,6 +33,8 @@ import { FactorialConnectionService } from './services/factorial/factorial.servi GustoConnectionService, PayfitConnectionService, FactorialConnectionService, + NamelyConnectionService, + BamboohrConnectionService, ], exports: [HrisConnectionsService], }) diff --git a/packages/api/src/@core/connections/hris/services/bamboohr/bamboohr.service.ts b/packages/api/src/@core/connections/hris/services/bamboohr/bamboohr.service.ts new file mode 100644 index 000000000..9bdb6c00e --- /dev/null +++ b/packages/api/src/@core/connections/hris/services/bamboohr/bamboohr.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { IHrisConnectionService } from '../../types'; +import { ServiceRegistry } from '../registry.service'; +import { + AuthStrategy, + CONNECTORS_METADATA, + OAuth2AuthData, + providerToType, +} from '@panora/shared'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { OAuthCallbackParams } from '@@core/connections/@utils/types'; + +export type BamboohrOAuthResponse = { + access_token: string; + token_type: string; + expires_in: number; + scope: string; + id_token: string; +}; + +@Injectable() +export class BamboohrConnectionService implements IHrisConnectionService { + private readonly type: string; + + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private env: EnvironmentService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + private cService: ConnectionsStrategiesService, + private connectionUtils: ConnectionUtils, + ) { + this.logger.setContext(BamboohrConnectionService.name); + this.registry.registerService('bamboohr', this); + this.type = providerToType('bamboohr', 'hris', AuthStrategy.oauth2); + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'bamboohr', + vertical: 'hris', + }, + }); + + //reconstruct the redirect URI that was passed in the githubend it must be the same + const REDIRECT_URI = `${ + this.env.getDistributionMode() == 'selfhost' + ? this.env.getWebhookIngress() + : this.env.getPanoraBaseUrl() + }/connections/oauth/callback`; + + const CREDENTIALS = (await this.cService.getCredentials( + projectId, + this.type, + )) as OAuth2AuthData; + + const formData = new URLSearchParams({ + redirect_uri: REDIRECT_URI, + code: code, + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + grant_type: 'authorization_code', + scope: CONNECTORS_METADATA['hris']['bamboohr'].scopes, + }); + const res = await axios.post( + `https://.bamboohr.com/token.php?request=token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: BamboohrOAuthResponse = res.data; + this.logger.log( + 'OAuth credentials : bamboohr hris ' + JSON.stringify(data), + ); + + const formData_ = new URLSearchParams({ + id_token: data.id_token, + applicationKey: '', // TODO + }); + //fetch the api key of the user + const res_ = await axios.post( + `https://api.bamboohr.com/api/gateway.php/{company}/v1/oidcLogin`, + formData_.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data_: { + success: boolean; + userId: number; + employeeId: number; + key: string; + } = res_.data; + this.logger.log( + 'OAuth credentials : bamboohr hris apikey res ' + JSON.stringify(data_), + ); + + let db_res; + const connection_token = uuidv4(); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data_.key), + account_url: `https://api.bamboohr.com/api/gateway.php/{company}`, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'bamboohr', + vertical: 'hris', + token_type: 'oauth', + account_url: `https://api.bamboohr.com/api/gateway.php/{company}`, + access_token: this.cryptoService.encrypt(data_.key), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + return db_res; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/@core/connections/hris/services/deel/deel.service.ts b/packages/api/src/@core/connections/hris/services/deel/deel.service.ts index 2cb181700..dc5dbeea1 100644 --- a/packages/api/src/@core/connections/hris/services/deel/deel.service.ts +++ b/packages/api/src/@core/connections/hris/services/deel/deel.service.ts @@ -107,7 +107,8 @@ export class DeelConnectionService implements IHrisConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['hris']['deel'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['deel'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -123,7 +124,8 @@ export class DeelConnectionService implements IHrisConnectionService { provider_slug: 'deel', vertical: 'hris', token_type: 'oauth', - account_url: CONNECTORS_METADATA['hris']['deel'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['deel'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/hris/services/factorial/factorial.service.ts b/packages/api/src/@core/connections/hris/services/factorial/factorial.service.ts index 168e5a52b..3bee5a524 100644 --- a/packages/api/src/@core/connections/hris/services/factorial/factorial.service.ts +++ b/packages/api/src/@core/connections/hris/services/factorial/factorial.service.ts @@ -108,7 +108,8 @@ export class FactorialConnectionService implements IHrisConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['hris']['factorial'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['factorial'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -124,7 +125,8 @@ export class FactorialConnectionService implements IHrisConnectionService { provider_slug: 'factorial', vertical: 'hris', token_type: 'oauth', - account_url: CONNECTORS_METADATA['hris']['factorial'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['factorial'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/hris/services/gusto/gusto.service.ts b/packages/api/src/@core/connections/hris/services/gusto/gusto.service.ts index 8dd1871c2..d482f05c0 100644 --- a/packages/api/src/@core/connections/hris/services/gusto/gusto.service.ts +++ b/packages/api/src/@core/connections/hris/services/gusto/gusto.service.ts @@ -101,7 +101,8 @@ export class GustoConnectionService implements IHrisConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['hris']['gusto'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['gusto'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -117,7 +118,8 @@ export class GustoConnectionService implements IHrisConnectionService { provider_slug: 'gusto', vertical: 'hris', token_type: 'oauth', - account_url: CONNECTORS_METADATA['hris']['gusto'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['gusto'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/hris/services/namely/namely.service.ts b/packages/api/src/@core/connections/hris/services/namely/namely.service.ts new file mode 100644 index 000000000..7df6d92a0 --- /dev/null +++ b/packages/api/src/@core/connections/hris/services/namely/namely.service.ts @@ -0,0 +1,218 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { + Action, + ActionType, + ConnectionsError, + format3rdPartyError, + throwTypedError, +} from '@@core/utils/errors'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { IHrisConnectionService } from '../../types'; +import { ServiceRegistry } from '../registry.service'; +import { + AuthStrategy, + CONNECTORS_METADATA, + OAuth2AuthData, + providerToType, +} from '@panora/shared'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + OAuthCallbackParams, + RefreshParams, +} from '@@core/connections/@utils/types'; + +export type NamelyOAuthResponse = { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; + scope: string; +}; + +@Injectable() +export class NamelyConnectionService implements IHrisConnectionService { + private readonly type: string; + + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private env: EnvironmentService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + private cService: ConnectionsStrategiesService, + private connectionUtils: ConnectionUtils, + ) { + this.logger.setContext(NamelyConnectionService.name); + this.registry.registerService('namely', this); + this.type = providerToType('namely', 'hris', AuthStrategy.oauth2); + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'namely', + vertical: 'hris', + }, + }); + + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; + + const CREDENTIALS = (await this.cService.getCredentials( + projectId, + this.type, + )) as OAuth2AuthData; + + const formData = new URLSearchParams({ + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + // redirect_uri: REDIRECT_URI, + code: code, + grant_type: 'authorization_code', + }); + const res = await axios.post( + `${CREDENTIALS.SUBDOMAIN}/api/v1/oauth2/token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: NamelyOAuthResponse = res.data; + this.logger.log( + 'OAuth credentials : namely hris ' + JSON.stringify(data), + ); + + let db_res; + const connection_token = uuidv4(); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + account_url: CONNECTORS_METADATA['hris']['namely'].urls + .apiUrl as string, + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'namely', + vertical: 'hris', + token_type: 'oauth', + account_url: CONNECTORS_METADATA['hris']['namely'].urls + .apiUrl as string, + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + return db_res; + } catch (error) { + throwTypedError( + new ConnectionsError({ + name: 'HANDLE_OAUTH_CALLBACK_HRIS', + message: `NamelyConnectionService.handleCallback() call failed ---> ${format3rdPartyError( + 'namely', + Action.oauthCallback, + ActionType.POST, + )}`, + cause: error, + }), + this.logger, + ); + } + } + async handleTokenRefresh(opts: RefreshParams) { + try { + const { connectionId, refreshToken, projectId } = opts; + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; + + const CREDENTIALS = (await this.cService.getCredentials( + projectId, + this.type, + )) as OAuth2AuthData; + + const formData = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: this.cryptoService.decrypt(refreshToken), + redirect_uri: REDIRECT_URI, + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + }); + + const res = await axios.post( + `${CREDENTIALS.SUBDOMAIN}/api/v1/oauth2/token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: NamelyOAuthResponse = res.data; + await this.prisma.connections.update({ + where: { + id_connection: connectionId, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + }, + }); + this.logger.log('OAuth credentials updated : namely '); + } catch (error) { + throwTypedError( + new ConnectionsError({ + name: 'HANDLE_OAUTH_REFRESH_HRIS', + message: `NamelyConnectionService.handleTokenRefresh() call failed ---> ${format3rdPartyError( + 'namely', + Action.oauthRefresh, + ActionType.POST, + )}`, + cause: error, + }), + this.logger, + ); + } + } +} diff --git a/packages/api/src/@core/connections/hris/services/payfit/payfit.service.ts b/packages/api/src/@core/connections/hris/services/payfit/payfit.service.ts index 135249738..24d911d55 100644 --- a/packages/api/src/@core/connections/hris/services/payfit/payfit.service.ts +++ b/packages/api/src/@core/connections/hris/services/payfit/payfit.service.ts @@ -107,7 +107,8 @@ export class PayfitConnectionService implements IHrisConnectionService { }, data: { access_token: this.cryptoService.encrypt(data.access_token), - account_url: CONNECTORS_METADATA['hris']['payfit'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['payfit'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -120,7 +121,8 @@ export class PayfitConnectionService implements IHrisConnectionService { provider_slug: 'payfit', vertical: 'hris', token_type: 'oauth', - account_url: CONNECTORS_METADATA['hris']['payfit'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['payfit'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), status: 'valid', created_at: new Date(), diff --git a/packages/api/src/@core/connections/hris/services/rippling/rippling.service.ts b/packages/api/src/@core/connections/hris/services/rippling/rippling.service.ts index 60794b0d2..b6150a2e4 100644 --- a/packages/api/src/@core/connections/hris/services/rippling/rippling.service.ts +++ b/packages/api/src/@core/connections/hris/services/rippling/rippling.service.ts @@ -109,7 +109,8 @@ export class RipplingConnectionService implements IHrisConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['hris']['rippling'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['rippling'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -125,7 +126,8 @@ export class RipplingConnectionService implements IHrisConnectionService { provider_slug: 'rippling', vertical: 'hris', token_type: 'oauth', - account_url: CONNECTORS_METADATA['hris']['rippling'].urls.apiUrl, + account_url: CONNECTORS_METADATA['hris']['rippling'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/management/services/notion/notion.service.ts b/packages/api/src/@core/connections/management/services/notion/notion.service.ts index 383465d65..2cddb7aae 100644 --- a/packages/api/src/@core/connections/management/services/notion/notion.service.ts +++ b/packages/api/src/@core/connections/management/services/notion/notion.service.ts @@ -89,7 +89,9 @@ export class NotionConnectionService implements IManagementConnectionService { }, ); const data: NotionOAuthResponse = res.data; - this.logger.log('OAuth credentials : notion management ' + JSON.stringify(data)); + this.logger.log( + 'OAuth credentials : notion management ' + JSON.stringify(data), + ); let db_res; const connection_token = uuidv4(); @@ -101,7 +103,8 @@ export class NotionConnectionService implements IManagementConnectionService { }, data: { access_token: this.cryptoService.encrypt(data.access_token), - account_url: CONNECTORS_METADATA['management']['notion'].urls.apiUrl, + account_url: CONNECTORS_METADATA['management']['notion'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -114,7 +117,8 @@ export class NotionConnectionService implements IManagementConnectionService { provider_slug: 'notion', vertical: 'management', token_type: 'oauth', - account_url: CONNECTORS_METADATA['management']['notion'].urls.apiUrl, + account_url: CONNECTORS_METADATA['management']['notion'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), status: 'valid', created_at: new Date(), diff --git a/packages/api/src/@core/connections/management/services/slack/slack.service.ts b/packages/api/src/@core/connections/management/services/slack/slack.service.ts index c1eb25f62..8f4df58d0 100644 --- a/packages/api/src/@core/connections/management/services/slack/slack.service.ts +++ b/packages/api/src/@core/connections/management/services/slack/slack.service.ts @@ -26,28 +26,29 @@ import { OAuthCallbackParams, RefreshParams, } from '@@core/connections/@utils/types'; +import { URLSearchParams } from 'url'; export type SlackOAuthResponse = { - ok: boolean; + ok: boolean; + access_token: string; + token_type: string; + scope: string; + bot_user_id: string; + app_id: string; + team: { + name: string; + id: string; + }; + enterprise?: { + name: string; + id: string; + }; + authed_user: { + id: string; + scope: string; access_token: string; token_type: string; - scope: string; - bot_user_id: string; - app_id: string; - team: { - name: string; - id: string; - }; - enterprise?: { - name: string; - id: string; - }; - authed_user: { - id: string; - scope: string; - access_token: string; - token_type: string; - }; + }; }; @Injectable() @@ -79,30 +80,30 @@ export class SlackConnectionService implements IManagementConnectionService { }, }); - const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; - const CREDENTIALS = (await this.cService.getCredentials( projectId, this.type, )) as OAuth2AuthData; - const formData = { + const formData = new URLSearchParams({ client_id: CREDENTIALS.CLIENT_ID, client_secret: CREDENTIALS.CLIENT_SECRET, code: code, grant_type: 'authorization_code', - }; + }); const res = await axios.post( `https://slack.com/api/oauth.v2.access`, - JSON.stringify(formData), + formData.toString(), { headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', }, }, ); const data: SlackOAuthResponse = res.data; - this.logger.log('OAuth credentials : slack management ' + JSON.stringify(data)); + this.logger.log( + 'OAuth credentials : slack management ' + JSON.stringify(data), + ); let db_res; const connection_token = uuidv4(); @@ -113,8 +114,11 @@ export class SlackConnectionService implements IManagementConnectionService { id_connection: isNotUnique.id_connection, }, data: { - access_token: this.cryptoService.encrypt(data.access_token), - account_url: CONNECTORS_METADATA['management']['slack'].urls.apiUrl, + access_token: this.cryptoService.encrypt( + data.authed_user.access_token, + ), + account_url: CONNECTORS_METADATA['management']['slack'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -127,8 +131,11 @@ export class SlackConnectionService implements IManagementConnectionService { provider_slug: 'slack', vertical: 'management', token_type: 'oauth', - account_url: CONNECTORS_METADATA['management']['slack'].urls.apiUrl, - access_token: this.cryptoService.encrypt(data.access_token), + account_url: CONNECTORS_METADATA['management']['slack'].urls + .apiUrl as string, + access_token: this.cryptoService.encrypt( + data.authed_user.access_token, + ), status: 'valid', created_at: new Date(), projects: { @@ -147,18 +154,7 @@ export class SlackConnectionService implements IManagementConnectionService { } return db_res; } catch (error) { - throwTypedError( - new ConnectionsError({ - name: 'HANDLE_OAUTH_CALLBACK_HRIS', - message: `SlackConnectionService.handleCallback() call failed ---> ${format3rdPartyError( - 'slack', - Action.oauthCallback, - ActionType.POST, - )}`, - cause: error, - }), - this.logger, - ); + throw error; } } } diff --git a/packages/api/src/@core/connections/marketingautomation/marketingautomation.connection.module.ts b/packages/api/src/@core/connections/marketingautomation/marketingautomation.connection.module.ts index 261fc089f..dfbd7f6b9 100644 --- a/packages/api/src/@core/connections/marketingautomation/marketingautomation.connection.module.ts +++ b/packages/api/src/@core/connections/marketingautomation/marketingautomation.connection.module.ts @@ -1,5 +1,4 @@ import { Module } from '@nestjs/common'; -import { PrismaService } from '@@core/prisma/prisma.service'; import { LoggerService } from '@@core/logger/logger.service'; import { WebhookService } from '@@core/webhook/webhook.service'; import { WebhookModule } from '@@core/webhook/webhook.module'; @@ -13,6 +12,7 @@ import { BrevoConnectionService } from './services/brevo/brevo.service'; import { PodiumConnectionService } from './services/podium/podium.service'; import { MailchimpConnectionService } from './services/mailchimp/mailchimp.service'; import { GetresponseConnectionService } from './services/getresponse/getresponse.service'; +import { KeapConnectionService } from './services/keap/keap.service'; @Module({ imports: [WebhookModule], @@ -30,6 +30,7 @@ import { GetresponseConnectionService } from './services/getresponse/getresponse PodiumConnectionService, MailchimpConnectionService, GetresponseConnectionService, + KeapConnectionService, ], exports: [MarketingAutomationConnectionsService], }) diff --git a/packages/api/src/@core/connections/marketingautomation/services/brevo/brevo.service.ts b/packages/api/src/@core/connections/marketingautomation/services/brevo/brevo.service.ts index d3febd0e3..60cb562d2 100644 --- a/packages/api/src/@core/connections/marketingautomation/services/brevo/brevo.service.ts +++ b/packages/api/src/@core/connections/marketingautomation/services/brevo/brevo.service.ts @@ -45,8 +45,8 @@ export class BrevoConnectionService }, data: { access_token: this.cryptoService.encrypt(opts.apikey), - account_url: - CONNECTORS_METADATA['marketingautomation']['brevo'].urls.apiUrl, + account_url: CONNECTORS_METADATA['marketingautomation']['brevo'] + .urls.apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -59,8 +59,8 @@ export class BrevoConnectionService provider_slug: 'brevo', vertical: 'marketingautomation', token_type: 'api_key', - account_url: - CONNECTORS_METADATA['marketingautomation']['brevo'].urls.apiUrl, + account_url: CONNECTORS_METADATA['marketingautomation']['brevo'] + .urls.apiUrl as string, access_token: this.cryptoService.encrypt(opts.apikey), status: 'valid', created_at: new Date(), diff --git a/packages/api/src/@core/connections/marketingautomation/services/getresponse/getresponse.service.ts b/packages/api/src/@core/connections/marketingautomation/services/getresponse/getresponse.service.ts index 40389566d..9319a1151 100644 --- a/packages/api/src/@core/connections/marketingautomation/services/getresponse/getresponse.service.ts +++ b/packages/api/src/@core/connections/marketingautomation/services/getresponse/getresponse.service.ts @@ -105,9 +105,9 @@ export class GetresponseConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['marketingautomation']['getresponse'].urls - .apiUrl, + account_url: CONNECTORS_METADATA['marketingautomation'][ + 'getresponse' + ].urls.apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -123,9 +123,9 @@ export class GetresponseConnectionService provider_slug: 'getresponse', vertical: 'marketingautomation', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['marketingautomation']['getresponse'].urls - .apiUrl, + account_url: CONNECTORS_METADATA['marketingautomation'][ + 'getresponse' + ].urls.apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/marketingautomation/services/keap/keap.service.ts b/packages/api/src/@core/connections/marketingautomation/services/keap/keap.service.ts new file mode 100644 index 000000000..530b02b37 --- /dev/null +++ b/packages/api/src/@core/connections/marketingautomation/services/keap/keap.service.ts @@ -0,0 +1,189 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { IMarketingAutomationConnectionService } from '../../types'; +import { ServiceRegistry } from '../registry.service'; +import { + OAuth2AuthData, + CONNECTORS_METADATA, + providerToType, +} from '@panora/shared'; +import { AuthStrategy } from '@panora/shared'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + OAuthCallbackParams, + RefreshParams, +} from '@@core/connections/@utils/types'; + +export type KeapOAuthResponse = { + access_token: string; + refresh_token: string; + expires_in: string; +}; + +@Injectable() +export class KeapConnectionService + implements IMarketingAutomationConnectionService +{ + private readonly type: string; + + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private env: EnvironmentService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + private cService: ConnectionsStrategiesService, + private connectionUtils: ConnectionUtils, + ) { + this.logger.setContext(KeapConnectionService.name); + this.registry.registerService('keap', this); + this.type = providerToType( + 'keap', + 'marketingautomation', + AuthStrategy.oauth2, + ); + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: `keap`, + vertical: 'marketingautomation', + }, + }); + + //reconstruct the redirect URI that was passed in the githubend it must be the same + const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`; + const CREDENTIALS = (await this.cService.getCredentials( + projectId, + this.type, + )) as OAuth2AuthData; + + const formData = new URLSearchParams({ + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + redirect_uri: REDIRECT_URI, + code: code, + grant_type: 'authorization_code', + }); + const res = await axios.post( + 'https://api.infusionsoft.com/token', + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: KeapOAuthResponse = res.data; + this.logger.log( + 'OAuth credentials : keap marketingautomation ' + JSON.stringify(data), + ); + + let db_res; + const connection_token = uuidv4(); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'keap', + vertical: 'marketingautomation', + token_type: 'oauth', + account_url: CONNECTORS_METADATA['marketingautomation']['keap'].urls + .apiUrl as string, + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + return db_res; + } catch (error) { + throw error; + } + } + + async handleTokenRefresh(opts: RefreshParams) { + try { + const { connectionId, refreshToken, projectId } = opts; + const formData = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: this.cryptoService.decrypt(refreshToken), + }); + const CREDENTIALS = (await this.cService.getCredentials( + projectId, + this.type, + )) as OAuth2AuthData; + + const res = await axios.post( + 'https://api.infusionsoft.com/token', + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Basic ${Buffer.from( + `${CREDENTIALS.CLIENT_ID}:${CREDENTIALS.CLIENT_SECRET}`, + ).toString('base64')}`, + }, + }, + ); + const data: KeapOAuthResponse = res.data; + await this.prisma.connections.update({ + where: { + id_connection: connectionId, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + }, + }); + this.logger.log('OAuth credentials updated : keap '); + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/@core/connections/marketingautomation/services/podium/podium.service.ts b/packages/api/src/@core/connections/marketingautomation/services/podium/podium.service.ts index 8fc5852da..e979ef9d6 100644 --- a/packages/api/src/@core/connections/marketingautomation/services/podium/podium.service.ts +++ b/packages/api/src/@core/connections/marketingautomation/services/podium/podium.service.ts @@ -101,8 +101,8 @@ export class PodiumConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: - CONNECTORS_METADATA['marketingautomation']['podium'].urls.apiUrl, + account_url: CONNECTORS_METADATA['marketingautomation']['podium'] + .urls.apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + 10 * 60 * 60 * 1000, ), @@ -118,8 +118,8 @@ export class PodiumConnectionService provider_slug: 'podium', vertical: 'marketingautomation', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['marketingautomation']['pdoum'].urls.apiUrl, + account_url: CONNECTORS_METADATA['marketingautomation']['pdoum'] + .urls.apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/ticketing/services/aha/aha.service.ts b/packages/api/src/@core/connections/ticketing/services/aha/aha.service.ts index 2e53065a5..3bd170e4b 100644 --- a/packages/api/src/@core/connections/ticketing/services/aha/aha.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/aha/aha.service.ts @@ -14,14 +14,15 @@ import { EnvironmentService } from '@@core/environment/environment.service'; import { EncryptionService } from '@@core/encryption/encryption.service'; import { ITicketingConnectionService } from '../../types'; import { ServiceRegistry } from '../registry.service'; -import { AuthStrategy, CONNECTORS_METADATA } from '@panora/shared'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, +} from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; import { ConnectionUtils } from '@@core/connections/@utils'; -import { - OAuthCallbackParams, - RefreshParams, -} from '@@core/connections/@utils/types'; +import { OAuthCallbackParams } from '@@core/connections/@utils/types'; export type AhaOAuthResponse = { access_token: string; @@ -77,7 +78,7 @@ export class AhaConnectionService implements ITicketingConnectionService { grant_type: 'authorization_code', }); const res = await axios.post( - `${CREDENTIALS.SUBDOMAIN}/oauth/token`, + `https://${CREDENTIALS.SUBDOMAIN}.aha.io/oauth/token`, formData.toString(), { headers: { @@ -92,10 +93,10 @@ export class AhaConnectionService implements ITicketingConnectionService { let db_res; const connection_token = uuidv4(); - //get the right BASE URL API - const BASE_API_URL = - CREDENTIALS.SUBDOMAIN + - CONNECTORS_METADATA['ticketing']['aha'].urls.apiUrl; + + const BASE_API_URL = ( + CONNECTORS_METADATA['ticketing']['aha'].urls.apiUrl as DynamicApiUrl + )(CREDENTIALS.SUBDOMAIN); if (isNotUnique) { db_res = await this.prisma.connections.update({ @@ -151,8 +152,4 @@ export class AhaConnectionService implements ITicketingConnectionService { ); } } - - async handleTokenRefresh(opts: RefreshParams) { - return; - } } diff --git a/packages/api/src/@core/connections/ticketing/services/asana/asana.service.ts b/packages/api/src/@core/connections/ticketing/services/asana/asana.service.ts index 587fd4dcf..2af7eb79c 100644 --- a/packages/api/src/@core/connections/ticketing/services/asana/asana.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/asana/asana.service.ts @@ -104,7 +104,8 @@ export class AsanaConnectionService implements ITicketingConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['ticketing']['asana'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['asana'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -117,7 +118,8 @@ export class AsanaConnectionService implements ITicketingConnectionService { provider_slug: 'asana', vertical: 'ticketing', token_type: 'oauth', - account_url: CONNECTORS_METADATA['ticketing']['asana'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['asana'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), status: 'valid', created_at: new Date(), diff --git a/packages/api/src/@core/connections/ticketing/services/clickup/clickup.service.ts b/packages/api/src/@core/connections/ticketing/services/clickup/clickup.service.ts index 10fe95b4b..647ff1ad2 100644 --- a/packages/api/src/@core/connections/ticketing/services/clickup/clickup.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/clickup/clickup.service.ts @@ -96,8 +96,8 @@ export class ClickupConnectionService implements ITicketingConnectionService { }, data: { access_token: this.cryptoService.encrypt(data.access_token), - account_url: - CONNECTORS_METADATA['ticketing']['clickup'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['clickup'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -110,8 +110,8 @@ export class ClickupConnectionService implements ITicketingConnectionService { provider_slug: 'clickup', vertical: 'ticketing', token_type: 'oauth', - account_url: - CONNECTORS_METADATA['ticketing']['clickup'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['clickup'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), status: 'valid', created_at: new Date(), diff --git a/packages/api/src/@core/connections/ticketing/services/dixa/dixa.service.ts b/packages/api/src/@core/connections/ticketing/services/dixa/dixa.service.ts index ee45eba1e..b6fa32116 100644 --- a/packages/api/src/@core/connections/ticketing/services/dixa/dixa.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/dixa/dixa.service.ts @@ -43,7 +43,8 @@ export class DixaConnectionService implements ITicketingConnectionService { }, data: { access_token: this.cryptoService.encrypt(opts.apikey), - account_url: CONNECTORS_METADATA['ticketing']['dixa'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['dixa'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -56,7 +57,8 @@ export class DixaConnectionService implements ITicketingConnectionService { provider_slug: 'dixa', vertical: 'ticketing', token_type: 'api_key', - account_url: CONNECTORS_METADATA['ticketing']['dixa'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['dixa'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(opts.apikey), status: 'valid', created_at: new Date(), diff --git a/packages/api/src/@core/connections/ticketing/services/front/front.service.ts b/packages/api/src/@core/connections/ticketing/services/front/front.service.ts index a4257edb9..9c32e4129 100644 --- a/packages/api/src/@core/connections/ticketing/services/front/front.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/front/front.service.ts @@ -103,7 +103,8 @@ export class FrontConnectionService implements ITicketingConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: CONNECTORS_METADATA['ticketing']['front'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['front'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_at) * 1000, ), @@ -119,7 +120,8 @@ export class FrontConnectionService implements ITicketingConnectionService { provider_slug: 'front', vertical: 'ticketing', token_type: 'oauth', - account_url: CONNECTORS_METADATA['ticketing']['front'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['front'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/ticketing/services/github/github.service.ts b/packages/api/src/@core/connections/ticketing/services/github/github.service.ts index bb6b3ecd3..29594b664 100644 --- a/packages/api/src/@core/connections/ticketing/services/github/github.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/github/github.service.ts @@ -97,7 +97,8 @@ export class GithubConnectionService implements ITicketingConnectionService { access_token: this.cryptoService.encrypt( data.match(/access_token=([^&]*)/)[1], ), - account_url: CONNECTORS_METADATA['ticketing']['github'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['github'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -113,7 +114,8 @@ export class GithubConnectionService implements ITicketingConnectionService { access_token: this.cryptoService.encrypt( data.match(/access_token=([^&]*)/)[1], ), - account_url: CONNECTORS_METADATA['ticketing']['github'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['github'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), projects: { diff --git a/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts b/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts index 74041edb7..6a7d84418 100644 --- a/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/gitlab/gitlab.service.ts @@ -115,7 +115,8 @@ export class GitlabConnectionService implements ITicketingConnectionService { connection_token: connection_token, provider_slug: 'gitlab', vertical: 'ticketing', - account_url: CONNECTORS_METADATA['ticketing']['gitlab'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['gitlab'].urls + .apiUrl as string, token_type: 'oauth', access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), diff --git a/packages/api/src/@core/connections/ticketing/services/gorgias/gorgias.service.ts b/packages/api/src/@core/connections/ticketing/services/gorgias/gorgias.service.ts index 6622340ba..6ddb52d79 100644 --- a/packages/api/src/@core/connections/ticketing/services/gorgias/gorgias.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/gorgias/gorgias.service.ts @@ -18,6 +18,7 @@ import { OAuth2AuthData, CONNECTORS_METADATA, providerToType, + DynamicApiUrl, } from '@panora/shared'; import { AuthStrategy } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; @@ -81,7 +82,7 @@ export class GorgiasConnectionService implements ITicketingConnectionService { grant_type: 'authorization_code', }); const res = await axios.post( - `${CREDENTIALS.SUBDOMAIN!}/oauth/token`, + `https://${CREDENTIALS.SUBDOMAIN!}.gorgias.com/oauth/token`, formData.toString(), { headers: { @@ -96,9 +97,10 @@ export class GorgiasConnectionService implements ITicketingConnectionService { let db_res; const connection_token = uuidv4(); - const BASE_API_URL = - CREDENTIALS.SUBDOMAIN + - CONNECTORS_METADATA['ticketing']['gorgias'].urls.apiUrl; + + const BASE_API_URL = ( + CONNECTORS_METADATA['ticketing']['gorgias'].urls.apiUrl as DynamicApiUrl + )(CREDENTIALS.SUBDOMAIN); if (isNotUnique) { db_res = await this.prisma.connections.update({ @@ -176,7 +178,7 @@ export class GorgiasConnectionService implements ITicketingConnectionService { )) as OAuth2AuthData; const res = await axios.post( - `${CREDENTIALS.SUBDOMAIN!}/oauth/token`, + `https://${CREDENTIALS.SUBDOMAIN!}.gorgias.com/oauth/token`, formData.toString(), { headers: { diff --git a/packages/api/src/@core/connections/ticketing/services/helpscout/helpscout.service.ts b/packages/api/src/@core/connections/ticketing/services/helpscout/helpscout.service.ts index a914dc9a5..5af93dd53 100644 --- a/packages/api/src/@core/connections/ticketing/services/helpscout/helpscout.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/helpscout/helpscout.service.ts @@ -43,8 +43,8 @@ export class HelpscoutConnectionService implements ITicketingConnectionService { }, data: { access_token: this.cryptoService.encrypt(opts.apikey), - account_url: - CONNECTORS_METADATA['ticketing']['helpscout'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['helpscout'].urls + .apiUrl as string, status: 'valid', created_at: new Date(), }, @@ -57,8 +57,8 @@ export class HelpscoutConnectionService implements ITicketingConnectionService { provider_slug: 'helpscout', vertical: 'ticketing', token_type: 'api_key', - account_url: - CONNECTORS_METADATA['ticketing']['helpscout'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['helpscout'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(opts.apikey), status: 'valid', created_at: new Date(), diff --git a/packages/api/src/@core/connections/ticketing/services/linear/linear.service.ts b/packages/api/src/@core/connections/ticketing/services/linear/linear.service.ts index f9133c1d1..50496ab7b 100644 --- a/packages/api/src/@core/connections/ticketing/services/linear/linear.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/linear/linear.service.ts @@ -98,7 +98,8 @@ export class LinearConnectionService implements ITicketingConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: '', - account_url: CONNECTORS_METADATA['ticketing']['linear'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['linear'].urls + .apiUrl as string, expiration_timestamp: new Date( new Date().getTime() + Number(data.expires_in) * 1000, ), @@ -114,7 +115,8 @@ export class LinearConnectionService implements ITicketingConnectionService { provider_slug: 'linear', vertical: 'ticketing', token_type: 'oauth', - account_url: CONNECTORS_METADATA['ticketing']['linear'].urls.apiUrl, + account_url: CONNECTORS_METADATA['ticketing']['linear'].urls + .apiUrl as string, access_token: this.cryptoService.encrypt(data.access_token), refresh_token: '', expiration_timestamp: new Date( diff --git a/packages/api/src/@core/connections/ticketing/services/wrike/wrike.service.ts b/packages/api/src/@core/connections/ticketing/services/wrike/wrike.service.ts new file mode 100644 index 000000000..58125621d --- /dev/null +++ b/packages/api/src/@core/connections/ticketing/services/wrike/wrike.service.ts @@ -0,0 +1,220 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { PrismaService } from '@@core/prisma/prisma.service'; +import { + Action, + ActionType, + ConnectionsError, + format3rdPartyError, + throwTypedError, +} from '@@core/utils/errors'; +import { LoggerService } from '@@core/logger/logger.service'; +import { v4 as uuidv4 } from 'uuid'; +import { EnvironmentService } from '@@core/environment/environment.service'; +import { EncryptionService } from '@@core/encryption/encryption.service'; +import { ITicketingConnectionService } from '../../types'; +import { ServiceRegistry } from '../registry.service'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, +} from '@panora/shared'; +import { OAuth2AuthData, providerToType } from '@panora/shared'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + OAuthCallbackParams, + RefreshParams, +} from '@@core/connections/@utils/types'; + +export type WrikeOAuthResponse = { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: string; + scope: string; + host: string; +}; + +@Injectable() +export class WrikeConnectionService implements ITicketingConnectionService { + private readonly type: string; + + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private env: EnvironmentService, + private cryptoService: EncryptionService, + private registry: ServiceRegistry, + private cService: ConnectionsStrategiesService, + private connectionUtils: ConnectionUtils, + ) { + this.logger.setContext(WrikeConnectionService.name); + this.registry.registerService('wrike', this); + this.type = providerToType('wrike', 'ticketing', AuthStrategy.oauth2); + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'wrike', + vertical: 'ticketing', + }, + }); + + const REDIRECT_URI = `${ + this.env.getDistributionMode() == 'selfhost' + ? this.env.getWebhookIngress() + : this.env.getPanoraBaseUrl() + }/connections/oauth/callback`; + + const CREDENTIALS = (await this.cService.getCredentials( + projectId, + this.type, + )) as OAuth2AuthData; + + const formData = new URLSearchParams({ + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + redirect_uri: REDIRECT_URI, + code: code, + grant_type: 'authorization_code', + }); + const res = await axios.post( + `https://login.wrike.com/oauth2/token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: WrikeOAuthResponse = res.data; + this.logger.log( + 'OAuth credentials : wrike ticketing ' + JSON.stringify(data), + ); + + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = ( + CONNECTORS_METADATA['ticketing']['wrike'].urls.apiUrl as DynamicApiUrl + )(data.host); + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + account_url: BASE_API_URL, + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'wrike', + vertical: 'ticketing', + token_type: 'oauth', + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + account_url: BASE_API_URL, + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + return db_res; + } catch (error) { + throwTypedError( + new ConnectionsError({ + name: 'HANDLE_OAUTH_CALLBACK_TICKETING', + message: `WrikeConnectionService.handleCallback() call failed ---> ${format3rdPartyError( + 'wrike', + Action.oauthCallback, + ActionType.POST, + )}`, + cause: error, + }), + this.logger, + ); + } + } + + async handleTokenRefresh(opts: RefreshParams) { + try { + const { connectionId, refreshToken, projectId } = opts; + const CREDENTIALS = (await this.cService.getCredentials( + projectId, + this.type, + )) as OAuth2AuthData; + const formData = new URLSearchParams({ + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + grant_type: 'refresh_token', + refresh_token: this.cryptoService.decrypt(refreshToken), + }); + + const res = await axios.post( + `https://login.wrike.com/oauth2/token`, + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: WrikeOAuthResponse = res.data; + await this.prisma.connections.update({ + where: { + id_connection: connectionId, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + refresh_token: this.cryptoService.encrypt(data.refresh_token), + expiration_timestamp: new Date( + new Date().getTime() + Number(data.expires_in) * 1000, + ), + }, + }); + this.logger.log('OAuth credentials updated : wrike '); + } catch (error) { + throwTypedError( + new ConnectionsError({ + name: 'HANDLE_OAUTH_REFRESH_TICKETING', + message: `WrikeConnectionService.handleTokenRefresh() call failed ---> ${format3rdPartyError( + 'wrike', + Action.oauthRefresh, + ActionType.POST, + )}`, + cause: error, + }), + this.logger, + ); + } + } +} diff --git a/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts b/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts index 43fcb1038..f083bad00 100644 --- a/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/zendesk/zendesk.service.ts @@ -14,14 +14,15 @@ import { EnvironmentService } from '@@core/environment/environment.service'; import { EncryptionService } from '@@core/encryption/encryption.service'; import { ITicketingConnectionService } from '../../types'; import { ServiceRegistry } from '../registry.service'; -import { AuthStrategy, CONNECTORS_METADATA } from '@panora/shared'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, +} from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; import { ConnectionUtils } from '@@core/connections/@utils'; -import { - OAuthCallbackParams, - RefreshParams, -} from '@@core/connections/@utils/types'; +import { OAuthCallbackParams } from '@@core/connections/@utils/types'; import { ManagedWebhooksService } from '@@core/managed-webhooks/managed-webhooks.service'; export interface ZendeskOAuthResponse { @@ -78,7 +79,7 @@ export class ZendeskConnectionService implements ITicketingConnectionService { this.logger.log('Data Form is ' + JSON.stringify(formData)); const res = await axios.post( - `${CREDENTIALS.SUBDOMAIN}/oauth/tokens`, + `https://${CREDENTIALS.SUBDOMAIN}.zendesk.com/oauth/tokens`, formData.toString(), { headers: { @@ -93,9 +94,9 @@ export class ZendeskConnectionService implements ITicketingConnectionService { let db_res; const connection_token = uuidv4(); - const BASE_API_URL = - CREDENTIALS.SUBDOMAIN + - CONNECTORS_METADATA['ticketing']['zendesk'].urls.apiUrl; + const BASE_API_URL = ( + CONNECTORS_METADATA['ticketing']['zendesk'].urls.apiUrl as DynamicApiUrl + )(CREDENTIALS.SUBDOMAIN); if (isNotUnique) { db_res = await this.prisma.connections.update({ @@ -192,10 +193,4 @@ export class ZendeskConnectionService implements ITicketingConnectionService { ); } } - - //todo: revoke ? - //ZENDESK TICKETING OAUTH TOKENS DONT EXPIRE BUT THEY MAY BE REVOKED - async handleTokenRefresh(opts: RefreshParams): Promise { - throw new Error('Method not implemented.'); - } } diff --git a/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts b/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts index edcfb2657..8d82ea437 100644 --- a/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts +++ b/packages/api/src/@core/connections/ticketing/ticketing.connection.module.ts @@ -22,6 +22,7 @@ import { ConnectionUtils } from '../@utils'; import { DixaConnectionService } from './services/dixa/dixa.service'; import { HelpscoutConnectionService } from './services/helpscout/helpscout.service'; import { AsanaConnectionService } from './services/asana/asana.service'; +import { WrikeConnectionService } from './services/wrike/wrike.service'; @Module({ imports: [WebhookModule, ManagedWebhooksModule], @@ -47,6 +48,7 @@ import { AsanaConnectionService } from './services/asana/asana.service'; DixaConnectionService, HelpscoutConnectionService, AsanaConnectionService, + WrikeConnectionService, ], exports: [TicketingConnectionsService], }) diff --git a/packages/api/src/@core/field-mapping/field-mapping.service.ts b/packages/api/src/@core/field-mapping/field-mapping.service.ts index 6fd35e53e..7871d7798 100644 --- a/packages/api/src/@core/field-mapping/field-mapping.service.ts +++ b/packages/api/src/@core/field-mapping/field-mapping.service.ts @@ -232,7 +232,7 @@ export class FieldMappingService { }, }); const provider = CONNECTORS_METADATA[vertical][providerId.toLowerCase()]; - if (!provider.urls.apiUrl || !provider.urls.customPropertiesUrl) + if (!provider.urls.customPropertiesUrl) throw new Error('proivder urls are invalid'); const resp = await axios.get(provider.urls.customPropertiesUrl, { diff --git a/packages/api/src/@core/passthrough/passthrough.service.ts b/packages/api/src/@core/passthrough/passthrough.service.ts index 1f33fc793..fb1828234 100644 --- a/packages/api/src/@core/passthrough/passthrough.service.ts +++ b/packages/api/src/@core/passthrough/passthrough.service.ts @@ -25,14 +25,16 @@ export class PassthroughService { linkedUserId: string, vertical: string, ): Promise { - try { + // TODO + return; + /*try { const { method, path, data, headers } = requestParams; const job_resp_create = await this.prisma.events.create({ data: { id_event: uuidv4(), status: 'initialized', // Use whatever status is appropriate - type: 'pull', + type: 'pull', method: method, url: '/pasthrough', provider: integrationId, @@ -101,6 +103,6 @@ export class PassthroughService { }), this.logger, ); - } + }*/ } } diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index 9d827af6c..47cff4720 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -271,77 +271,6 @@ ] } }, - "/connections/gorgias/oauth/install": { - "get": { - "operationId": "ConnectionsController_handleGorgiasAuthUrl", - "parameters": [ - { - "name": "account", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "response_type", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "nonce", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "scope", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "client_id", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "redirect_uri", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "state", - "required": true, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "" - } - }, - "tags": [ - "connections" - ] - } - }, "/connections/apikey/callback": { "post": { "operationId": "handleApiKeyCallback", diff --git a/packages/shared/src/authUrl.ts b/packages/shared/src/authUrl.ts index 897c16a33..fc8f237e7 100644 --- a/packages/shared/src/authUrl.ts +++ b/packages/shared/src/authUrl.ts @@ -1,6 +1,6 @@ import { CONNECTORS_METADATA } from './connectors/metadata'; -import { OAuth2AuthData, providerToType } from './envConfig'; -import { AuthStrategy, ProviderConfig } from './types'; +import { needsEndUserSubdomain, needsSubdomain, OAuth2AuthData, providerToType } from './envConfig'; +import { AuthStrategy, DynamicAuthorization, ProviderConfig, StringAuthorization } from './types'; import { randomString } from './utils'; interface AuthParams { @@ -85,16 +85,31 @@ const handleOAuth2Url = async (input: HandleOAuth2Url) => { const clientId = data.CLIENT_ID; if (!clientId) throw new ReferenceError(`No client id for type ${type}`) - const scopes = data.SCOPE + const scopes = data.SCOPE; const { urls: urls } = config; const { authBaseUrl: baseUrl } = urls; if (!baseUrl) throw new ReferenceError(`No authBaseUrl found for type ${type}`) + let BASE_URL: string; // construct the baseAuthUrl based on the fact that client may use custom subdomain - const BASE_URL: string = providerName === 'gorgias' ? `${apiUrl}${baseUrl}` : - data.SUBDOMAIN ? data.SUBDOMAIN + baseUrl : baseUrl; + if(needsSubdomain(providerName, vertical)){ + if(typeof baseUrl == "string"){ + BASE_URL = baseUrl; + }else{ + BASE_URL = (baseUrl as DynamicAuthorization)(data.SUBDOMAIN); + } + }else if (needsEndUserSubdomain(providerName, vertical)){ + if(typeof baseUrl == "string"){ + BASE_URL = baseUrl; + }else{ + BASE_URL = (baseUrl as DynamicAuthorization)("END_USER_SUBDOMAIN"); // TODO: get the END-USER domain from the hook (data coming from webapp client) + // TODO: add the end user subdomain as query param on the redirect uri ? + } + }else{ + BASE_URL = baseUrl as StringAuthorization; + } // console.log('BASE URL IS '+ BASE_URL) if (!baseUrl || !BASE_URL) { @@ -102,55 +117,54 @@ const handleOAuth2Url = async (input: HandleOAuth2Url) => { } // Default URL structure - let params = `client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodedRedirectUrl}&state=${state}`; - - // Adding scope for providers that require it, except for 'pipedrive' - const ignoreScopes = ['close'] - if (scopes && !ignoreScopes.includes(providerName)) { - params += `&scope=${encodeURIComponent(scopes)}`; + let params = `response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodedRedirectUrl}&state=${state}`; + + if (scopes) { + if(providerName == "slack"){ + params += `&scope=&user_scope=${encodeURIComponent(scopes)}`; + }else{ + params += `&scope=${encodeURIComponent(scopes)}`; + } } // Special cases for certain providers switch (providerName) { - case 'xero': - params += '&response_type=code&scope=offline_access openid profile email accounting.transactions' - break; case 'zoho': - params += '&response_type=code&access_type=offline'; + params += '&access_type=offline'; break; case 'jira': - params = `audience=api.atlassian.com&${params}&prompt=consent&response_type=code`; + params = `audience=api.atlassian.com&${params}&prompt=consent`; break; case 'jira_service_mgmt': - params = `audience=api.atlassian.com&${params}&prompt=consen&response_type=codet`; + params = `audience=api.atlassian.com&${params}&prompt=consent`; break; case 'gitlab': - params += '&response_type=code&code_challenge=&code_challenge_method='; + params += '&code_challenge=&code_challenge_method='; break; case 'gorgias': - params = `&response_type=code&nonce=${randomString()}`; + params = `&nonce=${randomString()}`; break; case 'googledrive': - params = `${params}&response_type=code&access_type=offline`; + params = `${params}&access_type=offline`; break; case 'dropbox': - params = `${params}&response_type=code&token_access_type=offline` + params = `${params}&token_access_type=offline` break; case 'basecamp': - params += `&type=web_server&response_type=code` + params += `&type=web_server` break; case 'lever': - params += `&audience=https://api.lever.co/v1/&response_type=code` + params += `&audience=https://api.lever.co/v1/` break; case 'notion': - params += `&owner=user&response_type=code` + params += `&owner=user` break; default: - params += '&response_type=code'; + break; } const finalAuthUrl = `${BASE_URL}?${params}`; - // console.log('Final Authentication : ', finalAuthUrl); + console.log('Final Authentication : ', finalAuthUrl); return finalAuthUrl; } diff --git a/packages/shared/src/categories.ts b/packages/shared/src/categories.ts index 3dacfa892..af4bc6e7b 100644 --- a/packages/shared/src/categories.ts +++ b/packages/shared/src/categories.ts @@ -6,6 +6,7 @@ export enum ConnectorCategory { Ticketing = 'ticketing', MarketingAutomation = 'marketingautomation', FileStorage = 'filestorage', + Management = 'management' } export const categoriesVerticals: ConnectorCategory[] = Object.values(ConnectorCategory); diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index 54a725324..ade45f9db 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -1,4 +1,4 @@ -// If authBaseUrl or apiUrl both start with / it means a subdomain is likely needed +// If authBaseUrl or apiUrl both start with / it means a company_subdomain is likely needed // If authBaseUrl is blank then it must be manually built in the client given the provider (meaning its not deterministic) import { AuthStrategy, ProvidersConfig } from '../types'; @@ -33,8 +33,8 @@ export const CONNECTORS_METADATA: ProvidersConfig = { scopes: 'ZohoCRM.modules.ALL', urls: { docsUrl: 'https://www.zoho.com/crm/developer/docs/api/v5/', - authBaseUrl: '/oauth/v2/auth', - apiUrl: '/crm/v3', + authBaseUrl: ' https://accounts.zoho.com/oauth/v2/auth', + apiUrl: (domain) => `${domain}/crm/v3`, // it is contained in the connection service customPropertiesUrl: '/settings/fields?module=Contact', }, logoPath: 'https://assets-global.website-files.com/64f68d43d25e5962af5f82dd/64f68d43d25e5962af5f9812_64ad8bbe47c78358489b29fc_645e3ccf636a8d659f320e25_Group%25252012.png', @@ -77,15 +77,17 @@ export const CONNECTORS_METADATA: ProvidersConfig = { authStrategy: AuthStrategy.oauth2, }, 'accelo': { - scopes: '', urls: { docsUrl: 'https://api.accelo.com/docs/#introduction', - authBaseUrl: '/oauth2/v0/authorize', - apiUrl: '/api/v0', + authBaseUrl: (domain) => `https://${domain}.api.accelo.com/oauth2/v0/authorize`, + apiUrl: (domain) => `https://${domain}.api.accelo.com/api/v0`, }, logoPath: 'https://play-lh.googleusercontent.com/j63K2u8ZXukgPs8QPgyXfyoxuNBl_ST7gLx5DEFeczCTtM9e5JNpDjjBy32qLxFS7p0', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, + options: { + company_subdomain: true + }, authStrategy: AuthStrategy.oauth2 }, 'active_campaign': { @@ -158,7 +160,6 @@ export const CONNECTORS_METADATA: ProvidersConfig = { authStrategy: AuthStrategy.api_key, }, 'keap': { - scopes: '', urls: { docsUrl: 'https://developer.infusionsoft.com/docs/restv2/', authBaseUrl: 'https://accounts.infusionsoft.com/app/oauth/authorize', @@ -252,7 +253,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { urls: { docsUrl: 'https://apidocs.teamwork.com/guides/teamwork/getting-started-with-the-teamwork-com-api', authBaseUrl: 'https://www.teamwork.com/launchpad/login', - apiUrl: '', // on purpose blank => everything is contained inside the accountUrl(subdomain) + apiUrl: '', // on purpose blank => everything is contained inside the apiEndPoint that teamwork returns }, logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQr6gYDMNagMEicBb4dhKz4BC1fQs72In45QF7Ls6-moA&s', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', @@ -302,12 +303,15 @@ export const CONNECTORS_METADATA: ProvidersConfig = { scopes: 'read write', urls: { docsUrl: 'https://developer.zendesk.com/api-reference/sales-crm/introduction/', - authBaseUrl: '/oauth/authorizations/new', - apiUrl: '/api/v2', - }, + apiUrl: (myDomain) => `https://${myDomain}.zendesk.com/api/v2`, + authBaseUrl: (myDomain) => `https://${myDomain}.zendesk.com/oauth/authorizations/new` + }, logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRNKVceZGVM7PbARp_2bjdOICUxlpS5B29UYlurvh6Z2Q&s', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', authStrategy: AuthStrategy.oauth2, + options: { + company_subdomain: true + }, realTimeWebhookMetadata: { method: 'API', events: [ @@ -321,12 +325,17 @@ export const CONNECTORS_METADATA: ProvidersConfig = { ] }, }, + // TODO 'gorgias': { scopes: 'write:all openid email profile offline', urls: { docsUrl: 'https://developers.gorgias.com/reference/introduction', - apiUrl: '/api', - authBaseUrl: `/connections/gorgias/oauth/install`, + apiUrl: (domain) => `https://${domain}.gorgias.com/api`, + authBaseUrl: (domain) => `https://${domain}.com/connections/gorgias/oauth/install`, + }, + active: false, + options: { + company_subdomain: true, }, logoPath: 'https://x5h8w2v3.rocketcdn.me/wp-content/uploads/2020/09/FS-AFFI-00660Gorgias.png', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', @@ -393,14 +402,15 @@ export const CONNECTORS_METADATA: ProvidersConfig = { 'aha': { urls: { docsUrl: 'https://www.aha.io/api', - apiUrl: '/api/v1', - authBaseUrl: '/oauth/authorize', + apiUrl: (domain) => `https://${domain}.aha.io/api/v1`, + authBaseUrl: (domain) => `https://${domain}.aha.io/oauth/authorize`, }, logoPath: 'https://www.aha.io/aha-logo-2x.png', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', active: false, authStrategy: AuthStrategy.oauth2, options: { + company_subdomain: true, local_redirect_uri_in_https: true } }, @@ -635,7 +645,6 @@ export const CONNECTORS_METADATA: ProvidersConfig = { description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', active: false, }, - // todo 'teamwork': { scopes: '', urls: { @@ -646,7 +655,6 @@ export const CONNECTORS_METADATA: ProvidersConfig = { description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', active: false, }, - // todo 'trello': { scopes: '', urls: { @@ -658,15 +666,18 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, }, 'wrike': { - scopes: '', urls: { docsUrl: 'https://developers.wrike.com/overview/', - apiUrl: '/api/v4', + apiUrl: (domain) => `https://${domain}/api/v4`, authBaseUrl: 'https://login.wrike.com/oauth2/authorize/v4', }, - logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRqz0aID6B-InxK_03P7tCtqpXNXdawBcro67CyEE0I5g&s', + logoPath: 'https://play-lh.googleusercontent.com/uh-GbLWEKoIefta2iNX0L0zUWA66YTjfJ4cBarNZWbc7mEzbKUWbWg8NjjrojgkFH5ni', description: 'Sync & Create accounts, tickets, comments, attachments, contacts, tags, teams and users', active: false, + options: { + company_subdomain: true, + local_redirect_uri_in_https: true + }, authStrategy: AuthStrategy.oauth2 }, 'zoho_bugtracker': { @@ -700,7 +711,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://c.clc2l.com/t/P/e/Pennylane-U9Wdby.png', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', - active: true, + active: false, authStrategy: AuthStrategy.oauth2, options: { local_redirect_uri_in_https: true @@ -840,7 +851,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { authStrategy: AuthStrategy.oauth2 }, 'xero': { - // scopes: 'openid profile email accounting.transactions accounting.reports.read accounting.contacts accounting.attachments accounting.budgets.read', + scopes: 'offline_access openid profile email accounting.transactions', urls: { docsUrl: 'https://developer.xero.com/documentation/getting-started-guide/', apiUrl: 'https://api.xero.com/api.xro/2.0', @@ -864,7 +875,6 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, authStrategy: AuthStrategy.api_key }, - // todo 'customerio': { scopes: '', urls: { @@ -886,7 +896,6 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, authStrategy: AuthStrategy.oauth2 }, - // todo 'hubspot_marketing_hub': { scopes: '', urls: { @@ -898,13 +907,12 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, }, 'keap': { - scopes: '', urls: { authBaseUrl: 'https://accounts.infusionsoft.com/app/oauth/authorize', docsUrl: 'https://developer.infusionsoft.com/docs/rest/', apiUrl: 'https://api.infusionsoft.com/crm/rest/v1/account/profile' }, - logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', + logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRPYsWSMe9KVWgCIQ8fw-vBOnfTlZaSS6p_43ZhEIx51A&s', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, authStrategy: AuthStrategy.oauth2 @@ -978,7 +986,6 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, }, 'ats': { - // todo 'applicantstack': { scopes: '', urls: { @@ -990,24 +997,26 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, }, 'ashby': { - scopes: '', urls: { docsUrl: 'https://developers.ashbyhq.com', apiUrl: 'https://api.ashbyhq.com' }, - logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', + logoPath: 'https://images.ctfassets.net/p03bi75xct27/2tVvkghDdMJxzkMca2QLnr/31b520c5e07db0103948af171fb54e99/ashby_logo_square.jpeg?q=80&fm=webp&w=2048', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, authStrategy: AuthStrategy.api_key }, - // todo 'bamboohr': { - scopes: '', + scopes: 'openid+email', urls: { docsUrl: 'https://documentation.bamboohr.com/docs/getting-started', - apiUrl: '' + apiUrl: '', + authBaseUrl: (END_USER_DOMAIN) => `https://${END_USER_DOMAIN}.bamboohr.com/authorize.php?request=authorize` }, - logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', + options: { + end_user_domain: true + }, + logoPath: 'https://play-lh.googleusercontent.com/c4BW9wr_QAiIeVBYHhP7rs06w99xJzxgLvmL5I1mkucC3_ATMyL1t7Doz0_LQ0X-qS0', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, authStrategy: AuthStrategy.api_key @@ -1190,7 +1199,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { docsUrl: '', apiUrl: '' }, - logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', + logoPath: 'https://advancedcommunities.com/wp-content/uploads/2023/03/group-1928.png', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, authStrategy: AuthStrategy.oauth2 @@ -1263,7 +1272,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { urls: { docsUrl: 'https://hire.lever.co/developer/documentation#introduction', apiUrl: 'https://api.lever.co/v1', - authBaseUrl: 'https://auth.lever.co/authorize' + authBaseUrl: 'https://auth.lever.co/authorize' // or https://sandbox-lever.auth0.com/authorize }, logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQbR9XSB1lbZnYlLWyqMe5Px80ghtEOUqHeqw&s', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', @@ -1451,14 +1460,14 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, }, 'workday': { - scopes: '', urls: { - docsUrl: '', - apiUrl: '' + docsUrl: 'https://apidocs.workdayspend.com/services/legacy/v3.html#tag/support', + apiUrl: "https://api.us.workdayspend.com/services" // todo other locations }, - logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', + logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSZTX2h9yFQ0u4ziDqvfQ224wW4N1s5JvJ5nA&s', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, + authStrategy: AuthStrategy.api_key }, 'zoho_recruit': { scopes: '', @@ -1523,12 +1532,16 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, }, 'bamboohr': { - scopes: '', + scopes: 'openid+email', urls: { - docsUrl: '', - apiUrl: '' + docsUrl: 'https://documentation.bamboohr.com/docs/getting-started', + apiUrl: '', + authBaseUrl: (END_USER_DOMAIN) => `https://${END_USER_DOMAIN}.bamboohr.com/authorize.php` }, - logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', + options: { + end_user_domain: true + }, + logoPath: 'https://play-lh.googleusercontent.com/c4BW9wr_QAiIeVBYHhP7rs06w99xJzxgLvmL5I1mkucC3_ATMyL1t7Doz0_LQ0X-qS0', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, }, @@ -1611,17 +1624,17 @@ export const CONNECTORS_METADATA: ProvidersConfig = { } }, 'rippling': { - scopes: '', urls: { docsUrl: 'https://developer.rippling.com/docs/rippling-api/9rw6guf819r5f-introduction-for-customers', apiUrl: 'https://api.rippling.com/platform/api', - authBaseUrl: 'https://app.rippling.com/apps/PLATFORM/{APPNAME}/authorize' + authBaseUrl: (APPNAME) => `https://app.rippling.com/apps/PLATFORM/${APPNAME}/authorize` }, logoPath: 'https://avatars.githubusercontent.com/u/19614805?s=280&v=4', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, authStrategy: AuthStrategy.oauth2, options: { + company_subdomain: true, local_redirect_uri_in_https: true } }, @@ -1674,7 +1687,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://cdn.runalloy.com/landing/uploads-new/Gusto_Logo_67ca008403.png', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', - active: true, + active: false, authStrategy: AuthStrategy.oauth2 }, 'hibob': { @@ -1852,14 +1865,18 @@ export const CONNECTORS_METADATA: ProvidersConfig = { active: false, }, 'namely': { - scopes: '', urls: { - docsUrl: '', - apiUrl: '' + docsUrl: 'https://developers.namely.com/docs/namely-api/ZG9jOjE1NTkwMDU5-authentication', + apiUrl: 'https://stoplight.io/mocks/namely/namely-api/182542', // TODO + authBaseUrl: (myDomain) => `https://${myDomain}.namely.com/api/v1/oauth2/authorize` }, - logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', + logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRZzUUYuH2sjtkUfh6BpOHoREyCe_ZV7DWIuQ&s', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, + options: { + company_subdomain: true + }, + authStrategy: AuthStrategy.oauth2 }, 'nmbrs': { scopes: '', @@ -2150,7 +2167,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { authStrategy: AuthStrategy.oauth2, }, 'onedrive': { - scopes: 'wl.basic onedrive.readwrite wl.offline_access', + scopes: 'Files.Read.All offline_access openid User.Read', urls: { docsUrl: 'https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0', apiUrl: 'https://graph.microsoft.com/v1.0', @@ -2173,11 +2190,14 @@ export const CONNECTORS_METADATA: ProvidersConfig = { authStrategy: AuthStrategy.oauth2, }, 'sharepoint': { - scopes: '', + scopes: 'Files.Read.All offline_access openid User.Read', urls: { docsUrl: 'https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0', apiUrl: 'https://graph.microsoft.com/v1.0', - authBaseUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize' + authBaseUrl: (END_USER_TENANT_NAME) => `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?resource=https://${END_USER_TENANT_NAME}.sharepoint.com` + }, + options: { + end_user_domain: true }, logoPath: 'https://pnghq.com/wp-content/uploads/pnghq.com-microsoft-sharepoint-logo-9.png', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', @@ -2210,7 +2230,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { authStrategy: AuthStrategy.oauth2, }, 'slack': { - scopes: '&user_scope=channels:history', + scopes: 'channels:history', urls: { docsUrl: 'https://api.slack.com/apis', apiUrl: 'https://slack.com/api', @@ -2219,6 +2239,9 @@ export const CONNECTORS_METADATA: ProvidersConfig = { logoPath: 'https://assets-global.website-files.com/621c8d7ad9e04933c4e51ffb/65eba5ffa14998827c92cc01_slack-octothorpe.png', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, + options: { + local_redirect_uri_in_https: true + }, authStrategy: AuthStrategy.oauth2, }, } diff --git a/packages/shared/src/envConfig.ts b/packages/shared/src/envConfig.ts index 3d58a5c1b..f1b92b684 100644 --- a/packages/shared/src/envConfig.ts +++ b/packages/shared/src/envConfig.ts @@ -91,12 +91,29 @@ export function needsSubdomain(provider: string, vertical: string): boolean { // Extract the provider's config const providerConfig = CONNECTORS_METADATA[vertical][provider]; + if(providerConfig.options && providerConfig.options.company_subdomain){ + return providerConfig.options.company_subdomain; + } + return false; +} - const authBaseUrlStartsWithSlash = providerConfig.urls.authBaseUrl!.substring(0,1) === '/'; - const apiUrlStartsWithSlash = providerConfig.urls.apiUrl!.substring(0,1) === '/'; - const apiUrlIsBlank = providerConfig.urls.apiUrl! === ''; +export function needsEndUserSubdomain(provider: string, vertical: string): boolean { + // Check if the vertical exists in the config + if (!CONNECTORS_METADATA[vertical]) { + console.error(`Vertical ${vertical} not found in CONNECTORS_METADATA.`); + return false; + } - // console.log("subdomain needed "+ authBaseUrlStartsWithSlash) + // Check if the provider exists under the specified vertical + if (!CONNECTORS_METADATA[vertical][provider]) { + console.error(`Provider ${provider} not found under vertical ${vertical}.`); + return false; + } - return authBaseUrlStartsWithSlash || apiUrlStartsWithSlash || apiUrlIsBlank; + // Extract the provider's config + const providerConfig = CONNECTORS_METADATA[vertical][provider]; + if(providerConfig.options && providerConfig.options.end_user_domain){ + return providerConfig.options.end_user_domain; + } + return false; } diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 7361e0e72..8653f0d39 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -8,6 +8,11 @@ export enum SoftwareMode { cloud = 'CLOUD', } +export type StringAuthorization = string; +export type DynamicAuthorization = ((...args: string[]) => string); +export type StaticApiUrl = string; +export type DynamicApiUrl = ((...args: string[]) => string); + export type ProviderConfig = { scopes?: string; logoPath: string; @@ -17,11 +22,14 @@ export type ProviderConfig = { authStrategy?: AuthStrategy; urls: { docsUrl: string; - apiUrl: string; - authBaseUrl?: string; // url used to authorize an application on behalf of the user (only when authStrategy is oauth2) + apiUrl: StaticApiUrl | DynamicApiUrl; + authBaseUrl?: StringAuthorization | DynamicAuthorization; // url used to authorize an application on behalf of the user (only when authStrategy is oauth2) customPropertiesUrl?: string; }; options?: { + // the Oauth flow is tricky sometimes, either end_user_domain or company_subdomain can be asked + end_user_domain?: boolean; // subdomain of the end-user connecting his account for the integration + company_subdomain?: boolean; // subdomain of the company that embeds the integration for its end-users local_redirect_uri_in_https?: boolean; // true if an https url is needed when creating oauth2 app in local for testing }; realTimeWebhookMetadata?: { diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 36acc21c4..ad257c2bb 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -1,6 +1,6 @@ import { CONNECTORS_METADATA } from './connectors/metadata'; import { ACCOUNTING_PROVIDERS, ATS_PROVIDERS, CRM_PROVIDERS, FILESTORAGE_PROVIDERS, HRIS_PROVIDERS, MARKETINGAUTOMATION_PROVIDERS, TICKETING_PROVIDERS } from './connectors'; -import { AuthStrategy, VerticalConfig } from './types'; +import { AuthStrategy, DynamicApiUrl, DynamicAuthorization, StaticApiUrl, StringAuthorization, VerticalConfig } from './types'; import { categoriesVerticals, ConnectorCategory } from './categories'; export const randomString = () => { @@ -44,14 +44,14 @@ export interface Provider { name: string; urls: { docsUrl: string; - apiUrl: string; - authBaseUrl?: string | null; + apiUrl: StaticApiUrl | DynamicApiUrl; + authBaseUrl?: StringAuthorization | DynamicAuthorization; }; - scopes?: string; + scopes?: string; logoPath: string; description?: string; authStrategy?: AuthStrategy; -}; +}; export function providersArray(vertical?: string): Provider[] { if (vertical) { @@ -60,7 +60,7 @@ export function providersArray(vertical?: string): Provider[] { return Object.entries(activeProviders).map(([providerName, config]) => ({ vertical: vertical.toLowerCase(), name: providerName, - urls: { + urls: { docsUrl: config.urls.docsUrl, apiUrl: config.urls.apiUrl, authBaseUrl: config.urls.authBaseUrl,