diff --git a/docker-compose.minimal.yml b/docker-compose.minimal.yml index 262323848..f8c17f677 100644 --- a/docker-compose.minimal.yml +++ b/docker-compose.minimal.yml @@ -16,8 +16,10 @@ services: RABBITMQ_DEFAULT_USER: guest RABBITMQ_DEFAULT_PASS: guest ports: - - "127.0.0.1:5672:5672" - - "127.0.0.1:15672:15672" + - "0.0.0.0:5672:5672" + - "0.0.0.0:15672:15672" + extra_hosts: + - "host.docker.internal:host-gateway" docker_mongo: restart: always diff --git a/docker-compose.yml b/docker-compose.yml index 92b7fd5cb..41371f32c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,8 @@ services: ports: - "0.0.0.0:5672:5672" - "0.0.0.0:15672:15672" + extra_hosts: + - "host.docker.internal:host-gateway" docker_mongo: restart: always diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 000000000..662c5495c --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "agentcloud", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{} diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 5e1ddc7f3..3f2f9d686 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -72,10 +72,14 @@ "nprogress": "^0.2.0", "openapi-client-axios": "^7.4.0", "passport": "^0.6.0", + "passport-custom": "^1.1.1", + "passport-forcedotcom": "^0.2.1", "passport-github": "^1.1.0", "passport-google-oauth20": "^2.0.0", "passport-hubspot-oauth2": "^1.0.3", + "passport-slack": "^0.0.7", "passport-stripe": "^0.2.3", + "passport-xero": "^1.0.0-a", "pdf.js-extract": "^0.2.1", "posthog-js": "^1.88.4", "posthog-node": "^4.1.0", @@ -15946,6 +15950,28 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-custom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", + "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/passport-forcedotcom": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/passport-forcedotcom/-/passport-forcedotcom-0.2.1.tgz", + "integrity": "sha512-s/yNf49QN1IcW8AcE98nmieZyNnlnaSYj0qYnOB7jyVG5I2sf5+6RUTdKb/btYktm5HnUnl65xqFDlR9aUGJdA==", + "dependencies": { + "passport-oauth2": "^1.6.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/passport-github": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz", @@ -16033,6 +16059,43 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" }, + "node_modules/passport-slack": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/passport-slack/-/passport-slack-0.0.7.tgz", + "integrity": "sha512-E9WDCfd1GO/2zYCz3L1MZcB0eYHTHB6yBi9lRkK+LFBl1p6wcV7pGF6+Vb+XLVq1G6+TLSJCTAxcW5O0rvE4/A==", + "dependencies": { + "passport-oauth": "~0.1.1", + "pkginfo": "0.2.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-slack/node_modules/passport": { + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.1.18.tgz", + "integrity": "sha512-qteYojKG/qth7UBbbGU7aqhe5ndJs6YaUkH2B6+7FWQ0OeyYmWknzOATpMhdoSTDcLLliq9n4Fcy1mGs80iUMw==", + "dependencies": { + "pause": "0.0.1", + "pkginfo": "0.2.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-slack/node_modules/passport-oauth": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/passport-oauth/-/passport-oauth-0.1.15.tgz", + "integrity": "sha512-ma4W++dGNS/WKxkInG03VDqCRPD/9K/eSaqhvMLBFhpLOfycBus8+FnhcoSR6ug+NzXLjYtjGxMPBE/Gt8KqqA==", + "dependencies": { + "oauth": "0.9.x", + "passport": "~0.1.1", + "pkginfo": "0.2.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -16055,6 +16118,30 @@ "node": ">= 0.4.0" } }, + "node_modules/passport-xero": { + "version": "1.0.0-a", + "resolved": "https://registry.npmjs.org/passport-xero/-/passport-xero-1.0.0-a.tgz", + "integrity": "sha512-/vSYV1YhUvPuJ8DGbRNn+KTa2HqRgnWH7ewNashK9e5BeCmbI+67I+mQfoAaMW8XeJsf420oDqQ33UykJwLdxA==", + "dependencies": { + "passport-oauth1": "~1.0.1" + }, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/passport-xero/node_modules/passport-oauth1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.0.1.tgz", + "integrity": "sha512-K2qszxPGMt+JwrxUMXJKQLgOpkjLj1p+YC4KRYa9rZtfMy7DS9NTapkzZ9extkX9vNIENp1lrBiTBZ6XcB/jQQ==", + "dependencies": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", diff --git a/webapp/package.json b/webapp/package.json index 81a1e998e..741f14502 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -88,10 +88,14 @@ "nprogress": "^0.2.0", "openapi-client-axios": "^7.4.0", "passport": "^0.6.0", + "passport-custom": "^1.1.1", + "passport-forcedotcom": "^0.2.1", "passport-github": "^1.1.0", "passport-google-oauth20": "^2.0.0", "passport-hubspot-oauth2": "^1.0.3", + "passport-slack": "^0.0.7", "passport-stripe": "^0.2.3", + "passport-xero": "^1.0.0-a", "pdf.js-extract": "^0.2.1", "posthog-js": "^1.88.4", "posthog-node": "^4.1.0", diff --git a/webapp/src/api.ts b/webapp/src/api.ts index cb44a1646..c9fb2b9a6 100644 --- a/webapp/src/api.ts +++ b/webapp/src/api.ts @@ -8,7 +8,7 @@ export function getAccount(body, dispatch, errorCallback, router) { ...(body?.resourceSlug ? { resourceSlug: body.resourceSlug } : {}) }).toString(); return ApiCall(`/account.json?${queryString}`, 'GET', null, dispatch, errorCallback, router); -} +} export function login(body, dispatch, errorCallback, router) { return ApiCall('/forms/account/login', 'POST', body, dispatch, errorCallback, router); } @@ -24,7 +24,7 @@ export function requestChangePassword(body, dispatch, errorCallback, router) { errorCallback, router ); -} +} export function logout(body, dispatch, errorCallback, router) { return ApiCall('/forms/account/logout', 'POST', body, dispatch, errorCallback, router); } @@ -36,7 +36,7 @@ export function verifyToken(body, dispatch, errorCallback, router) { } export function switchTeam(body, dispatch, errorCallback, router) { return ApiCall('/forms/account/switch', 'POST', body, dispatch, errorCallback, router); -}//@TEST +} //@TEST export function getPortalLink(body, dispatch, errorCallback, router) { return ApiCall('/stripe-portallink', 'POST', body, dispatch, errorCallback, router); } @@ -52,6 +52,10 @@ export function hasPaymentMethod(dispatch, errorCallback, router) { export function checkStripeReady(dispatch, errorCallback, router) { return ApiCall('/stripe-ready', 'GET', null, dispatch, errorCallback, router); } +//Airbyte OAuth endpoints, to get OAuth redirect url from internal airbyte api +export function getOauthRedirectUrl(body, dispatch, errorCallback, router) { + return ApiCall(`/oauthredirecturl`, 'POST', body, dispatch, errorCallback, router); +} export function updateOnboardedStatus(body, dispatch, errorCallback, router) { return ApiCall('/forms/account/onboarded', 'POST', body, dispatch, errorCallback, router); } @@ -68,9 +72,8 @@ export function updateRole(body, dispatch, errorCallback, router) { ); } - //Welcome -export function getWelcomeData(dispatch, errorCallback, router){ +export function getWelcomeData(dispatch, errorCallback, router) { return ApiCall(`/welcome.json`, 'GET', null, dispatch, errorCallback, router); } @@ -239,15 +242,20 @@ export function startSession(body, dispatch, errorCallback, router) { ); } - export function getMessages(body, dispatch, errorCallback, router) { const queryString = new URLSearchParams({ ...(body?.messageId ? { messageId: body.messageId } : {}) }).toString(); - return ApiCall(`/${body.resourceSlug}/session/${body.sessionId}/messages.json?${queryString}`, 'GET', null, dispatch, errorCallback, router); + return ApiCall( + `/${body.resourceSlug}/session/${body.sessionId}/messages.json?${queryString}`, + 'GET', + null, + dispatch, + errorCallback, + router + ); } //@TEST - // Agents export function addAgent(body, dispatch, errorCallback, router) { return ApiCall( @@ -268,7 +276,7 @@ export function editAgent(agentId, body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getAgent(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/agent/${body.agentId}.json`, @@ -278,10 +286,10 @@ export function getAgent(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getAgents(body, dispatch, errorCallback, router) { return ApiCall(`/${body.resourceSlug}/agents.json`, 'GET', null, dispatch, errorCallback, router); -}//@TEST +} //@TEST export function deleteAgent(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/agent/${body.agentId}`, @@ -291,41 +299,20 @@ export function deleteAgent(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST //APIKeys -export function addKey(body, dispatch, errorCallback, router){ - return ApiCall( - '/forms/account/apikey/add', - 'POST', - body, - dispatch, - errorCallback, - router - ) -}//@TEST +export function addKey(body, dispatch, errorCallback, router) { + return ApiCall('/forms/account/apikey/add', 'POST', body, dispatch, errorCallback, router); +} //@TEST export function getKeys(body, dispatch, errorCallback, router) { - return ApiCall( - `/apikeys.json`, - 'GET', - null, - dispatch, - errorCallback, - router - ); -}//@TEST + return ApiCall(`/apikeys.json`, 'GET', null, dispatch, errorCallback, router); +} //@TEST export function getKey(body, dispatch, errorCallback, router) { - return ApiCall( - `/apikey/${body.keyId}.json`, - 'GET', - null, - dispatch, - errorCallback, - router - ); -}//@TEST + return ApiCall(`/apikey/${body.keyId}.json`, 'GET', null, dispatch, errorCallback, router); +} //@TEST export function incrementKeyVersion(body, dispatch, errorCallback, router) { return ApiCall( @@ -336,7 +323,7 @@ export function incrementKeyVersion(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function deleteKey(body, dispatch, errorCallback, router) { return ApiCall( @@ -347,7 +334,7 @@ export function deleteKey(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST // Tasks export function getTasks(body, dispatch, errorCallback, router) { @@ -362,11 +349,11 @@ export function getTaskById(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getTask(body, dispatch: GetTaskByNameDispatch, errorCallback, router) { const queryString = new URLSearchParams({ ...(body.name ? { name: body.name } : {}), - ...(body.sessionId ? { sessionId: body.sessionId } : {}), //could go in params but whatever + ...(body.sessionId ? { sessionId: body.sessionId } : {}) //could go in params but whatever }); return ApiCall( `/${body.resourceSlug}/task?${queryString}`, @@ -376,7 +363,7 @@ export function getTask(body, dispatch: GetTaskByNameDispatch, errorCallback, ro errorCallback, router ); -}//@TEST +} //@TEST export function addTask(body, dispatch, errorCallback, router) { return ApiCall( @@ -387,7 +374,7 @@ export function addTask(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function deleteTask(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/task/${body.taskId}`, @@ -397,7 +384,7 @@ export function deleteTask(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function editTask(taskId, body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/task/${taskId}/edit`, @@ -407,12 +394,12 @@ export function editTask(taskId, body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST // Tools export function getTools(body, dispatch, errorCallback, router) { return ApiCall(`/${body.resourceSlug}/tools.json`, 'GET', null, dispatch, errorCallback, router); -}//@TEST +} //@TEST export function getTool(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/tool/${body.toolId}.json`, @@ -422,7 +409,7 @@ export function getTool(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function addTool(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/tool/add`, @@ -432,7 +419,7 @@ export function addTool(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function deleteTool(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/tool/${body.toolId}`, @@ -442,7 +429,7 @@ export function deleteTool(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function editTool(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/tool/${body.toolId}/edit`, @@ -452,7 +439,7 @@ export function editTool(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function applyToolRevision(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/revision/${body.revisionId}/apply`, @@ -462,7 +449,7 @@ export function applyToolRevision(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function deleteToolRevision(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/revision/${body.revisionId}`, @@ -472,12 +459,12 @@ export function deleteToolRevision(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST // Models export function getModels(body, dispatch, errorCallback, router) { return ApiCall(`/${body.resourceSlug}/models.json`, 'GET', null, dispatch, errorCallback, router); -}//@TEST +} //@TEST export function getModel(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/model/${body.modelId}.json`, @@ -487,7 +474,7 @@ export function getModel(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function addModel(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/model/add`, @@ -497,7 +484,7 @@ export function addModel(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function editModel(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/model/${body.modelId}/edit`, @@ -507,7 +494,7 @@ export function editModel(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function deleteModel(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/model/${body.modelId}`, @@ -517,7 +504,7 @@ export function deleteModel(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST // Asset export function addAsset(body, dispatch, errorCallback, router) { @@ -529,7 +516,7 @@ export function addAsset(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getAsset(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/asset/${body.assetId}.json`, @@ -539,7 +526,7 @@ export function getAsset(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function editAsset(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/asset/${body.assetId}/edit`, @@ -549,7 +536,7 @@ export function editAsset(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function deleteAsset(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/asset/${body.assetId}`, @@ -559,7 +546,7 @@ export function deleteAsset(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST // Datasources export function getDatasources(body, dispatch, errorCallback, router) { @@ -571,7 +558,7 @@ export function getDatasources(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getDatasource(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/datasource/${body.datasourceId}.json`, @@ -581,7 +568,7 @@ export function getDatasource(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function testDatasource(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/datasource/test`, @@ -591,17 +578,21 @@ export function testDatasource(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function addDatasource(body, dispatch, errorCallback, router) { + const queryString = new URLSearchParams({ + token: body.token, + provider: body.provider + }).toString(); return ApiCall( - `/${body.resourceSlug}/forms/datasource/add`, + `/${body.resourceSlug}/forms/datasource/add?${queryString}`, 'POST', body, dispatch, errorCallback, router ); -}//@TEST +} //@TEST export function updateDatasourceStreams(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/datasource/${body.datasourceId}/streams`, @@ -611,7 +602,7 @@ export function updateDatasourceStreams(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function updateDatasourceSchedule(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/datasource/${body.datasourceId}/schedule`, @@ -621,7 +612,7 @@ export function updateDatasourceSchedule(body, dispatch, errorCallback, router) errorCallback, router ); -}//@TEST +} //@TEST export function deleteDatasource(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/datasource/${body.datasourceId}`, @@ -631,7 +622,7 @@ export function deleteDatasource(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function editDatasource(datasourceId, body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/datasource/${datasourceId}/edit`, @@ -641,7 +632,7 @@ export function editDatasource(datasourceId, body, dispatch, errorCallback, rout errorCallback, router ); -}//@TEST +} //@TEST export function syncDatasource(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/datasource/${body.datasourceId}/sync`, @@ -651,7 +642,7 @@ export function syncDatasource(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST // Airbyte export function getConnectors(body, dispatch, errorCallback, router) { @@ -676,7 +667,7 @@ export function getSpecification(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getJobsList(body, dispatch, errorCallback, router) { const queryString = new URLSearchParams({ datasourceId: body.datasourceId @@ -689,7 +680,7 @@ export function getJobsList(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getDatasourceSchema(body, dispatch, errorCallback, router) { const queryString = new URLSearchParams({ datasourceId: body.datasourceId @@ -725,7 +716,7 @@ export function uploadDatasourceFileTemp(body, dispatch, errorCallback, router) errorCallback, router ); -}//@TEST +} //@TEST //Team (invites/members/etc) export function getNotifications(body, dispatch, errorCallback, router) { @@ -737,7 +728,7 @@ export function getNotifications(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function markNotificationsSeen(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/notification/seen`, @@ -747,10 +738,10 @@ export function markNotificationsSeen(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getTeam(body, dispatch, errorCallback, router) { return ApiCall(`/${body.resourceSlug}/team.json`, 'GET', null, dispatch, errorCallback, router); -}//@TEST +} //@TEST export function getTeamMember(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/team/${body.memberId}.json`, @@ -760,7 +751,7 @@ export function getTeamMember(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function inviteToTeam(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/team/invite`, @@ -770,7 +761,7 @@ export function inviteToTeam(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function deleteFromTeam(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/team/invite`, @@ -780,7 +771,7 @@ export function deleteFromTeam(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function addTeam(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/team/add`, @@ -790,7 +781,7 @@ export function addTeam(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function editTeamMember(body, dispatch, errorCallback, router) { return ApiCall( `/${body.get('resourceSlug')}/forms/team/${body.get('memberId')}/edit`, @@ -800,7 +791,7 @@ export function editTeamMember(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function deleteTeamMember(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/team/invite`, @@ -810,7 +801,7 @@ export function deleteTeamMember(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function editTeam(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/team/edit`, @@ -820,7 +811,7 @@ export function editTeam(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function transferTeamOwnership(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/team/transfer-ownership`, @@ -830,7 +821,7 @@ export function transferTeamOwnership(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function setDefaultModel(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/team/set-default-model`, @@ -840,7 +831,7 @@ export function setDefaultModel(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getTeamModels(body, dispatch: GetTeamModelsDispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/team/models.json`, @@ -850,10 +841,10 @@ export function getTeamModels(body, dispatch: GetTeamModelsDispatch, errorCallba errorCallback, router ); -}//@TEST +} //@TEST //get the vector storage usage on a team basis -export function getVectorStorageTeam(body, dispatch, errorCallback, router){ +export function getVectorStorageTeam(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/team/vectorstorage.json`, 'GET', @@ -861,12 +852,12 @@ export function getVectorStorageTeam(body, dispatch, errorCallback, router){ dispatch, errorCallback, router - ) + ); } export function getOrg(body, dispatch, errorCallback, router) { return ApiCall(`/${body.resourceSlug}/org.json`, 'GET', null, dispatch, errorCallback, router); -}//@TEST +} //@TEST export function getAllTeamVectorStorage(body, dispatch, errorCallback, router) { return ApiCall( @@ -876,7 +867,7 @@ export function getAllTeamVectorStorage(body, dispatch, errorCallback, router) { dispatch, errorCallback, router - ) + ); } export function editOrg(body, dispatch, errorCallback, router) { return ApiCall( @@ -887,7 +878,7 @@ export function editOrg(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getOrgMember(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/org/${body.memberId}.json`, @@ -897,7 +888,7 @@ export function getOrgMember(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function editOrgMember(body, dispatch, errorCallback, router) { return ApiCall( `/${body.get('resourceSlug')}/forms/org/${body.get('memberId')}/edit`, @@ -907,7 +898,7 @@ export function editOrgMember(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function deleteOrgMember(body, dispatch, errorCallback, router) { return ApiCall( `/${body.resourceSlug}/forms/org/invite`, @@ -917,22 +908,50 @@ export function deleteOrgMember(body, dispatch, errorCallback, router) { errorCallback, router ); -}//@TEST +} //@TEST export function getVariables(body, dispatch, errorCallback, router) { - return ApiCall(`/${body.resourceSlug}/variables.json`, 'GET', null, dispatch, errorCallback, router); + return ApiCall( + `/${body.resourceSlug}/variables.json`, + 'GET', + null, + dispatch, + errorCallback, + router + ); } -export function getVariable(body, dispatch:GetVariableDispatch, errorCallback, router) { - return ApiCall(`/${body.resourceSlug}/variable/${body.variableId}.json`, 'GET', null, dispatch, errorCallback, router); +export function getVariable(body, dispatch: GetVariableDispatch, errorCallback, router) { + return ApiCall( + `/${body.resourceSlug}/variable/${body.variableId}.json`, + 'GET', + null, + dispatch, + errorCallback, + router + ); } export function addVariable(body, dispatch, errorCallback, router) { - return ApiCall(`/${body.resourceSlug}/forms/variable/add`, 'POST', body, dispatch, errorCallback, router); + return ApiCall( + `/${body.resourceSlug}/forms/variable/add`, + 'POST', + body, + dispatch, + errorCallback, + router + ); } -export function updateVariable(body, dispatch , errorCallback, router) { - return ApiCall(`/${body.resourceSlug}/forms/variable/${body.variableId}/edit`, 'POST', body, dispatch, errorCallback, router); +export function updateVariable(body, dispatch, errorCallback, router) { + return ApiCall( + `/${body.resourceSlug}/forms/variable/${body.variableId}/edit`, + 'POST', + body, + dispatch, + errorCallback, + router + ); } export function deleteVariable(body, dispatch, errorCallback, router) { @@ -971,7 +990,7 @@ function buildOptions(_route, method, body) { } }; if (body != null) { - if(method === "GET"){ + if (method === 'GET') { throw new Error("GET request can't have body"); } //TODO: remove/change diff --git a/webapp/src/components/CreateDatasourceForm.tsx b/webapp/src/components/CreateDatasourceForm.tsx index 576022022..a865c05b4 100644 --- a/webapp/src/components/CreateDatasourceForm.tsx +++ b/webapp/src/components/CreateDatasourceForm.tsx @@ -32,6 +32,7 @@ import { StreamsList } from 'components/DatasourceStream'; import ToolTip from 'components/shared/ToolTip'; import FormContext from 'context/connectorform'; import cn from 'lib/cn'; +import OauthSecretProviderFactory from 'lib/oauthsecret'; import { defaultChunkingOptions } from 'misc/defaultchunkingoptions'; import { usePostHog } from 'posthog-js/react'; import { VectorDbDocument, VectorDbType } from 'struct/vectordb'; @@ -63,6 +64,10 @@ export default function CreateDatasourceForm({ fetchDatasources, spec, setSpec, + provider, + token, + name = '', + description = '', vectorDbs = [] }: { models?: any[]; @@ -74,6 +79,10 @@ export default function CreateDatasourceForm({ fetchDatasources?: Function; spec?: any; setSpec?: Function; + provider?: string; + token?: string; + name?: string; + description?: string; vectorDbs?: VectorDbDocument[]; }) { //TODO: fix any types @@ -86,8 +95,8 @@ export default function CreateDatasourceForm({ const { resourceSlug } = router.query; const [error, setError] = useState(null); const [files, setFiles] = useState(null); - const [datasourceName, setDatasourceName] = useState(''); - const [datasourceDescription, setDatasourceDescription] = useState(''); + const [datasourceName, setDatasourceName] = useState(name); + const [datasourceDescription, setDatasourceDescription] = useState(description); const [modelModalOpen, setModelModalOpen] = useState(false); const [vectorDbModalOpen, setVectorDbModalOpen] = useState(false); const [subscriptionModalOpen, setSubscriptionModalOpen] = useState(false); @@ -109,6 +118,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); @@ -149,6 +159,8 @@ export default function CreateDatasourceForm({ const [streamProperties, setStreamProperties] = useState(null); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); + const [oauthSubmitting, setOauthSubmitting] = useState(false); + const [oauthLoading, setOauthLoading] = useState(false); const [streamState, setStreamReducer] = useReducer(submittingReducer, {}); const [formData, setFormData] = useState(null); @@ -363,6 +375,207 @@ export default function CreateDatasourceForm({ } } + async function hubspotDatasourcePost(token: string) { + setOauthSubmitting(true); + const data = OauthSecretProviderFactory.getProviderPostData(token, oauthProvider.toLowerCase()); + console.log( + 'Datasource name and description from the state: ', + datasourceName, + ' ', + datasourceDescription + ); + + 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 { + setOauthSubmitting(false); + await new Promise(res => setTimeout(res, 750)); + } + } + + const [oauthProvider, setOauthProvider] = useState(provider); + const [oauthToken, setOauthToken] = useState(token); + const [initialized, setInitialized] = useState(false); + + //OAUTH LOGIC + useEffect(() => { + setOauthProvider(provider); + setOauthToken(token); + setDatasourceName(name); + setDatasourceDescription(description); + console.log('Form token and provider', oauthProvider, oauthToken); + if ( + provider && + token && + datasourceName !== '' && + datasourceDescription !== '' && + !initialized + ) { + setInitialized(true); // Mark as initialized after first successful set + } + }, [provider, token, name, description]); + + //once provider and token have been set this should run once to correctly set the connector based on OAuth + 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); + case 'airtable': + console.log('Posting with OAuth airtable creds'); + setConnector({ + airbyte_platform: 'oss', + connector_definition_id: '14c6e7ea-97ed-4f5e-a7b5-25e9a80b8212', + connector_name: 'Airtable', + connector_type: 'source', + connector_version: '4.4.0', + disabled: false, + docker_repository: 'airbyte/source-airtable', + icon: 'https://connectors.airbyte.com/files/metadata/airbyte/source-airtable/latest/icon.svg', + label: 'Airtable', + planAvailable: true, + sync_success_rate: 'high', + usage: 'high', + value: '14c6e7ea-97ed-4f5e-a7b5-25e9a80b8212' + }); + } + } + }, [provider, token, initialized]); + function getStepSection(_step) { //TODO: make steps enum switch (_step) { @@ -516,6 +729,7 @@ export default function CreateDatasourceForm({ return setSubscriptionModalOpen(v.label); } setLoading(v != null); + console.log('v', v); setConnector(v); if (v) { getSpecification(v.value); @@ -629,6 +843,11 @@ export default function CreateDatasourceForm({ schema={spec.schema.connectionSpecification} datasourcePost={datasourcePost} error={error} + name={connector.label} + icon={connector.icon} + redirectUrl={oauthRedirectUrl} + datasourceDescription={datasourceDescription} + datasourceName={datasourceName} /> @@ -636,35 +855,51 @@ export default function CreateDatasourceForm({ ) )} + {oauthSubmitting && ( +
+
+ +

