Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

603 better oauth datasources #611

Draft
wants to merge 40 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7036c8c
modifying .env.example to fix incorrect port used for internal airbyt…
NaderRNA Oct 10, 2024
dd52986
Merge branch 'develop' into 603-better_oauth_datasources
NaderRNA Oct 15, 2024
0722b51
adding secret factory for clientId and clientSecret for oauth
NaderRNA Oct 15, 2024
3fe4b99
adding new method to airbyte controller, adding api route and router …
NaderRNA Oct 16, 2024
4f68736
commiting to save progress, fetch non functional returning 405
NaderRNA Oct 16, 2024
cbde5b4
refactoring oauth to handle using passport.js instead of trying to ha…
NaderRNA Oct 18, 2024
d56d99b
refactoring redirect, modifying HubSpot OAuth scopes
NaderRNA Oct 20, 2024
edb07ea
successful authentication of hubspot, callback url called with valid …
NaderRNA Oct 21, 2024
9d3fa15
hubspot successfully integrated into datasource creation workflow, st…
NaderRNA Oct 22, 2024
a9c0ab5
migration to add new tool to each team
NaderRNA Oct 15, 2024
d1d9833
setting the rabbitmq host as the local host ip
ragyabraham Oct 8, 2024
a91b518
adding google and githun auth build args to webapp docker file
ragyabraham Oct 10, 2024
43b0f0e
modifying migration to correctly add tool as option to install for bu…
NaderRNA Oct 15, 2024
9dd0842
fixing small naming convention that breaks tool on prod
NaderRNA Oct 16, 2024
621c41c
Use singleton pattern for mongodb and limit pool size to 10
iandjx Oct 17, 2024
e7fdbaa
Merge branch 'develop' into 603-better_oauth_datasources
NaderRNA Oct 22, 2024
7b23881
merging with develop and fixing some front end bugs
NaderRNA Oct 22, 2024
23f672d
Merge branch 'installsh-dockercompose-changes' into 603-better_oauth_…
NaderRNA Oct 27, 2024
e6d818b
adding middleware to handle datasource name and datasource descriptio…
NaderRNA Oct 28, 2024
5437778
adding user feedback to show that datasource is being tested, incomplete
NaderRNA Oct 28, 2024
63199b1
Merge branch 'develop' into 603-better_oauth_datasources
NaderRNA Oct 28, 2024
1f90b12
functional user feedback on successful oauth
NaderRNA Oct 28, 2024
162af0d
functional hubspot oauth
NaderRNA Oct 28, 2024
0dcfbd4
lint
NaderRNA Oct 28, 2024
96f50af
initializing salesforce/forcedotcom strategy
NaderRNA Oct 29, 2024
3e3ad63
adding salesforce and xero passport strategy templates, need more work
NaderRNA Oct 29, 2024
db48429
fixing mistake with installing npm packages into root instead of webapp
NaderRNA Oct 29, 2024
a003104
removing node modules
NaderRNA Oct 29, 2024
e0bc5d9
lint
NaderRNA Oct 29, 2024
b303560
adding slack strategy
NaderRNA Oct 31, 2024
acf79ab
adding routes for other strategies
NaderRNA Nov 4, 2024
cea520e
lint
NaderRNA Nov 4, 2024
f50aaaf
Merge branch 'develop' into 603-better_oauth_datasources
NaderRNA Nov 4, 2024
6df80fc
adding custom strategy, starting custom strategy struct, array etc...…
NaderRNA Nov 5, 2024
0e14b90
successful redirect for airtable oauth, still need to rebuild a compa…
NaderRNA Nov 5, 2024
de9bd1e
resolving merge conflicts
NaderRNA Nov 20, 2024
e26a097
fixing issue where file upload wasn't correctly displaying
NaderRNA Nov 20, 2024
1d48ae9
corrctly making airtable request in custom strategy
NaderRNA Nov 21, 2024
712f9de
Revert "lint"
NaderRNA Nov 21, 2024
b4f9ea2
linting the revert
NaderRNA Nov 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 138 additions & 108 deletions webapp/src/api.ts

Large diffs are not rendered by default.

