From 3a1f35a52ae578a7bd0abd17270fe84f44dd4fe0 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Thu, 31 Oct 2024 12:27:21 +0800 Subject: [PATCH] feat: Allow integration to determine oauth scope via preConnect --- connectors/connector-google/server.ts | 36 +++++++++++++++++-- kits/cdk/internal/oauthConnector.ts | 19 +++++++--- .../engine-backend/router/connectorRouter.ts | 9 ++--- .../components/IntegrationSearch.tsx | 2 ++ .../hocs/WithConnectorConnect.tsx | 19 +++++++--- 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/connectors/connector-google/server.ts b/connectors/connector-google/server.ts index fc6394123..afbf4669e 100644 --- a/connectors/connector-google/server.ts +++ b/connectors/connector-google/server.ts @@ -32,12 +32,42 @@ export const googleServer = { // body: JSON.stringify(input.body), // }), // eslint-disable-next-line @typescript-eslint/require-await + async preConnect(_, context) { + // This returns auth options for Nango connect because it is an oauth integration + // this behavior is not type checked though and could use some improvement + // May be fixed if we turn nango into a connector + if (context.integrationExternalId === 'drive') { + return { + authorization_params: { + scope: 'https://www.googleapis.com/auth/drive', + }, + } + } + if (context.integrationExternalId === 'calendar') { + return { + authorization_params: { + scope: 'https://www.googleapis.com/auth/calendar', + }, + } + } + if (context.integrationExternalId === 'drive') { + return { + authorization_params: { + scope: 'https://www.googleapis.com/auth/gmail.readonly', + // • https://www.googleapis.com/auth/gmail.send (Send only) + // TODO: How do we determine more specific scopes here? + }, + } + } + return {} + }, + // eslint-disable-next-line @typescript-eslint/require-await async listIntegrations() { return { has_next_page: false, items: [ { - id: 'google-drive', + id: 'drive', name: 'Google Drive', // TODO: Differ oauth scope use in Connect based on which integration raw_data: {} as any, @@ -45,14 +75,14 @@ export const googleServer = { logo_url: '/_assets/logo-google-drive.svg', }, { - id: 'google-gmail', + id: 'gmail', name: 'Gmail', raw_data: {} as any, updated_at: new Date().toISOString(), logo_url: '/_assets/logo-google-gmail.svg', }, { - id: 'google-calendar', + id: 'calendar', name: 'Google Calendar', raw_data: {} as any, updated_at: new Date().toISOString(), diff --git a/kits/cdk/internal/oauthConnector.ts b/kits/cdk/internal/oauthConnector.ts index b97e136e3..f4213939b 100644 --- a/kits/cdk/internal/oauthConnector.ts +++ b/kits/cdk/internal/oauthConnector.ts @@ -80,23 +80,34 @@ export function oauthConnect({ connectorName, connectorConfigId, resourceId, + authOptions, }: { nangoFrontend: NangoFrontend connectorName: string connectorConfigId: Id['ccfg'] /** Should address the re-connect scenario, but let's see... */ resourceId?: Id['reso'] + authOptions?: { + authorization_params?: Record + } }): Promise { + // console.log('oauthConnect', { + // connectorName, + // connectorConfigId, + // resourceId, + // authOptions, + // }) return nangoFrontend .auth( connectorConfigId, resourceId ?? makeId('reso', connectorName, makeUlid()), { params: {}, - authorization_params: { - // TODO: Add integration specific scope right here... - scope: 'https://www.googleapis.com/auth/drive.readonly', - }, + ...authOptions, + // authOptions would tend to contain the authorization_params needed to make the initial connection + // authorization_params: { + // scope: 'https://www.googleapis.com/auth/drive.readonly', + // }, }, ) .then((r) => oauthBaseSchema.connectOutput.parse(r)) diff --git a/packages/engine-backend/router/connectorRouter.ts b/packages/engine-backend/router/connectorRouter.ts index c5266016f..da899f558 100644 --- a/packages/engine-backend/router/connectorRouter.ts +++ b/packages/engine-backend/router/connectorRouter.ts @@ -1,10 +1,5 @@ import {zodToOas31Schema} from '@opensdks/util-zod' -import { - extractId, - metaForConnector, - zId, - zVerticalKey, -} from '@openint/cdk' +import {extractId, metaForConnector, zId, zVerticalKey} from '@openint/cdk' import type {RouterMeta} from '@openint/trpc' import {TRPCError} from '@openint/trpc' import {R, z} from '@openint/util' @@ -156,7 +151,7 @@ const _connectorRouter = trpc.router({ ...res, items: res.items.map((item) => ({ ...item, - id: `${name}_${item.id}`, + id: `int_${name}_${item.id}`, connector_name: name, })), })) diff --git a/packages/engine-frontend/components/IntegrationSearch.tsx b/packages/engine-frontend/components/IntegrationSearch.tsx index 0eb6ae807..e05f4660c 100644 --- a/packages/engine-frontend/components/IntegrationSearch.tsx +++ b/packages/engine-frontend/components/IntegrationSearch.tsx @@ -2,6 +2,7 @@ import {Loader, Search} from 'lucide-react' import React from 'react' +import {Id} from '@openint/cdk' import {Input} from '@openint/ui' import {ConnectionCard} from '@openint/ui/domain-components/ConnectionCard' import type {ConnectorConfig} from '../hocs/WithConnectConfig' @@ -92,6 +93,7 @@ export function IntegrationSearch({ id: int.connector_config_id, connector: int.ccfg.connector, }} + integration={{id: int.id as Id['int']}} onEvent={(e) => { onEvent?.({ type: e.type, diff --git a/packages/engine-frontend/hocs/WithConnectorConnect.tsx b/packages/engine-frontend/hocs/WithConnectorConnect.tsx index 3f980d7fd..97f945259 100644 --- a/packages/engine-frontend/hocs/WithConnectorConnect.tsx +++ b/packages/engine-frontend/hocs/WithConnectorConnect.tsx @@ -40,11 +40,15 @@ const __DEBUG__ = Boolean( export const WithConnectorConnect = ({ connectorConfig: ccfg, + integration, resource, onEvent, children, }: { connectorConfig: {id: Id['ccfg']; connector: ConnectorMeta} + integration?: { + id: Id['int'] + } resource?: Resource onEvent?: (event: {type: ConnectEventType}) => void children: (props: { @@ -77,7 +81,7 @@ export const WithConnectorConnect = ({ openDialog: () => {}, }) ?? (nangoProvider - ? (_, {connectorConfigId}) => { + ? (connInput, {connectorConfigId}) => { if (!nangoFrontend) { throw new Error('Missing nango public key') } @@ -85,15 +89,20 @@ export const WithConnectorConnect = ({ connectorConfigId, nangoFrontend, connectorName: ccfg.connector.name, + resourceId: resource?.id, + authOptions: connInput, }) } : undefined) const resourceExternalId = resource ? extractId(resource.id)[2] : undefined + const integrationExternalId = integration + ? extractId(integration.id)[2] + : undefined // TODO: Handle preConnectInput schema and such... for example for Plaid const preConnect = _trpcReact.preConnect.useQuery( - [ccfg.id, {resourceExternalId}, {}], + [ccfg.id, {resourceExternalId, integrationExternalId}, {}], {enabled: ccfg.connector.hasPreConnect}, ) const postConnect = _trpcReact.postConnect.useMutation() @@ -150,7 +159,7 @@ export const WithConnectorConnect = ({ if (err === CANCELLATION_TOKEN) { return } - console.log(ccfg.connector.displayName + ' connection error:',err) + console.log(ccfg.connector.displayName + ' connection error:', err) toast({ title: `Failed to connect to ${ccfg.connector.displayName}`, // description: `${err}`, @@ -205,8 +214,8 @@ export const WithConnectorConnect = ({ {ccfg.connector.name === 'greenhouse' && (
- -
+ +

Generate a custom API key with{' '}