diff --git a/packages/api/scripts/init.sql b/packages/api/scripts/init.sql index 534ed146f..4be270a83 100644 --- a/packages/api/scripts/init.sql +++ b/packages/api/scripts/init.sql @@ -541,12 +541,15 @@ CREATE TABLE connector_sets crm_close boolean NULL, fs_box boolean NULL, tcg_github boolean NULL, + hris_gusto boolean NULL, + hris_deel boolean NULL, + hris_sage boolean NULL, ecom_woocommerce boolean NULL, ecom_shopify boolean NULL, ecom_amazon boolean NULL, ecom_squarespace boolean NULL, + ats_ashby boolean NULL, hris_gusto boolean NULL, - ats_bamboohr boolean NULL, CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set ) ); diff --git a/packages/api/scripts/seed.sql b/packages/api/scripts/seed.sql index dd3e6c570..233841fa2 100644 --- a/packages/api/scripts/seed.sql +++ b/packages/api/scripts/seed.sql @@ -1,10 +1,10 @@ INSERT INTO users (id_user, identification_strategy, email, password_hash, first_name, last_name) VALUES ('0ce39030-2901-4c56-8db0-5e326182ec6b', 'b2c','local@panora.dev', '$2b$10$Y7Q8TWGyGuc5ecdIASbBsuXMo3q/Rs3/cnY.mLZP4tUgfGUOCUBlG', 'local', 'Panora'); -INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github) VALUES - ('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), - ('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), - ('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE); +INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github, hris_deel, hris_sage, ats_ashby) VALUES + ('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE), + ('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE); INSERT INTO projects (id_project, name, sync_mode, id_user, id_connector_set) VALUES ('1e468c15-aa57-4448-aa2b-7fed640d1e3d', 'Project 1', 'pull', '0ce39030-2901-4c56-8db0-5e326182ec6b', '1709da40-17f7-4d3a-93a0-96dc5da6ddd7'), diff --git a/packages/api/src/@core/connections/connections.module.ts b/packages/api/src/@core/connections/connections.module.ts index 80c2a0d71..188d25c6c 100644 --- a/packages/api/src/@core/connections/connections.module.ts +++ b/packages/api/src/@core/connections/connections.module.ts @@ -12,6 +12,7 @@ import { ProductivityConnectionsModule } from './productivity/productivity.conne import { MarketingAutomationConnectionsModule } from './marketingautomation/marketingautomation.connection.module'; import { TicketingConnectionModule } from './ticketing/ticketing.connection.module'; import { EcommerceConnectionModule } from './ecommerce/ecommerce.connection.module'; +import { CybersecurityConnectionsModule } from './cybersecurity/cybersecurity.connection.module'; @Module({ controllers: [ConnectionsController], @@ -25,6 +26,7 @@ import { EcommerceConnectionModule } from './ecommerce/ecommerce.connection.modu FilestorageConnectionModule, HrisConnectionModule, EcommerceConnectionModule, + CybersecurityConnectionsModule, SyncModule, ], providers: [ValidateUserService, OAuthTokenRefreshService], @@ -40,6 +42,7 @@ import { EcommerceConnectionModule } from './ecommerce/ecommerce.connection.modu EcommerceConnectionModule, HrisConnectionModule, ProductivityConnectionsModule, + CybersecurityConnectionsModule, ], }) export class ConnectionsModule {} diff --git a/packages/api/src/@core/connections/cybersecurity/cybersecurity.connection.module.ts b/packages/api/src/@core/connections/cybersecurity/cybersecurity.connection.module.ts new file mode 100644 index 000000000..65508349f --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/cybersecurity.connection.module.ts @@ -0,0 +1,38 @@ +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { BullQueueModule } from '@@core/@core-services/queues/queue.module'; +import { WebhookModule } from '@@core/@core-services/webhooks/panora-webhooks/webhook.module'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { Module } from '@nestjs/common'; +import { CybersecurityConnectionsService } from './services/cybersecurity.connection.service'; +import { ServiceRegistry } from './services/registry.service'; +import { TenableConnectionService } from './services/tenable/tenable.service'; +import { QualysConnectionService } from './services/qualys/qualys.service'; +import { SemgrepConnectionService } from './services/semgrep/semgrep.service'; +import { SentineloneConnectionService } from './services/sentinelone/sentinelone.service'; +import { Rapid7ConnectionService } from './services/rapid7insightvm/rapid7.service'; +import { SnykConnectionService } from './services/snyk/snyk.service'; +import { CrowdstrikeConnectionService } from './services/crowdstrike/crowdstrike.service'; +import { MicrosoftdefenderConnectionService } from './services/microsoftdefender/microsoftdefender.service'; + +@Module({ + imports: [WebhookModule, BullQueueModule], + providers: [ + CybersecurityConnectionsService, + WebhookService, + EnvironmentService, + ServiceRegistry, + ConnectionsStrategiesService, + //PROVIDERS SERVICES + SemgrepConnectionService, + TenableConnectionService, + QualysConnectionService, + SentineloneConnectionService, + Rapid7ConnectionService, + SnykConnectionService, + CrowdstrikeConnectionService, + MicrosoftdefenderConnectionService, + ], + exports: [CybersecurityConnectionsService], +}) +export class CybersecurityConnectionsModule {} diff --git a/packages/api/src/@core/connections/cybersecurity/services/crowdstrike/crowdstrike.service.ts b/packages/api/src/@core/connections/cybersecurity/services/crowdstrike/crowdstrike.service.ts new file mode 100644 index 000000000..4fcff71f9 --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/crowdstrike/crowdstrike.service.ts @@ -0,0 +1,195 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + OAuth2AuthData, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import axios from 'axios'; + +export interface CrowdstrikeOAuthResponse { + access_token: string; + token_type: string; + scope: string; +} + +@Injectable() +export class CrowdstrikeConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(CrowdstrikeConnectionService.name); + this.registry.registerService('crowdstrike', this); + this.type = providerToType( + 'crowdstrike', + 'cybersecurity', + AuthStrategy.oauth2, + ); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + + const access_token = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + config.headers = { + ...config.headers, + ...headers, + Authorization: `Bearer ${access_token}`, + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'cybersecurity.crowdstrike.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'crowdstrike', + vertical: 'cybersecurity', + }, + }); + //reconstruct the redirect URI that was passed in the frontend it must be the same + const REDIRECT_URI = `${ + this.env.getDistributionMode() == 'selfhost' + ? this.env.getTunnelIngress() + : 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, + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + code: code, + grant_type: 'authorization_code', + }); + const res = await axios.post( + 'https://api.crowdstrike.com/oauth/access_token', + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: CrowdstrikeOAuthResponse = res.data; + // save tokens for this customer inside our db + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = CONNECTORS_METADATA['cybersecurity']['crowdstrike'] + .urls.apiUrl as string; + // get the site id for the token + const site = await axios.get('https://api.crowdstrike.com/v2/sites', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Bearer ${data.access_token}`, + }, + }); + const site_id = site.data.sites[0].id; + if (isNotUnique) { + // Update existing connection + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + status: 'valid', + created_at: new Date(), + }, + }); + } else { + // Create new connection + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'crowdstrike', + vertical: 'cybersecurity', + token_type: 'oauth2', + account_url: `${BASE_API_URL}/sites/${site_id}`, + access_token: this.cryptoService.encrypt(data.access_token), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + this.logger.log('Successfully added tokens inside DB ' + db_res); + return db_res; + } catch (error) { + throw error; + } + } + + async handleTokenRefresh(opts: RefreshParams) { + return; + } +} diff --git a/packages/api/src/@core/connections/cybersecurity/services/cybersecurity.connection.service.ts b/packages/api/src/@core/connections/cybersecurity/services/cybersecurity.connection.service.ts new file mode 100644 index 000000000..586f81b2e --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/cybersecurity.connection.service.ts @@ -0,0 +1,129 @@ +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; +import { + CallbackParams, + IConnectionCategory, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { Injectable } from '@nestjs/common'; +import { connections as Connection } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from './registry.service'; +import { CategoryConnectionRegistry } from '@@core/@core-services/registries/connections-categories.registry'; +import { PassthroughResponse } from '@@core/passthrough/types'; + +@Injectable() +export class CybersecurityConnectionsService implements IConnectionCategory { + constructor( + private serviceRegistry: ServiceRegistry, + private connectionCategoryRegistry: CategoryConnectionRegistry, + private webhook: WebhookService, + private logger: LoggerService, + private prisma: PrismaService, + ) { + this.logger.setContext(CybersecurityConnectionsService.name); + this.connectionCategoryRegistry.registerService('cybersecurity', this); + } + //STEP 1:[FRONTEND STEP] + //create a frontend SDK snippet in which an authorization embedded link is set up so when users click + // on it to grant access => they grant US the access and then when confirmed + /*const authUrl = + 'https://app.hubspot.com/oauth/authorize' + + `?client_id=${encodeURIComponent(CLIENT_ID)}` + + `&scope=${encodeURIComponent(SCOPES)}` + + `&redirect_uri=${encodeURIComponent(REDIRECT_URI)}`;*/ //oauth/callback + + // oauth server calls this redirect callback + // WE WOULD HAVE CREATED A DEV ACCOUNT IN THE 5 CRMs (Panora dev account) + // we catch the tmp token and swap it against oauth2 server for access/refresh tokens + // to perform actions on his behalf + // this call pass 1. integrationID 2. CustomerId 3. Panora Api Key + async handleCallBack( + providerName: string, + callbackOpts: CallbackParams, + type_strategy: 'oauth2' | 'apikey' | 'basic', + ) { + try { + const serviceName = providerName.toLowerCase(); + + const service = this.serviceRegistry.getService(serviceName); + + if (!service) { + throw new ReferenceError(`Unknown provider, found ${providerName}`); + } + const data: Connection = await service.handleCallback(callbackOpts); + const event = await this.prisma.events.create({ + data: { + id_connection: data.id_connection, + id_project: data.id_project, + id_event: uuidv4(), + status: 'success', + type: 'connection.created', + method: 'GET', + url: `/${type_strategy}/callback`, + provider: providerName.toLowerCase(), + direction: '0', + timestamp: new Date(), + id_linked_user: callbackOpts.linkedUserId, + }, + }); + //directly send the webhook + await this.webhook.dispatchWebhook( + data, + 'connection.created', + callbackOpts.projectId, + event.id_event, + ); + } catch (error) { + throw error; + } + } + + async handleTokensRefresh( + connectionId: string, + providerName: string, + refresh_token: string, + id_project: string, + account_url?: string, + ) { + try { + const serviceName = providerName.toLowerCase(); + const service = this.serviceRegistry.getService(serviceName); + if (!service) { + throw new ReferenceError(`Unknown provider, found ${providerName}`); + } + const refreshOpts: RefreshParams = { + connectionId: connectionId, + refreshToken: refresh_token, + account_url: account_url, + projectId: id_project, + }; + await service.handleTokenRefresh(refreshOpts); + } catch (error) { + throw error; + } + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + const serviceName = connection.provider_slug.toLowerCase(); + const service = this.serviceRegistry.getService(serviceName); + if (!service) { + throw new ReferenceError(`Unknown provider, found ${serviceName}`); + } + return await service.passthrough(input, connectionId); + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/@core/connections/cybersecurity/services/microsoftdefender/microsoftdefender.service.ts b/packages/api/src/@core/connections/cybersecurity/services/microsoftdefender/microsoftdefender.service.ts new file mode 100644 index 000000000..c874a92ba --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/microsoftdefender/microsoftdefender.service.ts @@ -0,0 +1,199 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + OAuth2AuthData, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import axios from 'axios'; + +export interface MicrosoftdefenderOAuthResponse { + access_token: string; + token_type: string; + scope: string; +} + +@Injectable() +export class MicrosoftdefenderConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(MicrosoftdefenderConnectionService.name); + this.registry.registerService('microsoftdefender', this); + this.type = providerToType( + 'microsoftdefender', + 'cybersecurity', + AuthStrategy.oauth2, + ); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + + const access_token = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + config.headers = { + ...config.headers, + ...headers, + Authorization: `Bearer ${access_token}`, + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'cybersecurity.microsoftdefender.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'microsoftdefender', + vertical: 'cybersecurity', + }, + }); + //reconstruct the redirect URI that was passed in the frontend it must be the same + const REDIRECT_URI = `${ + this.env.getDistributionMode() == 'selfhost' + ? this.env.getTunnelIngress() + : 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, + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + code: code, + grant_type: 'authorization_code', + }); + const res = await axios.post( + 'https://api.microsoftdefender.com/oauth/access_token', + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: MicrosoftdefenderOAuthResponse = res.data; + // save tokens for this customer inside our db + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = CONNECTORS_METADATA['cybersecurity'][ + 'microsoftdefender' + ].urls.apiUrl as string; + // get the site id for the token + const site = await axios.get( + 'https://api.microsoftdefender.com/v2/sites', + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Bearer ${data.access_token}`, + }, + }, + ); + const site_id = site.data.sites[0].id; + if (isNotUnique) { + // Update existing connection + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + status: 'valid', + created_at: new Date(), + }, + }); + } else { + // Create new connection + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'microsoftdefender', + vertical: 'cybersecurity', + token_type: 'oauth2', + account_url: `${BASE_API_URL}/sites/${site_id}`, + access_token: this.cryptoService.encrypt(data.access_token), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + this.logger.log('Successfully added tokens inside DB ' + db_res); + return db_res; + } catch (error) { + throw error; + } + } + + async handleTokenRefresh(opts: RefreshParams) { + return; + } +} diff --git a/packages/api/src/@core/connections/cybersecurity/services/qualys/qualys.service.ts b/packages/api/src/@core/connections/cybersecurity/services/qualys/qualys.service.ts new file mode 100644 index 000000000..7840147b9 --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/qualys/qualys.service.ts @@ -0,0 +1,156 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class QualysConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(QualysConnectionService.name); + this.registry.registerService('qualys', this); + this.type = providerToType('qualys', 'cybersecurity', AuthStrategy.oauth2); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + const decryptedData = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + + const { username, password } = decryptedData; + + config.headers = { + ...config.headers, + ...headers, + 'X-Requested-With': 'Curl Sample', + Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString( + 'base64', + )}`, + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'cybersecurity.qualys.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, body } = opts; + const { username, password, api_url } = body; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'qualys', + vertical: 'cybersecurity', + }, + }); + + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = ( + CONNECTORS_METADATA['cybersecurity']['qualys'].urls + .apiUrl as DynamicApiUrl + )(api_url); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt( + JSON.stringify({ username, password }), + ), + account_url: BASE_API_URL, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'qualys', + vertical: 'cybersecurity', + token_type: 'basic', + account_url: BASE_API_URL, + access_token: this.cryptoService.encrypt( + JSON.stringify({ username, password }), + ), + 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; + } + } + + handleTokenRefresh?(opts: RefreshParams): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/api/src/@core/connections/cybersecurity/services/rapid7insightvm/rapid7.service.ts b/packages/api/src/@core/connections/cybersecurity/services/rapid7insightvm/rapid7.service.ts new file mode 100644 index 000000000..12e5cd767 --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/rapid7insightvm/rapid7.service.ts @@ -0,0 +1,145 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class Rapid7ConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(Rapid7ConnectionService.name); + this.registry.registerService('rapid7', this); + this.type = providerToType('rapid7', 'cybersecurity', AuthStrategy.oauth2); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + config.headers = { + ...config.headers, + ...headers, + Authorization: `Basic ${Buffer.from( + `:${connection.access_token}`, + ).toString('base64')}`, + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'cybersecurity.rapid7.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, body } = opts; + const { api_key, region } = body; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'rapid7', + vertical: 'cybersecurity', + }, + }); + + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = ( + CONNECTORS_METADATA['cybersecurity']['rapid7'].urls + .apiUrl as DynamicApiUrl + )(region); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(api_key), + account_url: BASE_API_URL, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'rapid7', + vertical: 'cybersecurity', + token_type: 'basic', + account_url: BASE_API_URL, + access_token: this.cryptoService.encrypt(api_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; + } + } + + handleTokenRefresh?(opts: RefreshParams): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/api/src/@core/connections/cybersecurity/services/registry.service.ts b/packages/api/src/@core/connections/cybersecurity/services/registry.service.ts new file mode 100644 index 000000000..2e9e3b100 --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/registry.service.ts @@ -0,0 +1,25 @@ +import { IConnectionService } from '@@core/connections/@utils/types'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ServiceRegistry { + private serviceMap: Map; + + constructor() { + this.serviceMap = new Map(); + } + + registerService(serviceKey: string, service: IConnectionService) { + this.serviceMap.set(serviceKey, service); + } + + getService(integrationId: string): IConnectionService { + const service = this.serviceMap.get(integrationId); + if (!service) { + throw new ReferenceError( + `Service not found for integration ID: ${integrationId}`, + ); + } + return service; + } +} diff --git a/packages/api/src/@core/connections/cybersecurity/services/semgrep/semgrep.service.ts b/packages/api/src/@core/connections/cybersecurity/services/semgrep/semgrep.service.ts new file mode 100644 index 000000000..7bd36d5d0 --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/semgrep/semgrep.service.ts @@ -0,0 +1,143 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class SemgrepConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(SemgrepConnectionService.name); + this.registry.registerService('semgrep', this); + this.type = providerToType('semgrep', 'cybersecurity', AuthStrategy.oauth2); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + config.headers = { + ...config.headers, + ...headers, + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'cybersecurity.semgrep.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, body } = opts; + const { api_key } = body; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'semgrep', + vertical: 'cybersecurity', + }, + }); + + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = CONNECTORS_METADATA['cybersecurity']['semgrep'].urls + .apiUrl as string; + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(api_key), + account_url: BASE_API_URL, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'semgrep', + vertical: 'cybersecurity', + token_type: 'basic', + account_url: BASE_API_URL, + access_token: this.cryptoService.encrypt(api_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; + } + } + + handleTokenRefresh?(opts: RefreshParams): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/api/src/@core/connections/cybersecurity/services/sentinelone/sentinelone.service.ts b/packages/api/src/@core/connections/cybersecurity/services/sentinelone/sentinelone.service.ts new file mode 100644 index 000000000..7d6f02fb5 --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/sentinelone/sentinelone.service.ts @@ -0,0 +1,149 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class SentineloneConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(SentineloneConnectionService.name); + this.registry.registerService('sentinelone', this); + this.type = providerToType( + 'sentinelone', + 'cybersecurity', + AuthStrategy.oauth2, + ); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + config.headers = { + ...config.headers, + ...headers, + Authorization: `APIToken ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'cybersecurity.sentinelone.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, body } = opts; + const { api_key, host } = body; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'sentinelone', + vertical: 'cybersecurity', + }, + }); + + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = ( + CONNECTORS_METADATA['cybersecurity']['sentinelone'].urls + .apiUrl as DynamicApiUrl + )(host); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(api_key), + account_url: BASE_API_URL, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'sentinelone', + vertical: 'cybersecurity', + token_type: 'basic', + account_url: BASE_API_URL, + access_token: this.cryptoService.encrypt(api_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; + } + } + + handleTokenRefresh?(opts: RefreshParams): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/api/src/@core/connections/cybersecurity/services/snyk/snyk.service.ts b/packages/api/src/@core/connections/cybersecurity/services/snyk/snyk.service.ts new file mode 100644 index 000000000..ff77211ce --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/snyk/snyk.service.ts @@ -0,0 +1,191 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + OAuth2AuthData, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import axios from 'axios'; + +export interface SnykOAuthResponse { + access_token: string; + token_type: string; + scope: string; +} + +@Injectable() +export class SnykConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(SnykConnectionService.name); + this.registry.registerService('snyk', this); + this.type = providerToType('snyk', 'cybersecurity', AuthStrategy.oauth2); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + + const access_token = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + config.headers = { + ...config.headers, + ...headers, + Authorization: `Bearer ${access_token}`, + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'cybersecurity.snyk.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, code } = opts; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'snyk', + vertical: 'cybersecurity', + }, + }); + //reconstruct the redirect URI that was passed in the frontend it must be the same + const REDIRECT_URI = `${ + this.env.getDistributionMode() == 'selfhost' + ? this.env.getTunnelIngress() + : 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, + client_id: CREDENTIALS.CLIENT_ID, + client_secret: CREDENTIALS.CLIENT_SECRET, + code: code, + grant_type: 'authorization_code', + }); + const res = await axios.post( + 'https://api.snyk.com/oauth/access_token', + formData.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + }, + }, + ); + const data: SnykOAuthResponse = res.data; + // save tokens for this customer inside our db + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = CONNECTORS_METADATA['cybersecurity']['snyk'].urls + .apiUrl as string; + // get the site id for the token + const site = await axios.get('https://api.snyk.com/v2/sites', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', + Authorization: `Bearer ${data.access_token}`, + }, + }); + const site_id = site.data.sites[0].id; + if (isNotUnique) { + // Update existing connection + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(data.access_token), + status: 'valid', + created_at: new Date(), + }, + }); + } else { + // Create new connection + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'snyk', + vertical: 'cybersecurity', + token_type: 'oauth2', + account_url: `${BASE_API_URL}/sites/${site_id}`, + access_token: this.cryptoService.encrypt(data.access_token), + status: 'valid', + created_at: new Date(), + projects: { + connect: { id_project: projectId }, + }, + linked_users: { + connect: { + id_linked_user: await this.connectionUtils.getLinkedUserId( + projectId, + linkedUserId, + ), + }, + }, + }, + }); + } + this.logger.log('Successfully added tokens inside DB ' + db_res); + return db_res; + } catch (error) { + throw error; + } + } + + async handleTokenRefresh(opts: RefreshParams) { + return; + } +} diff --git a/packages/api/src/@core/connections/cybersecurity/services/tenable/tenable.service.ts b/packages/api/src/@core/connections/cybersecurity/services/tenable/tenable.service.ts new file mode 100644 index 000000000..7c125d3d8 --- /dev/null +++ b/packages/api/src/@core/connections/cybersecurity/services/tenable/tenable.service.ts @@ -0,0 +1,147 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class TenableConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(TenableConnectionService.name); + this.registry.registerService('tenable', this); + this.type = providerToType('tenable', 'cybersecurity', AuthStrategy.oauth2); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + const decryptedData = JSON.parse( + this.cryptoService.decrypt(connection.access_token), + ); + + const { access_key, secret_key } = decryptedData; // todo + + config.headers = { + ...config.headers, + ...headers, + 'X-ApiKeys': `${secret_key}`, + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'cybersecurity.tenable.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, body } = opts; + const { access_key, secret_key } = body; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'tenable', + vertical: 'cybersecurity', + }, + }); + + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = CONNECTORS_METADATA['cybersecurity']['tenable'].urls + .apiUrl as string; + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(secret_key), + account_url: BASE_API_URL, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'tenable', + vertical: 'cybersecurity', + token_type: 'basic', + account_url: BASE_API_URL, + access_token: this.cryptoService.encrypt(secret_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; + } + } + + handleTokenRefresh?(opts: RefreshParams): Promise { + throw new Error('Method not implemented.'); + } +} 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 6c70292b3..da5e50d4a 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,7 @@ import { NamelyConnectionService } from './services/namely/namely.service'; import { PayfitConnectionService } from './services/payfit/payfit.service'; import { ServiceRegistry } from './services/registry.service'; import { RipplingConnectionService } from './services/rippling/rippling.service'; +import { SageConnectionService } from './services/sage/sage.service'; @Module({ imports: [WebhookModule, BullQueueModule], @@ -30,6 +31,7 @@ import { RipplingConnectionService } from './services/rippling/rippling.service' FactorialConnectionService, NamelyConnectionService, BamboohrConnectionService, + SageConnectionService, ], exports: [HrisConnectionsService], }) diff --git a/packages/api/src/@core/connections/hris/services/sage/sage.service.ts b/packages/api/src/@core/connections/hris/services/sage/sage.service.ts new file mode 100644 index 000000000..cbcdf1199 --- /dev/null +++ b/packages/api/src/@core/connections/hris/services/sage/sage.service.ts @@ -0,0 +1,143 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { RetryHandler } from '@@core/@core-services/request-retry/retry.handler'; +import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service'; +import { ConnectionUtils } from '@@core/connections/@utils'; +import { + AbstractBaseConnectionService, + OAuthCallbackParams, + PassthroughInput, + RefreshParams, +} from '@@core/connections/@utils/types'; +import { PassthroughResponse } from '@@core/passthrough/types'; +import { Injectable } from '@nestjs/common'; +import { + AuthStrategy, + CONNECTORS_METADATA, + DynamicApiUrl, + providerToType, +} from '@panora/shared'; +import { v4 as uuidv4 } from 'uuid'; +import { ServiceRegistry } from '../registry.service'; + +@Injectable() +export class SageConnectionService extends AbstractBaseConnectionService { + private readonly type: string; + + constructor( + protected prisma: PrismaService, + private logger: LoggerService, + protected cryptoService: EncryptionService, + private registry: ServiceRegistry, + private connectionUtils: ConnectionUtils, + private cService: ConnectionsStrategiesService, + private retryService: RetryHandler, + ) { + super(prisma, cryptoService); + this.logger.setContext(SageConnectionService.name); + this.registry.registerService('sage', this); + this.type = providerToType('sage', 'hris', AuthStrategy.oauth2); + } + + async passthrough( + input: PassthroughInput, + connectionId: string, + ): Promise { + try { + const { headers } = input; + const config = await this.constructPassthrough(input, connectionId); + + const connection = await this.prisma.connections.findUnique({ + where: { + id_connection: connectionId, + }, + }); + + config.headers = { + ...config.headers, + ...headers, + 'X-Auth-Token': this.cryptoService.decrypt(connection.access_token), + }; + + return await this.retryService.makeRequest( + { + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + }, + 'hris.sage.passthrough', + config.linkedUserId, + ); + } catch (error) { + throw error; + } + } + + async handleCallback(opts: OAuthCallbackParams) { + try { + const { linkedUserId, projectId, body } = opts; + const { api_key, subdomain } = body; + const isNotUnique = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'sage', + vertical: 'hris', + }, + }); + + let db_res; + const connection_token = uuidv4(); + const BASE_API_URL = ( + CONNECTORS_METADATA['hris']['sage'].urls.apiUrl as DynamicApiUrl + )(subdomain); + + if (isNotUnique) { + db_res = await this.prisma.connections.update({ + where: { + id_connection: isNotUnique.id_connection, + }, + data: { + access_token: this.cryptoService.encrypt(api_key), + account_url: BASE_API_URL, + status: 'valid', + created_at: new Date(), + }, + }); + } else { + db_res = await this.prisma.connections.create({ + data: { + id_connection: uuidv4(), + connection_token: connection_token, + provider_slug: 'sage', + vertical: 'hris', + token_type: 'basic', + account_url: BASE_API_URL, + access_token: this.cryptoService.encrypt(api_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; + } + } + + handleTokenRefresh?(opts: RefreshParams): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/api/src/@core/utils/types/original/original.hris.ts b/packages/api/src/@core/utils/types/original/original.hris.ts index e85d40144..df980dafa 100644 --- a/packages/api/src/@core/utils/types/original/original.hris.ts +++ b/packages/api/src/@core/utils/types/original/original.hris.ts @@ -1,12 +1,21 @@ /* INPUT */ import { GustoBenefitOutput } from '@hris/benefit/services/gusto/types'; +import { DeelCompanyOutput } from '@hris/company/services/deel/types'; import { GustoCompanyOutput } from '@hris/company/services/gusto/types'; +import { DeelEmployeeOutput } from '@hris/employee/services/deel/types'; import { GustoEmployeeOutput } from '@hris/employee/services/gusto/types'; +import { SageEmployeeOutput } from '@hris/employee/services/sage/types'; import { GustoEmployerbenefitOutput } from '@hris/employerbenefit/services/gusto/types'; +import { DeelEmploymentOutput } from '@hris/employment/services/deel/types'; import { GustoEmploymentOutput } from '@hris/employment/services/gusto/types'; +import { DeelGroupOutput } from '@hris/group/services/deel/types'; import { GustoGroupOutput } from '@hris/group/services/gusto/types'; +import { SageGroupOutput } from '@hris/group/services/sage/types'; +import { DeelLocationOutput } from '@hris/location/services/deel/types'; import { GustoLocationOutput } from '@hris/location/services/gusto/types'; +import { SageTimeoffOutput } from '@hris/timeoff/services/sage/types'; +import { SageTimeoffbalanceOutput } from '@hris/timeoffbalance/services/sage/types'; /* bankinfo */ export type OriginalBankInfoInput = any; @@ -79,13 +88,16 @@ export type OriginalBankInfoOutput = any; export type OriginalBenefitOutput = GustoBenefitOutput; /* company */ -export type OriginalCompanyOutput = GustoCompanyOutput; +export type OriginalCompanyOutput = GustoCompanyOutput | DeelCompanyOutput; /* dependent */ export type OriginalDependentOutput = any; /* employee */ -export type OriginalEmployeeOutput = GustoEmployeeOutput; +export type OriginalEmployeeOutput = + | GustoEmployeeOutput + | SageEmployeeOutput + | DeelEmployeeOutput; /* employeepayrollrun */ export type OriginalEmployeePayrollRunOutput = any; @@ -94,13 +106,18 @@ export type OriginalEmployeePayrollRunOutput = any; export type OriginalEmployerBenefitOutput = GustoEmployerbenefitOutput; /* employment */ -export type OriginalEmploymentOutput = GustoEmploymentOutput; +export type OriginalEmploymentOutput = + | GustoEmploymentOutput + | DeelEmploymentOutput; /* group */ -export type OriginalGroupOutput = GustoGroupOutput; +export type OriginalGroupOutput = + | GustoGroupOutput + | DeelGroupOutput + | SageGroupOutput; /* location */ -export type OriginalLocationOutput = GustoLocationOutput; +export type OriginalLocationOutput = GustoLocationOutput | DeelLocationOutput; /* paygroup */ export type OriginalPayGroupOutput = any; @@ -109,10 +126,10 @@ export type OriginalPayGroupOutput = any; export type OriginalPayrollRunOutput = any; /* timeoff */ -export type OriginalTimeoffOutput = any; +export type OriginalTimeoffOutput = SageTimeoffOutput; /* timeoffbalance */ -export type OriginalTimeoffBalanceOutput = any; +export type OriginalTimeoffBalanceOutput = SageTimeoffbalanceOutput; /* timesheetentry */ export type OriginalTimesheetentryOutput = any; diff --git a/packages/api/src/hris/@lib/@utils/index.ts b/packages/api/src/hris/@lib/@utils/index.ts index 23b09c261..d4b9be7af 100644 --- a/packages/api/src/hris/@lib/@utils/index.ts +++ b/packages/api/src/hris/@lib/@utils/index.ts @@ -35,6 +35,21 @@ export class Utils { } } + async getGroupUuidFromRemoteId(id: string, connection_id: string) { + try { + const res = await this.prisma.hris_groups.findFirst({ + where: { + remote_id: id, + id_connection: connection_id, + }, + }); + if (!res) return; + return res.id_hris_group; + } catch (error) { + throw error; + } + } + async getEmployerBenefitUuidFromRemoteId(id: string, connection_id: string) { try { const res = await this.prisma.hris_employer_benefits.findFirst({ diff --git a/packages/api/src/hris/company/company.module.ts b/packages/api/src/hris/company/company.module.ts index eae8227f4..a1cfc0614 100644 --- a/packages/api/src/hris/company/company.module.ts +++ b/packages/api/src/hris/company/company.module.ts @@ -9,6 +9,8 @@ import { CoreUnification } from '@@core/@core-services/unification/core-unificat import { GustoCompanyMapper } from './services/gusto/mappers'; import { GustoService } from './services/gusto'; import { Utils } from '@hris/@lib/@utils'; +import { DeelService } from './services/deel'; +import { DeelCompanyMapper } from './services/deel/mappers'; @Module({ controllers: [CompanyController], providers: [ @@ -20,8 +22,10 @@ import { Utils } from '@hris/@lib/@utils'; ServiceRegistry, IngestDataService, GustoCompanyMapper, + DeelCompanyMapper, /* PROVIDERS SERVICES */ GustoService, + DeelService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/company/services/deel/index.ts b/packages/api/src/hris/company/services/deel/index.ts new file mode 100644 index 000000000..6eb47d4ea --- /dev/null +++ b/packages/api/src/hris/company/services/deel/index.ts @@ -0,0 +1,72 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { ICompanyService } from '@hris/company/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { DeelCompanyOutput } from './types'; +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { OriginalCompanyOutput } from '@@core/utils/types/original/original.hris'; + +@Injectable() +export class DeelService implements ICompanyService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.company.toUpperCase() + ':' + DeelService.name, + ); + this.registry.registerService('deel', this); + } + + addCompany( + companyData: DesunifyReturnType, + linkedUserId: string, + ): Promise> { + throw new Error('Method not implemented.'); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'deel', + vertical: 'hris', + }, + }); + + const resp = await axios.get( + `${connection.account_url}/rest/v2/legal-entities`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }, + ); + this.logger.log(`Synced deel companys !`); + + return { + data: resp.data.data, + message: 'Deel companys retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/company/services/deel/mappers.ts b/packages/api/src/hris/company/services/deel/mappers.ts new file mode 100644 index 000000000..5bdc9293c --- /dev/null +++ b/packages/api/src/hris/company/services/deel/mappers.ts @@ -0,0 +1,70 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Injectable } from '@nestjs/common'; +import { DeelCompanyOutput } from './types'; +import { + UnifiedHrisCompanyInput, + UnifiedHrisCompanyOutput, +} from '@hris/company/types/model.unified'; +import { ICompanyMapper } from '@hris/company/types'; +import { Utils } from '@hris/@lib/@utils'; + +@Injectable() +export class DeelCompanyMapper implements ICompanyMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'company', 'deel', this); + } + + async desunify( + source: UnifiedHrisCompanyInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + // Implementation for desunify (if needed) + return; + } + + async unify( + source: DeelCompanyOutput | DeelCompanyOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleCompanyToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((company) => + this.mapSingleCompanyToUnified( + company, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleCompanyToUnified( + company: DeelCompanyOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: company.id || null, + legal_name: company.name || null, + display_name: company.name || null, // Using name for display_name as well + eins: [], // Deel doesn't provide EINs in this data structure + remote_data: company, + locations: [], // Deel doesn't provide locations in this data structure + field_mappings: {}, + }; + } +} diff --git a/packages/api/src/hris/company/services/deel/types.ts b/packages/api/src/hris/company/services/deel/types.ts new file mode 100644 index 000000000..ec793698b --- /dev/null +++ b/packages/api/src/hris/company/services/deel/types.ts @@ -0,0 +1,7 @@ +export interface DeelCompanyOutput { + id: string; + name: string; + country: string; + entity_type: 'individual' | string; + entity_subtype: string; +} diff --git a/packages/api/src/hris/employee/employee.module.ts b/packages/api/src/hris/employee/employee.module.ts index ed03dd83f..e01e841db 100644 --- a/packages/api/src/hris/employee/employee.module.ts +++ b/packages/api/src/hris/employee/employee.module.ts @@ -9,6 +9,10 @@ import { CoreUnification } from '@@core/@core-services/unification/core-unificat import { GustoEmployeeMapper } from './services/gusto/mappers'; import { GustoService } from './services/gusto'; import { Utils } from '@hris/@lib/@utils'; +import { SageEmployeeMapper } from './services/sage/mappers'; +import { SageService } from './services/sage'; +import { DeelService } from './services/deel'; +import { DeelEmployeeMapper } from './services/deel/mappers'; @Module({ controllers: [EmployeeController], providers: [ @@ -20,8 +24,12 @@ import { Utils } from '@hris/@lib/@utils'; ServiceRegistry, IngestDataService, GustoEmployeeMapper, + SageEmployeeMapper, + DeelEmployeeMapper, /* PROVIDERS SERVICES */ GustoService, + SageService, + DeelService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/employee/services/deel/index.ts b/packages/api/src/hris/employee/services/deel/index.ts new file mode 100644 index 000000000..966c3ff66 --- /dev/null +++ b/packages/api/src/hris/employee/services/deel/index.ts @@ -0,0 +1,60 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { IEmployeeService } from '@hris/employee/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { DeelEmployeeOutput } from './types'; + +@Injectable() +export class DeelService implements IEmployeeService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.employee.toUpperCase() + ':' + DeelService.name, + ); + this.registry.registerService('deel', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'deel', + vertical: 'hris', + }, + }); + + const resp = await axios.get(`${connection.account_url}/rest/v2/people`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.cryptoService.decrypt( + connection.access_token, + )}`, + }, + }); + this.logger.log(`Synced deel employees !`); + + return { + data: resp.data.data, + message: 'Deel employees retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/employee/services/deel/mappers.ts b/packages/api/src/hris/employee/services/deel/mappers.ts new file mode 100644 index 000000000..0912bdb4d --- /dev/null +++ b/packages/api/src/hris/employee/services/deel/mappers.ts @@ -0,0 +1,170 @@ +import { Injectable } from '@nestjs/common'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { DeelEmployeeOutput } from './types'; +import { + UnifiedHrisEmployeeInput, + UnifiedHrisEmployeeOutput, +} from '@hris/employee/types/model.unified'; +import { IEmployeeMapper } from '@hris/employee/types'; +import { Utils } from '@hris/@lib/@utils'; +import { HrisObject } from '@panora/shared'; +import { UnifiedHrisEmploymentOutput } from '@hris/employment/types/model.unified'; +import { DeelEmploymentOutput } from '@hris/employment/services/deel/types'; +import { DeelLocationOutput } from '@hris/location/services/deel/types'; +import { UnifiedHrisLocationOutput } from '@hris/location/types/model.unified'; + +@Injectable() +export class DeelEmployeeMapper implements IEmployeeMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'employee', 'deel', this); + } + + async desunify( + source: UnifiedHrisEmployeeInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + // Implementation for desunify (if needed) + return; + } + + async unify( + source: DeelEmployeeOutput | DeelEmployeeOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleEmployeeToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((employee) => + this.mapSingleEmployeeToUnified( + employee, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleEmployeeToUnified( + employee: DeelEmployeeOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + const opts: any = {}; + + if (employee.client_legal_entity) { + const company_id = await this.utils.getCompanyUuidFromRemoteId( + employee.client_legal_entity.id, // Assuming client_legal_entity has an id field + connectionId, + ); + if (company_id) { + opts.company_id = company_id; + } + } + + if (employee.direct_manager) { + const manager_id = await this.utils.getEmployeeUuidFromRemoteId( + employee.direct_manager.id, + connectionId, + ); + if (manager_id) { + opts.manager_id = manager_id; + } + } + + if (employee.employments) { + const employments = await this.ingestService.ingestData< + UnifiedHrisEmploymentOutput, + DeelEmploymentOutput + >( + employee.employments, + 'deel', + connectionId, + 'hris', + HrisObject.employment, + [], + ); + if (employments) { + opts.employments = employments.map((emp) => emp.id_hris_employment); + } + } + + if (employee.addresses) { + const addresses = await this.ingestService.ingestData< + UnifiedHrisLocationOutput, + DeelLocationOutput + >( + employee.addresses.map((add) => { + return { + ...add, + type: 'HOME', + }; + }), + 'deel', + connectionId, + 'hris', + HrisObject.location, + [], + ); + if (addresses) { + opts.locations = addresses.map((add) => add.id_hris_location); + } + } + + const primaryEmployment = employee.employments.find((emp) => !emp.is_ended); + + return { + remote_id: employee.id, + remote_data: employee, + first_name: employee.first_name, + last_name: employee.last_name, + preferred_name: null, // Deel doesn't provide this information + display_full_name: employee.full_name, + work_email: + employee.emails.find((email) => email.type === 'work')?.value || null, + personal_email: + employee.emails.find((email) => email.type === 'personal')?.value || + null, + mobile_phone_number: null, // Deel doesn't provide this information in the given structure + start_date: primaryEmployment + ? new Date(primaryEmployment.start_date) + : null, + termination_date: employee.completion_date + ? new Date(employee.completion_date) + : null, + employment_status: this.mapEmploymentStatus(employee.hiring_status), + date_of_birth: employee.birth_date ? new Date(employee.birth_date) : null, + avatar_url: null, // Deel doesn't provide this information in the given structure + gender: null, // Deel doesn't provide this information in the given structure + ethnicity: null, // Deel doesn't provide this information in the given structure + marital_status: null, // Deel doesn't provide this information in the given structure + job_title: primaryEmployment ? primaryEmployment.job_title : null, + ...opts, + }; + } + + private mapEmploymentStatus( + status: string, + ): 'ACTIVE' | 'PENDING' | 'INACTIVE' { + switch (status.toUpperCase()) { + case 'ACTIVE': + return 'ACTIVE'; + case 'PENDING': + return 'PENDING'; + default: + return 'INACTIVE'; + } + } +} diff --git a/packages/api/src/hris/employee/services/deel/types.ts b/packages/api/src/hris/employee/services/deel/types.ts new file mode 100644 index 000000000..65eb03f3d --- /dev/null +++ b/packages/api/src/hris/employee/services/deel/types.ts @@ -0,0 +1,100 @@ +export type DeelEmployeeOutput = Partial<{ + id: string; + created_at: string; // Date-time in string format + first_name: string; + last_name: string; + full_name: string; + addresses: DeelAddress[]; + emails: DeelEmail[]; + birth_date: string; + start_date: string; // Date in string format + nationalities: string[]; + client_legal_entity: DeelClientLegalEntity; + state: string; + seniority: string; + completion_date: string | null; + direct_manager: DeelDirectManager | null; + direct_reports: DeelDirectManager[] | null; + direct_reports_count: number; + employments: DeelEmployment[]; + hiring_status: string; + new_hiring_status: string; + hiring_type: string; + job_title: string; + country: string; + timezone: string; + department: DeelDepartment; + work_location: string; + updated_at: string | null; // Date-time in string format +}>; + +export interface DeelAddress { + streetAddress: string; + locality: string; + region: string; + postalCode: string; + country: string; +} + +export interface DeelEmail { + type: string; + value: string; +} + +export interface DeelClientLegalEntity { + id: string; + name: string; +} + +export interface DeelDirectManager { + id: string; + last_name: string; + first_name: string; + work_email: string; +} + +export interface DeelTeam { + id: string; + name: string; +} + +export interface DeelPayment { + rate: number; + scale: string; + currency: string; + contract_name: string; +} +export interface DeelDepartment { + id: string; + name: string; + parent: string; +} + +export interface DeelEmployment { + id: string; + name: string; + team: DeelTeam; + email: string; + state: string; + country: string; + payment: DeelPayment; + is_ended: boolean; + timezone: string; + job_title: string; + seniority: string; + start_date: string; // Date in string format + work_email: string; + hiring_type: string; + hiring_status: string; + completion_date: string; + contract_status: string; + voluntarily_left: string; + contract_coverage: string[]; + new_hiring_status: string; + client_legal_entity: DeelClientLegalEntity; + has_eor_termination: string; + contract_is_archived: boolean; + contract_has_contractor: boolean; + is_user_contract_deleted: boolean; + hris_direct_employee_invitation: string; +} diff --git a/packages/api/src/hris/employee/services/sage/index.ts b/packages/api/src/hris/employee/services/sage/index.ts new file mode 100644 index 000000000..be8e2937c --- /dev/null +++ b/packages/api/src/hris/employee/services/sage/index.ts @@ -0,0 +1,58 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { IEmployeeService } from '@hris/employee/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { SageEmployeeOutput } from './types'; + +@Injectable() +export class SageService implements IEmployeeService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.employee.toUpperCase() + ':' + SageService.name, + ); + this.registry.registerService('sage', this); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'sage', + vertical: 'hris', + }, + }); + + const resp = await axios.get(`${connection.account_url}/api/employees`, { + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': this.cryptoService.decrypt(connection.access_token), + }, + }); + this.logger.log(`Synced sage employees !`); + + return { + data: resp.data.data, + message: 'Sage employees retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/employee/services/sage/mappers.ts b/packages/api/src/hris/employee/services/sage/mappers.ts new file mode 100644 index 000000000..3c0064346 --- /dev/null +++ b/packages/api/src/hris/employee/services/sage/mappers.ts @@ -0,0 +1,115 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Utils } from '@hris/@lib/@utils'; +import { IEmployeeMapper } from '@hris/employee/types'; +import { + EmploymentStatus, + Gender, + MartialStatus, + UnifiedHrisEmployeeInput, + UnifiedHrisEmployeeOutput, +} from '@hris/employee/types/model.unified'; +import { Injectable } from '@nestjs/common'; +import { SageEmployeeOutput } from './types'; + +@Injectable() +export class SageEmployeeMapper implements IEmployeeMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'employee', 'sage', this); + } + + async desunify( + source: UnifiedHrisEmployeeInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + // Implementation for desunify (if needed) + return; + } + + async unify( + source: SageEmployeeOutput | SageEmployeeOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleEmployeeToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((employee) => + this.mapSingleEmployeeToUnified( + employee, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleEmployeeToUnified( + employee: SageEmployeeOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: employee.id.toString(), + remote_data: employee, + first_name: employee.first_name, + last_name: employee.last_name, + display_full_name: `${employee.first_name} ${employee.last_name}`, + work_email: employee.email, + mobile_phone_number: employee.mobile_phone, + employments: [], // We would need to process employment history to populate this + groups: [employee.team], + start_date: new Date(employee.employment_start_date), + employment_status: this.mapEmploymentStatus(employee.employment_status), + date_of_birth: new Date(employee.date_of_birth), + gender: this.mapGender(employee.gender), + marital_status: this.mapMaritalStatus(employee.marital_status), + avatar_url: employee.picture_url, + ssn: employee.personal_identification_number, + employee_number: employee.employee_number, + }; + } + + private mapGender(sageGender: string): Gender { + switch (sageGender.toLowerCase()) { + case 'male': + return 'MALE'; + case 'female': + return 'FEMALE'; + default: + return 'OTHER'; + } + } + + private mapMaritalStatus(sageMaritalStatus: string): MartialStatus { + switch (sageMaritalStatus.toLowerCase()) { + case 'married': + return 'MARRIED_FILING_JOINTLY'; + case 'single': + return 'SINGLE'; + default: + return 'SINGLE'; // Default to single if unknown + } + } + + private mapEmploymentStatus(sageEmploymentStatus: string): EmploymentStatus { + switch (sageEmploymentStatus.toLowerCase()) { + case 'full-time': + case 'part-time': + return 'ACTIVE'; + default: + return 'INACTIVE'; + } + } +} diff --git a/packages/api/src/hris/employee/services/sage/types.ts b/packages/api/src/hris/employee/services/sage/types.ts new file mode 100644 index 000000000..c4f986e70 --- /dev/null +++ b/packages/api/src/hris/employee/services/sage/types.ts @@ -0,0 +1,55 @@ +export type SageEmployeeOutput = Partial<{ + id: number; + email: string; + first_name: string; + last_name: string; + picture_url: string; + employment_start_date: string; + date_of_birth: string; + team: string; + team_id: number; + position: string; + position_id: number; + reports_to_employee_id: number; + work_phone: string; + home_phone: string; + mobile_phone: string; + gender: string; + street_first: string; + street_second: string; + city: string; + post_code: number; + country: string; + employee_number: string; + employment_status: string; + nationality: string; + marital_status: string; + personal_identification_number: string; + tax_number: string; + irregular_contract_worker: boolean; + team_history: SageTeamHistory[]; + employment_status_history: SageEmploymentStatusHistory[]; + position_history: SagePositionHistory[]; +}>; + +export interface SageTeamHistory { + team_id: number; + start_date: string; + end_date: string; + team_name: string; +} + +export interface SageEmploymentStatusHistory { + employment_status_id: number; + start_date: string; + end_date: string; + employment_statu_name: string; // Note: This seems to be a typo in the original data +} + +export interface SagePositionHistory { + position_id: number; + start_date: string; + end_date: string; + position_name: string; + position_code: string; +} diff --git a/packages/api/src/hris/employment/employment.module.ts b/packages/api/src/hris/employment/employment.module.ts index e5d64d523..2ef02de82 100644 --- a/packages/api/src/hris/employment/employment.module.ts +++ b/packages/api/src/hris/employment/employment.module.ts @@ -8,6 +8,7 @@ import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/w import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; import { GustoEmploymentMapper } from './services/gusto/mappers'; import { Utils } from '@hris/@lib/@utils'; +import { DeelEmploymentMapper } from './services/deel/mappers'; @Module({ controllers: [EmploymentController], providers: [ @@ -19,6 +20,7 @@ import { Utils } from '@hris/@lib/@utils'; IngestDataService, Utils, GustoEmploymentMapper, + DeelEmploymentMapper, /* PROVIDERS SERVICES */ ], exports: [SyncService], diff --git a/packages/api/src/hris/employment/services/deel/mappers.ts b/packages/api/src/hris/employment/services/deel/mappers.ts new file mode 100644 index 000000000..bb892abf0 --- /dev/null +++ b/packages/api/src/hris/employment/services/deel/mappers.ts @@ -0,0 +1,136 @@ +import { Injectable } from '@nestjs/common'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Utils } from '@hris/@lib/@utils'; +import { IEmploymentMapper } from '@hris/employment/types'; +import { + FlsaStatus, + UnifiedHrisEmploymentInput, + UnifiedHrisEmploymentOutput, + EmploymentType, + PayFrequency, + PayPeriod, +} from '@hris/employment/types/model.unified'; +import { DeelEmploymentOutput } from './types'; +import { CurrencyCode } from '@@core/utils/types'; + +@Injectable() +export class DeelEmploymentMapper implements IEmploymentMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'employment', 'deel', this); + } + + async desunify( + source: UnifiedHrisEmploymentInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + // Implementation for desunify (if needed) + return; + } + + async unify( + source: DeelEmploymentOutput | DeelEmploymentOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleEmploymentToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((employment) => + this.mapSingleEmploymentToUnified( + employment, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleEmploymentToUnified( + employment: DeelEmploymentOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: employment.id, + remote_data: employment, + job_title: employment.job_title, + pay_rate: employment.payment?.rate, + pay_period: this.mapPayPeriod(employment.payment?.scale), + pay_frequency: this.mapPayFrequency(employment.payment?.scale), + pay_currency: employment.payment?.currency as CurrencyCode, + flsa_status: this.mapFlsaStatus(employment.hiring_type), + effective_date: employment.start_date + ? new Date(employment.start_date) + : null, + employment_type: this.mapEmploymentType(employment.hiring_type), + }; + } + + private mapPayPeriod(scale?: string): PayPeriod | undefined { + switch (scale?.toLowerCase()) { + case 'yearly': + return 'YEAR'; + case 'monthly': + return 'MONTH'; + case 'weekly': + return 'WEEK'; + case 'daily': + return 'DAY'; + case 'hourly': + return 'HOUR'; + default: + return undefined; + } + } + + private mapPayFrequency(scale?: string): PayFrequency | undefined { + switch (scale?.toLowerCase()) { + case 'yearly': + return 'ANNUALLY'; + case 'monthly': + return 'MONTHLY'; + case 'weekly': + return 'WEEKLY'; + case 'daily': + return 'WEEKLY'; // Assuming daily payment is done weekly + case 'hourly': + return 'WEEKLY'; // Assuming hourly payment is done weekly + default: + return undefined; + } + } + + private mapFlsaStatus(hiringType?: string): FlsaStatus | undefined { + switch (hiringType?.toLowerCase()) { + case 'employee': + return 'EXEMPT'; // Assuming employees are exempt + case 'contractor': + return 'NONEXEMPT'; // Assuming contractors are nonexempt + default: + return undefined; + } + } + + private mapEmploymentType(hiringType?: string): EmploymentType | undefined { + switch (hiringType?.toLowerCase()) { + case 'employee': + return 'FULL_TIME'; // Assuming employees are full-time + case 'contractor': + return 'CONTRACTOR'; + default: + return undefined; + } + } +} diff --git a/packages/api/src/hris/employment/services/deel/types.ts b/packages/api/src/hris/employment/services/deel/types.ts new file mode 100644 index 000000000..8b4ad5ae9 --- /dev/null +++ b/packages/api/src/hris/employment/services/deel/types.ts @@ -0,0 +1,50 @@ +export type DeelEmploymentOutput = Partial<{ + id: string; + name: string; + team: DeelTeam; + email: string; + state: string; + country: string; + payment: DeelPayment; + is_ended: boolean; + timezone: string; + job_title: string; + seniority: string; + start_date: string; // Date in string format + work_email: string; + hiring_type: string; + hiring_status: string; + completion_date: string; + contract_status: string; + voluntarily_left: string; + contract_coverage: string[]; + new_hiring_status: string; + client_legal_entity: DeelClientLegalEntity; + has_eor_termination: string; + contract_is_archived: boolean; + contract_has_contractor: boolean; + is_user_contract_deleted: boolean; + hris_direct_employee_invitation: string; +}>; + +export interface DeelTeam { + id: string; + name: string; +} + +export interface DeelPayment { + rate: number; + scale: string; + currency: string; + contract_name: string; +} +export interface DeelDepartment { + id: string; + name: string; + parent: string; +} + +export interface DeelClientLegalEntity { + id: string; + name: string; +} diff --git a/packages/api/src/hris/group/group.module.ts b/packages/api/src/hris/group/group.module.ts index 9e1bdaa36..78f6a35ee 100644 --- a/packages/api/src/hris/group/group.module.ts +++ b/packages/api/src/hris/group/group.module.ts @@ -9,6 +9,10 @@ import { CoreUnification } from '@@core/@core-services/unification/core-unificat import { GustoGroupMapper } from './services/gusto/mappers'; import { GustoService } from './services/gusto'; import { Utils } from '@hris/@lib/@utils'; +import { SageService } from './services/sage'; +import { SageGroupMapper } from './services/sage/mappers'; +import { DeelService } from './services/deel'; +import { DeelGroupMapper } from './services/deel/mappers'; @Module({ controllers: [GroupController], providers: [ @@ -20,8 +24,12 @@ import { Utils } from '@hris/@lib/@utils'; IngestDataService, GustoGroupMapper, Utils, + SageGroupMapper, + DeelGroupMapper, /* PROVIDERS SERVICES */ GustoService, + SageService, + DeelService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/group/services/deel/index.ts b/packages/api/src/hris/group/services/deel/index.ts new file mode 100644 index 000000000..f593070b1 --- /dev/null +++ b/packages/api/src/hris/group/services/deel/index.ts @@ -0,0 +1,66 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { IGroupService } from '@hris/group/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { DeelGroupOutput } from './types'; +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { OriginalGroupOutput } from '@@core/utils/types/original/original.hris'; + +@Injectable() +export class DeelService implements IGroupService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.group.toUpperCase() + ':' + DeelService.name, + ); + this.registry.registerService('deel', this); + } + addGroup( + groupData: DesunifyReturnType, + linkedUserId: string, + ): Promise> { + throw new Error('Method not implemented.'); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'deel', + vertical: 'hris', + }, + }); + + const resp = await axios.get(`${connection.account_url}/rest/v2/teams`, { + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': this.cryptoService.decrypt(connection.access_token), + }, + }); + this.logger.log(`Synced deel groups !`); + + return { + data: resp.data.data, + message: 'Deel groups retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/group/services/deel/mappers.ts b/packages/api/src/hris/group/services/deel/mappers.ts new file mode 100644 index 000000000..356c44695 --- /dev/null +++ b/packages/api/src/hris/group/services/deel/mappers.ts @@ -0,0 +1,61 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Utils } from '@hris/@lib/@utils'; +import { IGroupMapper } from '@hris/group/types'; +import { + UnifiedHrisGroupInput, + UnifiedHrisGroupOutput, +} from '@hris/group/types/model.unified'; +import { Injectable } from '@nestjs/common'; +import { DeelGroupOutput } from './types'; + +@Injectable() +export class DeelGroupMapper implements IGroupMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'group', 'deel', this); + } + + async desunify( + source: UnifiedHrisGroupInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: DeelGroupOutput | DeelGroupOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleGroupToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((group) => + this.mapSingleGroupToUnified(group, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleGroupToUnified( + group: DeelGroupOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: group.id, + remote_data: group, + name: group.name, + }; + } +} diff --git a/packages/api/src/hris/group/services/deel/types.ts b/packages/api/src/hris/group/services/deel/types.ts new file mode 100644 index 000000000..8964f34e3 --- /dev/null +++ b/packages/api/src/hris/group/services/deel/types.ts @@ -0,0 +1,4 @@ +export type DeelGroupOutput = { + id: string; + name: string; +}; diff --git a/packages/api/src/hris/group/services/sage/index.ts b/packages/api/src/hris/group/services/sage/index.ts new file mode 100644 index 000000000..08dff20ec --- /dev/null +++ b/packages/api/src/hris/group/services/sage/index.ts @@ -0,0 +1,66 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { IGroupService } from '@hris/group/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { SageGroupOutput } from './types'; +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { OriginalGroupOutput } from '@@core/utils/types/original/original.hris'; + +@Injectable() +export class SageService implements IGroupService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.group.toUpperCase() + ':' + SageService.name, + ); + this.registry.registerService('sage', this); + } + addGroup( + groupData: DesunifyReturnType, + linkedUserId: string, + ): Promise> { + throw new Error('Method not implemented.'); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'sage', + vertical: 'hris', + }, + }); + + const resp = await axios.get(`${connection.account_url}/api/teams`, { + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': this.cryptoService.decrypt(connection.access_token), + }, + }); + this.logger.log(`Synced sage groups !`); + + return { + data: resp.data.data, + message: 'Sage groups retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/group/services/sage/mappers.ts b/packages/api/src/hris/group/services/sage/mappers.ts new file mode 100644 index 000000000..c40072b9f --- /dev/null +++ b/packages/api/src/hris/group/services/sage/mappers.ts @@ -0,0 +1,61 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Utils } from '@hris/@lib/@utils'; +import { IGroupMapper } from '@hris/group/types'; +import { + UnifiedHrisGroupInput, + UnifiedHrisGroupOutput, +} from '@hris/group/types/model.unified'; +import { Injectable } from '@nestjs/common'; +import { SageGroupOutput } from './types'; + +@Injectable() +export class SageGroupMapper implements IGroupMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'group', 'sage', this); + } + + async desunify( + source: UnifiedHrisGroupInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: SageGroupOutput | SageGroupOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleGroupToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((group) => + this.mapSingleGroupToUnified(group, connectionId, customFieldMappings), + ), + ); + } + + private async mapSingleGroupToUnified( + group: SageGroupOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: String(group.id), + remote_data: group, + name: group.name, + }; + } +} diff --git a/packages/api/src/hris/group/services/sage/types.ts b/packages/api/src/hris/group/services/sage/types.ts new file mode 100644 index 000000000..dcd2d590b --- /dev/null +++ b/packages/api/src/hris/group/services/sage/types.ts @@ -0,0 +1,6 @@ +export type SageGroupOutput = { + id: number; + name: string; + manager_ids: string[]; + employee_ids: string[]; +}; diff --git a/packages/api/src/hris/location/location.module.ts b/packages/api/src/hris/location/location.module.ts index 443570585..c2f147465 100644 --- a/packages/api/src/hris/location/location.module.ts +++ b/packages/api/src/hris/location/location.module.ts @@ -9,6 +9,7 @@ import { CoreUnification } from '@@core/@core-services/unification/core-unificat import { Utils } from '@hris/@lib/@utils'; import { GustoLocationMapper } from './services/gusto/mappers'; import { GustoService } from './services/gusto'; +import { DeelLocationMapper } from './services/deel/mappers'; @Module({ controllers: [LocationController], providers: [ @@ -20,6 +21,7 @@ import { GustoService } from './services/gusto'; ServiceRegistry, IngestDataService, GustoLocationMapper, + DeelLocationMapper, /* PROVIDERS SERVICES */ GustoService, ], diff --git a/packages/api/src/hris/location/services/deel/mappers.ts b/packages/api/src/hris/location/services/deel/mappers.ts new file mode 100644 index 000000000..cecf44401 --- /dev/null +++ b/packages/api/src/hris/location/services/deel/mappers.ts @@ -0,0 +1,70 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Injectable } from '@nestjs/common'; +import { DeelLocationOutput } from './types'; +import { + UnifiedHrisLocationInput, + UnifiedHrisLocationOutput, +} from '@hris/location/types/model.unified'; +import { ILocationMapper } from '@hris/location/types'; +import { Utils } from '@hris/@lib/@utils'; + +@Injectable() +export class DeelLocationMapper implements ILocationMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'location', 'deel', this); + } + + async desunify( + source: UnifiedHrisLocationInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: DeelLocationOutput | DeelLocationOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleLocationToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((location) => + this.mapSingleLocationToUnified( + location, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleLocationToUnified( + location: DeelLocationOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: null, + remote_data: location, + street_1: location.streetAddress, + city: location.locality, + state: location.region, + zip_code: location.postalCode, + country: location.country, + location_type: location.type, + }; + } +} diff --git a/packages/api/src/hris/location/services/deel/types.ts b/packages/api/src/hris/location/services/deel/types.ts new file mode 100644 index 000000000..98db26aa7 --- /dev/null +++ b/packages/api/src/hris/location/services/deel/types.ts @@ -0,0 +1,8 @@ +export type DeelLocationOutput = Partial<{ + streetAddress: string; + locality: string; + region: string; + postalCode: string; + country: string; + type: 'WORK' | 'HOME'; +}>; diff --git a/packages/api/src/hris/timeoff/services/sage/index.ts b/packages/api/src/hris/timeoff/services/sage/index.ts new file mode 100644 index 000000000..c83e2d187 --- /dev/null +++ b/packages/api/src/hris/timeoff/services/sage/index.ts @@ -0,0 +1,69 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { ITimeoffService } from '@hris/timeoff/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { SageTimeoffOutput } from './types'; +import { DesunifyReturnType } from '@@core/utils/types/desunify.input'; +import { OriginalTimeoffOutput } from '@@core/utils/types/original/original.hris'; + +@Injectable() +export class SageService implements ITimeoffService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.timeoff.toUpperCase() + ':' + SageService.name, + ); + this.registry.registerService('sage', this); + } + addTimeoff( + timeoffData: DesunifyReturnType, + linkedUserId: string, + ): Promise> { + throw new Error('Method not implemented.'); + } + + async sync(data: SyncParam): Promise> { + try { + const { linkedUserId } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'sage', + vertical: 'hris', + }, + }); + + const resp = await axios.get( + `${connection.account_url}/api/leave-management/requests`, + { + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': this.cryptoService.decrypt(connection.access_token), + }, + }, + ); + this.logger.log(`Synced sage timeoffs !`); + + return { + data: resp.data.data, + message: 'Sage timeoffs retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/timeoff/services/sage/mappers.ts b/packages/api/src/hris/timeoff/services/sage/mappers.ts new file mode 100644 index 000000000..d8eeff03d --- /dev/null +++ b/packages/api/src/hris/timeoff/services/sage/mappers.ts @@ -0,0 +1,115 @@ +import { Injectable } from '@nestjs/common'; +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Utils } from '@hris/@lib/@utils'; +import { ITimeoffMapper } from '@hris/timeoff/types'; +import { + UnifiedHrisTimeoffInput, + UnifiedHrisTimeoffOutput, + Status, + RequestType, +} from '@hris/timeoff/types/model.unified'; +import { SageTimeoffOutput } from './types'; + +@Injectable() +export class SageTimeoffMapper implements ITimeoffMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService('hris', 'timeoff', 'sage', this); + } + + async desunify( + source: UnifiedHrisTimeoffInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + // Implementation for desunify (if needed) + return; + } + + async unify( + source: SageTimeoffOutput | SageTimeoffOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + if (!Array.isArray(source)) { + return this.mapSingleTimeoffToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((timeoff) => + this.mapSingleTimeoffToUnified( + timeoff, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleTimeoffToUnified( + timeoff: SageTimeoffOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + const employee = await this.utils.getEmployeeUuidFromRemoteId( + timeoff.employee_id.toString(), + connectionId, + ); + + return { + remote_id: timeoff.id.toString(), + remote_data: timeoff, + employee: employee, + status: this.mapStatus(timeoff.status_code), + employee_note: timeoff.details, + units: 'HOURS', + amount: timeoff.hours, + request_type: this.inferRequestType(timeoff.details), + start_time: new Date(timeoff.start_date), + end_time: new Date(timeoff.end_date), + }; + } + + private mapStatus(statusCode: string): Status { + switch (statusCode.toLowerCase()) { + case 'approved': + return 'APPROVED'; + case 'declined': + return 'DECLINED'; + case 'cancelled': + return 'CANCELLED'; + case 'deleted': + return 'DELETED'; + default: + return 'REQUESTED'; + } + } + + private inferRequestType(details: string): RequestType { + const lowerDetails = details.toLowerCase(); + if (lowerDetails.includes('vacation') || lowerDetails.includes('holiday')) { + return 'VACATION'; + } else if ( + lowerDetails.includes('sick') || + lowerDetails.includes('illness') + ) { + return 'SICK'; + } else if (lowerDetails.includes('jury')) { + return 'JURY_DUTY'; + } else if (lowerDetails.includes('bereavement')) { + return 'BEREAVEMENT'; + } else if (lowerDetails.includes('volunteer')) { + return 'VOLUNTEER'; + } else { + return 'PERSONAL'; + } + } +} diff --git a/packages/api/src/hris/timeoff/services/sage/types.ts b/packages/api/src/hris/timeoff/services/sage/types.ts new file mode 100644 index 000000000..f7b59bd94 --- /dev/null +++ b/packages/api/src/hris/timeoff/services/sage/types.ts @@ -0,0 +1,30 @@ +export interface SageTimeoffReplacement { + id: number; + full_name: string; +} + +export interface SageTimeoffField { + title: string; + answer: string; +} + +export type SageTimeoffOutput = Partial<{ + id: number; + status: string; + status_code: string; + policy_id: number; + employee_id: number; + replacement: SageTimeoffReplacement; + details: string; + is_multi_date: boolean; + is_single_day: boolean; + is_part_of_day: boolean; + first_part_of_day: boolean; + second_part_of_day: boolean; + start_date: string; + end_date: string; + request_date: string; + approval_date: string | null; + hours: number; + fields: SageTimeoffField[]; +}>; diff --git a/packages/api/src/hris/timeoff/timeoff.module.ts b/packages/api/src/hris/timeoff/timeoff.module.ts index 86d017aa9..0f66654af 100644 --- a/packages/api/src/hris/timeoff/timeoff.module.ts +++ b/packages/api/src/hris/timeoff/timeoff.module.ts @@ -7,6 +7,8 @@ import { IngestDataService } from '@@core/@core-services/unification/ingest-data import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; import { Utils } from '@hris/@lib/@utils'; +import { SageTimeoffMapper } from './services/sage/mappers'; +import { SageService } from './services/sage'; @Module({ controllers: [TimeoffController], providers: [ @@ -17,7 +19,9 @@ import { Utils } from '@hris/@lib/@utils'; WebhookService, ServiceRegistry, IngestDataService, + SageTimeoffMapper, /* PROVIDERS SERVICES */ + SageService, ], exports: [SyncService], }) diff --git a/packages/api/src/hris/timeoffbalance/services/sage/index.ts b/packages/api/src/hris/timeoffbalance/services/sage/index.ts new file mode 100644 index 000000000..61f928029 --- /dev/null +++ b/packages/api/src/hris/timeoffbalance/services/sage/index.ts @@ -0,0 +1,72 @@ +import { EncryptionService } from '@@core/@core-services/encryption/encryption.service'; +import { EnvironmentService } from '@@core/@core-services/environment/environment.service'; +import { LoggerService } from '@@core/@core-services/logger/logger.service'; +import { PrismaService } from '@@core/@core-services/prisma/prisma.service'; +import { ApiResponse } from '@@core/utils/types'; +import { SyncParam } from '@@core/utils/types/interface'; +import { HrisObject } from '@hris/@lib/@types'; +import { ITimeoffBalanceService } from '@hris/timeoffbalance/types'; +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { ServiceRegistry } from '../registry.service'; +import { SageTimeoffbalanceOutput } from './types'; + +@Injectable() +export class SageService implements ITimeoffBalanceService { + constructor( + private prisma: PrismaService, + private logger: LoggerService, + private cryptoService: EncryptionService, + private env: EnvironmentService, + private registry: ServiceRegistry, + ) { + this.logger.setContext( + HrisObject.timeoffbalance.toUpperCase() + ':' + SageService.name, + ); + this.registry.registerService('sage', this); + } + + async sync( + data: SyncParam, + ): Promise> { + try { + const { linkedUserId, id_employee } = data; + + const connection = await this.prisma.connections.findFirst({ + where: { + id_linked_user: linkedUserId, + provider_slug: 'sage', + vertical: 'hris', + }, + }); + + const employee = await this.prisma.hris_employees.findUnique({ + where: { + id_hris_employee: id_employee as string, + }, + select: { + remote_id: true, + }, + }); + + const resp = await axios.get( + `${connection.account_url}/api/employees/${employee.remote_id}/leave-management/balances`, + { + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': this.cryptoService.decrypt(connection.access_token), + }, + }, + ); + this.logger.log(`Synced sage timeoffbalances !`); + + return { + data: resp.data.data, + message: 'Sage timeoffbalances retrieved', + statusCode: 200, + }; + } catch (error) { + throw error; + } + } +} diff --git a/packages/api/src/hris/timeoffbalance/services/sage/mappers.ts b/packages/api/src/hris/timeoffbalance/services/sage/mappers.ts new file mode 100644 index 000000000..448a08afd --- /dev/null +++ b/packages/api/src/hris/timeoffbalance/services/sage/mappers.ts @@ -0,0 +1,73 @@ +import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry'; +import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; +import { IngestDataService } from '@@core/@core-services/unification/ingest-data.service'; +import { Utils } from '@hris/@lib/@utils'; +import { ITimeoffBalanceMapper } from '@hris/timeoffbalance/types'; +import { + UnifiedHrisTimeoffbalanceInput, + UnifiedHrisTimeoffbalanceOutput, +} from '@hris/timeoffbalance/types/model.unified'; +import { Injectable } from '@nestjs/common'; +import { SageTimeoffbalanceOutput } from './types'; + +@Injectable() +export class SageTimeoffbalanceMapper implements ITimeoffBalanceMapper { + constructor( + private mappersRegistry: MappersRegistry, + private utils: Utils, + private ingestService: IngestDataService, + private coreUnificationService: CoreUnification, + ) { + this.mappersRegistry.registerService( + 'hris', + 'timeoffbalance', + 'sage', + this, + ); + } + + async desunify( + source: UnifiedHrisTimeoffbalanceInput, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return; + } + + async unify( + source: SageTimeoffbalanceOutput | SageTimeoffbalanceOutput[], + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise< + UnifiedHrisTimeoffbalanceOutput | UnifiedHrisTimeoffbalanceOutput[] + > { + if (!Array.isArray(source)) { + return this.mapSingleTimeoffbalanceToUnified( + source, + connectionId, + customFieldMappings, + ); + } + return Promise.all( + source.map((timeoffbalance) => + this.mapSingleTimeoffbalanceToUnified( + timeoffbalance, + connectionId, + customFieldMappings, + ), + ), + ); + } + + private async mapSingleTimeoffbalanceToUnified( + timeoffbalance: SageTimeoffbalanceOutput, + connectionId: string, + customFieldMappings?: { slug: string; remote_id: string }[], + ): Promise { + return { + remote_id: null, + remote_data: timeoffbalance, + balance: timeoffbalance.available, + used: timeoffbalance.used, + }; + } +} diff --git a/packages/api/src/hris/timeoffbalance/services/sage/types.ts b/packages/api/src/hris/timeoffbalance/services/sage/types.ts new file mode 100644 index 000000000..b22f8a766 --- /dev/null +++ b/packages/api/src/hris/timeoffbalance/services/sage/types.ts @@ -0,0 +1,5 @@ +export type SageTimeoffbalanceOutput = Partial<{ + policy_id: number; + used: number; + available: number; +}>; diff --git a/packages/api/src/hris/timeoffbalance/timeoffbalance.module.ts b/packages/api/src/hris/timeoffbalance/timeoffbalance.module.ts index 106e1bdec..12f5213b3 100644 --- a/packages/api/src/hris/timeoffbalance/timeoffbalance.module.ts +++ b/packages/api/src/hris/timeoffbalance/timeoffbalance.module.ts @@ -7,6 +7,8 @@ import { IngestDataService } from '@@core/@core-services/unification/ingest-data import { WebhookService } from '@@core/@core-services/webhooks/panora-webhooks/webhook.service'; import { CoreUnification } from '@@core/@core-services/unification/core-unification.service'; import { Utils } from '@hris/@lib/@utils'; +import { SageTimeoffbalanceMapper } from './services/sage/mappers'; +import { SageService } from './services/sage'; @Module({ controllers: [TimeoffBalanceController], providers: [ @@ -17,7 +19,9 @@ import { Utils } from '@hris/@lib/@utils'; WebhookService, ServiceRegistry, IngestDataService, + SageTimeoffbalanceMapper, /* PROVIDERS SERVICES */ + SageService, ], exports: [SyncService], }) diff --git a/packages/shared/src/categories.ts b/packages/shared/src/categories.ts index 5cb99075e..00369f15b 100644 --- a/packages/shared/src/categories.ts +++ b/packages/shared/src/categories.ts @@ -7,7 +7,8 @@ export enum ConnectorCategory { MarketingAutomation = 'marketingautomation', FileStorage = 'filestorage', Productivity = 'productivity', - Ecommerce = 'ecommerce' + Ecommerce = 'ecommerce', + Cybersecurity = 'cybersecurity' } export const categoriesVerticals: ConnectorCategory[] = Object.values(ConnectorCategory); diff --git a/packages/shared/src/connectors/metadata.ts b/packages/shared/src/connectors/metadata.ts index 9f0368312..ccb733c2f 100644 --- a/packages/shared/src/connectors/metadata.ts +++ b/packages/shared/src/connectors/metadata.ts @@ -1110,6 +1110,20 @@ export const CONNECTORS_METADATA: ProvidersConfig = { strategy: AuthStrategy.oauth2 } }, + 'apollo': { + urls: { + docsUrl: 'https://apolloio.github.io/apollo-api-docs/?shell#introduction', + apiUrl: 'https://api.apollo.io' + }, + logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSM4trwPJLoj7tPkFYZG6TyMVzgCX1fn2zUyA&s', + description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', + active: false, + primaryColor: '#ffcf40', + authStrategy: { + strategy: AuthStrategy.api_key, + properties: ["api_key"] + } + }, 'hubspot_marketing_hub': { scopes: '', urls: { @@ -1240,7 +1254,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, 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, + active: true, primaryColor: '#4a3ead', authStrategy: { strategy: AuthStrategy.basic, @@ -1251,15 +1265,15 @@ export const CONNECTORS_METADATA: ProvidersConfig = { scopes: 'openid+email', urls: { docsUrl: 'https://documentation.bamboohr.com/docs/getting-started', - apiUrl: (companySubdomain) => `https://api.bamboohr.com/api/gateway.php/${companySubdomain}`, + apiUrl: (subdomain) => `https://api.bamboohr.com/api/gateway.php/${subdomain}`, }, logoPath: 'https://play-lh.googleusercontent.com/c4BW9wr_QAiIeVBYHhP7rs06w99xJzxgLvmL5I1mkucC3_ATMyL1t7Doz0_LQ0X-qS0', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', - active: true, + active: false, primaryColor: '#599D16', authStrategy: { strategy: AuthStrategy.basic, - properties: ['username', 'company_subdomain'] + properties: ['username', 'subdomain'] }, }, 'breezy': { @@ -1685,7 +1699,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { strategy: AuthStrategy.api_key } }, - 'sage_hr': { + 'sage': { scopes: '', urls: { docsUrl: '', @@ -2012,7 +2026,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = { }, logoPath: 'https://asset.brandfetch.io/id4NSNrRnG/idXzwlo3iL.jpeg', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', - active: false, + active: true, authStrategy: { strategy: AuthStrategy.oauth2 }, @@ -2449,11 +2463,13 @@ export const CONNECTORS_METADATA: ProvidersConfig = { docsUrl: '', apiUrl: '' }, - logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', + logoPath: 'https://assets.wheelhouse.com/media/_solution_logo_04042023_58844144.png', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', active: false, + primaryColor: '#F8A22D', authStrategy: { - strategy: AuthStrategy.api_key + strategy: AuthStrategy.api_key, + properties: ["api_key"] } }, 'payfit': { @@ -2549,18 +2565,19 @@ export const CONNECTORS_METADATA: ProvidersConfig = { strategy: AuthStrategy.api_key } }, - 'sage_hr': { - scopes: '', + 'sage': { urls: { - docsUrl: '', - apiUrl: '' + docsUrl: 'https://sagehr.docs.apiary.io/#reference', + apiUrl: (subdomain) => `https://${subdomain}.sage.hr` }, - logoPath: 'https://play-lh.googleusercontent.com/EMobDJKabP1eY_63QHgPS_-TK3eRfxXaeOnERbcRaWAw573iaV74pXS9xOv997dRZtM', + logoPath: 'https://appexchange.salesforce.com/partners/servlet/servlet.FileDownload?file=00P4V00000xPZsjUAG', description: 'Sync & Create contacts, deals, companies, notes, engagements, stages, tasks and users', - active: false, + active: true, + primaryColor: '#00d639', authStrategy: { - strategy: AuthStrategy.api_key - } + strategy: AuthStrategy.api_key, + properties: ["api_key", "subdomain"] + }, }, 'sap_successfactors': { scopes: '', @@ -2949,5 +2966,124 @@ export const CONNECTORS_METADATA: ProvidersConfig = { properties: ['username', 'password', 'store_url'] } }, + }, + 'cybersecurity': { + 'semgrep': { + urls: { + docsUrl: 'https://semgrep.dev/api/v1/docs/#section/Introduction', + apiUrl: 'https://semgrep.dev/api', + }, + logoPath: 'https://yt3.googleusercontent.com/NWVXYvuzHDgJJsbda7eyyz21Ba2qnq5WmuGrt9ax1rs6PP-mlDl5LCJ4ZO0Z2ZbiCq4ZoxqiGg=s900-c-k-c0x00ffffff-no-rj', + description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', + active: false, + primaryColor: '#10C096', + authStrategy: { + strategy: AuthStrategy.api_key, + properties: ['api_key'] + } + }, + 'snyk': { + scopes: '', + urls: { + docsUrl: 'https://docs.snyk.io/snyk-api/', + apiUrl: 'https://api.snyk.io', + authBaseUrl: 'https://app.snyk.io/oauth2/authorize' + }, + logoPath: 'https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1215%2Ffe4be452-1e68-444a-bf77-db21bf3a7bdc.png', + description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', + active: false, + authStrategy: { + strategy: AuthStrategy.oauth2 + }, + options: { + local_redirect_uri_in_https: true + } + }, + 'tenable': { + urls: { + docsUrl: 'https://developer.tenable.com/reference/navigate', + apiUrl: 'https://cloud.tenable.com', + }, + logoPath: 'https://pbs.twimg.com/profile_images/1410604377757216768/ocEKYniC_400x400.jpg', + description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', + active: false, + primaryColor: '#0D1E40', + authStrategy: { + strategy: AuthStrategy.basic, + properties: ['access_key', 'secret_key'] + } + }, + 'qualys': { + urls: { + docsUrl: 'https://docs.qualys.com/en/vm/api/scans/index.htm#t=get_started%2Fauthentication.htm', + apiUrl: (baseApi) => `https://${baseApi}/api` + }, + logoPath: 'https://companieslogo.com/img/orig/QLYS-68c2032c.png?t=1720244493', + description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', + active: false, + primaryColor: '#ED2E28', + authStrategy: { + strategy: AuthStrategy.basic, + properties: ['username', 'password', 'api_url'] + } + }, + 'rapid7insightvm': { + urls: { + docsUrl: 'https://help.rapid7.com/insightvm/en-us/api/index.html', + apiUrl: (region) => `https://${region}.api.insight.rapid7.com`, + }, + logoPath: 'https://images.saasworthy.com/insightvm_9113_logo_1635748346_lc0gr.png', + description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', + active: false, + primaryColor: '#E95722', + authStrategy: { + strategy: AuthStrategy.api_key, + properties: ['region', 'api_key'] + } + }, + 'crowdstrike': { + scopes: '', + urls: { + docsUrl: 'https://developer.crowdstrike.com/', + apiUrl: (dotHost) => `https://api${dotHost}.crowdstrike.com`, + authBaseUrl: '' + }, + logoPath: 'https://pbs.twimg.com/profile_images/1451022302578049024/6L-zG5oq_400x400.jpg', + description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', + active: false, + primaryColor: '#FC0001', + authStrategy: { + strategy: AuthStrategy.oauth2, + } + }, + 'sentinelone': { + urls: { + docsUrl: 'https://www.postman.com/api-evangelist/sentinelone/overview', + apiUrl: (host) => `https://${host}.sentinelone.net`, + }, + logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZHLye2za7fjLiggqC1upKhhM3T-laySJSLQ&s', + description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', + active: false, + primaryColor: '#522E74', + authStrategy: { + strategy: AuthStrategy.api_key, + properties: ['host', 'api_key'] + } + }, + 'microsoftdefender': { + scopes: '', + urls: { + docsUrl: 'https://learn.microsoft.com/en-us/defender-endpoint/api/apis-intro', + apiUrl: '', + authBaseUrl: '' + }, + logoPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSuR4GZElDP7UNKhXS9jDGpElBTdchjg8hSsA&s', + description: 'Sync & Create orders, fulfillments, fulfillment orders, customers and products', + active: false, + primaryColor: '#0078D8', + authStrategy: { + strategy: AuthStrategy.oauth2, + } + }, } };