diff --git a/packages/backend/src/apps/smartcar/actions/get-vehicle-location/index.ts b/packages/backend/src/apps/smartcar/actions/get-vehicle-location/index.ts new file mode 100644 index 00000000..c846a766 --- /dev/null +++ b/packages/backend/src/apps/smartcar/actions/get-vehicle-location/index.ts @@ -0,0 +1,36 @@ +import defineAction from '../../../../helpers/define-action'; + +export default defineAction({ + name: 'Get Vehicle Location', + key: 'getVehicleLocation', + description: 'Get the location of a vehicle', + arguments: [ + { + label: 'Vehicle', + key: 'vehicle', + type: 'dropdown' as const, + required: true, + description: 'The vehicle to get the location of.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listVehicles', + }, + ], + }, + }, + ], + + async run($) { + const { vehicle } = $.step.parameters; + const response = await $.http.get( + `/vehicles/${vehicle}/location?mode=simulated` + ); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/smartcar/actions/index.ts b/packages/backend/src/apps/smartcar/actions/index.ts new file mode 100644 index 00000000..73a09721 --- /dev/null +++ b/packages/backend/src/apps/smartcar/actions/index.ts @@ -0,0 +1,3 @@ +import getVehicleLocation from './get-vehicle-location'; + +export default [getVehicleLocation]; diff --git a/packages/backend/src/apps/smartcar/assets/favicon.svg b/packages/backend/src/apps/smartcar/assets/favicon.svg new file mode 100644 index 00000000..7ac87f18 --- /dev/null +++ b/packages/backend/src/apps/smartcar/assets/favicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/backend/src/apps/smartcar/auth/generate-auth-url.ts b/packages/backend/src/apps/smartcar/auth/generate-auth-url.ts new file mode 100644 index 00000000..01c2825e --- /dev/null +++ b/packages/backend/src/apps/smartcar/auth/generate-auth-url.ts @@ -0,0 +1,29 @@ +import { IField, IGlobalVariable } from '@automatisch/types'; +import { URLSearchParams } from 'url'; + +export default async function generateAuthUrl($: IGlobalVariable) { + const scopes = [ + 'read_odometer', + 'read_vehicle_info', + 'required:read_location', + ]; + + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value as string; + + const searchParams = new URLSearchParams({ + response_type: 'code', + client_id: $.auth.data.clientId as string, + scope: scopes.join(' '), + redirect_uri: redirectUri, + mode: 'simulated', + }); + + const url = `https://connect.smartcar.com/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/smartcar/auth/index.ts b/packages/backend/src/apps/smartcar/auth/index.ts new file mode 100644 index 00000000..c4622572 --- /dev/null +++ b/packages/backend/src/apps/smartcar/auth/index.ts @@ -0,0 +1,58 @@ +import generateAuthUrl from './generate-auth-url'; +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/twitter/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Smartcar OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'screenName', + label: 'Screen Name', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + 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, +}; diff --git a/packages/backend/src/apps/smartcar/auth/is-still-verified.ts b/packages/backend/src/apps/smartcar/auth/is-still-verified.ts new file mode 100644 index 00000000..d36919f2 --- /dev/null +++ b/packages/backend/src/apps/smartcar/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; + +const isStillVerified = async ($: IGlobalVariable) => { + const user = await getCurrentUser($); + return !!user; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/smartcar/auth/verify-credentials.ts b/packages/backend/src/apps/smartcar/auth/verify-credentials.ts new file mode 100644 index 00000000..14ac7774 --- /dev/null +++ b/packages/backend/src/apps/smartcar/auth/verify-credentials.ts @@ -0,0 +1,41 @@ +import { IGlobalVariable, IField } from '@automatisch/types'; +import { URLSearchParams } from 'url'; + +const verifyCredentials = async ($: IGlobalVariable) => { + const token = Buffer.from( + `${$.auth.data.clientId}:${$.auth.data.clientSecret}` + ).toString('base64'); + + const headers = { + Authorization: `Basic ${token}`, + }; + + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value as string; + + const params = new URLSearchParams({ + grant_type: 'authorization_code', + code: $.auth.data.code as string, + redirect_uri: redirectUri, + }); + + const response = await $.http.post( + `https://auth.smartcar.com/oauth/token`, + params.toString(), + { headers } + ); + + const responseData = Object.fromEntries(new URLSearchParams(response.data)); + + await $.auth.set({ + accessToken: responseData.access_token, + tokenType: responseData.token_type, + expiresIn: responseData.expires_in, + refreshToken: responseData.refresh_token, + screenName: $.auth.data.screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/smartcar/common/add-auth-header.ts b/packages/backend/src/apps/smartcar/common/add-auth-header.ts new file mode 100644 index 00000000..675302aa --- /dev/null +++ b/packages/backend/src/apps/smartcar/common/add-auth-header.ts @@ -0,0 +1,13 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + const { accessToken } = $.auth.data; + + if (accessToken) { + requestConfig.headers.Authorization = `Bearer ${accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/smartcar/common/get-current-user.ts b/packages/backend/src/apps/smartcar/common/get-current-user.ts new file mode 100644 index 00000000..b11e7694 --- /dev/null +++ b/packages/backend/src/apps/smartcar/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('/user'); + const currentUser = response.data; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/smartcar/dynamic-data/index.ts b/packages/backend/src/apps/smartcar/dynamic-data/index.ts new file mode 100644 index 00000000..f308bafc --- /dev/null +++ b/packages/backend/src/apps/smartcar/dynamic-data/index.ts @@ -0,0 +1,3 @@ +import listVehicles from './list-vehicles'; + +export default [listVehicles]; diff --git a/packages/backend/src/apps/smartcar/dynamic-data/list-vehicles/index.ts b/packages/backend/src/apps/smartcar/dynamic-data/list-vehicles/index.ts new file mode 100644 index 00000000..feea517a --- /dev/null +++ b/packages/backend/src/apps/smartcar/dynamic-data/list-vehicles/index.ts @@ -0,0 +1,31 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; + +export default { + name: 'List vehicles', + key: 'listVehicles', + + async run($: IGlobalVariable) { + const vehicles: { + data: IJSONObject[]; + error: IJSONObject | null; + } = { + data: [], + error: null, + }; + + const response: any = await $.http.get('/vehicles'); + + for (const vehicle of response.data.vehicles) { + const response: any = await $.http.get(`/vehicles/${vehicle}`); + + const vehicleName = `${response.data.make} - ${response.data.model} (${response.data.year})`; + + vehicles.data.push({ + value: vehicle as string, + name: vehicleName, + }); + } + + return vehicles; + }, +}; diff --git a/packages/backend/src/apps/smartcar/index.d.ts b/packages/backend/src/apps/smartcar/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/smartcar/index.ts b/packages/backend/src/apps/smartcar/index.ts new file mode 100644 index 00000000..c9ebeee7 --- /dev/null +++ b/packages/backend/src/apps/smartcar/index.ts @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; +import actions from './actions'; +import dynamicData from './dynamic-data'; + +export default defineApp({ + name: 'Smartcar', + key: 'smartcar', + iconUrl: '{BASE_URL}/apps/smartcar/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/smartcar/connection', + supportsConnections: true, + baseUrl: 'https://smartcar.com', + apiBaseUrl: 'https://api.smartcar.com/v2.0', + primaryColor: '000000', + beforeRequest: [addAuthHeader], + auth, + actions, + dynamicData, +});