Skip to content

Commit

Permalink
initial api
Browse files Browse the repository at this point in the history
  • Loading branch information
EDsCODE committed Dec 27, 2023
1 parent 5729897 commit 4d5a553
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 26 deletions.
39 changes: 29 additions & 10 deletions frontend/src/scenes/data-warehouse/external/SourceModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LemonButton, LemonDivider, LemonInput, LemonModal, LemonModalProps } from '@posthog/lemon-ui'
import { LemonButton, LemonDivider, LemonInput, LemonModal, LemonModalProps, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { Form } from 'kea-forms'
import { Field } from 'lib/forms/Field'
Expand All @@ -10,21 +10,40 @@ import { ConnectorConfigType, sourceModalLogic } from './sourceModalLogic'
interface SourceModalProps extends LemonModalProps {}

export default function SourceModal(props: SourceModalProps): JSX.Element {
const { tableLoading, isExternalDataSourceSubmitting, selectedConnector, isManualLinkFormVisible, connectors } =
useValues(sourceModalLogic)
const {
tableLoading,
isExternalDataSourceSubmitting,
selectedConnector,
isManualLinkFormVisible,
connectors,
addToHubspotUrl,
} = useValues(sourceModalLogic)
const { selectConnector, toggleManualLinkFormVisible, resetExternalDataSource, resetTable } =
useActions(sourceModalLogic)

const MenuButton = (config: ConnectorConfigType): JSX.Element => {
const onClick = (): void => {
selectConnector(config)
if (config.name === 'Stripe') {
const onClick = (): void => {
selectConnector(config)
}

return (
<LemonButton onClick={onClick} className="w-100" center type="secondary">
<img src={stripeLogo} alt={`stripe logo`} height={50} />
</LemonButton>
)
}
if (config.name === 'Hubspot') {
return (
<Link to={addToHubspotUrl() || ''}>
<LemonButton className="w-100" center type="secondary">
Hubspot
</LemonButton>
</Link>
)
}

return (
<LemonButton onClick={onClick} className="w-100" center type="secondary">
<img src={stripeLogo} alt={`stripe logo`} height={50} />
</LemonButton>
)
return <></>
}

const onClear = (): void => {
Expand Down
43 changes: 41 additions & 2 deletions frontend/src/scenes/data-warehouse/external/sourceModalLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea
import { forms } from 'kea-forms'
import { router } from 'kea-router'
import api from 'lib/api'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
import { urls } from 'scenes/urls'

import { ExternalDataStripeSourceCreatePayload } from '~/types'
Expand All @@ -12,6 +13,10 @@ import { dataWarehouseSettingsLogic } from '../settings/dataWarehouseSettingsLog
import { dataWarehouseSceneLogic } from './dataWarehouseSceneLogic'
import type { sourceModalLogicType } from './sourceModalLogicType'

export const getHubspotRedirectUri = (next: string = ''): string =>
`${window.location.origin.replace('http://', 'https://')}/data_warehouse/hubspot/redirect${
next ? '?next=' + encodeURIComponent(next) : ''
}`
export interface ConnectorConfigType {
name: string
fields: string[]
Expand All @@ -23,10 +28,16 @@ export interface ConnectorConfigType {
export const CONNECTORS: ConnectorConfigType[] = [
{
name: 'Stripe',
fields: ['accound_id', 'client_secret'],
fields: ['account_id', 'client_secret'],
caption: 'Enter your Stripe credentials to link your Stripe to PostHog',
disabledReason: null,
},
{
name: 'Hubspot',
fields: [],
caption: '',
disabledReason: null,
},
]

export const sourceModalLogic = kea<sourceModalLogicType>([
Expand All @@ -36,7 +47,14 @@ export const sourceModalLogic = kea<sourceModalLogicType>([
toggleManualLinkFormVisible: (visible: boolean) => ({ visible }),
}),
connect({
values: [dataWarehouseTableLogic, ['tableLoading'], dataWarehouseSettingsLogic, ['dataWarehouseSources']],
values: [
dataWarehouseTableLogic,
['tableLoading'],
dataWarehouseSettingsLogic,
['dataWarehouseSources'],
preflightLogic,
['preflight'],
],
actions: [
dataWarehouseSceneLogic,
['toggleSourceModal'],
Expand Down Expand Up @@ -77,6 +95,27 @@ export const sourceModalLogic = kea<sourceModalLogicType>([
}))
},
],
addToHubspotUrl: [
(s) => [s.preflight],
(preflight) => {
return (next: string = '') => {
const clientId = preflight?.data_warehouse_integrations?.hubspot.client_id

if (!clientId) {
return null
}

const scopes = ['crm.objects.contacts.read', 'crm.objects.companies.read']

const params = new URLSearchParams()
params.set('client_id', clientId)
params.set('redirect_uri', getHubspotRedirectUri(next))
params.set('scope', scopes.join(' '))

return `https://app.hubspot.com/oauth/authorize?${params.toString()}`
}
},
],
}),
forms(() => ({
externalDataSource: {
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2500,6 +2500,11 @@ export interface PreflightStatus {
available: boolean
client_id?: string
}
data_warehouse_integrations: {
hubspot: {
client_id?: string
}
}
/** Whether PostHog is running in DEBUG mode. */
is_debug?: boolean
licensed_users_available?: number | null
Expand Down
2 changes: 1 addition & 1 deletion posthog/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from posthog.settings.object_storage import *
from posthog.settings.temporal import *
from posthog.settings.web import *
from posthog.settings.airbyte import *
from posthog.settings.data_warehouse import *

from posthog.settings.utils import get_from_env, str_to_bool

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@
# for DLT
BUCKET_URL = os.getenv("BUCKET_URL", None)
AIRBYTE_BUCKET_NAME = os.getenv("AIRBYTE_BUCKET_NAME", None)

HUBSPOT_APP_CLIENT_ID = os.getenv("HUBSPOT_APP_CLIENT_ID", None)
HUBSPOT_APP_CLIENT_SECRET = os.getenv("HUBSPOT_APP_CLIENT_SECRET", None)
42 changes: 42 additions & 0 deletions posthog/temporal/data_imports/pipelines/hubspot/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import requests
from django.conf import settings
from typing import Tuple


def refresh_access_token(refresh_token: str) -> str:
res = requests.post(
"https://api.hubapi.com/oauth/v1/token",
data={
"grant_type": "refresh_token",
"client_id": settings.HUBSPOT_APP_CLIENT_ID,
"client_secret": settings.HUBSPOT_APP_CLIENT_SECRET,
"refresh_token": refresh_token,
},
)

if res.status_code != 200:
err_message = res.json()["message"]
raise Exception(err_message)

return res.json()["access_token"]


def get_access_token_from_code(code: str) -> Tuple[str, str]:
res = requests.post(
"https://api.hubapi.com/oauth/v1/token",
data={
"grant_type": "authorization_code",
"client_id": settings.HUBSPOT_APP_CLIENT_ID,
"client_secret": settings.HUBSPOT_APP_CLIENT_SECRET,
"redirect_uri": "https://app.posthog.com/setup/hubspot",
"code": code,
},
)

if res.status_code != 200:
err_message = res.json()["message"]
raise Exception(err_message)

payload = res.json()

return payload["access_token"], payload["refresh_token"]
2 changes: 2 additions & 0 deletions posthog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def security_txt(request):
@never_cache
def preflight_check(request: HttpRequest) -> JsonResponse:
slack_client_id = SlackIntegration.slack_config().get("SLACK_APP_CLIENT_ID")
hubspot_client_id = settings.HUBSPOT_APP_CLIENT_ID

response = {
"django": True,
Expand All @@ -113,6 +114,7 @@ def preflight_check(request: HttpRequest) -> JsonResponse:
"available": bool(slack_client_id),
"client_id": slack_client_id or None,
},
"data_warehouse_integrations": {"hubspot": {"client_id": hubspot_client_id}},
"object_storage": is_cloud() or is_object_storage_available(),
}

Expand Down
67 changes: 54 additions & 13 deletions posthog/warehouse/api/external_data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
from posthog.temporal.data_imports.pipelines.schemas import (
PIPELINE_TYPE_SCHEMA_DEFAULT_MAPPING,
)
from posthog.temporal.data_imports.pipelines.hubspot.auth import (
get_access_token_from_code,
)
import temporalio

logger = structlog.get_logger(__name__)
Expand Down Expand Up @@ -107,7 +110,6 @@ def get_queryset(self):
)

def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
client_secret = request.data["client_secret"]
prefix = request.data.get("prefix", None)
source_type = request.data["source_type"]

Expand All @@ -127,18 +129,12 @@ def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
)

# TODO: remove dummy vars
new_source_model = ExternalDataSource.objects.create(
source_id=str(uuid.uuid4()),
connection_id=str(uuid.uuid4()),
destination_id=str(uuid.uuid4()),
team=self.team,
status="Running",
source_type=source_type,
job_inputs={
"stripe_secret_key": client_secret,
},
prefix=prefix,
)
if source_type == ExternalDataSource.Type.STRIPE:
new_source_model = self._handle_stripe_source(request, *args, **kwargs)
elif source_type == ExternalDataSource.Type.HUBSPOT:
new_source_model = self._handle_hubspot_source(request, *args, **kwargs)
else:
raise NotImplementedError(f"Source type {source_type} not implemented")

schemas = PIPELINE_TYPE_SCHEMA_DEFAULT_MAPPING[source_type]
for schema in schemas:
Expand All @@ -156,6 +152,51 @@ def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:

return Response(status=status.HTTP_201_CREATED, data={"id": new_source_model.pk})

def _handle_stripe_source(self, request: Request, *args: Any, **kwargs: Any) -> ExternalDataSource:
client_secret = request.data["client_secret"]
prefix = request.data.get("prefix", None)
source_type = request.data["source_type"]

# TODO: remove dummy vars
new_source_model = ExternalDataSource.objects.create(
source_id=str(uuid.uuid4()),
connection_id=str(uuid.uuid4()),
destination_id=str(uuid.uuid4()),
team=self.team,
status="Running",
source_type=source_type,
job_inputs={
"stripe_secret_key": client_secret,
},
prefix=prefix,
)

return new_source_model

def _handle_hubspot_source(self, request: Request, *args: Any, **kwargs: Any) -> ExternalDataSource:
code = request.data["code"]
prefix = request.data.get("prefix", None)
source_type = request.data["source_type"]

access_token, refresh_token = get_access_token_from_code(code)

# TODO: remove dummy vars
new_source_model = ExternalDataSource.objects.create(
source_id=str(uuid.uuid4()),
connection_id=str(uuid.uuid4()),
destination_id=str(uuid.uuid4()),
team=self.team,
status="Running",
source_type=source_type,
job_inputs={
"hubspot_secret_key": access_token,
"hubspot_refresh_token": refresh_token,
},
prefix=prefix,
)

return new_source_model

def prefix_required(self, source_type: str) -> bool:
source_type_exists = ExternalDataSource.objects.filter(team_id=self.team.pk, source_type=source_type).exists()
return source_type_exists
Expand Down
1 change: 1 addition & 0 deletions posthog/warehouse/models/external_data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
class ExternalDataSource(CreatedMetaFields, UUIDModel):
class Type(models.TextChoices):
STRIPE = "Stripe", "Stripe"
HUBSPOT = "Hubspot", "Hubspot"

class Status(models.TextChoices):
RUNNING = "Running", "Running"
Expand Down

0 comments on commit 4d5a553

Please sign in to comment.