Skip to content

Commit

Permalink
[TASK] add add error handler for access-key missing access scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
dmh committed Jan 17, 2024
1 parent 5ebec38 commit e900602
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 43 deletions.
23 changes: 12 additions & 11 deletions lib/hubspot/auth/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getAccessToken } from '@hubspot/local-dev-lib/personalAccessKey'
import { validateConfig, getAndLoadConfigIfNeeded, writeConfig, setConfig, createEmptyConfigFile, deleteConfigFile } from '@hubspot/local-dev-lib/config'
import chalk from 'chalk'
import ora from 'ora'
import { getAuthDataFromEnvFile } from './env.js'
import { getAuthDataFromEnvFile, getAuthDataFromEnvVars } from './env.js'
import { savedConsoleDebug } from '../../utils/console.js'

/**
Expand All @@ -32,7 +32,8 @@ async function getAccessTokenData (accessKey) {
spinner.fail()
await deleteConfigFile()
if (error.cause.response.statusText === 'Unauthorized') {
console.error(`${chalk.red.bold('[Error]')} ${error.cause.response.statusText}. Check the personal access key and try again.`)
const msg = error.cause.response.data.message ? error.cause.response.data.message : 'Check the personal access key and try again.'
console.error(`${chalk.red.bold('[Error]')} ${error.cause.response.statusText}. ${msg}`)
} else {
console.error(error)
}
Expand All @@ -41,40 +42,40 @@ async function getAccessTokenData (accessKey) {
}

/**
* #### Load and validate Hubspot config
* #### Load and validate Hubspot auth config
* @async
* @param {string} themeName - theme name
* @returns {Promise<HUBSPOT_AUTH_CONFIG>} AccessToken data
* @returns {Promise<HUBSPOT_AUTH_CONFIG>} Hubspot auth config
*/
async function loadAuthConfig (themeName) {
if (process.env.HUBSPOT_PORTAL_ID && process.env.HUBSPOT_PERSONAL_ACCESS_KEY) {
// get access token data from Hubspot
const accessTokenData = await getAccessTokenData(process.env.HUBSPOT_PERSONAL_ACCESS_KEY)
const authConfig = await getAuthDataFromEnvVars()
// check if the portal ID and personal access key match
if (accessTokenData.portalId !== Number(process.env.HUBSPOT_PORTAL_ID)) {
if (authConfig.portals[0].portalId !== Number(process.env.HUBSPOT_PORTAL_ID)) {
console.error(`${chalk.red.bold('[Error]')} Portal ID and Personal Access Key do not match`)
process.exit(1)
}
console.debug = () => {}
// load Hubspot auth config
const hubAuthConfig = await getAndLoadConfigIfNeeded({ silenceErrors: false, useEnv: true })
await getAndLoadConfigIfNeeded({ silenceErrors: false, useEnv: true })
console.debug = savedConsoleDebug
await validateConfig()
return hubAuthConfig
return authConfig
} else {
// get auth data from .env file
const authDataFromEnvFile = await getAuthDataFromEnvFile(themeName)
const authConfig = await getAuthDataFromEnvFile(themeName)
console.debug = () => {}
await createEmptyConfigFile()
console.debug = savedConsoleDebug
// load Hubspot auth config
const hubAuthConfig = await setConfig(authDataFromEnvFile)
await setConfig(authConfig)
await validateConfig()
console.debug = () => {}
// add auth data to hubspot.config.yml file
await writeConfig()
console.debug = savedConsoleDebug
return hubAuthConfig
return authConfig
}
}

Expand Down
71 changes: 41 additions & 30 deletions lib/hubspot/auth/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,53 +108,64 @@ function getPortalsNames () {
}

/**
* #### Generate Hubspot auth config based on PERSONAL_ACCESS_KEY and chosen portal name
* #### Generate Hubspot auth config based on .env file
* @async
* @private
* @param {string} themeName - theme name
* @returns {Promise<HUBSPOT_AUTH_CONFIG|undefined>}> } Hubspot portal auth config
* @returns {Promise<HUBSPOT_AUTH_CONFIG>}> } Hubspot portal auth config
*/
async function generateAuthConfig (themeName) {
async function getAuthDataFromEnvFile (themeName) {
await initEnvFile()
const portalName = await choosePortal(getPortalsNames(), themeName)
const dotenvConfig = dotenv.config().parsed
const dotenvConfig = dotenv.config().parsed || {}
let envPortalName = ''
for (const env in dotenvConfig) {
if (env.replace('hub_', '') === portalName) {
envPortalName = env
}
}
if (dotenvConfig) {
const accessTokenData = await getAccessTokenData(dotenvConfig[envPortalName])
return {
defaultPortal: portalName,
allowUsageTracking: false,
portals: [
{
name: portalName,
portalId: accessTokenData.portalId,
env: 'prod',
authType: 'personalaccesskey',
personalAccessKey: dotenvConfig[envPortalName]
}
]
}
const accessTokenData = await getAccessTokenData(dotenvConfig[envPortalName])
return {
defaultPortal: portalName,
allowUsageTracking: false,
portals: [
{
name: portalName,
portalId: accessTokenData.portalId,
env: 'prod',
authType: 'personalaccesskey',
personalAccessKey: dotenvConfig[envPortalName],
scopeGroups: accessTokenData.scopeGroups
}
]
}
}

/**
* #### Get auth data from .env file
* #### Generate Hubspot auth config from environment variables
* @description Generate Hubspot auth config from environment variables (HUBSPOT_PORTAL_ID, HUBSPOT_PERSONAL_ACCESS_KEY)
* @async
* @param {string} themeName - theme name
* @returns {Promise<HUBSPOT_AUTH_CONFIG|undefined>} Hubspot auth config
* @returns {Promise<HUBSPOT_AUTH_CONFIG>} AccessToken data
*/
async function getAuthDataFromEnvFile (themeName) {
try {
await initEnvFile()
const authData = await generateAuthConfig(themeName)
return authData
} catch (error) {
console.error(error)
async function getAuthDataFromEnvVars () {
const portalId = process.env.HUBSPOT_PORTAL_ID
const accessKey = process.env.HUBSPOT_PERSONAL_ACCESS_KEY
if (!portalId || !accessKey) {
process.exit(0)
}
const accessTokenData = await getAccessTokenData(accessKey)
return {
allowUsageTracking: false,
portals: [
{
portalId: parseInt(portalId),
env: 'prod',
authType: 'personalaccesskey',
personalAccessKey: accessKey,
scopeGroups: accessTokenData.scopeGroups
}
]
}
}

export { getAuthDataFromEnvFile }
export { getAuthDataFromEnvFile, getAuthDataFromEnvVars }
136 changes: 136 additions & 0 deletions lib/hubspot/auth/scopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import chalk from 'chalk'
import * as TYPES from '../../types/types.js' // eslint-disable-line no-unused-vars

/**
* @ignore
* @typedef {TYPES.HUBSPOT_AUTH_CONFIG} HUBSPOT_AUTH_CONFIG {@link HUBSPOT_AUTH_CONFIG}
*/

/**
* #### scope details
* @typedef {Object} SCOPE_DETAILS
* @property {boolean} selected - is selected
* @property {string} title - scope title
* @property {string} description - scope description
*/

/**
* #### scopes
* @typedef {Object} SCOPES
* @property {SCOPE_DETAILS} cms_pages - View and download website data.
* @property {SCOPE_DETAILS} crm_objects - Read data from HubSpot objects in the CRM.
* @property {SCOPE_DETAILS} custom_objects - Create, delete, or make changes to custom objects in the CRM.
* @property {SCOPE_DETAILS} design_manager - Upload and download templates, modules, and other files that developers need to write the code for websites and emails.
* @property {SCOPE_DETAILS} developer_projects - Upload and download developer projects.
* @property {SCOPE_DETAILS} file_manager - Upload and download files that can be used across HubSpot tools when creating content.
* @property {SCOPE_DETAILS} graphql_data_fetching - Execute GraphQL queries and fetch GraphQL Schema.
* @property {SCOPE_DETAILS} hubdb - Create, update, and delete HubDB tables.
* @property {SCOPE_DETAILS} serverless_functions - View logs, manage secrets, and interact with other serverless functionality.
*/

/**
* #### parse scopes
* @private
* @param {HUBSPOT_AUTH_CONFIG} config - hubspot authentication config
* @returns {SCOPES} scopes
*/
function parseScopes (config) {
/**
* @type {SCOPES}
*/
const scopes = {
cms_pages: {
selected: false,
title: 'CMS Pages',
description: 'View and download website data.'
},
crm_objects: {
selected: false,
title: 'CRM Objects',
description: 'Read data from HubSpot objects in the CRM.'
},
custom_objects: {
selected: false,
title: 'Custom Objects',
description: 'Create, delete, or make changes to custom objects in the CRM.'
},
design_manager: {
selected: false,
title: 'Design Manager',
description: 'Upload and download templates, modules, and other files that developers need to write the code for websites and emails.'
},
developer_projects: {
selected: false,
title: 'Developer Projects',
description: 'Upload and download developer projects.'
},
file_manager: {
selected: false,
title: 'File Manager',
description: 'Upload and download files that can be used across HubSpot tools when creating content.'
},
graphql_data_fetching: {
selected: false,
title: 'GraphQL Data Fetching',
description: 'Execute GraphQL queries and fetch GraphQL Schema.'
},
hubdb: {
selected: false,
title: 'HubDB',
description: 'Create, update, and delete HubDB tables.'
},
serverless_functions: {
selected: false,
title: 'Serverless functions',
description: 'View logs, manage secrets, and interact with other serverless functionality.'
}
}

if (config.portals[0].scopeGroups.includes('cms.pages.landing_pages.read' && 'cms.pages.site_pages.read')) {
scopes.cms_pages.selected = true
}
if (config.portals[0].scopeGroups.includes('crm.objects.companies.read' && 'crm.objects.contacts.read' && 'crm.objects.deals.read' && 'crm.objects.owners.read' && 'crm.schemas.companies.read' && 'crm.schemas.contacts.read' && 'crm.schemas.deals.read')) {
scopes.crm_objects.selected = true
}
if (config.portals[0].scopeGroups.includes('crm.objects.custom.read' && 'crm.objects.custom.write' && 'crm.schemas.custom.read' && 'crm.schemas.custom.write')) {
scopes.custom_objects.selected = true
}
if (config.portals[0].scopeGroups.includes('cms.source_code.write' && 'cms.source_code.read')) {
scopes.design_manager.selected = true
}
if (config.portals[0].scopeGroups.includes('developer.projects.write')) {
scopes.developer_projects.selected = true
}
if (config.portals[0].scopeGroups.includes('files')) {
scopes.file_manager.selected = true
}
if (config.portals[0].scopeGroups.includes('collector.graphql_query.execute' && 'collector.graphql_schema.read')) {
scopes.graphql_data_fetching.selected = true
}
if (config.portals[0].scopeGroups.includes('hubdb')) {
scopes.hubdb.selected = true
}
if (config.portals[0].scopeGroups.includes('cms.functions.read' && 'cms.functions.write')) {
scopes.serverless_functions.selected = true
}
return scopes
}

/**
* #### throw error if missing scopes
* @param {HUBSPOT_AUTH_CONFIG} config - hubspot authentication config
* @param {'cms_pages'|'crm_objects'|'custom_objects'|'design_manager'|'developer_projects'|'file_manager'|'graphql_data_fetching'|'hubdb'|'serverless_functions'} scopeName - scope name
* @returns undefined
*/
function throwErrorIfMissingScope (config, scopeName) {
const scopes = parseScopes(config)
if (!scopes[scopeName].selected) {
const msg = 'This app hasn\'t been granted all required scopes to make this call. Read more about required scopes here: https://developers.hubspot.com/docs/cms/personal-access-key.'
console.error(`${chalk.red.bold('[MISSING_SCOPES]')} ${msg}`)
console.error('All of the following scopes are required:')
console.error(`[${chalk.red(scopes[scopeName].title)}] - ${scopes[scopeName].description}`)
process.exit(1)
}
}

export { throwErrorIfMissingScope }
3 changes: 3 additions & 0 deletions lib/hubspot/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import logger from '@hubspot/cli-lib/logger.js'
import * as ui from '../utils/ui.js'
import { savedConsoleDebug } from '../utils/console.js'
import { getThemeOptions } from '../utils/options.js'
import { throwErrorIfMissingScope } from './auth/scopes.js'

/**
* @ignore
Expand Down Expand Up @@ -34,6 +35,7 @@ async function fetchModules (config, themeName) {
const options = {
overwrite: true
}
throwErrorIfMissingScope(config, 'design_manager')
logger.setLogLevel(2)
console.debug = () => {}
await downloadFileOrFolder({ accountId, src, dest, mode, options })
Expand Down Expand Up @@ -63,6 +65,7 @@ async function fetch (config, themeName) {
const options = {
overwrite: true
}
throwErrorIfMissingScope(config, 'design_manager')
logger.setLogLevel(2)
console.debug = () => {}
await downloadFileOrFolder({ accountId, src, dest, mode, options })
Expand Down
3 changes: 3 additions & 0 deletions lib/hubspot/hubdb/hubdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { selectTables, selectFiles, confirmTableOverwrite } from './prompts.js'
import ora, { oraPromise } from 'ora'
import { getFileList } from '../../utils/fs.js'
import { createRequire } from 'module'
import { throwErrorIfMissingScope } from '../auth/scopes.js'
const require = createRequire(import.meta.url)
const cmslibOptions = getThemeOptions()

Expand All @@ -30,6 +31,7 @@ async function fetchHubDb (config) {
const timeStart = ui.startTask('fetchDb')
const dest = `${process.cwd()}/${cmslibOptions.hubdbFolder}`
const accountId = config.portals[0].portalId
throwErrorIfMissingScope(config, 'hubdb')
console.debug = () => {}
// fetch all tables from the account
const tables = await fetchTables(accountId)
Expand Down Expand Up @@ -59,6 +61,7 @@ async function uploadHubDb (config) {
const timeStart = ui.startTask('uploadDb')
const hubdbFolder = `${process.cwd()}/${cmslibOptions.hubdbFolder}`
const accountId = config.portals[0].portalId
throwErrorIfMissingScope(config, 'hubdb')
// get all json files from the hubdb folder
const files = await getFileList(`${hubdbFolder}/*.json`, { objectMode: true })
if (files === undefined || files.length === 0) {
Expand Down
3 changes: 3 additions & 0 deletions lib/hubspot/lighthouse.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { savedConsoleDebug } from '../utils/console.js'
import { getThemeOptions } from '../utils/options.js'
import * as ui from '../utils/ui.js'
import { cleanUploadThemeTemplates } from '../hubspot/upload.js'
import { throwErrorIfMissingScope } from './auth/scopes.js'

/**
* @ignore
Expand All @@ -27,6 +28,8 @@ import { cleanUploadThemeTemplates } from '../hubspot/upload.js'
async function lighthouseScore (config, themeName) {
try {
// reupload all templates
throwErrorIfMissingScope(config, 'design_manager')

await cleanUploadThemeTemplates(config, themeName)

const timeStart = ui.startTask('LighthouseScore')
Expand Down
3 changes: 3 additions & 0 deletions lib/hubspot/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { walk } from '@hubspot/local-dev-lib/fs'
import * as ui from '../utils/ui.js'
import { savedConsoleDebug } from '../utils/console.js'
import { getThemeOptions } from '../utils/options.js'
import { throwErrorIfMissingScope } from './auth/scopes.js'
import ora from 'ora'

/**
Expand Down Expand Up @@ -60,6 +61,7 @@ async function uploadTheme (config, themeName) {
const dest = themeName
const portalId = config.portals[0].portalId

throwErrorIfMissingScope(config, 'design_manager')
logger.setLogLevel(2)
const uploadableFileList = await getUploadableFileList(src)
console.debug = () => {}
Expand Down Expand Up @@ -95,6 +97,7 @@ async function cleanUploadThemeTemplates (config, themeName) {
const dest = `${themeName}/templates`
const portalId = config.portals[0].portalId

throwErrorIfMissingScope(config, 'design_manager')
logger.setLogLevel(0)
const uploadableFileList = await getUploadableFileList(src)
console.debug = () => {}
Expand Down

0 comments on commit e900602

Please sign in to comment.