diff --git a/packages/backend/src/apps/salesforce/assets/favicon.svg b/packages/backend/src/apps/salesforce/assets/favicon.svg new file mode 100644 index 00000000..e82db677 --- /dev/null +++ b/packages/backend/src/apps/salesforce/assets/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/salesforce/auth/create-auth-data.ts b/packages/backend/src/apps/salesforce/auth/create-auth-data.ts new file mode 100644 index 00000000..eb83ab73 --- /dev/null +++ b/packages/backend/src/apps/salesforce/auth/create-auth-data.ts @@ -0,0 +1,24 @@ +import { IField, IGlobalVariable } from '@automatisch/types'; +import qs from 'qs'; + +export default async function createAuthData($: IGlobalVariable) { + try { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = qs.stringify({ + client_id: $.auth.data.consumerKey as string, + redirect_uri: redirectUri, + response_type: 'code' + }) + + await $.auth.set({ + url: `${$.auth.data.oauth2Url}/authorize?${searchParams}`, + }); + } catch (error) { + throw new Error( + `Error occured while verifying credentials: ${error}` + ); + } +} diff --git a/packages/backend/src/apps/salesforce/auth/index.ts b/packages/backend/src/apps/salesforce/auth/index.ts new file mode 100644 index 00000000..c89aede1 --- /dev/null +++ b/packages/backend/src/apps/salesforce/auth/index.ts @@ -0,0 +1,152 @@ +import createAuthData from './create-auth-data'; +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string' as const, + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/salesforce/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Salesforce OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'oauth2Url', + label: 'Salesforce Environment', + type: 'dropdown' as const, + required: true, + readOnly: false, + value: 'https://login.salesforce.com/services/oauth2', + placeholder: null, + description: 'Most people should choose the default, "production".', + clickToCopy: false, + options: [ + { + label: 'production', + value: 'https://login.salesforce.com/services/oauth2', + }, + { + label: 'sandbox', + value: 'https://test.salesforce.com/services/oauth2', + } + ] + }, + { + key: 'consumerKey', + label: 'Consumer Key', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'consumerSecret', + label: 'Consumer Secret', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + authenticationSteps: [ + { + step: 1, + type: 'mutation' as const, + name: 'createConnection', + arguments: [ + { + name: 'key', + value: '{key}', + }, + { + name: 'formattedData', + value: null, + properties: [ + { + name: 'oauth2Url', + value: '{fields.oauth2Url}' + }, + { + name: 'consumerKey', + value: '{fields.consumerKey}', + }, + { + name: 'consumerSecret', + value: '{fields.consumerSecret}', + }, + ], + }, + ], + }, + { + step: 2, + type: 'mutation' as const, + name: 'createAuthData', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + }, + { + step: 3, + type: 'openWithPopup' as const, + name: 'openAuthPopup', + arguments: [ + { + name: 'url', + value: '{createAuthData.url}', + }, + ], + }, + { + step: 4, + type: 'mutation' as const, + name: 'updateConnection', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + { + name: 'formattedData', + value: null, + properties: [ + { + name: 'code', + value: '{openAuthPopup.code}', + }, + ], + }, + ], + }, + { + step: 5, + type: 'mutation' as const, + name: 'verifyConnection', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + }, + ], + + createAuthData, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/salesforce/auth/is-still-verified.ts b/packages/backend/src/apps/salesforce/auth/is-still-verified.ts new file mode 100644 index 00000000..8f578344 --- /dev/null +++ b/packages/backend/src/apps/salesforce/auth/is-still-verified.ts @@ -0,0 +1,13 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; + +const isStillVerified = async ($: IGlobalVariable) => { + try { + const user = await getCurrentUser($); + return !!user; + } catch (error) { + return false; + } +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/salesforce/auth/verify-credentials.ts b/packages/backend/src/apps/salesforce/auth/verify-credentials.ts new file mode 100644 index 00000000..41651050 --- /dev/null +++ b/packages/backend/src/apps/salesforce/auth/verify-credentials.ts @@ -0,0 +1,41 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; +import qs from 'qs'; + +const verifyCredentials = async ($: IGlobalVariable) => { + try { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = qs.stringify({ + code: $.auth.data.code, + grant_type: 'authorization_code', + client_id: $.auth.data.consumerKey as string, + client_secret: $.auth.data.consumerSecret as string, + redirect_uri: redirectUri + }); + const { data } = await $.http.post( + `${$.auth.data.oauth2Url}/token?${searchParams}`, + ); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + instanceUrl: data.instance_url, + signature: data.signature, + userId: data.id, + screenName: data.instance_url, + }); + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + screenName: `${currentUser.displayName} - ${data.instance_url}`, + }); + } catch (error) { + throw new Error(error.response.data); + } +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/salesforce/common/add-auth-header.ts b/packages/backend/src/apps/salesforce/common/add-auth-header.ts new file mode 100644 index 00000000..3a9848da --- /dev/null +++ b/packages/backend/src/apps/salesforce/common/add-auth-header.ts @@ -0,0 +1,17 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + const { instanceUrl, tokenType, accessToken } = $.auth.data; + + if (instanceUrl) { + requestConfig.baseURL = instanceUrl as string; + } + + if (tokenType && accessToken) { + requestConfig.headers.Authorization = `${tokenType} ${accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/salesforce/common/get-current-user.ts b/packages/backend/src/apps/salesforce/common/get-current-user.ts new file mode 100644 index 00000000..cc82e4a2 --- /dev/null +++ b/packages/backend/src/apps/salesforce/common/get-current-user.ts @@ -0,0 +1,10 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; + +const getCurrentUser = async ($: IGlobalVariable): Promise => { + const response = await $.http.get('/services/data/v55.0/chatter/users/me'); + const currentUser = response.data; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/salesforce/index.d.ts b/packages/backend/src/apps/salesforce/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/salesforce/index.ts b/packages/backend/src/apps/salesforce/index.ts new file mode 100644 index 00000000..2a89e164 --- /dev/null +++ b/packages/backend/src/apps/salesforce/index.ts @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; + +export default defineApp({ + name: 'Salesforce', + key: 'salesforce', + iconUrl: '{BASE_URL}/apps/salesforce/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/connections/salesforce', + supportsConnections: true, + baseUrl: 'https://salesforce.com', + apiBaseUrl: '', + primaryColor: '00A1E0', + beforeRequest: [addAuthHeader], + auth, +}); diff --git a/packages/web/src/components/InputCreator/index.tsx b/packages/web/src/components/InputCreator/index.tsx index d2a1456e..5c19f53a 100644 --- a/packages/web/src/components/InputCreator/index.tsx +++ b/packages/web/src/components/InputCreator/index.tsx @@ -22,7 +22,6 @@ type RawOption = { }; const optionGenerator = (options: RawOption[]): IFieldDropdownOption[] => options?.map(({ name, value }) => ({ label: name as string, value: value })); -const getOption = (options: IFieldDropdownOption[], value?: string | boolean) => options?.find(option => option.value === value); export default function InputCreator(props: InputCreatorProps): React.ReactElement { const { @@ -62,7 +61,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme disableClearable={required} options={preparedOptions} renderInput={(params) => } - value={getOption(preparedOptions, value)} + defaultValue={value as string} onChange={console.log} description={description} loading={loading}