diff --git a/__tests__/lib/cleanHeaders.test.js b/__tests__/lib/cleanHeaders.test.js new file mode 100644 index 000000000..45ac48659 --- /dev/null +++ b/__tests__/lib/cleanHeaders.test.js @@ -0,0 +1,26 @@ +const { cleanHeaders } = require('../../src/lib/cleanHeaders'); + +describe('cleanHeaders', () => { + it('should b64-encode key in ReadMe-friendly format', () => { + expect(cleanHeaders('test')).toStrictEqual({ Authorization: 'Basic dGVzdDo=' }); + }); + + it('should filter out undefined headers', () => { + expect(cleanHeaders('test', { 'x-readme-version': undefined })).toStrictEqual({ Authorization: 'Basic dGVzdDo=' }); + }); + + it('should filter out null headers', () => { + expect(cleanHeaders('test', { 'x-readme-version': undefined, Accept: null })).toStrictEqual({ + Authorization: 'Basic dGVzdDo=', + }); + }); + + it('should pass in properly defined headers', () => { + expect( + cleanHeaders('test', { 'x-readme-version': undefined, Accept: null, 'Content-Type': 'application/json' }) + ).toStrictEqual({ + Authorization: 'Basic dGVzdDo=', + 'Content-Type': 'application/json', + }); + }); +}); diff --git a/src/cmds/docs/edit.js b/src/cmds/docs/edit.js index d2d77d8a7..48b631407 100644 --- a/src/cmds/docs/edit.js +++ b/src/cmds/docs/edit.js @@ -3,6 +3,7 @@ const fs = require('fs'); const editor = require('editor'); const { promisify } = require('util'); const APIError = require('../../lib/apiError'); +const { cleanHeaders } = require('../../lib/cleanHeaders'); const { getProjectVersion } = require('../../lib/versionSelect'); const { handleRes } = require('../../lib/handleRes'); const fetch = require('node-fetch'); @@ -52,15 +53,13 @@ exports.run = async function (opts) { }); const filename = `${slug}.md`; - const encodedString = Buffer.from(`${key}:`).toString('base64'); const existingDoc = await fetch(`${config.host}/api/v1/docs/${slug}`, { method: 'get', - headers: { + headers: cleanHeaders(key, { 'x-readme-version': selectedVersion, - Authorization: `Basic ${encodedString}`, Accept: 'application/json', - }, + }), }).then(res => handleRes(res)); await writeFile(filename, existingDoc.body); @@ -72,11 +71,10 @@ exports.run = async function (opts) { return fetch(`${config.host}/api/v1/docs/${slug}`, { method: 'put', - headers: { + headers: cleanHeaders(key, { 'x-readme-version': selectedVersion, - Authorization: `Basic ${encodedString}`, 'Content-Type': 'application/json', - }, + }), body: JSON.stringify( Object.assign(existingDoc, { body: updatedDoc, diff --git a/src/cmds/docs/index.js b/src/cmds/docs/index.js index f59934a8c..da9704ef0 100644 --- a/src/cmds/docs/index.js +++ b/src/cmds/docs/index.js @@ -5,6 +5,7 @@ const config = require('config'); const crypto = require('crypto'); const frontMatter = require('gray-matter'); const { promisify } = require('util'); +const { cleanHeaders } = require('../../lib/cleanHeaders'); const { getProjectVersion } = require('../../lib/versionSelect'); const { handleRes } = require('../../lib/handleRes'); const fetch = require('node-fetch'); @@ -70,18 +71,15 @@ exports.run = async function (opts) { return Promise.reject(new Error(`We were unable to locate Markdown files in ${folder}.`)); } - const encodedString = Buffer.from(`${key}:`).toString('base64'); - function createDoc(slug, file, hash, err) { if (err.error !== 'DOC_NOTFOUND') return Promise.reject(err); return fetch(`${config.host}/api/v1/docs`, { method: 'post', - headers: { + headers: cleanHeaders(key, { 'x-readme-version': selectedVersion, - Authorization: `Basic ${encodedString}`, 'Content-Type': 'application/json', - }, + }), body: JSON.stringify({ slug, body: file.content, @@ -98,11 +96,10 @@ exports.run = async function (opts) { return fetch(`${config.host}/api/v1/docs/${slug}`, { method: 'put', - headers: { + headers: cleanHeaders(key, { 'x-readme-version': selectedVersion, - Authorization: `Basic ${encodedString}`, 'Content-Type': 'application/json', - }, + }), body: JSON.stringify( Object.assign(existingDoc, { body: file.content, @@ -124,11 +121,10 @@ exports.run = async function (opts) { return fetch(`${config.host}/api/v1/docs/${slug}`, { method: 'get', - headers: { + headers: cleanHeaders(key, { 'x-readme-version': selectedVersion, - Authorization: `Basic ${encodedString}`, Accept: 'application/json', - }, + }), }) .then(res => res.json()) .then(res => { diff --git a/src/cmds/openapi.js b/src/cmds/openapi.js index 27f68070f..982811061 100644 --- a/src/cmds/openapi.js +++ b/src/cmds/openapi.js @@ -5,6 +5,7 @@ const { prompt } = require('enquirer'); const OASNormalize = require('oas-normalize'); const promptOpts = require('../lib/prompts'); const APIError = require('../lib/apiError'); +const { cleanHeaders } = require('../lib/cleanHeaders'); const { getProjectVersion } = require('../lib/versionSelect'); const fetch = require('node-fetch'); const FormData = require('form-data'); @@ -61,8 +62,6 @@ exports.run = async function (opts) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); } - const encodedString = Buffer.from(`${key}:`).toString('base64'); - async function callApi(specPath, versionCleaned) { // @todo Tailor messaging to what is actually being handled here. If the user is uploading a Swagger file, never mention that they uploaded/updated an OpenAPI file. @@ -122,12 +121,11 @@ exports.run = async function (opts) { formData.append('spec', bundledSpec); const options = { - headers: { + headers: cleanHeaders(key, { 'x-readme-version': versionCleaned, 'x-readme-source': 'cli', - Authorization: `Basic ${encodedString}`, Accept: 'application/json', - }, + }), body: formData, }; @@ -163,10 +161,9 @@ exports.run = async function (opts) { function getSpecs(url) { return fetch(`${config.host}${url}`, { method: 'get', - headers: { + headers: cleanHeaders(key, { 'x-readme-version': versionCleaned, - Authorization: `Basic ${encodedString}`, - }, + }), }); } diff --git a/src/cmds/versions/create.js b/src/cmds/versions/create.js index 0ca1020db..d5518b097 100644 --- a/src/cmds/versions/create.js +++ b/src/cmds/versions/create.js @@ -3,6 +3,7 @@ const semver = require('semver'); const { prompt } = require('enquirer'); const promptOpts = require('../../lib/prompts'); const APIError = require('../../lib/apiError'); +const { cleanHeaders } = require('../../lib/cleanHeaders'); const { handleRes } = require('../../lib/handleRes'); const fetch = require('node-fetch'); @@ -54,7 +55,6 @@ exports.args = [ exports.run = async function (opts) { let versionList; const { key, version, codename, fork, main, beta, isPublic } = opts; - const encodedString = Buffer.from(`${key}:`).toString('base64'); if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); @@ -69,9 +69,7 @@ exports.run = async function (opts) { if (!fork) { versionList = await fetch(`${config.host}/api/v1/version`, { method: 'get', - headers: { - Authorization: `Basic ${encodedString}`, - }, + headers: cleanHeaders(key), }).then(res => handleRes(res)); } @@ -84,11 +82,10 @@ exports.run = async function (opts) { return fetch(`${config.host}/api/v1/version`, { method: 'post', - headers: { + headers: cleanHeaders(key, { Accept: 'application/json', 'Content-Type': 'application/json', - Authorization: `Basic ${encodedString}`, - }, + }), body: JSON.stringify({ version, codename: codename || '', diff --git a/src/cmds/versions/delete.js b/src/cmds/versions/delete.js index 79ea6c3f3..466a362ca 100644 --- a/src/cmds/versions/delete.js +++ b/src/cmds/versions/delete.js @@ -1,6 +1,7 @@ const config = require('config'); const APIError = require('../../lib/apiError'); const { getProjectVersion } = require('../../lib/versionSelect'); +const { cleanHeaders } = require('../../lib/cleanHeaders'); const fetch = require('node-fetch'); exports.command = 'versions:delete'; @@ -25,7 +26,6 @@ exports.args = [ exports.run = async function (opts) { const { key, version } = opts; - const encodedString = Buffer.from(`${key}:`).toString('base64'); if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); @@ -37,9 +37,7 @@ exports.run = async function (opts) { return fetch(`${config.host}/api/v1/version/${selectedVersion}`, { method: 'delete', - headers: { - Authorization: `Basic ${encodedString}`, - }, + headers: cleanHeaders(key), }).then(res => { if (res.error) { return Promise.reject(new APIError(res)); diff --git a/src/cmds/versions/index.js b/src/cmds/versions/index.js index 002346320..6afffab0e 100644 --- a/src/cmds/versions/index.js +++ b/src/cmds/versions/index.js @@ -2,6 +2,7 @@ const Table = require('cli-table'); const config = require('config'); const versionsCreate = require('./create'); const APIError = require('../../lib/apiError'); +const { cleanHeaders } = require('../../lib/cleanHeaders'); const fetch = require('node-fetch'); exports.command = 'versions'; @@ -82,7 +83,6 @@ const getVersionFormatted = version => { exports.run = function (opts) { const { key, version, raw } = opts; - const encodedString = Buffer.from(`${key}:`).toString('base64'); if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); @@ -92,9 +92,7 @@ exports.run = function (opts) { return fetch(uri, { method: 'get', - headers: { - Authorization: `Basic ${encodedString}`, - }, + headers: cleanHeaders(key), }) .then(res => res.json()) .then(data => { diff --git a/src/cmds/versions/update.js b/src/cmds/versions/update.js index 1f1b80ee6..424576a5a 100644 --- a/src/cmds/versions/update.js +++ b/src/cmds/versions/update.js @@ -2,6 +2,7 @@ const config = require('config'); const { prompt } = require('enquirer'); const promptOpts = require('../../lib/prompts'); const APIError = require('../../lib/apiError'); +const { cleanHeaders } = require('../../lib/cleanHeaders'); const { getProjectVersion } = require('../../lib/versionSelect'); const fetch = require('node-fetch'); const { handleRes } = require('../../lib/handleRes'); @@ -47,7 +48,6 @@ exports.args = [ exports.run = async function (opts) { const { key, version, codename, newVersion, main, beta, isPublic, deprecated } = opts; - const encodedString = Buffer.from(`${key}:`).toString('base64'); if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); @@ -59,20 +59,17 @@ exports.run = async function (opts) { const foundVersion = await fetch(`${config.host}/api/v1/version/${selectedVersion}`, { method: 'get', - headers: { - Authorization: `Basic ${encodedString}`, - }, + headers: cleanHeaders(key), }).then(res => handleRes(res)); const promptResponse = await prompt(promptOpts.createVersionPrompt([{}], opts, foundVersion)); return fetch(`${config.host}/api/v1/version/${selectedVersion}`, { method: 'put', - headers: { + headers: cleanHeaders(key, { Accept: 'application/json', 'Content-Type': 'application/json', - Authorization: `Basic ${encodedString}`, - }, + }), body: JSON.stringify({ codename: codename || '', version: newVersion || promptResponse.newVersion, diff --git a/src/lib/cleanHeaders.js b/src/lib/cleanHeaders.js new file mode 100644 index 000000000..5f1b0786a --- /dev/null +++ b/src/lib/cleanHeaders.js @@ -0,0 +1,23 @@ +/** + * Returns the basic auth header and any other defined headers for use in node-fetch API calls. + * @param {string} key The ReadMe project API key + * @param {Object} inputHeaders Any additional headers to be cleaned + * @returns An object with cleaned request headers for usage in the node-fetch requests to the ReadMe API. + */ +function cleanHeaders(key, inputHeaders = {}) { + const encodedKey = Buffer.from(`${key}:`).toString('base64'); + const headers = { + Authorization: `Basic ${encodedKey}`, + }; + + Object.keys(inputHeaders).forEach(header => { + // For some reason, node-fetch will send in the string 'undefined' + // if you pass in an undefined value for a header, + // so that's why headers are added incrementally. + if (typeof inputHeaders[header] === 'string') headers[header] = inputHeaders[header]; + }); + + return headers; +} + +module.exports = { cleanHeaders }; diff --git a/src/lib/versionSelect.js b/src/lib/versionSelect.js index 07364b265..5cb0f4da8 100644 --- a/src/lib/versionSelect.js +++ b/src/lib/versionSelect.js @@ -1,17 +1,16 @@ const { prompt } = require('enquirer'); const promptOpts = require('./prompts'); +const { cleanHeaders } = require('./cleanHeaders'); const fetch = require('node-fetch'); const config = require('config'); const APIError = require('./apiError'); async function getProjectVersion(versionFlag, key, allowNewVersion) { - const encodedString = Buffer.from(`${key}:`).toString('base64'); - try { if (versionFlag) { return await fetch(`${config.host}/api/v1/version/${versionFlag}`, { method: 'get', - headers: { Authorization: `Basic ${encodedString}` }, + headers: cleanHeaders(key), }) .then(res => res.json()) .then(res => res.version); @@ -19,7 +18,7 @@ async function getProjectVersion(versionFlag, key, allowNewVersion) { const versionList = await fetch(`${config.host}/api/v1/version`, { method: 'get', - headers: { Authorization: `Basic ${encodedString}` }, + headers: cleanHeaders(key), }).then(res => res.json()); if (allowNewVersion) { @@ -29,7 +28,7 @@ async function getProjectVersion(versionFlag, key, allowNewVersion) { await fetch(`${config.host}/api/v1/version`, { method: 'post', - headers: { Authorization: `Basic ${encodedString}`, 'Content-Type': 'application/json' }, + headers: cleanHeaders(key, { 'Content-Type': 'application/json' }), body: JSON.stringify({ from: versionList[0].version, version: newVersion,