+ Authentication Successful! +

+
+ +

+ Testing Datasource Connection (this might take a while) +

+
+ )} ); case 3: - return ( - discoveredSchema && ( -
{ - e.preventDefault(); - setStep(4); - }} - > - -
- -
- - ) + return discoveredSchema ? ( +
{ + e.preventDefault(); + setStep(4); + }} + > + +
+ +
+ + ) : ( + <> +

Testing datasource...

+ ); case 4: return ( diff --git a/webapp/src/components/connectorform/DynamicConnectorForm.tsx b/webapp/src/components/connectorform/DynamicConnectorForm.tsx index 44245186b..b7ccf25c9 100644 --- a/webapp/src/components/connectorform/DynamicConnectorForm.tsx +++ b/webapp/src/components/connectorform/DynamicConnectorForm.tsx @@ -1,9 +1,13 @@ import ButtonSpinner from 'components/ButtonSpinner'; +import classNames from 'components/ClassNames'; import ErrorAlert from 'components/ErrorAlert'; +import Spinner from 'components/Spinner'; 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'; @@ -12,6 +16,12 @@ interface DynamicFormProps { schema: Schema; datasourcePost: (arg: any) => Promise; error?: string; + name?: string; + icon?: any; + oauthPost?: boolean; + redirectUrl?: boolean; + datasourceName?: any; + datasourceDescription?: any; } const ISODatePattern = '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$'; @@ -100,7 +110,16 @@ function updateDateStrings( }); } -const DynamicConnectorForm = ({ schema, datasourcePost, error }: DynamicFormProps) => { +const DynamicConnectorForm = ({ + schema, + datasourcePost, + error, + name, + icon, + datasourceName, + datasourceDescription, + redirectUrl +}: DynamicFormProps) => { const { handleSubmit } = useFormContext(); const [submitting, setSubmitting] = useState(false); @@ -120,24 +139,54 @@ const DynamicConnectorForm = ({ schema, datasourcePost, error }: DynamicFormProp }, [schema]); return ( -
- - {schema.additionalProperties && } - - {error && ( -
- + <> + {name.toUpperCase() in AIRBYTE_OAUTH_PROVIDERS ? ( +
+ + {icon && } + Log in with {name} + + {redirectUrl &&

Redirecting...

}
+ ) : ( + + + {schema.additionalProperties && } + + {error && ( +
+ +
+ )} + + )} - - + ); }; diff --git a/webapp/src/controllers/airbyte.ts b/webapp/src/controllers/airbyte.ts index 2ed730d4b..90a6e9813 100644 --- a/webapp/src/controllers/airbyte.ts +++ b/webapp/src/controllers/airbyte.ts @@ -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, @@ -341,6 +341,29 @@ 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 +// } + export async function checkAirbyteConnection(req, res, next) { const status = await airbyteSetup.checkAirbyteStatus(); diff --git a/webapp/src/controllers/datasource.ts b/webapp/src/controllers/datasource.ts index 7371acfe4..0dc2ccab3 100644 --- a/webapp/src/controllers/datasource.ts +++ b/webapp/src/controllers/datasource.ts @@ -24,6 +24,7 @@ import { getVectorDbById, getVectorDbsByTeam } from 'db/vectordb'; 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'; @@ -135,7 +136,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, [ diff --git a/webapp/src/controllers/oauth.ts b/webapp/src/controllers/oauth.ts index 3284cf7e5..8fd339a65 100644 --- a/webapp/src/controllers/oauth.ts +++ b/webapp/src/controllers/oauth.ts @@ -6,16 +6,20 @@ import debug from 'debug'; import createAccount from 'lib/account/create'; import { ObjectId } from 'mongodb'; import SecretKeys from 'secret/secretkeys'; -import { OAUTH_PROVIDER, OAuthStrategy } from 'struct/oauth'; +import { CustomOAuthStrategy, OAUTH_PROVIDER, OAuthStrategy } from 'struct/oauth'; import { addOrg } from '../db/org'; import { addTeam } from '../db/team'; const log = debug('webapp:oauth'); //To reduce some boilerplace in the router, allows us to just loop and create handlers for each service +import { Strategy as CustomStrategy } from 'passport-custom'; +import { Strategy as SalesforceStrategy } from 'passport-forcedotcom'; import { Strategy as GitHubStrategy } from 'passport-github'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; -// import { Strategy as HubspotStrategy } from 'passport-hubspot-oauth2'; +import { Strategy as HubspotStrategy } from 'passport-hubspot-oauth2'; +import { Strategy as SlackStrategy } from 'passport-slack'; +import { Strategy as XeroStrategy } from 'passport-xero'; // import { Strategy as StripeStrategy } from 'passport-stripe'; export const OAUTH_STRATEGIES: OAuthStrategy[] = [ @@ -40,11 +44,97 @@ export const OAUTH_STRATEGIES: OAuthStrategy[] = [ extra: { /* N/A */ } - } + }, // { strategy: StripeStrategy, secretKeys: { clientId: SecretKeys.OAUTH_STRIPE_CLIENT_ID, secret: SecretKeys.OAUTH_STRIPE_CLIENT_SECRET }, callback: stripeCallback, path: '/auth/stripe/callback', extra: { /* N/A */ } }, - // { strategy: HubspotStrategy, secretKeys: { clientId: SecretKeys.OAUTH_HUBSPOT_CLIENT_ID, secret: SecretKeys.OAUTH_HUBSPOT_CLIENT_SECRET }, callback: hubspotCallback, path: '/auth/hubspot/callback', extra: { /* N/A */ } }, + { + strategy: HubspotStrategy, + secretKeys: { + clientId: SecretKeys.OAUTH_HUBSPOT_CLIENT_ID, + secret: SecretKeys.OAUTH_HUBSPOT_CLIENT_SECRET + }, + callback: hubspotDatasourceCallback, + path: '/auth/hubspot/callback', + extra: {} + }, + { + strategy: SalesforceStrategy, + secretKeys: { + clientId: SecretKeys.OAUTH_SALESFORCE_CLIENT_ID, + secret: SecretKeys.OAUTH_SALESFORCE_CLIENT_SECRET + }, + callback: salesForceDatasourceCallback, + path: '/auth/salesforce/callback', + extra: { + // for salesforce specifically scopes need to go here + scope: ['full', 'refresh_token'] + } + }, + { + strategy: XeroStrategy, + secretKeys: { + clientId: SecretKeys.OAUTH_XERO_CLIENT_ID, + secret: SecretKeys.OAUTH_XERO_CLIENT_SECRET + }, + callback: xeroDatasourceCallback, + path: '/auth/xero/callback', + extra: { + consumerKey: SecretKeys.OAUTH_XERO_CLIENT_ID, + consumerSecret: SecretKeys.OAUTH_XERO_CLIENT_SECRET //xero has a different name for the clientId and clientSecret + } + }, + { + strategy: SlackStrategy, + secretKeys: { + clientId: SecretKeys.OAUTH_SLACK_CLIENT_ID, + secret: SecretKeys.OAUTH_SLACK_CLIENT_SECRET + }, + callback: slackDatasourceCallback, + path: '/auth/slack/callback', + extra: {} + } + //need to add custom strategy for airtable + //google ads?? ]; +export async function slackDatasourceCallback(accessToken, refreshToken, profile, done) { + const slackCallbackLog = debug('webapp:oauth:datasourceOauth:slack:callback'); + slackCallbackLog(`Got refreshToken ${refreshToken} from callback`); + + profile.refreshToken = refreshToken; + + done(null, profile); +} + +export async function xeroDatasourceCallback(token, tokenSecret, profile, done) { + //token is what's used by airbyte + const xeroCallbackLog = debug('webapp:oauth:datasourceOauth:xero:callback'); + xeroCallbackLog( + `Got access token: ${token} from callback\nAlso got tokenSecret: ${tokenSecret} (Maybe refreshToken?) from callback` + ); + + profile.refreshToken = token; //even though this isn't necessarily a refreshToken it's the token we need to pass back to airbyte so keep it like this + + done(null, profile); +} + +export async function salesForceDatasourceCallback(accessToken, refreshToken, profile, done) { + const salesForceCallbackLog = debug('webapp:oauth:datasourceoauth:salesforce:callback'); + salesForceCallbackLog( + `Got refreshToken: ${refreshToken} \nAnd accessToken: ${accessToken} from callback` + ); + + profile.refreshToken = refreshToken; + done(null, profile); +} + +export async function hubspotDatasourceCallback(accessToken, refreshToken, profile, done) { + console.log(`Hubspot datasource callback with refreshToken: ${refreshToken}`); + //create the datasouce here, call done + + profile.refreshToken = refreshToken; + done(null, profile); +} + export async function githubCallback(accessToken, refreshToken, profile, done) { log(`githubCallback profile: ${JSON.stringify(profile, null, '\t')}`); const emails = await fetch('https://api.github.com/user/emails', { @@ -105,13 +195,29 @@ export async function googleCallback(accessToken, refreshToken, profile, done) { export async function serializeHandler(user, done) { log('serializeHandler user', user); - done(null, { oauthId: user.id, provider: user.provider }); + log('serializeHandler user', user?.id); + log('serializeHandler user', user?.refreshToken); + log('serializeHandler user', user?.provider); + const newUser = { + oauthId: user?.id, + provider: user?.provider, + refreshToken: user?.refreshToken + }; + log(newUser); + done(null, newUser); } export async function deserializeHandler(obj, done) { log('deserializeHandler obj', obj); const { oauthId, provider } = obj; - // Use provider information to retrieve the user e.g. + + // Special case for "airtable" provider + if (provider === 'airtable') { + log('Provider is "airtable", returning input object directly as user'); + return done(null, obj); // Return the input `obj` as the user object + } + + // For other providers, use provider information to retrieve the user const account: Account = await getAccountByOAuthOrEmail(oauthId, provider, null); if (account) { const accountObj = { @@ -127,6 +233,9 @@ export async function deserializeHandler(obj, done) { }; return done(null, accountObj); } + + // If no account is found, return null + log('No account found for oauthId:', oauthId, 'and provider:', provider); done(null, null); } diff --git a/webapp/src/lib/airbyte/setup.ts b/webapp/src/lib/airbyte/setup.ts index 78d75b57c..077f8066c 100644 --- a/webapp/src/lib/airbyte/setup.ts +++ b/webapp/src/lib/airbyte/setup.ts @@ -10,6 +10,7 @@ const lookup = util.promisify(dns.lookup); import * as process from 'node:process'; import getAirbyteApi, { AirbyteApiType, getAirbyteAuthToken } from 'airbyte/api'; +import OauthSecretProviderFactory from 'lib/oauthsecret'; import SecretProviderFactory from 'lib/secret'; import getAirbyteInternalApi from './internal'; @@ -203,18 +204,35 @@ async function updateWebhookUrls(workspaceId: string) { return updateWorkspaceRes; } -// async function overrideOauthCreds(workspaceId, name) { -// const internalApi = await getAirbyteInternalApi(); -// log("workspaceID: ", workspaceId); -// if(workspaceId !== undefined){ -// log("workspaceID: ", workspaceId); -// const updateOauthCredsRes = await internalApi.createOrUpdateWorkspaceOAuthCredentials({actorType: 'source', name, workspaceId}) -// .then(({ data }) => console.log(data)) -// .catch(err => console.error(err)); -// return (updateOauthCredsRes); -// } -// return null; -// } +async function overrideOauthCreds(workspaceId, name, clientId, clientSecret) { + log('workspaceID: ', workspaceId); + if (workspaceId !== undefined) { + log('workspaceID: ', workspaceId); + const updateOauthCredsRes = await fetch( + `${process.env.AIRBYTE_API_URL}/v1/workspaces/${workspaceId}/oauthCredentials`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${await getAirbyteAuthToken()}` + }, + body: { + actorType: 'source', + name, + workspaceId, + configuration: { + credentials: { + client_id: clientId, + client_secret: clientSecret + } + } + } + } + ); + return updateOauthCredsRes; + } + return null; +} // Main logic to handle Airbyte setup and configuration export async function init() { @@ -289,9 +307,15 @@ export async function init() { const updatedWebhookUrls = await updateWebhookUrls(airbyteAdminWorkspaceId); log('UPDATED_WEBHOOK_URLS', JSON.stringify(updatedWebhookUrls)); - // log('Overriding default ClientID and client secret for datasource OAuth integration'); + log('Overriding default ClientID and client secret for datasource OAuth integration'); // for (let provider in AIRBYTE_OAUTH_PROVIDERS) { - // overrideOauthCreds(airbyteAdminWorkspaceId, provider.toLowerCase()); + // const { clientId, clientSecret } = OauthSecretProviderFactory.getSecretProvider( + // provider.toLowerCase() + // ); + // log( + // `Overriding ${provider.toLowerCase()} clientId and clientSecret to ${clientId} and ${clientSecret}` + // ); + // overrideOauthCreds(airbyteAdminWorkspaceId, provider.toLowerCase(), clientId, clientSecret); // } process.env.NEXT_PUBLIC_IS_AIRBYTE_ENABLED = 'true'; return true; diff --git a/webapp/src/lib/middleware/auth/fetchsession.ts b/webapp/src/lib/middleware/auth/fetchsession.ts index 33261f6ba..2a7a18aba 100644 --- a/webapp/src/lib/middleware/auth/fetchsession.ts +++ b/webapp/src/lib/middleware/auth/fetchsession.ts @@ -5,7 +5,8 @@ import debug from 'debug'; const log = debug('webapp:session'); export default async function fetchSession(req, res, next) { - // log('req.session:', req.session); + log('req.session.passport:', req.session.passport); + // Proceed with the rest of fetchSession logic if (req.session && (req.session.accountId || req.session.passport?.user)) { let account: Account; if (req.session.accountId) { @@ -28,6 +29,27 @@ export default async function fetchSession(req, res, next) { oauth: account.oauth, permissions: account.permissions }; + if (req.session.passport?.user?.refreshToken) { + // log('found refreshToken: ', req.session.passport?.user?.refreshToken); + let provider = req.session.passport?.user?.provider; + if (provider === 'forcedotcom') { + //the passport strategy used for salesforce is forcedotcom, this is owned by salesforce but goes under a different name, so if we get 'forcedotcom' we actually mean 'salesforce' + provider = 'salesforce'; + } + const refreshToken = req.session.passport?.user?.refreshToken; + res.locals.datasourceOAuth = { + provider, + refreshToken + }; + } + + // Retrieve and store oauthData in res.locals if it exists in session + if (req.session.oauthData) { + const { datasourceName, datasourceDescription } = req.session.oauthData; + res.locals.oauthData = { datasourceName, datasourceDescription }; + // log('Retrieved oauthData from session:', res.locals.oauthData); + } + return next(); } req.session.destroy(); diff --git a/webapp/src/lib/middleware/oauth/fetchDatasource.ts b/webapp/src/lib/middleware/oauth/fetchDatasource.ts new file mode 100644 index 000000000..0340bf8c8 --- /dev/null +++ b/webapp/src/lib/middleware/oauth/fetchDatasource.ts @@ -0,0 +1,17 @@ +'use strict'; + +import debug from 'debug'; +const log = debug('webapp:fetchDatasource'); + +export default async function fetchDatasource(req, res, next) { + // Store datasourceName and datasourceDescription in the session if they exist in the query, this is used to get the datasource name, description and other relevant data from the datasourceModal between the webapp and the oauth callback + if (req.query.datasourceName && req.query.datasourceDescription) { + req.session.oauthData = { + datasourceName: req.query.datasourceName as string, + datasourceDescription: req.query.datasourceDescription as string + }; + log('Stored oauthData in session:', req.session.oauthData); + } + + next(); +} diff --git a/webapp/src/lib/middleware/passportmanager.ts b/webapp/src/lib/middleware/passportmanager.ts index ba4588a3b..a2fda9520 100644 --- a/webapp/src/lib/middleware/passportmanager.ts +++ b/webapp/src/lib/middleware/passportmanager.ts @@ -1,5 +1,6 @@ import debug from 'debug'; import passport from 'passport'; +import { Strategy as CustomStrategy } from 'passport-custom'; import SecretProviderFactory from 'secret/index'; import SecretKeys from 'secret/secretkeys'; import { OAuthStrategy } from 'struct/oauth'; @@ -56,6 +57,75 @@ class PassportManager { log('Successfully setup oauth authentication strategy for %s', s.path); }) ); + //airtable uses a custom connector so initialize it here + log('Setting up custom strategies'); + passport.use( + 'airtable', + new CustomStrategy(async (req, done) => { + try { + const code = typeof req.query.code === 'string' ? req.query.code : null; + if (!code) { + return done({ message: 'Authorization code not provided or invalid' }, null); + } + //generate valid base64 url encoded string for clientid and clientsecret + const credentials = `${process.env.OAUTH_AIRTABLE_CLIENT_ID}:${process.env.OAUTH_AIRTABLE_CLIENT_SECRET}`; + + // Step 2: Base64 encode the credentials + const base64 = Buffer.from(credentials).toString('base64'); + + // Step 3: Convert to URL-safe Base64 + // const base64Url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + + log(`Found code: ${code}`); + //Exchange auth code for access token + const tokenResponse = await fetch('https://airtable.com/oauth2/v1/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${base64}` + }, + body: new URLSearchParams({ + client_id: process.env.OAUTH_AIRTABLE_CLIENT_ID, + client_secret: process.env.OAUTH_AIRTABLE_CLIENT_SECRET, + code: code, + grant_type: 'authorization_code', + redirect_uri: `${process.env.URL_APP}/auth/airtable/callback`, + code_verifier: + 'pfkS9G3OpWoY_.laomB4YA_c3yEjZ26_ccha-7pw0x6RZgzesBoFsEFoUrNhLvh6kUqVj8Qp29Yh7l4X398ahPhM0AKkS6b.' + }) + }); + if (tokenResponse.status !== 200) { + throw new Error('Failed to retrieve tokens'); + } + + const { access_token, refresh_token, expires_in } = await tokenResponse.json(); + + //retrieve user information from Airtable api using the creds we just got + const userResponse = await fetch('https://api.airtable.com/v0/meta/whoami', { + headers: { + authorization: `Bearer ${access_token}` + } + }); + + if (userResponse.status !== 200) { + throw new Error('Failed to get user information'); + } + + let user = await userResponse.json(); + + //attach auth tokens to user object and return + user.accessToken = access_token; + user.refreshToken = refresh_token; + user.tokenExpiresIn = expires_in; + user.provider = 'airtable'; + + return done(null, user); + } catch (error) { + return done(error); + } + }) + ); + log('Passport manager initialized successfully'); } diff --git a/webapp/src/lib/oauthsecret/index.ts b/webapp/src/lib/oauthsecret/index.ts new file mode 100644 index 000000000..ef7ee6b2e --- /dev/null +++ b/webapp/src/lib/oauthsecret/index.ts @@ -0,0 +1,150 @@ +export default class OauthSecretProviderFactory { + static getSecretProvider(provider: string = 'local') { + switch (provider) { + case 'hubspot': + let clientId = process.env.OAUTH_HUBSPOT_CLIENT_ID || 'NOTFOUND'; + let clientSecret = process.env.OAUTH_HUBSPOT_CLIENT_SECRET || 'NOTFOUND'; + return { clientId, clientSecret }; + case 'salesforce': + clientId = process.env.OAUTH_SALESFORCE_CLIENT_ID || 'NOTFOUND'; + clientSecret = process.env.OAUTH_SALESFORCE_CLIENT_SECRET || 'NOTFOUND'; + return { clientId, clientSecret }; + case 'xero': + clientId = process.env.OAUTH_XERO_CLIENT_ID || 'NOTFOUND'; + clientSecret = process.env.OAUTH_XERO_CLIENT_SECRET || 'NOTFOUND'; + return { clientId, clientSecret }; + case 'slack': + clientId = process.env.OAUTH_SLACK_CLIENT_ID || 'NOTFOUND'; + clientSecret = process.env.OAUTH_SLACK_CLIENT_SECRET || 'NOTFOUND'; + return { clientId, clientSecret }; + } + } + + static getProviderScopes(provider: string = 'local') { + switch (provider) { + case 'hubspot-free': + const hubspotScopesBase = new Set([ + 'crm.lists.read', + 'crm.objects.contacts.read', + 'crm.objects.custom.read', + 'crm.objects.deals.read', + 'crm.objects.line_items.read', + 'crm.objects.marketing_events.read', + 'crm.objects.owners.read', + 'crm.objects.quotes.read', + 'crm.schemas.companies.read', + 'crm.schemas.contacts.read', + 'crm.schemas.deals.read', + 'crm.schemas.line_items.read', + 'crm.schemas.quotes.read', + 'settings.currencies.read', + 'settings.users.read', + 'settings.users.teams.read', + 'business-intelligence', + 'conversations.read', + 'crm.export', + 'forms', + 'forms-uploaded-files', + 'oauth', + 'integration-sync', + 'media_bridge.read', + 'sales-email-read', + 'tickets', + 'timeline' + ]); + + return [...hubspotScopesBase]; + case 'hubspot-professional': + const hubspotScopesProfessional = [ + 'cms.knowledge_base.articles.read', + 'cms.knowledge_base.settings.read', + 'crm.objects.feedback_submissions.read', + 'crm.objects.goals.read', + 'collector.graphql_query.execute', + 'collector.graphql_schema.read', + 'content', + 'ctas.read', + 'e-commerce' + ]; + + case 'hubspot-enterprise': + const hubspotScopesEnterprise = [ + 'crm.lists.read', + 'crm.lists.write', + 'crm.objects.companies.read', + 'crm.objects.companies.write', + 'crm.objects.contacts.read', + 'crm.objects.contacts.write', + 'crm.objects.deals.read', + 'crm.objects.deals.write', + 'crm.objects.line_items.read', + 'crm.objects.line_items.write', + 'crm.objects.marketing_events.read', + 'crm.objects.marketing_events.write', + 'crm.objects.owners.read', + 'crm.objects.quotes.read', + 'crm.objects.quotes.write', + 'crm.schemas.companies.read', + 'crm.schemas.contacts.read', + 'crm.schemas.deals.read', + 'crm.schemas.line_items.read', + 'crm.schemas.quotes.read', + 'crm.export', + 'crm.import', + 'forms', + 'forms-uploaded-files', + 'tickets', + 'timeline', + 'settings.currencies.read', + 'settings.users.read', + 'settings.users.teams.read', + 'account-info.security.read', + 'accounting', + 'actions', + 'oauth', + 'conversations.read', + 'conversations.write', + 'media_bridge.read', + 'media_bridge.write', + 'files', + 'integration-sync' + ]; + return hubspotScopesEnterprise; + } + } + + static getProviderPostData(token: string, provider: string) { + let data; + switch (provider) { + case 'hubspot': + let { clientId, clientSecret } = OauthSecretProviderFactory.getSecretProvider('hubspot'); + data = { + credentials: { + credentials_title: 'OAuth Credentials', + client_id: clientId, + clientSecret: clientSecret, + refresh_token: token + } + }; + return data; + case 'salesforce': + clientId = OauthSecretProviderFactory.getSecretProvider('salesforce').clientId; + clientSecret = OauthSecretProviderFactory.getSecretProvider('salesforce').clientSecret; + data = { + sourceType: 'salesforce', + auth_type: 'Client', + client_id: clientId, + client_secret: clientSecret, + refresh_token: token + }; + case 'xero': + clientId = OauthSecretProviderFactory.getSecretProvider('xero').clientId; + clientSecret = OauthSecretProviderFactory.getSecretProvider('xero').clientSecret; + data = {}; + case 'slack': + clientId = OauthSecretProviderFactory.getSecretProvider('slack').clientId; + clientSecret = OauthSecretProviderFactory.getSecretProvider('slack').clientSecret; + data = {}; + } + } +} diff --git a/webapp/src/lib/secret/hubspot.ts b/webapp/src/lib/secret/hubspot.ts new file mode 100644 index 000000000..b36488321 --- /dev/null +++ b/webapp/src/lib/secret/hubspot.ts @@ -0,0 +1,19 @@ +'use strict'; + +import { SecretManagerServiceClient } from '@google-cloud/secret-manager'; +import dotenv from 'dotenv'; +import SecretProvider from 'secret/provider'; +dotenv.config({ path: '.env' }); + +class HubspotSecretProvider extends SecretProvider { + #secretClient: any; + #cache = {}; + + async getSecret(key = '', bypassCache = false): Promise { + const clientId = process.env.OAUTH_HUBSPOT_CLIENT_ID; + const clientSecret = process.env.OAUTH_HUBSPOT_CLIENT_SECRET; + return { clientId, clientSecret }; + } +} + +export default new HubspotSecretProvider(); diff --git a/webapp/src/lib/secret/secretkeys.ts b/webapp/src/lib/secret/secretkeys.ts index 32316d3ae..c60906a6b 100644 --- a/webapp/src/lib/secret/secretkeys.ts +++ b/webapp/src/lib/secret/secretkeys.ts @@ -10,6 +10,14 @@ const SecretKeys = { OAUTH_GOOGLE_CLIENT_SECRET: 'OAUTH_GOOGLE_CLIENT_SECRET', OAUTH_HUBSPOT_CLIENT_SECRET: 'OAUTH_HUBSPOT_CLIENT_SECRET', OAUTH_HUBSPOT_CLIENT_ID: 'OAUTH_HUBSPOT_CLIENT_ID', + OAUTH_SALESFORCE_CLIENT_ID: 'OAUTH_SALESFORCE_CLIENT_ID', + OAUTH_SALESFORCE_CLIENT_SECRET: 'OAUTH_SALESFORCE_CLIENT_SECRET', + OAUTH_XERO_CLIENT_ID: 'OAUTH_XERO_CLIENT_ID', + OAUTH_XERO_CLIENT_SECRET: 'OAUTH_XERO_CLIENT_SECRET', + OAUTH_SLACK_CLIENT_ID: 'OAUTH_SLACK_CLIENT_ID', + OAUTH_SLACK_CLIENT_SECRET: 'OAUTH_SLACK_CLIENT_SECRET', + OAUTH_AIRTABLE_CLIENT_ID: 'OAUTH_AIRTABLE_CLIENT_ID', + OAUTH_AIRTABLE_CLIENT_SECRET: 'OAUTH_AIRTABLE_CLIENT_SECRET', STRIPE_ACCOUNT_SECRET: 'STRIPE_ACCOUNT_SECRET', STRIPE_WEBHOOK_SECRET: 'STRIPE_WEBHOOK_SECRET' }; diff --git a/webapp/src/lib/struct/oauth.ts b/webapp/src/lib/struct/oauth.ts index f4965855d..5be86aef0 100644 --- a/webapp/src/lib/struct/oauth.ts +++ b/webapp/src/lib/struct/oauth.ts @@ -2,7 +2,8 @@ export enum OAUTH_PROVIDER { GOOGLE = 'google', - GITHUB = 'github' + GITHUB = 'github', + HUBSPOT = 'hubspot' } export type OAuthStrategy = { @@ -16,6 +17,20 @@ export type OAuthStrategy = { extra?: any; // Stuff like scope (this object is a different shape depending on provider hence any) }; +export type CustomOAuthStrategy = { + strategy: any; + name: string; + verify: Function; + callback: Function; + secretKeys: { + clientId: string; + secret: string; + }; + path: string; + extra?: any; +}; + export enum AIRBYTE_OAUTH_PROVIDERS { //OAuth to initiate airbyte datasource connection is handled seperately from OAuth to register/log in to agent cloud - HUBSPOT = 'hubspot' + HUBSPOT = 'hubspot', + SALESFORCE = 'salesforce' } diff --git a/webapp/src/pages/[resourceSlug]/datasource/add.tsx b/webapp/src/pages/[resourceSlug]/datasource/add.tsx index 125a301f3..ac114139c 100644 --- a/webapp/src/pages/[resourceSlug]/datasource/add.tsx +++ b/webapp/src/pages/[resourceSlug]/datasource/add.tsx @@ -15,6 +15,11 @@ export default function AddDatasource(props) { const { account, csrf, teamName } = accountContext as any; const router = useRouter(); const { resourceSlug } = router.query; + const [provider, setProvider] = useState(null); + const [token, setToken] = useState(null); + const [datasourceName, setDatasourceName] = useState(null); + const [datasourceDescription, setDatasourceDescription] = useState(null); + console.log('provider and token: ', provider, token); const [state, dispatch] = useState(props); const [error, setError] = useState(); const [models, setModels] = useState(); @@ -28,6 +33,23 @@ export default function AddDatasource(props) { fetchDatasourceFormData(); }, [resourceSlug]); + useEffect(() => { + if (typeof location != undefined) { + const retrievedToken = new URLSearchParams(location.search).get('token'); + setToken(retrievedToken); + const retrievedProvider = new URLSearchParams(location.search).get('provider'); + setProvider(retrievedProvider); + const retrievedName = new URLSearchParams(location.search).get('datasourceName'); + setDatasourceName(retrievedName); + const retrievedDescription = new URLSearchParams(location.search).get( + 'datasourceDescription' + ); + setDatasourceDescription(retrievedDescription); + console.log('retrievedName: ', retrievedName); + console.log('retrievedDescription: ', retrievedDescription); + } + }, []); + if (models == null) { return ; } @@ -48,6 +70,10 @@ export default function AddDatasource(props) { setSpec={setSpec} fetchDatasourceFormData={fetchDatasourceFormData} initialStep={2} + token={token} + provider={provider} + name={datasourceName} + description={datasourceDescription} /> ); diff --git a/webapp/src/pages/[resourceSlug]/datasources.tsx b/webapp/src/pages/[resourceSlug]/datasources.tsx index 9b0900655..3f34fb7c0 100644 --- a/webapp/src/pages/[resourceSlug]/datasources.tsx +++ b/webapp/src/pages/[resourceSlug]/datasources.tsx @@ -23,7 +23,8 @@ export default function Datasources(props) { const { resourceSlug } = router.query; const [state, dispatch] = useState(props); const [error, setError] = useState(null); - const { datasources, models, vectorDbs } = state; + const { datasources, models, datasourceOAuth, vectorDbs } = state; + console.log('datasourceOAuth: ', datasourceOAuth); const filteredDatasources = datasources?.filter(x => !x.hidden); const [open, setOpen] = useState(false); const [spec, setSpec] = useState(null); diff --git a/webapp/src/router.ts b/webapp/src/router.ts index c93061e84..cfe4ffd16 100644 --- a/webapp/src/router.ts +++ b/webapp/src/router.ts @@ -7,6 +7,7 @@ import { setParamOrgAndTeam } from '@mw/auth/checkresourceslug'; import checkSession from '@mw/auth/checksession'; +import checkSessionWelcome from '@mw/auth/checksessionwelcome'; import { checkSubscriptionBoolean, checkSubscriptionLimit, @@ -21,6 +22,7 @@ import useJWT from '@mw/auth/usejwt'; import useSession from '@mw/auth/usesession'; import onboardedMiddleware from '@mw/checkonboarded'; import homeRedirect from '@mw/homeredirect'; +import fetchDatasource from '@mw/oauth/fetchDatasource'; import PassportManager from '@mw/passportmanager'; import * as hasPerms from '@mw/permissions/hasperms'; import renderStaticPage from '@mw/render/staticpage'; @@ -33,7 +35,7 @@ import { PlanLimitsKeys, pricingMatrix, SubscriptionPlan } from 'struct/billing' const unauthedMiddlewareChain = [useSession, useJWT, fetchSession, onboardedMiddleware]; const authedMiddlewareChain = [...unauthedMiddlewareChain, checkSession, csrfMiddleware]; -import checkSessionWelcome from '@mw/auth/checksessionwelcome'; +import { restrictToFirstScrollableAncestor } from '@dnd-kit/modifiers'; import * as accountController from 'controllers/account'; import * as agentController from 'controllers/agent'; import * as airbyteProxyController from 'controllers/airbyte'; @@ -43,6 +45,7 @@ import * as assetController from 'controllers/asset'; import * as datasourceController from 'controllers/datasource'; import * as modelController from 'controllers/model'; import * as notificationController from 'controllers/notification'; +import * as oauthController from 'controllers/oauth'; import * as orgController from 'controllers/org'; import * as sessionController from 'controllers/session'; import * as sharelinkController from 'controllers/sharelink'; @@ -52,6 +55,8 @@ import * as teamController from 'controllers/team'; import * as toolController from 'controllers/tool'; import * as variableController from 'controllers/variables'; import * as vectorDbController from 'controllers/vectordb'; +import debug from 'debug'; +import OauthSecretProviderFactory from 'lib/oauthsecret'; export default function router(server, app) { server.use('/static', express.static('static')); @@ -104,6 +109,117 @@ export default function router(server, app) { res.redirect(`/auth/redirect?to=${encodeURIComponent('/')}`); } ); + oauthRouter.get( + '/hubspot', + fetchDatasource, + passportInstance.authenticate('hubspot', {scope: OauthSecretProviderFactory.getProviderScopes('hubspot-free')}), + fetchSession, + ); + + oauthRouter.get( + '/hubspot/professional', + passportInstance.authenticate('hubspot', {scope: OauthSecretProviderFactory.getProviderScopes('hubspot-professional'), redirect_uri: `${process.env.URL_APP}/auth/hubspot/callback`}), + fetchSession + ); + oauthRouter.get( + '/hubspot/enterprise', + passportInstance.authenticate('hubspot', {scope: OauthSecretProviderFactory.getProviderScopes('hubspot-enterprise'), redirect_uri: `${process.env.URL_APP}/auth/hubspot/callback`}), + fetchSession + ); + oauthRouter.get( + '/hubspot/callback', + useSession, + useJWT, + fetchSession, + // passportInstance.authenticate('hubspot'), + (req, res) => { + const log = debug('webapp:oauth:hubspot:callback'); + const datasourceName = res.locals.oauthData.datasourceName || 'NOTFOUND'; + const datasourceDescription = res.locals.oauthData.datasourceDescription || 'NOTFOUND'; + log("Received query params \ndatasourceName: ", datasourceName, "\ndatasourceDescription: ", datasourceDescription); + + res.redirect(`/${res.locals.account.currentTeam}/datasource/add?token=${encodeURIComponent(res.locals.datasourceOAuth.refreshToken)}&provider=${encodeURIComponent(res.locals.datasourceOAuth.provider)}&datasourceName=${encodeURIComponent(datasourceName)}&datasourceDescription=${encodeURIComponent(datasourceDescription)}`); + } + ); + oauthRouter.get( + '/salesforce', + fetchDatasource, + passportInstance.authenticate('forcedotcom'), + fetchSession + ); + oauthRouter.get( + '/salesforce/callback', + useSession, + useJWT, + fetchSession, + // passportInstance.authenticate('forcedotcom'), don't need to call verify function + (req, res) => { + const log = debug('webapp:router:salesforce:callback'); + const datasourceName = res.locals.oauthData.datasourceName || 'NOTFOUND'; + const datasourceDescription = res.locals.oauthData.datasourceDescription || 'NOTFOUND'; + log(`Recieved query params \ndatasourceName: ${datasourceName}\ndatasourceDescription: ${datasourceDescription}`); + + res.redirect(`/${res.locals.account.currentTeam}/datasource/add?token=${encodeURIComponent(res.locals.datasourceOAuth.refreshToken)}&provider=${encodeURIComponent(res.locals.datasourceOAuth.provider)}&datasourceName=${encodeURIComponent(datasourceName)}&datasourceDescription=${encodeURIComponent(datasourceDescription)}`) + } + ); + oauthRouter.get( + '/xero', + fetchDatasource, + passportInstance.authenticate('xero'), + fetchSession + ); + oauthRouter.get( + '/xero/callback', + useSession, + useJWT, + fetchSession, + // passportInstance.authenticate('xero'), + (req, res) => { + const log = debug('webapp:router:salesforce:callback'); + const datasourceName = res.locals.oauthData.datasourceName || 'NOTFOUND'; + const datasourceDescription = res.locals.oauthData.datasourceDescription || 'NOTFOUND'; + log(`Recieved query params \ndatasourceName: ${datasourceName}\ndatasourceDescription: ${datasourceDescription}`); + + res.redirect(`/${res.locals.account.currentTeam}/datasource/add?token=${encodeURIComponent(res.locals.datasourceOAuth.refreshToken)}&provider=${encodeURIComponent(res.locals.datasourceOAuth.provider)}&datasourceName=${encodeURIComponent(datasourceName)}&datasourceDescription=${encodeURIComponent(datasourceDescription)}`) + } + ); + + oauthRouter.get( + '/airtable', + fetchDatasource, + (req, res) => { + const staticCodeVerifier = 'pfkS9G3OpWoY_.laomB4YA_c3yEjZ26_ccha-7pw0x6RZgzesBoFsEFoUrNhLvh6kUqVj8Qp29Yh7l4X398ahPhM0AKkS6b.'; + const staticCodeChallenge = 'MkUeCGFfmUWZfJ-MC4p3FQ5fZr-yhqQUIesX3LVpu24'; // SHA-256 of "test_verifier" + const authorizationUrl = 'https://airtable.com/oauth2/v1/authorize'; //because airtable isn't a supported passport strategy we need to manually construct and execute the redirect + const params = new URLSearchParams({ + client_id: process.env.OAUTH_AIRTABLE_CLIENT_ID, + redirect_uri: `${process.env.URL_APP}/auth/airtable/callback`, + response_type: 'code', + scope: 'data.records:read', + state: 'static_state_string', //state is a required parameter for the request to airtable + code_challenge: staticCodeChallenge, // Use static challenge + code_challenge_method: 'S256' // Method for PKCE + }); + + res.redirect(`${authorizationUrl}?${params.toString()}`); + } + ); + oauthRouter.get( + '/airtable/callback', + useSession, + useJWT, + fetchSession, + passportInstance.authenticate('airtable'), + (req, res) => { + const log = debug('webapp:router:airtable:callback'); + const datasourceName = res.locals.oauthData.datasourceName || 'NOTFOUND'; + const datasourceDescription = res.locals.oauthData.datasourceDescription || 'NOTFOUND'; + log(`Recieved query params \ndatasourceName: ${datasourceName}\ndatasourceDescription: ${datasourceDescription}`); + + res.redirect(`/${res.locals.account.currentTeam}/datasource/add?token=${encodeURIComponent(res.locals.datasourceOAuth.refreshToken)}&provider=${encodeURIComponent(res.locals.datasourceOAuth.provider)}&datasourceName=${encodeURIComponent(datasourceName)}&datasourceDescription=${encodeURIComponent(datasourceDescription)}`) + } + ) + server.use('/auth', useSession, passportInstance.session(), oauthRouter); // Body and query parsing middleware @@ -176,6 +292,7 @@ export default function router(server, app) { accountController.welcomeJson ); + //TODO: move and rename all these server.post( '/stripe-portallink',