187 changes: 186 additions & 1 deletion webapp/src/components/CreateDatasourceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import DatasourceChunkingForm from 'components/DatasourceChunkingForm';
import { StreamsList } from 'components/DatasourceStream';
import ToolTip from 'components/shared/ToolTip';
import FormContext from 'context/connectorform';
import OauthSecretProviderFactory from 'lib/oauthsecret';
import { defaultChunkingOptions } from 'misc/defaultchunkingoptions';
import { usePostHog } from 'posthog-js/react';
import submittingReducer from 'utils/submittingreducer';
Expand Down Expand Up @@ -59,7 +60,9 @@ export default function CreateDatasourceForm({
initialStep = 0,
fetchDatasources,
spec,
setSpec
setSpec,
provider,
token
}: {
models?: any[];
compact?: boolean;
Expand All @@ -70,6 +73,8 @@ export default function CreateDatasourceForm({
fetchDatasources?: Function;
spec?: any;
setSpec?: Function;
provider?: string;
token?: string;
}) {
//TODO: fix any types

Expand Down Expand Up @@ -97,6 +102,7 @@ export default function CreateDatasourceForm({
const [chunkingConfig, setChunkingConfig] = useReducer(submittingReducer, {
...defaultChunkingOptions
});
const [oauthRedirectUrl, setOauthRedirectUrl] = useState(false);

//TODO: move into RetrievalStrategyComponent, keep the setters passed as props
const [toolRetriever, setToolRetriever] = useState(Retriever.SELF_QUERY);
Expand Down Expand Up @@ -349,6 +355,181 @@ export default function CreateDatasourceForm({
}
}

async function hubspotDatasourcePost(token: string) {
const data = {
credentials: {
credentials_title: 'OAuth Credentials',
client_id: process.env.OAUTH_HUBSPOT_CLIENT_ID,
client_secret: process.env.OAUTH_HUBSPOT_CLIENT_SECRET,
refresh_token: token
}
};

setSubmitting(true);
setError(null);
const posthogEvent = step === 2 ? 'testDatasource' : 'createDatasource';
try {
if (step === 2) {
const body = {
sourceConfig: data,
_csrf: csrf,
connectorId: connector.value,
connectorName: connector.label,
resourceSlug,
scheduleType,
timeUnit,
units,
cronExpression,
datasourceName,
datasourceDescription,
embeddingField
};

console.log('post body: ', body);
//step 2, getting schema and testing connection
await API.testDatasource(
body,
stagedDatasource => {
posthog.capture(posthogEvent, {
datasourceName,
connectorId: connector?.value,
connectorName: connector?.label,
syncSchedule: scheduleType
});
if (stagedDatasource) {
setDatasourceId(stagedDatasource.datasourceId);
setDiscoveredSchema(stagedDatasource.discoveredSchema);
setStreamProperties(stagedDatasource.streamProperties);
setStep(3);
} else {
setError('Datasource connection test failed.'); //TODO: any better way to get error?
}
// nothing to toast here
},
res => {
posthog.capture(posthogEvent, {
datasourceName,
connectorId: connector?.value,
connectorName: connector?.label,
syncSchedule: scheduleType,
error: res
});
setError(res);
},
compact ? null : router
);
// callback && stagedDatasource && callback(stagedDatasource._id);
} else {
//step 4, saving datasource
const filteredStreamState = Object.fromEntries(
Object.entries(streamState).filter(
(e: [string, StreamConfig]) => e[1].checkedChildren.length > 0
)
);
const body = {
_csrf: csrf,
datasourceId: datasourceId,
resourceSlug,
scheduleType,
timeUnit,
units,
modelId,
cronExpression,
streamConfig: filteredStreamState,
datasourceName,
datasourceDescription,
embeddingField,
retriever: toolRetriever,
retriever_config: {
timeWeightField: toolTimeWeightField,
decay_rate: toolDecayRate,
k: topK
},
chunkingConfig,
enableConnectorChunking
};
const addedDatasource: any = await API.addDatasource(
body,
() => {
posthog.capture(posthogEvent, {
datasourceName,
connectorId: connector?.value,
connectorName: connector?.label,
numStreams: Object.keys(streamState)?.length,
syncSchedule: scheduleType
});
toast.success('Added datasource');
},
res => {
posthog.capture(posthogEvent, {
datasourceName,
connectorId: connector?.value,
connectorName: connector?.label,
syncSchedule: scheduleType,
numStreams: Object.keys(streamState)?.length,
error: res
});
toast.error(res);
},
compact ? null : router
);
callback && addedDatasource && callback(addedDatasource._id);
}
} catch (e) {
posthog.capture(posthogEvent, {
datasourceName,
connectorId: connector?.value,
connectorName: connector?.label,
syncSchedule: scheduleType,
numStreams: Object.keys(streamState)?.length,
error: e?.message || e
});
console.error(e);
} finally {
await new Promise(res => setTimeout(res, 750));
setSubmitting(false);
}
}

const [oauthProvider, setOauthProvider] = useState(provider);
const [oauthToken, setOauthToken] = useState(token);

//OAUTH LOGIC
useEffect(() => {
setOauthProvider(provider);
setOauthToken(token);
setDatasourceName('Hubspot');
setDatasourceDescription('Hubspot OAuth Datasource');
console.log('Form token and provider', oauthProvider, oauthToken);
}, [provider, token]);

//once provider and token have been set this should run once
useEffect(() => {
if (provider !== null && token !== null) {
setStep(2);
switch (provider) {
case 'hubspot':
console.log('Posting with OAuth credentials');
setConnector({
airbyte_platform: 'oss',
connector_definition_id: '36c891d9-4bd9-43ac-bad2-10e12756272c',
connector_name: 'HubSpot',
connector_type: 'source',
connector_version: '4.2.22',
disabled: false,
docker_repository: 'airbyte/source-hubspot',
icon: 'https://connectors.airbyte.com/files/metadata/airbyte/source-hubspot/latest/icon.svg',
label: 'HubSpot',
planAvailable: true,
sync_success_rate: 'high',
usage: 'high',
value: '36c891d9-4bd9-43ac-bad2-10e12756272c'
});
hubspotDatasourcePost(token);
}
}
}, []);

function getStepSection(_step) {
//TODO: make steps enum
switch (_step) {
Expand Down Expand Up @@ -501,6 +682,7 @@ export default function CreateDatasourceForm({
return setSubscriptionModalOpen(v.label);
}
setLoading(v != null);
console.log('v', v);
setConnector(v);
if (v) {
getSpecification(v.value);
Expand Down Expand Up @@ -614,6 +796,9 @@ export default function CreateDatasourceForm({
schema={spec.schema.connectionSpecification}
datasourcePost={datasourcePost}
error={error}
name={connector.label}
icon={connector.icon}
redirectUrl={oauthRedirectUrl}
/>
</FormContext>
</>
Expand Down
64 changes: 48 additions & 16 deletions webapp/src/components/connectorform/DynamicConnectorForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import ButtonSpinner from 'components/ButtonSpinner';
import ErrorAlert from 'components/ErrorAlert';
import dayjs from 'dayjs';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { FieldValues, useFormContext } from 'react-hook-form';
import { Schema } from 'struct/form';
import { AIRBYTE_OAUTH_PROVIDERS } from 'struct/oauth';

import AdditionalFields from './AdditionalFields';
import FormSection from './FormSection';
Expand All @@ -12,6 +14,10 @@ interface DynamicFormProps {
schema: Schema;
datasourcePost: (arg: any) => Promise<void>;
error?: string;
name?: string;
icon?: any;
oauthPost?: boolean;
redirectUrl?: boolean;
}

const ISODatePattern = '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$';
Expand Down Expand Up @@ -100,7 +106,15 @@ function updateDateStrings(
});
}

const DynamicConnectorForm = ({ schema, datasourcePost, error }: DynamicFormProps) => {
const DynamicConnectorForm = ({
schema,
datasourcePost,
error,
name,
icon,
oauthPost,
redirectUrl
}: DynamicFormProps) => {
const { handleSubmit } = useFormContext();
const [submitting, setSubmitting] = useState(false);

Expand All @@ -120,24 +134,42 @@ const DynamicConnectorForm = ({ schema, datasourcePost, error }: DynamicFormProp
}, [schema]);

return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormSection properties={schema.properties} requiredFields={schema.required} />
{schema.additionalProperties && <AdditionalFields />}
<>
{name.toUpperCase() in AIRBYTE_OAUTH_PROVIDERS ? (
<div className='flex flex-col'>
<a //when the user hits this button then redirect them to the authentication link in a new window/tab
className='max-w-[25%] rounded-md disabled:bg-slate-400 bg-indigo-600 mx-3 my-5 px-5 py-3 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 mt-3'
href={`/auth/${name.toLowerCase()}/free`}
target='_blank'
rel='noopener noreferrer'
>
{icon && <img src={icon} loading='lazy' className='inline-flex me-2 w-6 w-6' />}
Log in with {name}
</a>

{error && (
<div className='mb-4'>
<ErrorAlert error={error} />
{redirectUrl && <p>penis</p>}
</div>
) : (
<form onSubmit={handleSubmit(onSubmit)}>
<FormSection properties={schema.properties} requiredFields={schema.required} />
{schema.additionalProperties && <AdditionalFields />}

{error && (
<div className='mb-4'>
<ErrorAlert error={error} />
</div>
)}
<button
disabled={submitting}
type='submit'
className='w-full rounded-md disabled:bg-slate-400 bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 mt-3'
>
{submitting && <ButtonSpinner />}
{submitting ? 'Testing connection...' : 'Submit'}
</button>
</form>
)}
<button
disabled={submitting}
type='submit'
className='w-full rounded-md disabled:bg-slate-400 bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 mt-3'
>
{submitting && <ButtonSpinner />}
{submitting ? 'Testing connection...' : 'Submit'}
</button>
</form>
</>
);
};

Expand Down
25 changes: 24 additions & 1 deletion webapp/src/controllers/airbyte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { dynamicResponse } from '@dr';
import { io } from '@socketio';
import getAirbyteApi, { AirbyteApiType } from 'airbyte/api';
import getAirbyteApi, { AirbyteApiType, getAirbyteAuthToken } from 'airbyte/api';
import getAirbyteInternalApi from 'airbyte/internal';
import {
getDatasourceByConnectionId,
Expand Down Expand Up @@ -339,3 +339,26 @@ export async function handleSuccessfulEmbeddingWebhook(req, res, next) {

return dynamicResponse(req, res, 200, {});
}

//this is an old implementation of airbyte oauth. If we end up going to airbyte cloud then this will be useful but until then so long as we override the oauth credentials then we'll need to use a normal passport.js auth
// export async function getOAuthRedirectLink(req, res, next) {
// //generate a link to redirect the user to, use this api spec: https://reference.airbyte.com/reference/initiateoauth
// const { sourceType } = req.body;
// const redirectUrl = `https://app.agentcloud.dev/welcome`; //TODO: Set up this endpoint and redirect to it (maybe store the secret in persistent storage? But this could pose a security risk)
// console.log("sourceType: ", sourceType);
// const workspaceId = process.env.AIRBYTE_ADMIN_WORKSPACE_ID

// const sourcesApi = await getAirbyteApi(AirbyteApiType.SOURCES);
// const body = {
// sourceType: sourceType,
// redirectUrl: redirectUrl,
// workspaceId: workspaceId
// }; //body for the fetch request

// const oauthRedirect = await sourcesApi.initiateOAuth(body).then(({data}) => log(data));
// return oauthRedirect;
// }

// export async function handleOAuthWebhook(req, res, next) {
// //airbyte hits the oauth callback with a secretId in the body
// }
5 changes: 5 additions & 0 deletions webapp/src/controllers/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { addTool, deleteToolsForDatasource, editToolsForDatasource } from 'db/to
import debug from 'debug';
import dotenv from 'dotenv';
import { convertCronToQuartz, convertUnitToCron } from 'lib/airbyte/cronconverter';
import OauthSecretProviderFactory from 'lib/oauthsecret';
import { chainValidations } from 'lib/utils/validationutils';
import VectorDBProxyClient from 'lib/vectorproxy/client';
import { isVectorLimitReached } from 'lib/vectorproxy/limit';
Expand Down Expand Up @@ -126,7 +127,11 @@ export async function testDatasourceApi(req, res, next) {

const currentPlan = res.locals?.subscription?.stripePlan;
const allowedPeriods = pricingMatrix[currentPlan]?.cronProps?.allowedPeriods || [];
const { clientId, clientSecret } = OauthSecretProviderFactory.getSecretProvider('hubspot');
sourceConfig.credentials.client_id = clientId;
sourceConfig.credentials.client_secret = clientSecret;

log('Source config for test API: ', sourceConfig);
let validationError = chainValidations(
req.body,
[
Expand Down
Loading
Loading