Skip to content

Commit

Permalink
🐛 Added shopify oauth
Browse files Browse the repository at this point in the history
  • Loading branch information
naelob committed Jul 18, 2024
1 parent 6caab6f commit b6f5a6a
Show file tree
Hide file tree
Showing 70 changed files with 3,006 additions and 1,287 deletions.
3 changes: 2 additions & 1 deletion packages/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,7 @@ model connector_sets {
tcg_front Boolean?
crm_zendesk Boolean?
crm_close Boolean?
fs_box Boolean?
projects projects[]
}

Expand Down Expand Up @@ -1218,7 +1219,7 @@ model ecom_customers {
id_connection String @db.Uuid
ecom_customer_addresses ecom_customer_addresses[]
ecom_orders ecom_orders[]
}
}

/// 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 ecom_fulfilments {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FileStorageObject } from '@filestorage/@lib/@types';
import { HrisObject } from '@hris/@lib/@types';
import { MarketingAutomationObject } from '@marketingautomation/@lib/@types';
import { Injectable } from '@nestjs/common';
import { ConnectorCategory } from '@panora/shared';
import { ConnectorCategory, EcommerceObject } from '@panora/shared';
import { TicketingObject } from '@ticketing/@lib/@types';
import { TargetObject, Unified, UnifyReturnType } from '../../utils/types';
import { DesunifyReturnType } from '../../utils/types/desunify.input';
Expand Down
16 changes: 8 additions & 8 deletions packages/api/src/@core/connections/@utils/types.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
type CommonCallbackParams = {
export type OAuthCallbackParams = {
projectId: string;
linkedUserId: string;
code: string;
[key: string]: any;
};

export type APIKeyCallbackParams = CommonCallbackParams & {
export type APIKeyCallbackParams = {
projectId: string;
linkedUserId: string;
apikey: string;
body_data?: { [key: string]: any };
};

// Define the specific callback parameters for OAUTH
export type OAuthCallbackParams = CommonCallbackParams & {
code: string;
location?: string; // for zoho
};

// Define the discriminated union type for callback parameters
export type CallbackParams = APIKeyCallbackParams | OAuthCallbackParams;

Expand All @@ -38,4 +36,6 @@ export interface IConnectionCategory {
id_project: string,
account_url?: string,
): Promise<void>;

redirectUponConnection?(...params: any[]): void;
}
11 changes: 8 additions & 3 deletions packages/api/src/@core/connections/connections.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class ConnectionsController {
@Get('oauth/callback')
async handleOAuthCallback(@Res() res: Response, @Query() query: any) {
try {
const { state, code, location } = query;
const { state, code, ...otherParams } = query;
if (!code) {
throw new ConnectionsError({
name: 'OAUTH_CALLBACK_CODE_NOT_FOUND_ERROR',
Expand All @@ -81,11 +81,16 @@ export class ConnectionsController {
);
await service.handleCallBack(
providerName,
{ linkedUserId, projectId, code, location },
{ linkedUserId, projectId, code, otherParams },
'oauth',
);

res.redirect(returnUrl);
if (providerName == 'shopify') {
// we must redirect using shop and host to get a valid session on shopify server
service.redirectUponConnection(res, otherParams);
} else {
res.redirect(`/`);
}

/*if (
CONNECTORS_METADATA[vertical.toLowerCase()][providerName.toLowerCase()]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,97 @@
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 { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service';
import { ConnectionUtils } from '@@core/connections/@utils';
import { APIKeyCallbackParams } from '@@core/connections/@utils/types';
import { OAuthCallbackParams } from '@@core/connections/@utils/types';
import { Injectable } from '@nestjs/common';
import { CONNECTORS_METADATA } from '@panora/shared';
import {
AuthStrategy,
CONNECTORS_METADATA,
DynamicApiUrl,
OAuth2AuthData,
providerToType,
} from '@panora/shared';
import { v4 as uuidv4 } from 'uuid';
import { IEcommerceConnectionService } from '../../types';
import { ServiceRegistry } from '../registry.service';
import axios from 'axios';

export type ShopifyOAuthResponse = {
access_token: string;
scope: string;
};

@Injectable()
export class ShopifyConnectionService implements IEcommerceConnectionService {
private readonly type: string;

constructor(
private prisma: PrismaService,
private logger: LoggerService,
private cryptoService: EncryptionService,
private registry: ServiceRegistry,
private connectionUtils: ConnectionUtils,
private cService: ConnectionsStrategiesService,
) {
this.logger.setContext(ShopifyConnectionService.name);
this.registry.registerService('ashby', this);
this.registry.registerService('shopify', this);
this.type = providerToType('shopify', 'ecommerce', AuthStrategy.oauth2);
}

async handleCallback(opts: APIKeyCallbackParams) {
async handleCallback(opts: OAuthCallbackParams) {
try {
const { linkedUserId, projectId } = opts;
const { linkedUserId, projectId, code, hmac, shop } = opts;
const isNotUnique = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'ashby',
provider_slug: 'shopify',
vertical: 'ecommerce',
},
});

const shopRegex = /^[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com/;

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

if (!shopRegex.test(shop)) {
throw new Error('Invalid shop received through shopify request');
}

//todo: check hmac

const formData = new URLSearchParams({
code: code,
client_id: CREDENTIALS.CLIENT_ID,
client_secret: CREDENTIALS.CLIENT_SECRET,
});
const res = await axios.post(
`https://${shop}.myshopify.com/admin/oauth/access_token`,
formData.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
},
);
const data: ShopifyOAuthResponse = res.data;

let db_res;
const connection_token = uuidv4();

const BASE_API_URL = (
CONNECTORS_METADATA['ecommerce']['shopify'].urls.apiUrl as DynamicApiUrl
)(shop);
if (isNotUnique) {
db_res = await this.prisma.connections.update({
where: {
id_connection: isNotUnique.id_connection,
},
data: {
access_token: this.cryptoService.encrypt(opts.apikey),
account_url: CONNECTORS_METADATA['ecommerce']['ashby'].urls
.apiUrl as string,
access_token: this.cryptoService.encrypt(data.access_token),
account_url: BASE_API_URL,
status: 'valid',
created_at: new Date(),
},
Expand All @@ -54,12 +101,11 @@ export class ShopifyConnectionService implements IEcommerceConnectionService {
data: {
id_connection: uuidv4(),
connection_token: connection_token,
provider_slug: 'ashby',
provider_slug: 'shopify',
vertical: 'ecommerce',
token_type: 'api_key',
account_url: CONNECTORS_METADATA['ecommerce']['ashby'].urls
.apiUrl as string,
access_token: this.cryptoService.encrypt(opts.apikey),
token_type: 'oauth',
account_url: BASE_API_URL,
access_token: this.cryptoService.encrypt(data.access_token),
status: 'valid',
created_at: new Date(),
projects: {
Expand All @@ -81,4 +127,10 @@ export class ShopifyConnectionService implements IEcommerceConnectionService {
throw error;
}
}

redirectUponConnection(...params: any[]): void {
const [{ res, host, shop }] = params;

return res.redirect(`/?shop=${shop}&host=${encodeURIComponent(host)}`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import { connections as Connection } from '@prisma/client';
export interface IEcommerceConnectionService {
handleCallback(opts: CallbackParams): Promise<Connection>;
handleTokenRefresh?(opts: RefreshParams): Promise<any>;
redirectUponConnection?(...params: any[]): void;
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';
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 { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service';
import { ConnectionUtils } from '@@core/connections/@utils';
import {
Action,
ActionType,
ConnectionsError,
format3rdPartyError,
throwTypedError,
} from '@@core/utils/errors';
import { LoggerService } from '@@core/@core-services/logger/logger.service';
import { v4 as uuidv4 } from 'uuid';
import { EnvironmentService } from '@@core/@core-services/environment/environment.service';
import { EncryptionService } from '@@core/@core-services/encryption/encryption.service';
import { IFilestorageConnectionService } from '../../types';
import { ServiceRegistry } from '../registry.service';
OAuthCallbackParams,
RefreshParams,
} from '@@core/connections/@utils/types';
import { Injectable } from '@nestjs/common';
import {
AuthStrategy,
CONNECTORS_METADATA,
OAuth2AuthData,
providerToType,
} from '@panora/shared';
import { ConnectionsStrategiesService } from '@@core/connections-strategies/connections-strategies.service';
import { ConnectionUtils } from '@@core/connections/@utils';
import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard';
import {
OAuthCallbackParams,
RefreshParams,
} from '@@core/connections/@utils/types';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { IFilestorageConnectionService } from '../../types';
import { ServiceRegistry } from '../registry.service';

export type BoxOAuthResponse = {
access_token: string;
Expand Down Expand Up @@ -86,9 +78,6 @@ export class BoxConnectionService implements IFilestorageConnectionService {
},
);
const data: BoxOAuthResponse = res.data;
this.logger.log(
'OAuth credentials : box filestorage ' + JSON.stringify(data),
);

let db_res;
const connection_token = uuidv4();
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/@core/utils/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
UnifiedMarketingAutomation,
} from '@marketingautomation/@lib/@types';
import { UnifiedEcommerce } from '@ecommerce/@lib/@types';
import { EcommerceObject } from '@panora/shared';

export type Unified =
| UnifiedCrm
Expand Down
25 changes: 5 additions & 20 deletions packages/api/src/@core/utils/types/original/original.ecommerce.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
/* INPUT */

import {
ShopifyProductInput,
ShopifyProductOutput,
} from '@ecommerce/product/services/shopify/types';
import {
ShopifyOrderInput,
ShopifyOrderOutput,
} from '@ecommerce/order/services/shopify/types';
import {
ShopifyFulfillmentOrdersInput,
ShopifyFulfillmentOrdersOutput,
} from '@ecommerce/fulfillmentorders/services/shopify/types';
import {
ShopifyCustomerInput,
ShopifyCustomerOutput,
} from '@ecommerce/customer/services/shopify/types';
import {
ShopifyFulfillmentInput,
ShopifyFulfillmentOrderOutput,
} from '@ecommerce/fulfillment/services/shopify/types';
import { ShopifyCustomerInput } from '@ecommerce/customer/services/shopify/types';
import { ShopifyFulfillmentInput } from '@ecommerce/fulfillment/services/shopify/types';
import { ShopifyFulfillmentOrdersInput } from '@ecommerce/fulfillmentorders/services/shopify/types';
import { ShopifyOrderInput } from '@ecommerce/order/services/shopify/types';
import { ShopifyProductInput } from '@ecommerce/product/services/shopify/types';

/* product */
export type OriginalProductInput = ShopifyProductInput;
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { FileStorageModule } from './filestorage/filestorage.module';
import { HrisModule } from './hris/hris.module';
import { MarketingAutomationModule } from './marketingautomation/marketingautomation.module';
import { CoreSharedModule } from '@@core/@core-services/module';
import { EcommerceModule } from '@ecommerce/ecommerce.module';

@Module({
imports: [
Expand All @@ -26,6 +27,7 @@ import { CoreSharedModule } from '@@core/@core-services/module';
AtsModule,
AccountingModule,
FileStorageModule,
EcommerceModule,
CrmModule,
TicketingModule,
ThrottlerModule.forRoot([
Expand Down
Loading

0 comments on commit b6f5a6a

Please sign in to comment.