Skip to content

Commit

Permalink
Merge pull request #61 from panoratech/connections-auth
Browse files Browse the repository at this point in the history
feat: start auth core
  • Loading branch information
naelob authored Nov 10, 2023
2 parents aa716f3 + cd6ac4f commit df02752
Show file tree
Hide file tree
Showing 21 changed files with 1,032 additions and 10 deletions.
22 changes: 21 additions & 1 deletion packages/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
ENV=dev
PROD_DISTRIBUTION=managed # could be self-host if you want to run it locally
DATABASE_URL=
JWT_SECRET="SECRET"
POSTGRES_HOST=
POSTGRES_HOST=

# Sentry for logging errors
SENTRY_DSN=your_sentry_dsn_here

# INTEGRATIONS PROVIDER CREDENTIALS FOR PANORA
# CRM
HUBSPOT_CLIENT_ID=
HUBSPOT_CLIENT_SECRET=
ZOHOCRM_CLIENT_ID=
ZOHOCRM_CLIENT_SECRET=
PIPEDRIVE_CLIENT_ID=
PIPEDRIVE_CLIENT_SECRET=
FRESHSALES_CLIENT_ID=
FRESHSALES_CLIENT_SECRET=
ZENDESK_CLIENT_ID=
ZENDESK_CLIENT_SECRET=


OAUTH_REDIRECT_BASE='http://localhost:3000'
3 changes: 3 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.14",
"@prisma/client": "^5.4.2",
"@sentry/node": "^7.80.0",
"@sentry/tracing": "^7.80.0",
"axios": "^1.5.1",
"bcrypt": "^5.1.1",
"crypto": "^1.0.1",
Expand Down
43 changes: 39 additions & 4 deletions packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ datasource db {
model api_keys {
id_api_key BigInt @id(map: "id_") @default(autoincrement())
api_key_hash String @unique(map: "unique_api_keys")
id_project Int
id_user Int
id_project BigInt
projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_7")
users users @relation(fields: [id_user], references: [id_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_8")
@@index([id_project], map: "fk_1")
@@index([id_user], map: "fk_2")
@@index([id_project], map: "fk_api_keys_projects")
}

model crm_contact_email_addresses {
Expand Down Expand Up @@ -81,11 +81,13 @@ model organizations {
}

model projects {
id_project Int @id(map: "pk_projects") @default(autoincrement())
id_project BigInt @id(map: "pk_projects") @default(autoincrement())
name String
id_organization BigInt
api_keys api_keys[]
organizations organizations @relation(fields: [id_organization], references: [id_organization], onDelete: NoAction, onUpdate: NoAction, map: "fk_6")
connections connections[]
linked_users linked_users[]
organizations organizations @relation(fields: [id_organization], references: [id_organization], onDelete: NoAction, onUpdate: NoAction, map: "fk_6")
@@index([id_organization], map: "fk_1_projects")
}
Expand All @@ -105,3 +107,36 @@ model users {
@@index([id_organization], map: "fk_1_users")
}

/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model connections {
id_connection BigInt @id(map: "pk_connections") @default(autoincrement())
provider_slug String
account_url String?
token_type String
access_token String?
refresh_token String?
expiration_timestamp DateTime? @db.Timestamp(6)
created_at DateTime @db.Timestamp(6)
id_project BigInt
id_linked_user BigInt
linked_users linked_users @relation(fields: [id_linked_user], references: [id_linked_user], onDelete: NoAction, onUpdate: NoAction, map: "fk_11")
projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_9")
@@unique([access_token, refresh_token], map: "index_3")
@@index([id_project], map: "fk_1")
@@index([id_linked_user], map: "fk_connections_to_linkedusersid")
}

/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model linked_users {
id_linked_user BigInt @id(map: "key_id_linked_users") @default(autoincrement())
linked_user_origin_id String
alias String
status String
id_project BigInt
connections connections[]
projects projects @relation(fields: [id_project], references: [id_project], onDelete: NoAction, onUpdate: NoAction, map: "fk_10")
@@index([id_project], map: "fk_proectid_linked_users")
}
20 changes: 20 additions & 0 deletions packages/api/src/@core/connections/connections.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConnectionsController } from './connections.controller';
import { ConnectionsService } from './services/connections.service';

describe('ConnectionsController', () => {
let controller: ConnectionsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ConnectionsController],
providers: [ConnectionsService],
}).compile();

controller = module.get<ConnectionsController>(ConnectionsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
30 changes: 30 additions & 0 deletions packages/api/src/@core/connections/connections.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Controller, Get, Query, Res } from '@nestjs/common';
import { ConnectionsService } from './services/connections.service';
import { Response } from 'express'; // Importing the Express Response type for type checking

@Controller('connections')
export class ConnectionsController {
constructor(private readonly connectionsService: ConnectionsService) {}

@Get('oauth/crm/callback')
handleCRMCallback(
@Res() res: Response,
@Query('projectId') projectId: string,
@Query('linkedUserId') linkedUserId: string,
@Query('providerName') providerName: string,
@Query('returnUrl') returnUrl: string,
@Query('code') code: string,
@Query('accountURL') zohoAccountURL?: string,
) {
//TODO; ADD VERIFICATION OF PARAMS

this.connectionsService.handleCRMCallBack(
projectId,
linkedUserId,
providerName,
code,
zohoAccountURL,
);
res.redirect(returnUrl);
}
}
11 changes: 11 additions & 0 deletions packages/api/src/@core/connections/connections.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ConnectionsService } from './services/connections.service';
import { ConnectionsController } from './connections.controller';
import { CrmConnectionsService } from './services/crm/crm-connection.service';
import { PrismaService } from '../prisma/prisma.service';

@Module({
controllers: [ConnectionsController],
providers: [ConnectionsService, CrmConnectionsService, PrismaService],
})
export class ConnectionsModule {}
24 changes: 24 additions & 0 deletions packages/api/src/@core/connections/dto/create-connection.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class BaseConnectionDto {
customerId: string;
providerName: string;
}

interface OAuth {
projectId: string;
returnUrl: string;
}

interface ApiKey {
apiKey: string;
}

interface AccessKeys {
accessKeyId: string;
secretAccessKey: string;
}

export type CreateConnectionDto<T> = BaseConnectionDto & T;

export type CreateConnectionDtoOauth = CreateConnectionDto<OAuth>;
export type CreateConnectionDtoApiKey = CreateConnectionDto<ApiKey>;
export type CreateConnectionDtoAccessKeys = CreateConnectionDto<AccessKeys>;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class Connection {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConnectionsService } from './connections.service';

describe('ConnectionsService', () => {
let service: ConnectionsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ConnectionsService],
}).compile();

