diff --git a/packages/backend/src/apps/hubspot/actions/create-contact/index.ts b/packages/backend/src/apps/hubspot/actions/create-contact/index.ts new file mode 100644 index 00000000..9d1622c8 --- /dev/null +++ b/packages/backend/src/apps/hubspot/actions/create-contact/index.ts @@ -0,0 +1,83 @@ +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Create contact', + key: 'createContact', + description: `Create contact on user's account.`, + arguments: [ + { + label: 'Company name', + key: 'company', + type: 'string' as const, + required: false, + variables: true, + }, + { + label: 'Email', + key: 'email', + type: 'string' as const, + required: false, + variables: true, + }, + { + label: 'First name', + key: 'firstName', + type: 'string' as const, + required: false, + variables: true, + }, + { + label: 'Last name', + key: 'lastName', + type: 'string' as const, + required: false, + description: 'Last name', + variables: true, + }, + { + label: 'Phone', + key: 'phone', + type: 'string' as const, + required: false, + variables: true, + }, + { + label: 'Website URL', + key: 'website', + type: 'string' as const, + required: false, + variables: true, + }, + { + label: 'Owner ID', + key: 'hubspotOwnerId', + type: 'string' as const, + required: false, + variables: true, + }, + ], + + async run($) { + const company = $.step.parameters.company as string; + const email = $.step.parameters.email as string; + const firstName = $.step.parameters.firstName as string; + const lastName = $.step.parameters.lastName as string; + const phone = $.step.parameters.phone as string; + const website = $.step.parameters.website as string; + const hubspotOwnerId = $.step.parameters.hubspotOwnerId as string; + + const response = await $.http.post(`crm/v3/objects/contacts`, { + properties: { + company, + email, + firstname: firstName, + lastname: lastName, + phone, + website, + hubspot_owner_id: hubspotOwnerId, + }, + }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/hubspot/actions/index.ts b/packages/backend/src/apps/hubspot/actions/index.ts new file mode 100644 index 00000000..fe753ed8 --- /dev/null +++ b/packages/backend/src/apps/hubspot/actions/index.ts @@ -0,0 +1,3 @@ +import createContact from './create-contact'; + +export default [ createContact ]; diff --git a/packages/backend/src/apps/hubspot/assets/favicon.svg b/packages/backend/src/apps/hubspot/assets/favicon.svg new file mode 100644 index 00000000..c21891fb --- /dev/null +++ b/packages/backend/src/apps/hubspot/assets/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/backend/src/apps/hubspot/auth/generate-auth-url.ts b/packages/backend/src/apps/hubspot/auth/generate-auth-url.ts new file mode 100644 index 00000000..d8f33dd4 --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/generate-auth-url.ts @@ -0,0 +1,20 @@ +import { IField, IGlobalVariable } from '@automatisch/types'; +import { URLSearchParams } from 'url'; +import scopes from '../common/scopes'; + +export default async function generateAuthUrl($: IGlobalVariable) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const callbackUrl = oauthRedirectUrlField.value as string; + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId as string, + redirect_uri: callbackUrl, + scope: scopes.join(' '), + }); + + const url = `https://app.hubspot.com/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ url }); +} diff --git a/packages/backend/src/apps/hubspot/auth/index.ts b/packages/backend/src/apps/hubspot/auth/index.ts new file mode 100644 index 00000000..c7f57dbf --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/index.ts @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url'; +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; +import refreshToken from './refresh-token'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string' as const, + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/hubspot/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in HubSpot OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/hubspot/auth/is-still-verified.ts b/packages/backend/src/apps/hubspot/auth/is-still-verified.ts new file mode 100644 index 00000000..cccd73be --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/is-still-verified.ts @@ -0,0 +1,10 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getAccessTokenInfo from '../common/get-access-token-info'; + +const isStillVerified = async ($: IGlobalVariable) => { + await getAccessTokenInfo($); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/hubspot/auth/refresh-token.ts b/packages/backend/src/apps/hubspot/auth/refresh-token.ts new file mode 100644 index 00000000..c1fc8a9d --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/refresh-token.ts @@ -0,0 +1,28 @@ +import { IGlobalVariable, IField } from '@automatisch/types'; +import { URLSearchParams } from 'url'; + +const refreshToken = async ($: IGlobalVariable) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + + const callbackUrl = oauthRedirectUrlField.value as string; + + const params = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: $.auth.data.clientId as string, + client_secret: $.auth.data.clientSecret as string, + redirect_uri: callbackUrl, + refresh_token: $.auth.data.refreshToken as string, + }); + + const { data } = await $.http.post('/oauth/v1/token', params.toString()); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/hubspot/auth/verify-credentials.ts b/packages/backend/src/apps/hubspot/auth/verify-credentials.ts new file mode 100644 index 00000000..130bf171 --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/verify-credentials.ts @@ -0,0 +1,52 @@ +import { IGlobalVariable, IField } from '@automatisch/types'; +import { URLSearchParams } from 'url'; +import getAccessTokenInfo from '../common/get-access-token-info'; + +const verifyCredentials = async ($: IGlobalVariable) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const callbackUrl = oauthRedirectUrlField.value as string; + const params = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: $.auth.data.clientId as string, + client_secret: $.auth.data.clientSecret as string, + redirect_uri: callbackUrl, + code: $.auth.data.code as string, + }); + + const { data: verifiedCredentials } = await $.http.post( + '/oauth/v1/token', + params.toString() + ); + + const { + access_token: accessToken, + refresh_token: refreshToken, + expires_in: expiresIn, + } = verifiedCredentials; + + await $.auth.set({ + accessToken, + refreshToken, + expiresIn, + }); + + const accessTokenInfo = await getAccessTokenInfo($); + + await $.auth.set({ + screenName: accessTokenInfo.user, + hubDomain: accessTokenInfo.hub_domain, + scopes: accessTokenInfo.scopes, + scopeToScopeGroupPks: accessTokenInfo.scope_to_scope_group_pks, + trialScopes: accessTokenInfo.trial_scopes, + trialScopeToScoreGroupPks: accessTokenInfo.trial_scope_to_scope_group_pks, + hubId: accessTokenInfo.hub_id, + appId: accessTokenInfo.app_id, + userId: accessTokenInfo.user_id, + expiresIn: accessTokenInfo.expires_in, + tokenType: accessTokenInfo.token_type, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/hubspot/common/add-auth-header.ts b/packages/backend/src/apps/hubspot/common/add-auth-header.ts new file mode 100644 index 00000000..d16f394f --- /dev/null +++ b/packages/backend/src/apps/hubspot/common/add-auth-header.ts @@ -0,0 +1,14 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + if (requestConfig.additionalProperties?.skipAddingAuthHeader) return requestConfig; + + if ($.auth.data?.accessToken) { + const authorizationHeader = `Bearer ${$.auth.data.accessToken}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/hubspot/common/get-access-token-info.ts b/packages/backend/src/apps/hubspot/common/get-access-token-info.ts new file mode 100644 index 00000000..55ed8ff2 --- /dev/null +++ b/packages/backend/src/apps/hubspot/common/get-access-token-info.ts @@ -0,0 +1,11 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; + +const getAccessTokenInfo = async ($: IGlobalVariable): Promise => { + const response = await $.http.get( + `/oauth/v1/access-tokens/${$.auth.data.accessToken}` + ); + + return response.data; +}; + +export default getAccessTokenInfo; diff --git a/packages/backend/src/apps/hubspot/common/scopes.ts b/packages/backend/src/apps/hubspot/common/scopes.ts new file mode 100644 index 00000000..38cb30a3 --- /dev/null +++ b/packages/backend/src/apps/hubspot/common/scopes.ts @@ -0,0 +1,3 @@ +const scopes = ['crm.objects.contacts.read', 'crm.objects.contacts.write']; + +export default scopes; diff --git a/packages/backend/src/apps/hubspot/index.d.ts b/packages/backend/src/apps/hubspot/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/hubspot/index.ts b/packages/backend/src/apps/hubspot/index.ts new file mode 100644 index 00000000..9f35bade --- /dev/null +++ b/packages/backend/src/apps/hubspot/index.ts @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import actions from './actions'; +import auth from './auth'; + +export default defineApp({ + name: 'HubSpot', + key: 'hubspot', + iconUrl: '{BASE_URL}/apps/hubspot/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/hubspot/connection', + supportsConnections: true, + baseUrl: 'https://www.hubspot.com', + apiBaseUrl: 'https://api.hubapi.com', + primaryColor: 'F95C35', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index d87539a5..c8844642 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -151,6 +151,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/http-request/connection' }, ], }, + { + text: 'HubSpot', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/hubspot/actions' }, + { text: 'Connection', link: '/apps/hubspot/connection' }, + ], + }, { text: 'Mattermost', collapsible: true, diff --git a/packages/docs/pages/apps/hubspot/actions.md b/packages/docs/pages/apps/hubspot/actions.md new file mode 100644 index 00000000..f2ae7746 --- /dev/null +++ b/packages/docs/pages/apps/hubspot/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/hubspot.svg +items: + - name: Create a contact + desc: Create a contact on user's account. +--- + + + + diff --git a/packages/docs/pages/apps/hubspot/connection.md b/packages/docs/pages/apps/hubspot/connection.md new file mode 100644 index 00000000..37864d08 --- /dev/null +++ b/packages/docs/pages/apps/hubspot/connection.md @@ -0,0 +1,22 @@ +# HubSpot + +:::info +This page explains the steps you need to follow to set up the Hubspot connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [HubSpot Developer page](https://developers.hubspot.com/). +2. Login into your developer account. +3. Click on the **Manage apps** button. +4. Click on the **Create app** button. +5. Fill the **Public app name** field with the name of your API app. +6. Go to the **Auth** tab. +7. Fill the **Redirect URL(s)** field with the OAuth Redirect URL from the Automatisch connection creation page. +8. Go to the **Scopes** tab. +9. Select the scopes you want to use with Automatisch. +10. Click on the **Create App** button. +11. Go back to the **Auth** tab. +12. Copy the **Client ID** and **Client Secret** values. +13. Paste the **Client ID** value into Automatisch as **Client ID**, respectively. +14. Paste the **Client Secret** value into Automatisch as **Client Secret**, respectively. +15. Click the **Submit** button on Automatisch. +16. Now, you can start using the HubSpot connection with Automatisch. diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md index 63578431..45a52157 100644 --- a/packages/docs/pages/guide/available-apps.md +++ b/packages/docs/pages/guide/available-apps.md @@ -15,6 +15,7 @@ The following integrations are currently supported by Automatisch. - [Google Forms](/apps/google-forms/triggers) - [Google Sheets](/apps/google-sheets/triggers) - [HTTP Request](/apps/http-request/actions) +- [HubSpot](/apps/hubspot/actions) - [Mattermost](/apps/mattermost/actions) - [Notion](/apps/notion/triggers) - [Ntfy](/apps/ntfy/actions) diff --git a/packages/docs/pages/public/favicons/hubspot.svg b/packages/docs/pages/public/favicons/hubspot.svg new file mode 100644 index 00000000..c21891fb --- /dev/null +++ b/packages/docs/pages/public/favicons/hubspot.svg @@ -0,0 +1,8 @@ + + + + + + + +