From 690484a34e9769c23e829f3c3eca982dea34a388 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Mon, 5 Feb 2024 17:58:02 -0500 Subject: [PATCH 1/5] Changes from DPG review --- api/domains.js | 14 ++- api/preview.js | 4 +- lib/config.js | 15 ++++ lib/preview.js | 22 ++--- lib/preview/createRoutes.js | 8 +- lib/preview/httpsRedirectingServer.js | 9 +- lib/preview/previewUtils.js | 64 ++++++-------- lib/preview/proxyPage.js | 5 +- lib/preview/routes/index.js | 31 +++---- lib/preview/routes/module.js | 50 ++++------- .../routes/proxyPageResourceRedirect.js | 2 +- .../routes/proxyPathPageResourceRedirect.js | 7 +- .../routes/proxyPathPageRouteHandler.js.js | 54 +++++------- lib/preview/routes/template.js | 49 ++++------- lib/preview/shadowDevServer.js | 86 ------------------- lib/preview/sprocketMenuServer.js | 84 ++++++++++++++++++ package.json | 1 - personalAccessKey.js | 32 +++++-- 18 files changed, 259 insertions(+), 278 deletions(-) delete mode 100644 lib/preview/shadowDevServer.js create mode 100644 lib/preview/sprocketMenuServer.js diff --git a/api/domains.js b/api/domains.js index fd1f97a..18d7675 100644 --- a/api/domains.js +++ b/api/domains.js @@ -3,16 +3,12 @@ const http = require('../http'); const DOMAINS_API_PATH = `/cms/v3/domains`; async function fetchDomains(accountId) { - try { - const result = await http.get(accountId, { - uri: DOMAINS_API_PATH, - json: true, - }); + const result = await http.get(accountId, { + uri: DOMAINS_API_PATH, + json: true, + }); - return result.results; - } catch (err) { - throw err; - } + return result.results; } module.exports = { diff --git a/api/preview.js b/api/preview.js index 915a705..c428608 100644 --- a/api/preview.js +++ b/api/preview.js @@ -1,3 +1,5 @@ +const { request } = require('../http'); + async function fetchPreviewRender(url, sessionInfo) { const { sessionToken } = sessionInfo; @@ -6,7 +8,7 @@ async function fetchPreviewRender(url, sessionInfo) { urlObject.searchParams.append('localPreviewToken', sessionToken); urlObject.searchParams.append('hsCacheBuster', Date.now()); - return fetch(urlObject.href).then(res => res.text()); + return request(urlObject.href).then(res => res.text()); } module.exports = { diff --git a/lib/config.js b/lib/config.js index 453127a..852ef62 100644 --- a/lib/config.js +++ b/lib/config.js @@ -763,7 +763,22 @@ const isConfigFlagEnabled = flag => { return config[flag] || false; }; + +const getAuthType = accountId => { + let authType = 'unknown'; + + if (accountId) { + const accountConfig = getAccountConfig(accountId); + if (accountConfig && accountConfig.authType) { + authType = accountConfig.authType; + } + } + + return authType; +}; + module.exports = { + getAuthType, getAndLoadConfigIfNeeded, getEnv, getConfig, diff --git a/lib/preview.js b/lib/preview.js index 3017326..f0c572a 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -23,7 +23,7 @@ const { isUngatedForPreview, } = require('./preview/previewUtils'); const { markRemoteFsDirty } = require('./preview/routes/meta'); -const { startShadowDevServer } = require('./preview/shadowDevServer'); +const { startSprocketMenuServer } = require('./preview/sprocketMenuServer'); const { createHttpsRedirectingServer, } = require('./preview/httpsRedirectingServer'); @@ -108,7 +108,7 @@ const buildDeleteFileFromPreviewBufferCallback = (sessionInfo, type) => { }; const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { - const { portalId, src, dest, notify } = sessionInfo; + const { accountId, src, dest, notify } = sessionInfo; return async filePath => { if (!isAllowedExtension(filePath)) { @@ -120,15 +120,15 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { return; } const destPath = getDesignManagerPath(src, dest, filePath); - const uploadPromise = uploadFile(portalId, filePath, destPath); + const uploadPromise = uploadFile(accountId, filePath, destPath); triggerNotify(notify, notifyMessage, filePath, uploadPromise); }; }; const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { - const { portalId, src, dest } = sessionInfo; + const { accountId, src, dest } = sessionInfo; - return uploadFolder(portalId, src, dest, fileMapperArgs, {}, filePaths); + return uploadFolder(accountId, src, dest, fileMapperArgs, {}, filePaths); }; const startPreviewWatcher = async sessionInfo => { @@ -158,7 +158,7 @@ const startPreviewWatcher = async sessionInfo => { ); watcher.on('ready', () => { - console.log('Local file watching service has started!'); + logger.log('Local file watching service has started!'); watcherIsReady = true; }); watcher.on('add', addFileCallback); @@ -194,7 +194,7 @@ const preview = async ( ) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); - const sessionToken = '96cd331a-189d-41f2-8a4c-a12485402eff'; + const sessionToken = '7c964654-7676-4181-93b2-daae33e0b2d5';//uuidv4(); const PORT = port || 3000; const protocol = noSsl ? 'http' : 'https'; @@ -202,7 +202,7 @@ const preview = async ( src, dest: `@preview/${sessionToken}/${dest}`, portalName: accountConfig.name, - portalId: accountId, + accountId, env: accountConfig.env, personalAccessKey: accountConfig.personalAccessKey, // we find hublet later in the content metadata fetch @@ -215,7 +215,7 @@ const preview = async ( }; const ungated = await isUngatedForPreview(sessionInfo); if (!ungated) { - console.log( + logger.log( `Portal ${accountId} is missing a required gate for this feature.` ); process.exit(); @@ -241,8 +241,8 @@ const preview = async ( const httpServer = http.createServer(expressServer); httpServer.listen(PORT); } - startShadowDevServer(sessionInfo); - console.log( + startSprocketMenuServer(sessionInfo); + logger.log( `HubSpot preview local dev server hosting at ${protocol}://hslocal.net:${PORT}, portalId=${accountId}` ); }; diff --git a/lib/preview/createRoutes.js b/lib/preview/createRoutes.js index f13b379..ba743c7 100644 --- a/lib/preview/createRoutes.js +++ b/lib/preview/createRoutes.js @@ -1,5 +1,6 @@ const { Router } = require('express'); -const cors = require('cors'); +const { logger } = require('./../../logger'); + const { buildIndexRouteHandler } = require('./routes/index.js'); const { buildModuleRouteHandler } = require('./routes/module.js'); const { buildTemplateRouteHandler } = require('./routes/template.js'); @@ -15,8 +16,9 @@ const createPreviewServerRoutes = async (sessionInfo) => { previewServerRouter.get('/proxy', buildProxyRouteHandler(sessionInfo)); previewServerRouter.get('/module/:modulePath(*)', buildModuleRouteHandler(sessionInfo)); previewServerRouter.get('/template/:templatePath(*)', buildTemplateRouteHandler(sessionInfo)); + // fetches server metadata from the client (used by refresh script to check if fs has been changed) previewServerRouter.get('/meta', buildMetaRouteHandler(sessionInfo)); - + // handles resources on the proxied page, so a fetch from relative path gets proxied too previewServerRouter.get('/*', proxyPathPageResourceRedirect) previewServerRouter.get('/*', proxyPageResourceRedirect); previewServerRouter.post('/*', proxyPageResourceRedirect); @@ -25,7 +27,7 @@ const createPreviewServerRoutes = async (sessionInfo) => { previewServerRouter.put('/*', proxyPageResourceRedirect); previewServerRouter.options('/*', proxyPageResourceRedirect); previewServerRouter.get('/*', buildProxyPageRouteHandler(sessionInfo)); - + // index route previewServerRouter.get('/', buildIndexRouteHandler(sessionInfo)); return previewServerRouter; diff --git a/lib/preview/httpsRedirectingServer.js b/lib/preview/httpsRedirectingServer.js index ed21398..604530c 100644 --- a/lib/preview/httpsRedirectingServer.js +++ b/lib/preview/httpsRedirectingServer.js @@ -1,6 +1,7 @@ const http = require('http'); const https = require('https'); const net = require('net'); +const os = require('os') const { unlinkSync } = require('fs'); const { silenceConsoleWhile } = require('./previewUtils'); @@ -14,11 +15,11 @@ const createCert = async (domainsToProxy) => { const hosts = ['localhost', 'hslocal.net', ...additionalMkcertHosts]; const { createCertificate } = await import('mkcert-cli'); const { key, cert } = await silenceConsoleWhile(createCertificate, { - keyFilePath: `${__dirname}/key.pem`, - certFilePath: `${__dirname}/cert.pem` + keyFilePath: `${os.tmpdir()}/hstmp/hsLocalSshKey.pem`, + certFilePath: `${os.tmpdir()}/hstmp/hsLocalSshCert.pem` }, hosts); - unlinkSync(`${__dirname}/key.pem`); - unlinkSync(`${__dirname}/cert.pem`); + unlinkSync(`${os.tmpdir()}/hstmp/hsLocalSshKey.pem`); + unlinkSync(`${os.tmpdir()}/hstmp/hsLocalSshCert.pem`); return { key, cert }; } diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index 50b78d6..fc968c1 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -2,34 +2,29 @@ const { fetchDomains } = require('../../api/domains'); const { getAccountId, isTrackingAllowed, getAccountConfig } = require('../config'); const { platform, release } = require('os'); const { trackUsage } = require('../../api/fileMapper'); -const unAuth = require('../../api/localDevAuth/unauthenticated'); - +const { enabledFeaturesForPersonalAccessKey } = require('../../personalAccessKey'); +const { stringify } = require('querystring'); +const { logger } = require('./../../logger'); +const { getAuthType } = require('./../../lib/config'); const VALID_PROXY_DOMAIN_SUFFIXES = ['localhost', 'hslocal.net']; const HS_PREVIEW_GATE = "cms:localHublPreviews"; -const getPortalDomains = async (portalId) => { +const getPortalDomains = async (accountId) => { try { - const result = await fetchDomains(portalId); + const result = await fetchDomains(accountId); return result; } catch (error) { - console.log("There was a problem fetching domains for your portal. You may be missing a scope necessary for this feature.") + logger.error("There was a problem fetching domains for your portal. You may be missing a scope necessary for this feature.") return []; } } const getPreviewUrl = (sessionInfo, queryParams) => { - const { portalId, env, hublet } = sessionInfo; + const { accountId, env, hublet } = sessionInfo; - return `http://${portalId}.hubspotpreview${ + return `http://${accountId}.hubspotpreview${ env === 'qa' ? 'qa' : '' - }-${hublet}.com/_hcms/preview/template/multi?${stringifyQuery(queryParams)}`; -} - -const stringifyQuery = (query) => { - return Object.keys(query) - .sort() - .map(key => `${key}=${query[key]}`) - .join('&'); + }-${hublet}.com/_hcms/preview/template/multi?${stringify(queryParams)}`; } const insertAtEndOfBody = (html, script) => { @@ -78,10 +73,8 @@ const getSubDomainFromValidLocalDomain = hostname => { return hostname.slice(0, -1 * validProxyDomainSuffix.length - 1); } } - return undefined; }; -// From(ish) https://git.hubteam.com/HubSpot/cloudflare-workers/blob/master/worker-lib/src/Constants.ts const internalRoutes = { HCMS: '/_hcms/', HS_FS: '/hs-fs/', @@ -93,7 +86,7 @@ const isInternalCMSRoute = (req) => const silenceConsoleWhile = async (act, ...args) => { const tmpConsole = console; - console = { log: () => {} } + console = { log: () => {} } // ! const result = await act(...args); console = tmpConsole; return result; @@ -135,38 +128,30 @@ const trackPreviewEvent = async (action) => { accountId ).catch( (err) => { - console.error(`trackUsage failed: ${JSON.stringify(err, null, 2)}`); + logger.error(`trackUsage failed: ${JSON.stringify(err, null, 2)}`); } ); } -const getAuthType = (accountId) => { - let authType = 'unknown'; - - if (accountId) { - const accountConfig = getAccountConfig(accountId); - authType = - accountConfig && accountConfig.authType - ? accountConfig.authType - : 'apikey'; - } - - return authType; -}; - const isUngatedForPreview = async (sessionInfo) => { - const { portalId, env, personalAccessKey } = sessionInfo; + const { accountId } = sessionInfo; - const { enabledFeatures = {} } = await unAuth.fetchAccessToken( - personalAccessKey, - env, - portalId - ); + const enabledFeatures = await enabledFeaturesForPersonalAccessKey(accountId); return (Object.keys(enabledFeatures).includes(HS_PREVIEW_GATE) && enabledFeatures[HS_PREVIEW_GATE] === true) } +const buildHTMLResponse = (content) => { + return ` + + + + + ${content} + + `; + } module.exports = { isInternalCMSRoute, @@ -180,4 +165,5 @@ module.exports = { hidePreviewInDest, trackPreviewEvent, isUngatedForPreview, + buildHTMLResponse, } diff --git a/lib/preview/proxyPage.js b/lib/preview/proxyPage.js index 492655e..d2c6c28 100644 --- a/lib/preview/proxyPage.js +++ b/lib/preview/proxyPage.js @@ -12,12 +12,11 @@ const proxyPage = async ( const embeddedHtml = addRefreshScript(pageHtml); res.status(200).set({ 'Content-Type': 'text/html' }).end(embeddedHtml); } catch (error) { - const { portalId } = sessionInfo; - // TODO change error.stack to error.message before we publish + const { accountId } = sessionInfo; res .status(500) .end( - `Failed proxy render of page ${urlToProxy} hub id = ${portalId}\n\n${error.stack}` + `Failed proxy render of page ${urlToProxy} hub id = ${accountId}\n\n${error.message}` ); return; } diff --git a/lib/preview/routes/index.js b/lib/preview/routes/index.js index 0e9f67a..34b32a0 100644 --- a/lib/preview/routes/index.js +++ b/lib/preview/routes/index.js @@ -7,6 +7,8 @@ const { fetchPreviewModules } = require('../../../api/designManager'); const { parse: pathParse } = require('path'); +const { logger } = require('./../../../logger'); +const { isCodedFile } = require('./../../../templates'); const buildIndexRouteHandler = (sessionInfo) => { return async (req, res) => { @@ -62,9 +64,9 @@ const listify = (objects, hrefBuilder, labelBuilder) => { } const getModulesForDisplayToUser = async (sessionInfo) => { - const { portalId, sessionToken } = sessionInfo; + const { accountId, sessionToken } = sessionInfo; try { - const res = await fetchPreviewModules(portalId, sessionToken); + const res = await fetchPreviewModules(accountId, sessionToken); const modulePaths = res .objects .map(moduleObj => moduleObj.path); @@ -72,25 +74,24 @@ const getModulesForDisplayToUser = async (sessionInfo) => { const htmlToRender = renderFilesGroupedByFolder(filesGroupedByFolder, 'module') return htmlToRender; } catch (err) { - console.log(`Failed to fetch modules for index page: ${err}`); + logger.error(`Failed to fetch modules for index page: ${err}`); return undefined; } } const getTemplatesForDisplayToUser = async (sessionInfo) => { - const { portalId, sessionToken } = sessionInfo; + const { accountId, sessionToken } = sessionInfo; try { - const res = await fetchPreviewTemplates(portalId, sessionToken); + const res = await fetchPreviewTemplates(accountId, sessionToken); const templatePaths = res .objects - .filter(templateObj => templateObj.filename.endsWith('html')) + .filter(templateObj => isCodedFile(templateObj.filename)) .map(templateObj => templateObj.path); const filesGroupedByFolder = groupByFolder(templatePaths) const htmlToRender = renderFilesGroupedByFolder(filesGroupedByFolder, 'template') return htmlToRender; } catch (err) { - console.log(`Failed to fetch templates for index page: ${err}`); - return undefined; + logger.error(`Failed to fetch templates for index page: ${err}`); } } @@ -110,15 +111,15 @@ const renderFilesGroupedByFolder = (filesGroupedByFolder, endpoint) => { const folders = Object.keys(filesGroupedByFolder); return folders.reduce((outer_acc, folder) => { const fakeDest = hidePreviewInDest(folder); + console.log(filesGroupedByFolder[folder]); const folderDisplay = '' + fakeDest - + ''; + + '' + + listify( + filesGroupedByFolder[folder], + x => `/${endpoint}/${fakeDest}/${x}`, + x => x + ); outer_acc += folderDisplay; return outer_acc; }, ''); diff --git a/lib/preview/routes/module.js b/lib/preview/routes/module.js index 91c580c..1386894 100644 --- a/lib/preview/routes/module.js +++ b/lib/preview/routes/module.js @@ -4,13 +4,15 @@ const { addRefreshScript, getPreviewUrl, memoize, - trackPreviewEvent + trackPreviewEvent, + buildHTMLResponse } = require('../previewUtils'); +const { logger } = require('./../../../logger'); const cachedFetchModulesByPath = memoize(fetchModulesByPath); const buildModuleRouteHandler = (sessionInfo) => { - const { portalId, sessionToken } = sessionInfo; + const { accountId, sessionToken } = sessionInfo; return async (req, res) => { trackPreviewEvent('view-module-route'); @@ -25,9 +27,9 @@ const buildModuleRouteHandler = (sessionInfo) => { const calculatedPath = `@preview/${sessionToken}/${modulePath}.module`; let customWidgetInfo; try { - customWidgetInfo = await cachedFetchModulesByPath(portalId, calculatedPath); + customWidgetInfo = await cachedFetchModulesByPath(accountId, calculatedPath); } catch (err) { - console.log(`Failed to fetch module preview for ${calculatedPath}`) + logger.error(`Failed to fetch module preview for ${calculatedPath}`) } if (!customWidgetInfo || !('moduleId' in customWidgetInfo && 'previewKey' in customWidgetInfo)) { res.status(200).set({ 'Content-Type': 'text/html' }).end(buildErrorIndex()); @@ -40,7 +42,7 @@ const buildModuleRouteHandler = (sessionInfo) => { ...req.query } const previewUrl = new URL(getPreviewUrl(sessionInfo, params)) ; - const result = await http.get(portalId, { + const result = await http.get(accountId, { baseUrl: previewUrl.origin, uri: previewUrl.pathname, query: params, @@ -52,33 +54,19 @@ const buildModuleRouteHandler = (sessionInfo) => { } } -const buildErrorIndex = () => { - return ` - - - - -
-

Error

-

Failed to fetch module data.

-
- - `; -} +const buildErrorIndex = () => buildHTMLResponse(` +
+

Error

+

Failed to fetch module data.

+
+`); -const buildModuleIndex = () => { - return ` - - - - -
-

Module Index

-

Incomplete!

-
- - `; -} +const buildModuleIndex = () => buildHTMLResponse(` +
+

Module Index

+

Incomplete!

+
+`); module.exports = { buildModuleRouteHandler diff --git a/lib/preview/routes/proxyPageResourceRedirect.js b/lib/preview/routes/proxyPageResourceRedirect.js index e345e97..143651d 100644 --- a/lib/preview/routes/proxyPageResourceRedirect.js +++ b/lib/preview/routes/proxyPageResourceRedirect.js @@ -35,7 +35,7 @@ const proxyPageResourceRedirect = (req, res, next) => { !header.startsWith('sec-') ); - let body = undefined; + let body; if (req.body) { if (isSendingJSON) { diff --git a/lib/preview/routes/proxyPathPageResourceRedirect.js b/lib/preview/routes/proxyPathPageResourceRedirect.js index 0fdb560..bfd5a0e 100644 --- a/lib/preview/routes/proxyPathPageResourceRedirect.js +++ b/lib/preview/routes/proxyPathPageResourceRedirect.js @@ -1,4 +1,3 @@ -const fetch = require('node-fetch-commonjs'); const { isInternalCMSRoute } = require('../previewUtils'); const proxyPathPageResourceRedirect = (req, res, next) => { @@ -10,7 +9,7 @@ const proxyPathPageResourceRedirect = (req, res, next) => { try { refererUrl = new URL(req.headers.referer); proxyPageUrl = new URL( - new URL(req.headers.referer).searchParams.get('page') + refererUrl.searchParams.get('page') ); } catch (e) { next(); @@ -31,8 +30,8 @@ const proxyPathPageResourceRedirect = (req, res, next) => { } const urlToProxy = `https://${proxyPageUrl.host}${req.url}`; - fetch(urlToProxy) - .then(response => response.body.pipe(res)) // https://github.com/node-fetch/node-fetch#bodybody + require(urlToProxy) + .then(response => response.body.pipe(res)) .catch(err => console.error(err)); } diff --git a/lib/preview/routes/proxyPathPageRouteHandler.js.js b/lib/preview/routes/proxyPathPageRouteHandler.js.js index 38149f7..efad899 100644 --- a/lib/preview/routes/proxyPathPageRouteHandler.js.js +++ b/lib/preview/routes/proxyPathPageRouteHandler.js.js @@ -1,5 +1,6 @@ const { proxyPage } = require('../proxyPage'); -const { trackPreviewEvent } = require('../previewUtils'); +const { trackPreviewEvent, buildHTMLResponse } = require('../previewUtils'); +const { logger } = require('./../../../logger'); const buildProxyRouteHandler = (sessionInfo) => { @@ -16,13 +17,13 @@ const buildProxyRouteHandler = (sessionInfo) => { } catch (e) { const message = 'Please provide a valid page query parameter, e.g., http://localhost:3000/proxy?page=https://yourdomain.com/path'; - console.warn(message); + logger.warn(message); return res.status(400).send(message); } // validate proxy page URL query params if (proxyPageUrl.searchParams.has('hs_preview')) { const message = `Can't make a proxy request, you cannot proxy URLs that include internal query params like hs_preview`; - console.warn(message); + logger.warn(message); return res.status(400).send(message); } trackPreviewEvent('proxy-path-route'); @@ -39,36 +40,23 @@ const buildProxyRouteHandler = (sessionInfo) => { } } -const buildProxyIndex = () => { - // If we want to embed a script or something into the proxy - // we can wrap it here or parse the returned HTML into the render - // but that's maybe trickier... - return ` - - - - -
-

Local Proxy

-
- - - -
-
- - `; -} - - - +const buildProxyIndex = () => buildHTMLResponse(` +
+

Local Proxy

+
+ + + +
+
+`); module.exports = { buildProxyRouteHandler diff --git a/lib/preview/routes/template.js b/lib/preview/routes/template.js index 271160b..39da3b0 100644 --- a/lib/preview/routes/template.js +++ b/lib/preview/routes/template.js @@ -1,11 +1,12 @@ const http = require('../../../http'); const { fetchTemplatesByPath } = require('../../../api/designManager'); -const { getPreviewUrl, addRefreshScript, memoize } = require('../previewUtils'); +const { getPreviewUrl, addRefreshScript, memoize, buildHTMLResponse } = require('../previewUtils'); +const { logger } = require('./../../../logger'); const cachedFetchTemplatesByPath = memoize(fetchTemplatesByPath); const buildTemplateRouteHandler = (sessionInfo) => { - const { portalId, sessionToken } = sessionInfo; + const { accountId, sessionToken } = sessionInfo; return async (req, res) => { const { templatePath } = req.params; @@ -18,9 +19,9 @@ const buildTemplateRouteHandler = (sessionInfo) => { const calculatedPath = `@preview/${sessionToken}/${templatePath}`; let templateInfo; try { - templateInfo = await cachedFetchTemplatesByPath(portalId, calculatedPath); + templateInfo = await cachedFetchTemplatesByPath(accountId, calculatedPath); } catch (err) { - console.log(`Failed to fetch template info for ${calculatedPath}: ${err}`); + logger.error(`Failed to fetch template info for ${calculatedPath}: ${err}`); } if (!templateInfo || !('previewKey' in templateInfo)) { res.status(502).set({ 'Content-Type': 'text/html' }).end(buildErrorIndex()); @@ -32,7 +33,7 @@ const buildTemplateRouteHandler = (sessionInfo) => { ...req.query } const previewUrl = new URL(getPreviewUrl(sessionInfo, params)); - const result = await http.get(portalId, { + const result = await http.get(accountId, { baseUrl: previewUrl.origin, uri: previewUrl.pathname, query: params, @@ -44,33 +45,19 @@ const buildTemplateRouteHandler = (sessionInfo) => { } } -const buildErrorIndex = () => { - return ` - - - - -
-

Error

-

Failed to fetch template data.

-
- - `; -} +const buildErrorIndex = () => buildHTMLResponse(` +
+

Error

+

Failed to fetch template data.

+
+`) -const buildTemplateIndex = () => { - return ` - - - - -
-

Template

-

Provide a template path in the request

-
- - `; -} +const buildTemplateIndex = () => buildHTMLResponse(` +
+

Template

+

Provide a template path in the request

+
+`) module.exports = { buildTemplateRouteHandler diff --git a/lib/preview/shadowDevServer.js b/lib/preview/shadowDevServer.js deleted file mode 100644 index b72136a..0000000 --- a/lib/preview/shadowDevServer.js +++ /dev/null @@ -1,86 +0,0 @@ -const net = require('net'); -const express = require('express'); -const { Router } = require('express'); -const cors = require("cors"); - -const SHADOW_PORT = 1442; - -const startShadowDevServer = async (sessionInfo) => { - const portIsTaken = await new Promise((res, rej) => { - console.log(`Starting proxy link server on port ${SHADOW_PORT}`); - const testNetServer = net.createServer(); - - testNetServer.once('error', err => { - if (err['code'] === 'EADDRINUSE') { - console.error( - `Port ${SHADOW_PORT} is in use. HubSpot is unable to automatically create proxy links in the Sprocket Menu` - ); - res(true); - } else { - rej(err); - } - }); - - testNetServer.once('listening', () => { - testNetServer.close(); - }); - - testNetServer.once('close', () => { - res(false); - }); - - testNetServer.listen(SHADOW_PORT); - }); - - if (portIsTaken) { - return; - } - - const shadowDevServer = express(); - - shadowDevServer.listen(SHADOW_PORT); - - shadowDevServer.use( - '/', - await createShadowDevServerRoutes(sessionInfo) - ); -} - -const createShadowDevServerRoutes = async (sessionInfo) => { - const shadowDevServerRoutes = Router(); - - shadowDevServerRoutes.get( - '/check-if-local-dev-server', - cors(), - shadowDevServerCheckHandler(sessionInfo), - ); - - return shadowDevServerRoutes; -} - -const shadowDevServerCheckHandler = (sessionInfo) => async (req, res) => { - const { PORT } = sessionInfo; - const { query } = req; - - if (query) { - const { - hostName, - pathName, - } = query; - - let hasJSBuildingBlocks; - let localProxyUrl; - - hasJSBuildingBlocks = false; - localProxyUrl = `http://${hostName}.hslocal.net:${PORT}${pathName}`; - - res - .status(200) - .set({ 'Content-Type': 'application/json' }) - .json({ hasJSBuildingBlocks, localProxyUrl }); -}; -} - -module.exports = { - startShadowDevServer -} diff --git a/lib/preview/sprocketMenuServer.js b/lib/preview/sprocketMenuServer.js new file mode 100644 index 0000000..9bd25c2 --- /dev/null +++ b/lib/preview/sprocketMenuServer.js @@ -0,0 +1,84 @@ +const net = require('net'); +const express = require('express'); +const { Router } = require('express'); +const cors = require("cors"); +const { logger } = require('../../logger'); + +const SPROCKET_MENU_PORT = 1442; + +const startSprocketMenuServer = async (sessionInfo) => { + const portIsTaken = await new Promise((res, rej) => { + logger.log(`Starting proxy link server on port ${SPROCKET_MENU_PORT}`); + const testNetServer = net.createServer(); + + testNetServer.once('error', err => { + if (err['code'] === 'EADDRINUSE') { + logger.error( + `Port ${SPROCKET_MENU_PORT} is in use. HubSpot is unable to automatically create proxy links in the Sprocket Menu` + ); + res(true); + } else { + rej(err); + } + }); + + testNetServer.once('listening', () => { + testNetServer.close(); + }); + + testNetServer.once('close', () => { + res(false); + }); + + testNetServer.listen(SPROCKET_MENU_PORT); + }); + + if (portIsTaken) { + return; + } + + const sprocketMenuServer = express(); + + sprocketMenuServer.listen(SPROCKET_MENU_PORT); + + sprocketMenuServer.use( + '/', + await createSprocketMenuServerRoutes(sessionInfo) + ); +} + +const createSprocketMenuServerRoutes = async (sessionInfo) => { + const sprocketMenuServerRoutes = Router(); + + sprocketMenuServerRoutes.get( + '/check-if-local-dev-server', + cors(), + sprocketMenuServerCheckHandler(sessionInfo), + ); + + return sprocketMenuServerRoutes; +} + +const sprocketMenuServerCheckHandler = (sessionInfo) => async (req, res) => { + const { PORT } = sessionInfo; + const { query } = req; + + if (query) { + const { + hostName, + pathName, + } = query; + + let hasJSBuildingBlocks = false; + let localProxyUrl = `http://${hostName}.hslocal.net:${PORT}${pathName}`; + + res + .status(200) + .set({ 'Content-Type': 'application/json' }) + .json({ hasJSBuildingBlocks, localProxyUrl }); +}; +} + +module.exports = { + startSprocketMenuServer +} diff --git a/package.json b/package.json index 32d1998..f425d67 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "js-yaml": "^4.1.0", "mkcert-cli": "^1.5.0", "moment": "^2.24.0", - "node-fetch-commonjs": "^3.3.2", "p-queue": "^6.0.2", "prettier": "^1.19.1", "request": "^2.88.2", diff --git a/personalAccessKey.js b/personalAccessKey.js index d93096b..ef981a1 100644 --- a/personalAccessKey.js +++ b/personalAccessKey.js @@ -43,6 +43,7 @@ async function getAccessToken( accessToken: response.oauthAccessToken, expiresAt: moment(response.expiresAtMillis), scopeGroups: response.scopeGroups, + enabledFeatures: response.enabledFeatures, encodedOauthRefreshToken: response.encodedOauthRefreshToken, }; } @@ -52,11 +53,12 @@ async function refreshAccessToken( personalAccessKey, env = ENVIRONMENTS.PROD ) { - const { accessToken, expiresAt } = await getAccessToken( + const accessTokenResponse = await getAccessToken( personalAccessKey, env, accountId ); + const { accessToken, expiresAt } = accessTokenResponse const config = getAccountConfig(accountId); updateAccountConfig({ @@ -69,15 +71,15 @@ async function refreshAccessToken( }); writeConfig(); - return accessToken; + return accessTokenResponse; } -async function getNewAccessToken(accountId, personalAccessKey, expiresAt, env) { +async function getNewAccessToken(accountId, personalAccessKey, expiresAt, env, fullAPIResponse=false) { const key = getRefreshKey(personalAccessKey, expiresAt); if (refreshRequests.has(key)) { return refreshRequests.get(key); } - let accessToken; + let accessTokenResponse; try { const refreshAccessPromise = refreshAccessToken( accountId, @@ -87,14 +89,17 @@ async function getNewAccessToken(accountId, personalAccessKey, expiresAt, env) { if (key) { refreshRequests.set(key, refreshAccessPromise); } - accessToken = await refreshAccessPromise; + accessTokenResponse = await refreshAccessPromise; } catch (e) { if (key) { refreshRequests.delete(key); } throw e; } - return accessToken; + if (fullAPIResponse) { + return accessTokenResponse; + } + return accessTokenResponse.accessToken; } async function accessTokenForPersonalAccessKey(accountId) { @@ -118,6 +123,20 @@ async function accessTokenForPersonalAccessKey(accountId) { return auth.tokenInfo.accessToken; } +async function enabledFeaturesForPersonalAccessKey(accountId) { + const { auth, personalAccessKey, env } = getAccountConfig(accountId); + const authTokenInfo = auth && auth.tokenInfo; + + const accessTokenResponse = await getNewAccessToken( + accountId, + personalAccessKey, + authTokenInfo && authTokenInfo.expiresAt, + env, + fullAPIResponse=true + ) + return accessTokenResponse.enabledFeatures; +} + /** * Adds a account to the config using authType: personalAccessKey @@ -179,6 +198,7 @@ const updateConfigWithPersonalAccessKey = async (configData, makeDefault) => { module.exports = { accessTokenForPersonalAccessKey, + enabledFeaturesForPersonalAccessKey, updateConfigWithPersonalAccessKey, getAccessToken, }; From 09f13a20286fae10cdd10f5bd172ad64151c9986 Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Wed, 7 Feb 2024 14:30:47 -0500 Subject: [PATCH 2/5] Changes for pre-empathy test --- api/preview.js | 2 +- lang/en.lyaml | 7 ++- lib/preview.js | 66 ++++++++++++++++++++++--- lib/preview/previewUtils.js | 7 ++- lib/preview/routes/index.js | 10 ++-- lib/preview/sprocketMenuServer.js | 3 +- lib/uploadFolder.js | 81 ++++++++++++++++++++----------- 7 files changed, 130 insertions(+), 46 deletions(-) diff --git a/api/preview.js b/api/preview.js index c428608..cbc3d9b 100644 --- a/api/preview.js +++ b/api/preview.js @@ -8,7 +8,7 @@ async function fetchPreviewRender(url, sessionInfo) { urlObject.searchParams.append('localPreviewToken', sessionToken); urlObject.searchParams.append('hsCacheBuster', Date.now()); - return request(urlObject.href).then(res => res.text()); + return request(urlObject.href); } module.exports = { diff --git a/lang/en.lyaml b/lang/en.lyaml index 9051199..78e8bfd 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -14,6 +14,11 @@ en: debug: templates: creatingPath: "Making {{ path }} if needed" + preview: + initialProgressBar: + start: "Starting..." + uploading: "Uploading..." + finish: "Complete!" commands: create: subcommands: @@ -52,5 +57,3 @@ en: fieldsJsSyntaxError: "There was an error converting JS file \"{{ path }}\"" fieldsJsNotReturnArray: "There was an error loading JS file \"{{ path }}\". Expected type \"Array\" but received type \"{{ returned }}\" . Make sure that your function returns an array" fieldsJsNotFunction: "There was an error loading JS file \"{{ path }}\". Expected type \"Function\" but received type \"{{ returned }}\". Make sure that your default export is a function." - - diff --git a/lib/preview.js b/lib/preview.js index f0c572a..adf1454 100644 --- a/lib/preview.js +++ b/lib/preview.js @@ -9,7 +9,7 @@ const { logApiErrorInstance, logApiUploadErrorInstance, } = require('../errorHandlers'); -const { uploadFolder } = require('./uploadFolder'); +const { uploadFolder, FileUploadResultType } = require('./uploadFolder'); const { shouldIgnoreFile, ignoreFile } = require('../ignoreRules'); const { getFileMapperQueryValues } = require('../fileMapper'); const { upload, deleteFile } = require('../api/fileMapper'); @@ -27,6 +27,10 @@ const { startSprocketMenuServer } = require('./preview/sprocketMenuServer'); const { createHttpsRedirectingServer, } = require('./preview/httpsRedirectingServer'); +const { i18n } = require('../lib/lang'); +const cliProgress = require('cli-progress'); + +const i18nKey = 'cli.lib.preview'; const fileMapperArgs = getFileMapperQueryValues({ mode: 'publish', @@ -127,8 +131,53 @@ const buildUploadFileToPreviewBufferCallback = (sessionInfo, notifyMessage) => { const initialPreviewBufferUpload = async (sessionInfo, filePaths) => { const { accountId, src, dest } = sessionInfo; - - return uploadFolder(accountId, src, dest, fileMapperArgs, {}, filePaths); + let uploadOptions = {}; + const progressBar = new cliProgress.SingleBar( + { + gracefulExit: true, + format: '[{bar}] {percentage}% | {value}/{total} | {label}', + hideCursor: true + }, + cliProgress.Presets.rect + ); + progressBar.start(filePaths.length, 0, { + label: i18n(`${i18nKey}.initialProgressBar.start`), + }); + let uploadsHaveStarted = false; + uploadOptions = { + onAttemptCallback: () => {}, + onSuccessCallback: () => { + progressBar.increment() + if (!uploadsHaveStarted) { + uploadsHaveStarted = true; + progressBar.update(0, { + label: i18n(`${i18nKey}.initialProgressBar.uploading`) + }) + } + }, + onFirstErrorCallback: () => { /* Intentionally blank */ }, + onRetryCallback: () => { /* Intentionally blank */}, + onFinalErrorCallback: () => progressBar.increment() + }; + const results = await uploadFolder(accountId, src, dest, fileMapperArgs, uploadOptions, filePaths); + progressBar.update(filePaths.length, { + label: i18n(`${i18nKey}.initialProgressBar.finish`) + }); + progressBar.stop(); + results.forEach((result) => { + if (result.resultType == FileUploadResultType.FAILURE) { + logger.error('Uploading file "%s" to "%s" failed', result.file, dest); + logApiUploadErrorInstance( + result.error, + new ApiErrorContext({ + accountId, + request: dest, + payload: result.file, + }) + ); + } + } + ) }; const startPreviewWatcher = async sessionInfo => { @@ -158,11 +207,14 @@ const startPreviewWatcher = async sessionInfo => { ); watcher.on('ready', () => { - logger.log('Local file watching service has started!'); watcherIsReady = true; }); watcher.on('add', addFileCallback); watcher.on('change', changeFileCallback); + watcher.on('error', error => + logger.error(`Error in file watching: ${error}`) + ); + watcher.on('unlink', deleteFileCallback); watcher.on('unlinkDir', deleteFolderCallback); @@ -180,7 +232,6 @@ const startPreviewWatcher = async sessionInfo => { const createLocalHttpServer = async sessionInfo => { const expressServer = express(); - //expressServer.use(bodyParser.json()); expressServer.use('/', await createPreviewServerRoutes(sessionInfo)); return expressServer; @@ -194,13 +245,14 @@ const preview = async ( ) => { const accountConfig = getAccountConfig(accountId); const domains = await getPortalDomains(accountId); - const sessionToken = '7c964654-7676-4181-93b2-daae33e0b2d5';//uuidv4(); + const sessionToken = uuidv4(); const PORT = port || 3000; const protocol = noSsl ? 'http' : 'https'; const sessionInfo = { src, dest: `@preview/${sessionToken}/${dest}`, + fakeDest: dest, portalName: accountConfig.name, accountId, env: accountConfig.env, @@ -243,7 +295,7 @@ const preview = async ( } startSprocketMenuServer(sessionInfo); logger.log( - `HubSpot preview local dev server hosting at ${protocol}://hslocal.net:${PORT}, portalId=${accountId}` + `Local dev server started at ${protocol}://hslocal.net:${PORT} for portal ${accountId}` ); }; diff --git a/lib/preview/previewUtils.js b/lib/preview/previewUtils.js index fc968c1..fc91901 100644 --- a/lib/preview/previewUtils.js +++ b/lib/preview/previewUtils.js @@ -7,6 +7,7 @@ const { stringify } = require('querystring'); const { logger } = require('./../../logger'); const { getAuthType } = require('./../../lib/config'); const VALID_PROXY_DOMAIN_SUFFIXES = ['localhost', 'hslocal.net']; + const HS_PREVIEW_GATE = "cms:localHublPreviews"; const getPortalDomains = async (accountId) => { @@ -14,7 +15,6 @@ const getPortalDomains = async (accountId) => { const result = await fetchDomains(accountId); return result; } catch (error) { - logger.error("There was a problem fetching domains for your portal. You may be missing a scope necessary for this feature.") return []; } } @@ -39,6 +39,7 @@ const addRefreshScript = (html) => { const refreshScript = `