service = module.get<ConnectionsService>(ConnectionsService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
125 changes: 125 additions & 0 deletions packages/api/src/@core/connections/services/connections.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Injectable } from '@nestjs/common';
import { CrmConnectionsService } from './crm/crm-connection.service';
import { NotFoundError } from 'src/@core/utils/errors';

@Injectable()
export class ConnectionsService {
//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
constructor(private crmConnectionService: CrmConnectionsService) {}

async handleCRMCallBack(
projectId: string,
linkedUserId: string,
providerName: string,
code: string,
zohoAccountURL?: string,
) {
try {
switch (providerName) {
case 'hubspot':
if (!code) {
throw new NotFoundError('no hubspot code found');
}
return this.crmConnectionService.handleHubspotCallback(
linkedUserId,
projectId,
code,
);
case 'zoho':
if (!code || !zohoAccountURL) {
throw new NotFoundError('no zoho code/ zoho AccountURL found');
}
return this.crmConnectionService.handleZohoCallback(
linkedUserId,
projectId,
code,
zohoAccountURL,
);
case 'pipedrive':
if (!code) {
throw new NotFoundError('no pipedrive code found');
}
return this.crmConnectionService.handlePipedriveCallback(
linkedUserId,
projectId,
code,
);
case 'freshsales':
//todo: LATER
break;
case 'zendesk':
if (!code) {
throw new NotFoundError('no zendesk code found');
}
return this.crmConnectionService.handleZendeskCallback(
linkedUserId,
projectId,
code,
);
default:
return;
}
} catch (error) {
if (error instanceof NotFoundError) {
console.log(error);
}
return error;
}
}

async handleCRMTokensRefresh(
connectionId: bigint,
providerId: string,
refresh_token: string,
account_url?: string,
) {
try {
switch (providerId) {
case 'hubspot':
return this.crmConnectionService.handleHubspotTokenRefresh(
connectionId,
refresh_token,
);
case 'zoho':
return this.crmConnectionService.handleZohoTokenRefresh(
connectionId,
refresh_token,
account_url,
);
case 'pipedrive':
return this.crmConnectionService.handlePipedriveTokenRefresh(
connectionId,
refresh_token,
);
case 'freshsales':
//todo: LATER
break;
case 'zendesk':
return this.crmConnectionService.handleZendeskTokenRefresh(
connectionId,
refresh_token,
);
default:
return;
}
} catch (error) {
if (error instanceof NotFoundError) {
console.log(error);
}
return error;
}
}
}
Loading

0 comments on commit df02752

Please sign in to comment.