From 2edc14527cb22d6220bd75215c60b51bbae8170a Mon Sep 17 00:00:00 2001 From: nael Date: Fri, 12 Apr 2024 18:08:42 +0200 Subject: [PATCH] :sparkles: New providers oauth --- packages/api/scripts/oauthConnector.js | 10 ++-- .../services/freeagent/freeagent.service.ts | 50 ++++++++++------- .../services/freshbooks/freshbooks.service.ts | 54 +++++++++++-------- .../services/moneybird/moneybird.service.ts | 54 +++++++++++-------- .../ticketing/services/asana/asana.service.ts | 51 ++++++++++-------- .../ticketing/services/wrike/wrike.service.ts | 45 +++++++++------- 6 files changed, 159 insertions(+), 105 deletions(-) diff --git a/packages/api/scripts/oauthConnector.js b/packages/api/scripts/oauthConnector.js index d6391eecb..33b572217 100755 --- a/packages/api/scripts/oauthConnector.js +++ b/packages/api/scripts/oauthConnector.js @@ -57,6 +57,7 @@ export type ${providerUpper}OAuthResponse = { access_token: string; refresh_token: string; expires_in: string; + token_type: string; }; @Injectable() @@ -166,14 +167,15 @@ export class ${providerUpper}ConnectionService implements I${verticalUpper}Conne 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 formData = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: this.cryptoService.decrypt(refreshToken), + }); const res = await axios.post( "", 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 552b8c859..404f1c178 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 @@ -12,14 +12,16 @@ import { IAccountingConnectionService, } from '../../types'; import { ServiceRegistry } from '../registry.service'; -import { AuthStrategy } from '@panora/shared'; +import { AuthStrategy, providersConfig } from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; export type FreeagentOAuthResponse = { access_token: string; refresh_token: string; - expires_at: string; + expires_in: string | number; + refresh_token_expires_in: number; + token_type: string; }; @Injectable() @@ -60,18 +62,22 @@ export class FreeagentConnectionService )) 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 subdomain = 'panora'; - const res = await axios.post('', formData.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + const res = await axios.post( + 'https://api.freeagent.com/v2/token_endpoint', + 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: FreeagentOAuthResponse = res.data; this.logger.log( 'OAuth credentials : freeagent ticketing ' + JSON.stringify(data), @@ -88,9 +94,9 @@ export class FreeagentConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: '', + account_url: providersConfig['accounting']['freshagent'].apiUrl, expiration_timestamp: new Date( - new Date().getTime() + Number(data.expires_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -104,11 +110,11 @@ export class FreeagentConnectionService provider_slug: 'freeagent', vertical: 'accounting', token_type: 'oauth', - account_url: '', + account_url: providersConfig['accounting']['freshagent'].apiUrl, 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -139,12 +145,18 @@ export class FreeagentConnectionService this.type, )) as OAuth2AuthData; - const res = await axios.post('', formData.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - Authorization: `Basic Q1JFREVOVElBTFMuQ0xJRU5UX0lEfTokewogICAgICAgICAgICAgICAgICBDUkVERU5USUFMUy5DTElFTlRfU0VDUkVUCiAgICAgICAgICAgICAgfQ==`, + const res = await axios.post( + 'https://api.freeagent.com/v2/token_endpoint', + 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: FreeagentOAuthResponse = res.data; await this.prisma.connections.update({ where: { @@ -154,7 +166,7 @@ export class FreeagentConnectionService 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), }, }); 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 189fe6f5a..f3c06c80d 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 @@ -12,14 +12,16 @@ import { IAccountingConnectionService, } from '../../types'; import { ServiceRegistry } from '../registry.service'; -import { AuthStrategy } from '@panora/shared'; +import { AuthStrategy, providersConfig } from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; export type FreshbooksOAuthResponse = { access_token: string; refresh_token: string; - expires_at: string; + expires_in: string; + created_at: number; + token_type: string; }; @Injectable() @@ -66,12 +68,15 @@ export class FreshbooksConnectionService code: code, grant_type: 'authorization_code', }); - const subdomain = 'panora'; - const res = await axios.post('', formData.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + const res = await axios.post( + 'https://api.freshbooks.com/auth/oauth/token', + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, }, - }); + ); const data: FreshbooksOAuthResponse = res.data; this.logger.log( 'OAuth credentials : freshbooks ticketing ' + JSON.stringify(data), @@ -88,9 +93,9 @@ export class FreshbooksConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: '', + account_url: providersConfig['accounting']['freshbooks'].apiUrl, expiration_timestamp: new Date( - new Date().getTime() + Number(data.expires_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -104,11 +109,11 @@ export class FreshbooksConnectionService provider_slug: 'freshbooks', vertical: 'accounting', token_type: 'oauth', - account_url: '', + account_url: providersConfig['accounting']['freshbooks'].apiUrl, 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -135,21 +140,28 @@ export class FreshbooksConnectionService 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('', formData.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - Authorization: `Basic Q1JFREVOVElBTFMuQ0xJRU5UX0lEfTokewogICAgICAgICAgICAgICAgICBDUkVERU5USUFMUy5DTElFTlRfU0VDUkVUCiAgICAgICAgICAgICAgfQ==`, - }, + 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://api.freshbooks.com/auth/oauth/token', + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); const data: FreshbooksOAuthResponse = res.data; await this.prisma.connections.update({ where: { @@ -159,7 +171,7 @@ export class FreshbooksConnectionService 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), }, }); 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 31b601458..7c6e158a8 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 @@ -12,14 +12,17 @@ import { IAccountingConnectionService, } from '../../types'; import { ServiceRegistry } from '../registry.service'; -import { AuthStrategy } from '@panora/shared'; +import { AuthStrategy, providersConfig } from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; export type MoneybirdOAuthResponse = { access_token: string; refresh_token: string; - expires_at: string; + expires_in: string; + token_type: string; + scope: string; + created_at: number; }; @Injectable() @@ -66,12 +69,15 @@ export class MoneybirdConnectionService code: code, grant_type: 'authorization_code', }); - const subdomain = 'panora'; - const res = await axios.post('', formData.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + const res = await axios.post( + ' https://moneybird.com/oauth/token', + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, }, - }); + ); const data: MoneybirdOAuthResponse = res.data; this.logger.log( 'OAuth credentials : moneybird ticketing ' + JSON.stringify(data), @@ -88,9 +94,9 @@ export class MoneybirdConnectionService data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: '', + account_url: providersConfig['accounting']['moneybird'].apiUrl, expiration_timestamp: new Date( - new Date().getTime() + Number(data.expires_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -104,11 +110,11 @@ export class MoneybirdConnectionService provider_slug: 'moneybird', vertical: 'accounting', token_type: 'oauth', - account_url: '', + account_url: providersConfig['accounting']['moneybird'].apiUrl, 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -130,21 +136,25 @@ export class MoneybirdConnectionService 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('', formData.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - Authorization: `Basic Q1JFREVOVElBTFMuQ0xJRU5UX0lEfTokewogICAgICAgICAgICAgICAgICBDUkVERU5USUFMUy5DTElFTlRfU0VDUkVUCiAgICAgICAgICAgICAgfQ==`, - }, + 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://moneybird.com/oauth/token', + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); const data: MoneybirdOAuthResponse = res.data; await this.prisma.connections.update({ where: { @@ -154,7 +164,7 @@ export class MoneybirdConnectionService 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), }, }); 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 87208da0a..df90bb82c 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 @@ -1,4 +1,3 @@ - import { Injectable } from '@nestjs/common'; import axios from 'axios'; import { PrismaService } from '@@core/prisma/prisma.service'; @@ -7,20 +6,27 @@ 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 { +import { CallbackParams, RefreshParams, ITicketingConnectionService, } from '../../types'; import { ServiceRegistry } from '../registry.service'; -import { AuthStrategy } from '@panora/shared'; +import { AuthStrategy, providersConfig } from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; export type AsanaOAuthResponse = { access_token: string; refresh_token: string; - expires_at: string; + expires_in: number; + token_type: string; + data: { + id: number; + gid: string; + name: string; + email: string; + }; }; @Injectable() @@ -47,13 +53,16 @@ export class AsanaConnectionService implements ITicketingConnectionService { where: { id_linked_user: linkedUserId, provider_slug: 'asana', - vertical: 'ticketing' + vertical: 'ticketing', }, }); //reconstruct the redirect URI that was passed in the githubend it must be the same const REDIRECT_URI = `${this.env.getOAuthRredirectBaseUrl()}/connections/oauth/callback`; - const CREDENTIALS = (await this.cService.getCredentials(projectId, this.type)) as OAuth2AuthData; + const CREDENTIALS = (await this.cService.getCredentials( + projectId, + this.type, + )) as OAuth2AuthData; const formData = new URLSearchParams({ client_id: CREDENTIALS.CLIENT_ID, @@ -62,9 +71,8 @@ export class AsanaConnectionService implements ITicketingConnectionService { code: code, grant_type: 'authorization_code', }); - const subdomain = 'panora'; const res = await axios.post( - "", + 'https://app.asana.com/-/oauth_token', formData.toString(), { headers: { @@ -88,9 +96,9 @@ export class AsanaConnectionService implements ITicketingConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: "", + account_url: providersConfig['ticketing']['asana'].apiUrl, expiration_timestamp: new Date( - new Date().getTime() + Number(data.expires_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -104,11 +112,11 @@ export class AsanaConnectionService implements ITicketingConnectionService { provider_slug: 'asana', vertical: 'ticketing', token_type: 'oauth', - account_url: "", + account_url: providersConfig['ticketing']['asana'].apiUrl, 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -126,21 +134,22 @@ export class AsanaConnectionService implements ITicketingConnectionService { handleServiceError(error, this.logger, 'asana', Action.oauthCallback); } } - + 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 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://app.asana.com/-/oauth_token', formData.toString(), { headers: { @@ -158,7 +167,7 @@ export class AsanaConnectionService implements ITicketingConnectionService { 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), }, }); @@ -167,4 +176,4 @@ export class AsanaConnectionService implements ITicketingConnectionService { handleServiceError(error, this.logger, 'asana', Action.oauthRefresh); } } -} +} 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 index 9deeac018..769decfc7 100644 --- a/packages/api/src/@core/connections/ticketing/services/wrike/wrike.service.ts +++ b/packages/api/src/@core/connections/ticketing/services/wrike/wrike.service.ts @@ -12,14 +12,16 @@ import { ITicketingConnectionService, } from '../../types'; import { ServiceRegistry } from '../registry.service'; -import { AuthStrategy } from '@panora/shared'; +import { AuthStrategy, providersConfig } from '@panora/shared'; import { OAuth2AuthData, providerToType } from '@panora/shared'; import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; export type WrikeOAuthResponse = { access_token: string; refresh_token: string; - expires_at: string; + expires_in: string; + host: string; + token_type: string; }; @Injectable() @@ -64,12 +66,15 @@ export class WrikeConnectionService implements ITicketingConnectionService { code: code, grant_type: 'authorization_code', }); - const subdomain = 'panora'; - const res = await axios.post('', formData.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + 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), @@ -86,9 +91,11 @@ export class WrikeConnectionService implements ITicketingConnectionService { data: { access_token: this.cryptoService.encrypt(data.access_token), refresh_token: this.cryptoService.encrypt(data.refresh_token), - account_url: '', + account_url: + `https://${data.host}` + + providersConfig['ticketing']['wriker'].apiUrl, expiration_timestamp: new Date( - new Date().getTime() + Number(data.expires_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -102,11 +109,13 @@ export class WrikeConnectionService implements ITicketingConnectionService { provider_slug: 'wrike', vertical: 'ticketing', token_type: 'oauth', - account_url: '', + account_url: + `https://${data.host}` + + providersConfig['ticketing']['wriker'].apiUrl, 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), status: 'valid', created_at: new Date(), @@ -128,19 +137,19 @@ export class WrikeConnectionService implements ITicketingConnectionService { 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 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('', formData.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', - Authorization: `Basic Q1JFREVOVElBTFMuQ0xJRU5UX0lEfTokewogICAgICAgICAgICAgICAgICBDUkVERU5USUFMUy5DTElFTlRfU0VDUkVUCiAgICAgICAgICAgICAgfQ==`, }, }); const data: WrikeOAuthResponse = res.data; @@ -152,7 +161,7 @@ export class WrikeConnectionService implements ITicketingConnectionService { 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_at) * 1000, + new Date().getTime() + Number(data.expires_in) * 1000, ), }, });