Skip to content

Commit

Permalink
Merge pull request panoratech#637 from panoratech/feat/amazon-integra…
Browse files Browse the repository at this point in the history
…tion

✨ Amazon integ start
  • Loading branch information
naelob authored Aug 10, 2024
2 parents 84d7bea + 52f7133 commit 77fc6f9
Show file tree
Hide file tree
Showing 13 changed files with 740 additions and 18 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ BOX_FILESTORAGE_CLOUD_CLIENT_ID=
BOX_FILESTORAGE_CLOUD_CLIENT_SECRET=



# ================================================
# Webapp settings
# Must be set in the perspective of the end user browser
Expand Down
2 changes: 1 addition & 1 deletion apps/magic-link/src/lib/ProviderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ interface IBasicAuthFormData {

const domainFormats: { [key: string]: string } = {
microsoftdynamicssales: 'YOURORGNAME.api.crm12.dynamics.com',
bigcommerce: 'If your api domain is https://api.bigcommerce.com/stores/eubckcvkzg/v3 then store_hash is eubckcvkzg'
bigcommerce: 'If your api domain is https://api.bigcommerce.com/stores/eubckcvkzg/v3 then store_hash is eubckcvkzg',
};

const ProviderModal = () => {
Expand Down
10 changes: 10 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,22 @@ services:
IRONCLAD_TICKETING_CLOUD_CLIENT_SECRET: ${IRONCLAD_TICKETING_CLOUD_CLIENT_SECRET}
SHOPIFY_ECOMMERCE_CLOUD_CLIENT_ID: ${SHOPIFY_ECOMMERCE_CLOUD_CLIENT_ID}
SHOPIFY_ECOMMERCE_CLOUD_CLIENT_SECRET: ${SHOPIFY_ECOMMERCE_CLOUD_CLIENT_SECRET}
SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_ID: ${SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_ID}
SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_SECRET: ${SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_SECRET}
EBAY_ECOMMERCE_CLOUD_CLIENT_ID: ${EBAY_ECOMMERCE_CLOUD_CLIENT_ID}
EBAY_ECOMMERCE_CLOUD_CLIENT_SECRET: ${EBAY_ECOMMERCE_CLOUD_CLIENT_SECRET}
EBAY_ECOMMERCE_CLOUD_SUBDOMAIN: ${EBAY_ECOMMERCE_CLOUD_SUBDOMAIN}
FAIRE_ECOMMERCE_CLOUD_CLIENT_ID: ${FAIRE_ECOMMERCE_CLOUD_CLIENT_ID}
FAIRE_ECOMMERCE_CLOUD_CLIENT_SECRET: ${FAIRE_ECOMMERCE_CLOUD_CLIENT_SECRET}
WEBFLOW_ECOMMERCE_CLOUD_CLIENT_ID: ${WEBFLOW_ECOMMERCE_CLOUD_CLIENT_ID}
WEBFLOW_ECOMMERCE_CLOUD_CLIENT_SECRET: ${WEBFLOW_ECOMMERCE_CLOUD_CLIENT_SECRET}
EMAIL_SENDING_ADDRESS: ${EMAIL_SENDING_ADDRESS}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_USER: ${SMTP_USER}
SMTP_PASSWORD: ${SMTP_PASSWORD}


restart: unless-stopped
ports:
- 3000:3000
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ services:
IRONCLAD_TICKETING_CLOUD_CLIENT_SECRET: ${IRONCLAD_TICKETING_CLOUD_CLIENT_SECRET}
SHOPIFY_ECOMMERCE_CLOUD_CLIENT_ID: ${SHOPIFY_ECOMMERCE_CLOUD_CLIENT_ID}
SHOPIFY_ECOMMERCE_CLOUD_CLIENT_SECRET: ${SHOPIFY_ECOMMERCE_CLOUD_CLIENT_SECRET}
SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_ID: ${SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_ID}
SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_SECRET: ${SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_SECRET}
EBAY_ECOMMERCE_CLOUD_CLIENT_ID: ${EBAY_ECOMMERCE_CLOUD_CLIENT_ID}
EBAY_ECOMMERCE_CLOUD_CLIENT_SECRET: ${EBAY_ECOMMERCE_CLOUD_CLIENT_SECRET}
EBAY_ECOMMERCE_CLOUD_SUBDOMAIN: ${EBAY_ECOMMERCE_CLOUD_SUBDOMAIN}
FAIRE_ECOMMERCE_CLOUD_CLIENT_ID: ${FAIRE_ECOMMERCE_CLOUD_CLIENT_ID}
FAIRE_ECOMMERCE_CLOUD_CLIENT_SECRET: ${FAIRE_ECOMMERCE_CLOUD_CLIENT_SECRET}
WEBFLOW_ECOMMERCE_CLOUD_CLIENT_ID: ${WEBFLOW_ECOMMERCE_CLOUD_CLIENT_ID}
WEBFLOW_ECOMMERCE_CLOUD_CLIENT_SECRET: ${WEBFLOW_ECOMMERCE_CLOUD_CLIENT_SECRET}
EMAIL_SENDING_ADDRESS: ${EMAIL_SENDING_ADDRESS}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ services:
IRONCLAD_TICKETING_CLOUD_CLIENT_SECRET: ${IRONCLAD_TICKETING_CLOUD_CLIENT_SECRET}
SHOPIFY_ECOMMERCE_CLOUD_CLIENT_ID: ${SHOPIFY_ECOMMERCE_CLOUD_CLIENT_ID}
SHOPIFY_ECOMMERCE_CLOUD_CLIENT_SECRET: ${SHOPIFY_ECOMMERCE_CLOUD_CLIENT_SECRET}
SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_ID: ${SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_ID}
SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_SECRET: ${SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_SECRET}
EBAY_ECOMMERCE_CLOUD_CLIENT_ID: ${EBAY_ECOMMERCE_CLOUD_CLIENT_ID}
EBAY_ECOMMERCE_CLOUD_CLIENT_SECRET: ${EBAY_ECOMMERCE_CLOUD_CLIENT_SECRET}
EBAY_ECOMMERCE_CLOUD_SUBDOMAIN: ${EBAY_ECOMMERCE_CLOUD_SUBDOMAIN}
FAIRE_ECOMMERCE_CLOUD_CLIENT_ID: ${FAIRE_ECOMMERCE_CLOUD_CLIENT_ID}
FAIRE_ECOMMERCE_CLOUD_CLIENT_SECRET: ${FAIRE_ECOMMERCE_CLOUD_CLIENT_SECRET}
WEBFLOW_ECOMMERCE_CLOUD_CLIENT_ID: ${WEBFLOW_ECOMMERCE_CLOUD_CLIENT_ID}
WEBFLOW_ECOMMERCE_CLOUD_CLIENT_SECRET: ${WEBFLOW_ECOMMERCE_CLOUD_CLIENT_SECRET}
EMAIL_SENDING_ADDRESS: ${EMAIL_SENDING_ADDRESS}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
Expand Down
24 changes: 18 additions & 6 deletions packages/api/src/@core/connections/connections.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,33 +83,45 @@ export class ConnectionsController {
}

let stateData: StateDataType;
if (state.includes(':')) {

// Step 1: Check for HTML entities
if (state.includes('"') || state.includes('&')) {
// Step 2: Replace HTML entities
const decodedState = state
.replace(/"/g, '"') // Replace " with "
.replace(/&/g, '&'); // Replace & with &

// Step 3: Parse the JSON
stateData = JSON.parse(decodedState);
console.log(stateData);
} else if (state.includes(':')) {
// squarespace asks for a random alphanumeric value
// Split the random part and the base64 part
const [randomPart, base64Part] = decodeURIComponent(state).split(':');
// Decode the base64 part to get the original JSON
const jsonString = Buffer.from(base64Part, 'base64').toString('utf-8');
stateData = JSON.parse(jsonString);
} else {
// Handle the regular encoded URI case
const decoded = decodeURIComponent(state);
stateData = JSON.parse(decoded);
// If no HTML entities are present, parse directly
stateData = JSON.parse(state);
console.log(stateData);
}

const {
projectId,
vertical,
linkedUserId,
providerName,
returnUrl,
resource,
...dynamicParams
} = stateData;

const service = this.categoryConnectionRegistry.getService(
vertical.toLowerCase(),
);
await service.handleCallBack(
providerName,
{ linkedUserId, projectId, code, otherParams, resource },
{ linkedUserId, projectId, code, otherParams, ...dynamicParams },
'oauth2',
);
if (providerName == 'shopify') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { WoocommerceConnectionService } from './services/woocommerce/woocommerce
import { SquarespaceConnectionService } from './services/squarespace/squarespace.service';
import { BigcommerceConnectionService } from './services/bigcommerce/bigcommerce.service';
import { EbayConnectionService } from './services/ebay/ebay.service';
import { WebflowConnectionService } from './services/webflow/webflow.service';
import { FaireConnectionService } from './services/faire/faire.service';
import { MercadolibreConnectionService } from './services/mercadolibre/mercadolibre.service';

@Module({
imports: [WebhookModule, BullQueueModule],
Expand All @@ -26,6 +29,9 @@ import { EbayConnectionService } from './services/ebay/ebay.service';
SquarespaceConnectionService,
BigcommerceConnectionService,
EbayConnectionService,
WebflowConnectionService,
FaireConnectionService,
MercadolibreConnectionService,
],
exports: [EcommerceConnectionsService],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,15 @@ export class EbayConnectionService extends AbstractBaseConnectionService {
vertical: 'ecommerce',
},
});
if (isNotUnique) return;
//reconstruct the redirect URI that was passed in the frontend it must be the same
const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`;

const CREDENTIALS = (await this.cService.getCredentials(
projectId,
this.type,
)) as OAuth2AuthData;

const formData = new URLSearchParams({
grant_type: 'authorization_code',
redirect_uri: REDIRECT_URI,
redirect_uri: CREDENTIALS.SUBDOMAIN,
code: code,
});
const res = await axios.post(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
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 FaireOAuthResponse {
accessToken: string;
tokenType: string;
}

@Injectable()
export class FaireConnectionService 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(FaireConnectionService.name);
this.registry.registerService('faire', this);
this.type = providerToType('faire', 'ecommerce', AuthStrategy.oauth2);
}

async passthrough(
input: PassthroughInput,
connectionId: string,
): Promise<PassthroughResponse> {
try {
const { headers } = input;
const config = await this.constructPassthrough(input, connectionId);

const connection = await this.prisma.connections.findUnique({
where: {
id_connection: connectionId,
},
});

const CREDENTIALS = (await this.cService.getCredentials(
connection.id_project,
this.type,
)) as OAuth2AuthData;

const access_token = JSON.parse(
this.cryptoService.decrypt(connection.access_token),
);
config.headers = {
...config.headers,
...headers,
'X-FAIRE-OAUTH-ACCESS-TOKEN': access_token,
'X-FAIRE-APP-CREDENTIALS': Buffer.from(
`${CREDENTIALS.CLIENT_ID}:${CREDENTIALS.CLIENT_SECRET}`,
).toString('base64'),
};

return await this.retryService.makeRequest(
{
method: config.method,
url: config.url,
data: config.data,
headers: config.headers,
},
'ecommerce.faire.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: 'faire',
vertical: 'ecommerce',
},
});
if (isNotUnique) return;
//reconstruct the redirect URI that was passed in the frontend it must be the same
const REDIRECT_URI = `${this.env.getPanoraBaseUrl()}/connections/oauth/callback`;

const CREDENTIALS = (await this.cService.getCredentials(
projectId,
this.type,
)) as OAuth2AuthData;

const formData = new URLSearchParams({
redirect_url: REDIRECT_URI,
applicationId: CREDENTIALS.CLIENT_ID,
applicationSecret: CREDENTIALS.CLIENT_SECRET,
scope: CONNECTORS_METADATA['ecommerce']['faire'].scopes,
authorization_code: code,
grant_type: 'AUTHORIZATION_CODE',
});
const res = await axios.post(
'https://www.faire.com/api/external-api-oauth2/token',
formData.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
},
);
const data: FaireOAuthResponse = res.data;
// save tokens for this customer inside our db
let db_res;
const connection_token = uuidv4();
const BASE_API_URL = CONNECTORS_METADATA['ecommerce']['faire'].urls
.apiUrl as string;

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.accessToken),
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: 'faire',
vertical: 'ecommerce',
token_type: 'oauth2',
account_url: BASE_API_URL,
access_token: this.cryptoService.encrypt(data.accessToken),
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;
}
}
Loading

0 comments on commit 77fc6f9

Please sign in to comment.