From 858afdbf03de69fec1da6449e6a4a74ce3090466 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 23 Sep 2024 11:32:03 +0530 Subject: [PATCH 01/27] feat: preferences sidebar restructure (#3170) --- .../{ => Display}/Font/StyledWrapper.js | 0 .../Preferences/{ => Display}/Font/index.js | 4 +- .../{ => Display}/Theme/StyledWrapper.js | 0 .../Preferences/{ => Display}/Theme/index.js | 0 .../components/Preferences/Display/index.js | 22 ++++++++++ .../components/Preferences/General/index.js | 2 +- .../Preferences/ProxySettings/index.js | 2 +- .../components/Preferences/StyledWrapper.js | 15 ++++--- .../src/components/Preferences/index.js | 42 ++++++++----------- 9 files changed, 53 insertions(+), 34 deletions(-) rename packages/bruno-app/src/components/Preferences/{ => Display}/Font/StyledWrapper.js (100%) rename packages/bruno-app/src/components/Preferences/{ => Display}/Font/index.js (93%) rename packages/bruno-app/src/components/Preferences/{ => Display}/Theme/StyledWrapper.js (100%) rename packages/bruno-app/src/components/Preferences/{ => Display}/Theme/index.js (100%) create mode 100644 packages/bruno-app/src/components/Preferences/Display/index.js diff --git a/packages/bruno-app/src/components/Preferences/Font/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/Display/Font/StyledWrapper.js similarity index 100% rename from packages/bruno-app/src/components/Preferences/Font/StyledWrapper.js rename to packages/bruno-app/src/components/Preferences/Display/Font/StyledWrapper.js diff --git a/packages/bruno-app/src/components/Preferences/Font/index.js b/packages/bruno-app/src/components/Preferences/Display/Font/index.js similarity index 93% rename from packages/bruno-app/src/components/Preferences/Font/index.js rename to packages/bruno-app/src/components/Preferences/Display/Font/index.js index ef6ac9f2f..622ea0817 100644 --- a/packages/bruno-app/src/components/Preferences/Font/index.js +++ b/packages/bruno-app/src/components/Preferences/Display/Font/index.js @@ -39,7 +39,7 @@ const Font = ({ close }) => {
- + { />
- + { + return ( +
+
+ + Theme + + +
+
+
+ +
+
+ ); +}; + +export default Display; diff --git a/packages/bruno-app/src/components/Preferences/General/index.js b/packages/bruno-app/src/components/Preferences/General/index.js index 9855c2747..0d26c955d 100644 --- a/packages/bruno-app/src/components/Preferences/General/index.js +++ b/packages/bruno-app/src/components/Preferences/General/index.js @@ -100,7 +100,7 @@ const General = ({ close }) => { return (
-
+
{ return ( -
+
diff --git a/packages/bruno-app/src/components/Preferences/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/StyledWrapper.js index 9cb6e7f7e..f43047925 100644 --- a/packages/bruno-app/src/components/Preferences/StyledWrapper.js +++ b/packages/bruno-app/src/components/Preferences/StyledWrapper.js @@ -2,13 +2,12 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` div.tabs { - margin-top: -0.5rem; - div.tab { - padding: 6px 0px; + width: 100%; + min-width: 120px; + padding: 7px 10px; border: none; border-bottom: solid 2px transparent; - margin-right: 1.25rem; color: var(--color-tab-inactive); cursor: pointer; @@ -22,8 +21,12 @@ const StyledWrapper = styled.div` } &.active { - color: ${(props) => props.theme.tabs.active.color} !important; - border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important; + color: ${(props) => props.theme.sidebar.color} !important; + background: ${(props) => props.theme.sidebar.collection.item.bg}; + + &:hover { + background: ${(props) => props.theme.sidebar.collection.item.bg} !important; + } } } } diff --git a/packages/bruno-app/src/components/Preferences/index.js b/packages/bruno-app/src/components/Preferences/index.js index 843fd8228..03b1d9ef8 100644 --- a/packages/bruno-app/src/components/Preferences/index.js +++ b/packages/bruno-app/src/components/Preferences/index.js @@ -3,10 +3,9 @@ import classnames from 'classnames'; import React, { useState } from 'react'; import Support from './Support'; import General from './General'; -import Font from './Font'; -import Theme from './Theme'; import Proxy from './ProxySettings'; import StyledWrapper from './StyledWrapper'; +import Display from './Display/index'; const Preferences = ({ onClose }) => { const [tab, setTab] = useState('general'); @@ -27,41 +26,36 @@ const Preferences = ({ onClose }) => { return ; } - case 'theme': { - return ; + case 'display': { + return ; } case 'support': { return ; } - - case 'font': { - return ; - } } }; return ( -
-
setTab('general')}> - General -
-
setTab('theme')}> - Theme -
-
setTab('font')}> - Font -
-
setTab('proxy')}> - Proxy -
-
setTab('support')}> - Support +
+
+
setTab('general')}> + General +
+
setTab('display')}> + Display +
+
setTab('proxy')}> + Proxy +
+
setTab('support')}> + Support +
+
{getTabPanel(tab)}
-
{getTabPanel(tab)}
); From eb33504f1962e30236c34ed842c34570874b2f55 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:22:03 +0530 Subject: [PATCH 02/27] bugfix(#2431) Refactor URL construction in Postman collection processing (#2445) * Refactor URL construction in Postman collection processing * Updated the constructUrl function and made it more loose. Also now when there is a param with its key as undefined we discard it. * Handled the case when the url is an object and dosen't have a raw value. * Added missing return. * Removed the URL fragments * Removed unused destructures. * Minor changes. --- .../src/utils/importers/postman-collection.js | 67 +++++++++++++++---- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index 4486e0efc..053a55723 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -54,13 +54,54 @@ const convertV21Auth = (array) => { }, {}); }; +const constructUrlFromParts = (url) => { + const { protocol = 'http', host, path, port, query, hash } = url || {}; + const hostStr = Array.isArray(host) ? host.filter(Boolean).join('.') : host || ''; + const pathStr = Array.isArray(path) ? path.filter(Boolean).join('/') : path || ''; + const portStr = port ? `:${port}` : ''; + const queryStr = + query && Array.isArray(query) && query.length > 0 + ? `?${query + .filter((q) => q.key) + .map((q) => `${q.key}=${q.value || ''}`) + .join('&')}` + : ''; + const urlStr = `${protocol}://${hostStr}${portStr}${pathStr ? `/${pathStr}` : ''}${queryStr}`; + return urlStr; +}; + +const constructUrl = (url) => { + if (!url) return ''; + + if (typeof url === 'string') { + return url; + } + + if (typeof url === 'object') { + const { raw } = url; + + if (raw && typeof raw === 'string') { + // If the raw URL contains url-fragments remove it + if (raw.includes('#')) { + return raw.split('#')[0]; // Returns the part of raw URL without the url-fragment part. + } + return raw; + } + + // If no raw value exists, construct the URL from parts + return constructUrlFromParts(url); + } + + return ''; +}; + let translationLog = {}; const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => { brunoParent.items = brunoParent.items || []; const folderMap = {}; const requestMap = {}; - + each(item, (i) => { if (isItemAFolder(i)) { const baseFolderName = i.name; @@ -86,20 +127,15 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) = } else { if (i.request) { const baseRequestName = i.name; - let requestName = baseRequestName; + let requestName = baseRequestName; let count = 1; while (requestMap[requestName]) { requestName = `${baseRequestName}_${count}`; count++; } - - let url = ''; - if (typeof i.request.url === 'string') { - url = i.request.url; - } else { - url = get(i, 'request.url.raw') || ''; - } + + const url = constructUrl(i.request.url); const brunoRequestItem = { uid: uuid(), @@ -107,7 +143,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) = type: 'http-request', request: { url: url, - method: i.request.method, + method: i?.request?.method?.toUpperCase(), auth: { mode: 'none', basic: null, @@ -313,12 +349,17 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) = }); }); - each(get(i, 'request.url.variable'), (param) => { + each(get(i, 'request.url.variable', []), (param) => { + if (!param.key) { + // If no key, skip this iteration and discard the param + return; + } + brunoRequestItem.request.params.push({ uid: uuid(), name: param.key, - value: param.value, - description: param.description, + value: param.value ?? '', + description: param.description ?? '', type: 'path', enabled: true }); From ed20eccc25d627a67e00fec944b5ec1daf7d8484 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 23 Sep 2024 15:54:54 +0530 Subject: [PATCH 03/27] fix: multipart/form-data body interpolation (#3142) * feat: updates * feat: updates * feat: updates * feat: updates --- .../bruno-cli/src/runner/interpolate-vars.js | 9 ++++ .../bruno-cli/src/runner/prepare-request.js | 10 +--- .../src/runner/run-single-request.js | 24 ++++------ packages/bruno-cli/src/utils/common.js | 33 +++++++++++++- .../bruno-electron/src/ipc/network/index.js | 12 ++++- .../src/ipc/network/interpolate-vars.js | 9 ++++ .../src/ipc/network/prepare-request.js | 43 +++++++++--------- packages/bruno-tests/collection/bruno.json | 2 +- packages/bruno-tests/collection/bruno.png | Bin 0 -> 795 bytes .../collection/echo/echo form-url-encoded.bru | 23 ++++++++++ .../echo/echo multipart scripting.bru | 22 +++++++++ .../collection/echo/echo multipart.bru | 24 ++++++++++ .../collection/environments/Prod.bru | 1 + 13 files changed, 165 insertions(+), 47 deletions(-) create mode 100644 packages/bruno-tests/collection/bruno.png create mode 100644 packages/bruno-tests/collection/echo/echo form-url-encoded.bru create mode 100644 packages/bruno-tests/collection/echo/echo multipart scripting.bru create mode 100644 packages/bruno-tests/collection/echo/echo multipart.bru diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index c35456993..39e92a6ec 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -1,5 +1,6 @@ const { interpolate } = require('@usebruno/common'); const { each, forOwn, cloneDeep, find } = require('lodash'); +const FormData = require('form-data'); const getContentType = (headers = {}) => { let contentType = ''; @@ -78,6 +79,14 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn request.data = JSON.parse(parsed); } catch (err) {} } + } else if (contentType === 'multipart/form-data') { + if (typeof request.data === 'object' && !(request?.data instanceof FormData)) { + try { + let parsed = JSON.stringify(request.data); + parsed = _interpolate(parsed); + request.data = JSON.parse(parsed); + } catch (err) {} + } } else { request.data = _interpolate(request.data); } diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index e30f8337f..8ba86472b 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -120,16 +120,10 @@ const prepareRequest = (request, collectionRoot) => { } if (request.body.mode === 'multipartForm') { + axiosRequest.headers['content-type'] = 'multipart/form-data'; const params = {}; const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - each(enabledParams, (p) => { - if (p.type === 'file') { - params[p.name] = p.value.map((path) => fs.createReadStream(path)); - } else { - params[p.name] = p.value; - } - }); - axiosRequest.headers['content-type'] = 'multipart/form-data'; + each(enabledParams, (p) => (params[p.name] = p.value)); axiosRequest.data = params; } diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index b260f6be9..c36a9b97f 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -19,6 +19,7 @@ const { makeAxiosInstance } = require('../utils/axios-instance'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'); const path = require('path'); +const { createFormData } = require('../utils/common'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const onConsoleLog = (type, args) => { @@ -45,21 +46,6 @@ const runSingleRequest = async function ( const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = runtime; - // make axios work in node using form data - // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 - if (request.headers && request.headers['content-type'] === 'multipart/form-data') { - const form = new FormData(); - forOwn(request.data, (value, key) => { - if (value instanceof Array) { - each(value, (v) => form.append(key, v)); - } else { - form.append(key, value); - } - }); - extend(request.headers, form.getHeaders()); - request.data = form; - } - // run pre request script const requestScriptFile = compact([ get(collectionRoot, 'request.script.req'), @@ -195,6 +181,14 @@ const runSingleRequest = async function ( request.data = qs.stringify(request.data); } + if (request?.headers?.['content-type'] === 'multipart/form-data') { + if (!(request?.data instanceof FormData)) { + let form = createFormData(request.data, collectionPath); + request.data = form; + extend(request.headers, form.getHeaders()); + } + } + let response, responseTime; try { // run request diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js index 704928022..16c2d1a7b 100644 --- a/packages/bruno-cli/src/utils/common.js +++ b/packages/bruno-cli/src/utils/common.js @@ -1,3 +1,8 @@ +const fs = require('fs'); +const FormData = require('form-data'); +const { forOwn } = require('lodash'); +const path = require('path'); + const lpad = (str, width) => { let paddedStr = str; while (paddedStr.length < width) { @@ -14,7 +19,33 @@ const rpad = (str, width) => { return paddedStr; }; +const createFormData = (datas, collectionPath) => { + // make axios work in node using form data + // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 + const form = new FormData(); + forOwn(datas, (value, key) => { + if (typeof value == 'string') { + form.append(key, value); + return; + } + + const filePaths = value || []; + filePaths?.forEach?.((filePath) => { + let trimmedFilePath = filePath.trim(); + + if (!path.isAbsolute(trimmedFilePath)) { + trimmedFilePath = path.join(collectionPath, trimmedFilePath); + } + + form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + }); + }); + return form; +}; + + module.exports = { lpad, - rpad + rpad, + createFormData }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index d7ed96d28..b0dfbfb68 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -9,7 +9,7 @@ const decomment = require('decomment'); const contentDispositionParser = require('content-disposition'); const mime = require('mime-types'); const { ipcMain } = require('electron'); -const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash'); +const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); const prepareRequest = require('./prepare-request'); const prepareCollectionRequest = require('./prepare-collection-request'); @@ -37,6 +37,8 @@ const { } = require('./oauth2-helper'); const Oauth2Store = require('../../store/oauth2'); const iconv = require('iconv-lite'); +const FormData = require('form-data'); +const { createFormData } = prepareRequest; const safeStringifyJSON = (data) => { try { @@ -423,6 +425,14 @@ const registerNetworkIpc = (mainWindow) => { request.data = qs.stringify(request.data); } + if (request.headers['content-type'] === 'multipart/form-data') { + if (!(request.data instanceof FormData)) { + let form = createFormData(request.data, collectionPath); + request.data = form; + extend(request.headers, form.getHeaders()); + } + } + return scriptResult; }; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index b6aeaa078..da1c9bab3 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -1,5 +1,6 @@ const { interpolate } = require('@usebruno/common'); const { each, forOwn, cloneDeep, find } = require('lodash'); +const FormData = require('form-data'); const getContentType = (headers = {}) => { let contentType = ''; @@ -76,6 +77,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.data = JSON.parse(parsed); } catch (err) {} } + } else if (contentType === 'multipart/form-data') { + if (typeof request.data === 'object' && !(request.data instanceof FormData)) { + try { + let parsed = JSON.stringify(request.data); + parsed = _interpolate(parsed); + request.data = JSON.parse(parsed); + } catch (err) {} + } } else { request.data = _interpolate(request.data); } diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 75b0f2c0e..61bbd7a30 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -1,5 +1,5 @@ const os = require('os'); -const { get, each, filter, extend, compact } = require('lodash'); +const { get, each, filter, compact, forOwn } = require('lodash'); const decomment = require('decomment'); const FormData = require('form-data'); const fs = require('fs'); @@ -165,27 +165,26 @@ const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => { } }; -const parseFormData = (datas, collectionPath) => { +const createFormData = (datas, collectionPath) => { // make axios work in node using form data // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 const form = new FormData(); - datas.forEach((item) => { - const value = item.value; - const name = item.name; - if (item.type === 'file') { - const filePaths = value || []; - filePaths.forEach((filePath) => { - let trimmedFilePath = filePath.trim(); - - if (!path.isAbsolute(trimmedFilePath)) { - trimmedFilePath = path.join(collectionPath, trimmedFilePath); - } - - form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); - }); - } else { - form.append(name, value); + forOwn(datas, (value, key) => { + if (typeof value == 'string') { + form.append(key, value); + return; } + + const filePaths = value || []; + filePaths?.forEach?.((filePath) => { + let trimmedFilePath = filePath.trim(); + + if (!path.isAbsolute(trimmedFilePath)) { + trimmedFilePath = path.join(collectionPath, trimmedFilePath); + } + + form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + }); }); return form; }; @@ -400,10 +399,11 @@ const prepareRequest = (item, collection) => { } if (request.body.mode === 'multipartForm') { + axiosRequest.headers['content-type'] = 'multipart/form-data'; + const params = {}; const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - const form = parseFormData(enabledParams, collectionPath); - extend(axiosRequest.headers, form.getHeaders()); - axiosRequest.data = form; + each(enabledParams, (p) => (params[p.name] = p.value)); + axiosRequest.data = params; } if (request.body.mode === 'graphql') { @@ -433,3 +433,4 @@ const prepareRequest = (item, collection) => { module.exports = prepareRequest; module.exports.setAuthHeaders = setAuthHeaders; +module.exports.createFormData = createFormData; diff --git a/packages/bruno-tests/collection/bruno.json b/packages/bruno-tests/collection/bruno.json index b6d437bbb..ada36145a 100644 --- a/packages/bruno-tests/collection/bruno.json +++ b/packages/bruno-tests/collection/bruno.json @@ -15,7 +15,7 @@ "bypassProxy": "" }, "scripts": { - "moduleWhitelist": ["crypto", "buffer"], + "moduleWhitelist": ["crypto", "buffer", "form-data"], "filesystemAccess": { "allow": true } diff --git a/packages/bruno-tests/collection/bruno.png b/packages/bruno-tests/collection/bruno.png new file mode 100644 index 0000000000000000000000000000000000000000..c2a7f878fb5bd695f7b67db54fa0ad121740d862 GIT binary patch literal 795 zcmV+$1LXXPP)`Dq^c7q<0*i%&IgAFX`BKTkzNHoGr zR-lAudQ$U^D&V=)7()e5)U9m8?lMzvb4%FfQV z*VoriUGE{*s>+Q?DHk;wO`l9A!*2w? z|pt-p@u=3`Grm_=J zanVXC7((zbshCAVOc-{rlBllUeV$O)*j!#zR76<_x;j5*7o-OrQOHI3A8{aqWs%In z(}jis!CS^;GD+0)9A~1HQnYqDK`HRy#W#GldobP2h5-zXxbeQct`OV+npr zT_1tSut4}Ig^yDBWu+()Bx2#6q!a*2ik*YDwzdu2BxT(Ah2oF8mSLQ|X9YlUmqRb$sk6)dk`Y# zVEgXHv)6JAhHzG~3~NPl zRGWv8wQ~XT>iC*PD>ff+(Xfhwf`S1E2>zkfYQx6*TP{xZ_Z`(LmV{6WTycGJ*hQ+c z)We!{X=!N_n1)~$>Xor4j|Y>&G4CV{!I7y(qfv($Fqp;ts_N_2f$~J1NS<6 Date: Mon, 23 Sep 2024 15:57:13 +0530 Subject: [PATCH 04/27] fix: runner result error display (#3176) --- .../src/components/RunnerResults/ResponsePane/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js index 007d398c0..8fd8de9d9 100644 --- a/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js +++ b/packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js @@ -15,7 +15,7 @@ import StyledWrapper from './StyledWrapper'; const ResponsePane = ({ rightPaneWidth, item, collection }) => { const [selectedTab, setSelectedTab] = useState('response'); - const { requestSent, responseReceived, testResults, assertionResults } = item; + const { requestSent, responseReceived, testResults, assertionResults, error } = item; const headers = get(item, 'responseReceived.headers', []); const status = get(item, 'responseReceived.status', 0); @@ -36,6 +36,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { data={responseReceived.data} dataBuffer={responseReceived.dataBuffer} headers={responseReceived.headers} + error={error} key={item.filename} /> ); From 6e4d7a6f7629a9d334202a8ec1b20a4ee66f1262 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 23 Sep 2024 16:01:17 +0530 Subject: [PATCH 05/27] fix: request timer issue (#3175) --- .../src/components/StopWatch/index.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/bruno-app/src/components/StopWatch/index.js b/packages/bruno-app/src/components/StopWatch/index.js index e2b069532..debba9cd8 100644 --- a/packages/bruno-app/src/components/StopWatch/index.js +++ b/packages/bruno-app/src/components/StopWatch/index.js @@ -1,25 +1,23 @@ import React, { useState, useEffect } from 'react'; -const StopWatch = ({ requestTimestamp }) => { +const StopWatch = () => { const [milliseconds, setMilliseconds] = useState(0); - const tickInterval = 200; + const tickInterval = 100; const tick = () => { - setMilliseconds(milliseconds + tickInterval); + setMilliseconds(_milliseconds => _milliseconds + tickInterval); }; useEffect(() => { - let timerID = setInterval(() => tick(), tickInterval); + let timerID = setInterval(() => { + tick() + }, tickInterval); return () => { - clearInterval(timerID); + clearTimeout(timerID); }; - }); + }, []); - useEffect(() => { - setMilliseconds(Date.now() - requestTimestamp); - }, [requestTimestamp]); - - if (milliseconds < 1000) { + if (milliseconds < 250) { return 'Loading...'; } @@ -27,4 +25,4 @@ const StopWatch = ({ requestTimestamp }) => { return {seconds.toFixed(1)}s; }; -export default StopWatch; +export default React.memo(StopWatch); From bebb18fc9906a387ad4cb080f4eb881944738560 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 23 Sep 2024 16:02:16 +0530 Subject: [PATCH 06/27] fix: close saved tabs (#3174) --- .../src/components/RequestTabs/RequestTab/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index f03cd8779..e73313c13 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -17,6 +17,7 @@ import CloneCollectionItem from 'components/Sidebar/Collections/Collection/Colle import NewRequest from 'components/Sidebar/NewRequest/index'; import CloseTabIcon from './CloseTabIcon'; import DraftTabIcon from './DraftTabIcon'; +import { flattenItems } from 'utils/collections/index'; const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => { const dispatch = useDispatch(); @@ -246,8 +247,9 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col function handleCloseSavedTabs(event) { event.stopPropagation(); - const savedTabs = collection.items.filter((item) => !item.draft); - const savedTabIds = savedTabs.map((item) => item.uid) || []; + const items = flattenItems(collection?.items); + const savedTabs = items?.filter?.((item) => !item.draft); + const savedTabIds = savedTabs?.map((item) => item.uid) || []; dispatch(closeTabs({ tabUids: savedTabIds })); } From 4d820af4e086b36027c6fa7924fb58f32721d953 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:46:31 +0530 Subject: [PATCH 07/27] Improved Feat/wsse auth (#3172) * adding wsse auth logic * adding wsse auth logic to electron * adding wsse auth formatting * Refactoring WSSE 'secret' to 'password' * Incorporating PR feedback * Removed unused packages from package.json * Fixed issue caused when resolving merge conflicts and added new route to test wsse * Removed deprecated package usages from bruno-cli * Fixed tests --------- Co-authored-by: dwolter-emarsys --- package-lock.json | 2 +- .../CollectionSettings/Auth/AuthMode/index.js | 9 +++ .../Auth/WsseAuth/StyledWrapper.js | 16 ++++ .../CollectionSettings/Auth/WsseAuth/index.js | 71 +++++++++++++++++ .../CollectionSettings/Auth/index.js | 4 + .../RequestPane/Auth/AuthMode/index.js | 10 ++- .../Auth/WsseAuth/StyledWrapper.js | 17 +++++ .../RequestPane/Auth/WsseAuth/index.js | 76 +++++++++++++++++++ .../src/components/RequestPane/Auth/index.js | 4 + .../ReduxStore/slices/collections/index.js | 7 ++ .../bruno-app/src/utils/collections/index.js | 11 ++- .../bruno-cli/src/runner/prepare-request.js | 19 +++++ .../src/ipc/network/interpolate-vars.js | 6 ++ .../src/ipc/network/prepare-request.js | 35 +++++++++ packages/bruno-js/src/bruno-request.js | 3 +- packages/bruno-lang/v2/src/bruToJson.js | 20 ++++- .../bruno-lang/v2/src/collectionBruToJson.js | 18 ++++- packages/bruno-lang/v2/src/jsonToBru.js | 9 +++ .../bruno-lang/v2/src/jsonToCollectionBru.js | 9 +++ .../v2/tests/fixtures/collection.bru | 5 ++ .../v2/tests/fixtures/collection.json | 4 + .../bruno-lang/v2/tests/fixtures/request.bru | 5 ++ .../bruno-lang/v2/tests/fixtures/request.json | 4 + .../bruno-schema/src/collections/index.js | 24 ++++-- packages/bruno-tests/src/auth/index.js | 2 + packages/bruno-tests/src/auth/wsse.js | 70 +++++++++++++++++ 26 files changed, 447 insertions(+), 13 deletions(-) create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js create mode 100644 packages/bruno-tests/src/auth/wsse.js diff --git a/package-lock.json b/package-lock.json index be8bc091c..67bfa6c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18862,4 +18862,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index c8e208acf..7dabb4c71 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -52,6 +52,15 @@ const AuthMode = ({ collection }) => { > Basic Auth
+
{ + dropdownTippyRef.current.hide(); + onModeChange('wsse'); + }} + > + WSSE Auth +
{ diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js new file mode 100644 index 000000000..c2bb5d207 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js new file mode 100644 index 000000000..45efc7b1e --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js @@ -0,0 +1,71 @@ +import React from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; +import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const WsseAuth = ({ collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const wsseAuth = get(collection, 'root.request.auth.wsse', {}); + + const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); + + const handleUserChange = (username) => { + dispatch( + updateCollectionAuth({ + mode: 'wsse', + collectionUid: collection.uid, + content: { + username, + password: wsseAuth.password + } + }) + ); + }; + + const handlePasswordChange = (password) => { + dispatch( + updateCollectionAuth({ + mode: 'wsse', + collectionUid: collection.uid, + content: { + username: wsseAuth.username, + password + } + }) + ); + }; + + return ( + + +
+ handleUserChange(val)} + collection={collection} + /> +
+ + +
+ handlePasswordChange(val)} + collection={collection} + /> +
+
+ ); +}; + +export default WsseAuth; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js index 85673782c..05efc17b2 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js @@ -6,6 +6,7 @@ import AwsV4Auth from './AwsV4Auth'; import BearerAuth from './BearerAuth'; import BasicAuth from './BasicAuth'; import DigestAuth from './DigestAuth'; +import WsseAuth from './WsseAuth'; import ApiKeyAuth from './ApiKeyAuth/'; import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; @@ -34,6 +35,9 @@ const Auth = ({ collection }) => { case 'oauth2': { return ; } + case 'wsse': { + return ; + } case 'apikey': { return ; } diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js index 9de35e5f2..dfbaba7fa 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -30,7 +30,6 @@ const AuthMode = ({ item, collection }) => { }) ); }; - return (
@@ -80,6 +79,15 @@ const AuthMode = ({ item, collection }) => { > OAuth 2.0
+
{ + dropdownTippyRef?.current?.hide(); + onModeChange('wsse'); + }} + > + WSSE Auth +
{ diff --git a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/StyledWrapper.js new file mode 100644 index 000000000..316d3a7c5 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/StyledWrapper.js @@ -0,0 +1,17 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + + .single-line-editor-wrapper { + max-width: 400px; + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js new file mode 100644 index 000000000..76a20e6f6 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js @@ -0,0 +1,76 @@ +import React from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const WsseAuth = ({ item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const wsseAuth = item.draft ? get(item, 'draft.request.auth.wsse', {}) : get(item, 'request.auth.wsse', {}); + + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleUserChange = (username) => { + dispatch( + updateAuth({ + mode: 'wsse', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + username, + password: wsseAuth.password + } + }) + ); + }; + + const handlePasswordChange = (password) => { + dispatch( + updateAuth({ + mode: 'wsse', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + username: wsseAuth.username, + password + } + }) + ); + }; + + return ( + + +
+ handleUserChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handlePasswordChange(val)} + onRun={handleRun} + collection={collection} + /> +
+
+ ); +}; + +export default WsseAuth; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index 2786f6d68..1515e5224 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -5,6 +5,7 @@ import AwsV4Auth from './AwsV4Auth'; import BearerAuth from './BearerAuth'; import BasicAuth from './BasicAuth'; import DigestAuth from './DigestAuth'; +import WsseAuth from './WsseAuth'; import ApiKeyAuth from './ApiKeyAuth'; import StyledWrapper from './StyledWrapper'; import { humanizeRequestAuthMode } from 'utils/collections/index'; @@ -33,6 +34,9 @@ const Auth = ({ item, collection }) => { case 'oauth2': { return ; } + case 'wsse': { + return ; + } case 'apikey': { return ; } diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 34a6c6af9..b7ef2f86e 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -477,6 +477,10 @@ export const collectionsSlice = createSlice({ item.draft.request.auth.mode = 'oauth2'; item.draft.request.auth.oauth2 = action.payload.content; break; + case 'wsse': + item.draft.request.auth.mode = 'wsse'; + item.draft.request.auth.wsse = action.payload.content; + break; case 'apikey': item.draft.request.auth.mode = 'apikey'; item.draft.request.auth.apikey = action.payload.content; @@ -1141,6 +1145,9 @@ export const collectionsSlice = createSlice({ case 'oauth2': set(collection, 'root.request.auth.oauth2', action.payload.content); break; + case 'wsse': + set(collection, 'root.request.auth.wsse', action.payload.content); + break; case 'apikey': set(collection, 'root.request.auth.apikey', action.payload.content); break; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 99d9b269c..ea8712be5 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -379,7 +379,12 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} placement: get(si.request, 'auth.apikey.placement', 'header') }; break; - + case 'wsse': + di.request.auth.wsse = { + username: get(si.request, 'auth.wsse.username', ''), + password: get(si.request, 'auth.wsse.password', '') + }; + break; default: break; } @@ -669,6 +674,10 @@ export const humanizeRequestAuthMode = (mode) => { label = 'OAuth 2.0'; break; } + case 'wsse': { + label = 'WSSE Auth'; + break; + } case 'apikey': { label = 'API Key'; break; diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 8ba86472b..d6688a1ff 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -2,6 +2,7 @@ const { get, each, filter } = require('lodash'); const fs = require('fs'); var JSONbig = require('json-bigint'); const decomment = require('decomment'); +const crypto = require('node:crypto'); const prepareRequest = (request, collectionRoot) => { const headers = {}; @@ -69,6 +70,24 @@ const prepareRequest = (request, collectionRoot) => { if (request.auth.mode === 'bearer') { axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; } + + if (request.auth.mode === 'wsse') { + const username = get(request, 'auth.wsse.username', ''); + const password = get(request, 'auth.wsse.password', ''); + + const ts = new Date().toISOString(); + const nonce = crypto.randomBytes(16).toString('base64'); + + // Create the password digest using SHA-256 + const hash = crypto.createHash('sha256'); + hash.update(nonce + ts + password); + const digest = hash.digest('base64'); + + // Construct the WSSE header + axiosRequest.headers[ + 'X-WSSE' + ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`; + } } request.body = request.body || {}; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index da1c9bab3..90b072658 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -215,6 +215,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.digestConfig.password = _interpolate(request.digestConfig.password) || ''; } + // interpolate vars for wsse auth + if (request.wsse) { + request.wsse.username = _interpolate(request.wsse.username) || ''; + request.wsse.password = _interpolate(request.wsse.password) || ''; + } + return request; }; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 61bbd7a30..0bac42af9 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -4,6 +4,7 @@ const decomment = require('decomment'); const FormData = require('form-data'); const fs = require('fs'); const path = require('path'); +const crypto = require('node:crypto'); const { getTreePathFromCollectionToItem } = require('../../utils/collection'); const { buildFormUrlEncodedPayload } = require('../../utils/common'); @@ -218,6 +219,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { password: get(collectionAuth, 'digest.password') }; break; + case 'wsse': + const username = get(request, 'auth.wsse.username', ''); + const password = get(request, 'auth.wsse.password', ''); + + const ts = new Date().toISOString(); + const nonce = crypto.randomBytes(16).toString('base64'); + + // Create the password digest using SHA-256 + const hash = crypto.createHash('sha256'); + hash.update(nonce + ts + password); + const digest = hash.digest('base64'); + + // Construct the WSSE header + axiosRequest.headers[ + 'X-WSSE' + ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`; + break; case 'apikey': const apiKeyAuth = get(collectionAuth, 'apikey'); if (apiKeyAuth.placement === 'header') { @@ -295,6 +313,23 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { break; } break; + case 'wsse': + const username = get(request, 'auth.wsse.username', ''); + const password = get(request, 'auth.wsse.password', ''); + + const ts = new Date().toISOString(); + const nonce = crypto.randomBytes(16).toString('base64'); + + // Create the password digest using SHA-256 + const hash = crypto.createHash('sha256'); + hash.update(nonce + ts + password); + const digest = hash.digest('base64'); + + // Construct the WSSE header + axiosRequest.headers[ + 'X-WSSE' + ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Created="${ts}", Nonce="${nonce}"`; + break; case 'apikey': const apiKeyAuth = get(request, 'auth.apikey'); if (apiKeyAuth.placement === 'header') { diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js index cf5f59aca..b0d22b6ac 100644 --- a/packages/bruno-js/src/bruno-request.js +++ b/packages/bruno-js/src/bruno-request.js @@ -43,7 +43,6 @@ class BrunoRequest { getMethod() { return this.req.method; } - getAuthMode() { if (this.req?.oauth2) { return 'oauth2'; @@ -55,6 +54,8 @@ class BrunoRequest { return 'awsv4'; } else if (this.req?.digestConfig) { return 'digest'; + } else if (this.headers?.['X-WSSE'] || this.req?.auth?.username) { + return 'wsse'; } else { return 'none'; } diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 84890c92f..c84d36d07 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils'); */ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authapikey + auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart params = paramspath | paramsquery @@ -88,6 +88,7 @@ const grammar = ohm.grammar(`Bru { authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary authOAuth2 = "auth:oauth2" dictionary + authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary body = "body" st* "{" nl* textblock tagend @@ -484,6 +485,23 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authwsse(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + + const userKey = _.find(auth, { name: 'username' }); + const secretKey = _.find(auth, { name: 'password' }); + const username = userKey ? userKey.value : ''; + const password = secretKey ? secretKey.value : ''; + + return { + auth: { + wsse: { + username, + password + } + } + }; + }, authapikey(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 513c4102d..5180f0193 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils'); const grammar = ohm.grammar(`Bru { BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)* - auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authapikey + auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2 | authwsse | authapikey nl = "\\r"? "\\n" st = " " | "\\t" @@ -43,6 +43,7 @@ const grammar = ohm.grammar(`Bru { authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary authOAuth2 = "auth:oauth2" dictionary + authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary script = scriptreq | scriptres @@ -294,6 +295,21 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authwsse(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const userKey = _.find(auth, { name: 'username' }); + const secretKey = _.find(auth, { name: 'password' }); + const username = userKey ? userKey.value : ''; + const password = secretKey ? secretKey.value : ''; + return { + auth: { + wsse: { + username, + password + } + } + } + }, authapikey(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 30bec13ef..8d3a5fdee 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -136,6 +136,15 @@ ${indentString(`username: ${auth?.basic?.username || ''}`)} ${indentString(`password: ${auth?.basic?.password || ''}`)} } +`; + } + + if (auth && auth.wsse) { + bru += `auth:wsse { +${indentString(`username: ${auth?.wsse?.username || ''}`)} +${indentString(`password: ${auth?.wsse?.password || ''}`)} +} + `; } diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index 6462efb3c..8b162b7a6 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -94,6 +94,15 @@ ${indentString(`username: ${auth.basic.username}`)} ${indentString(`password: ${auth.basic.password}`)} } +`; + } + + if (auth && auth.wsse) { + bru += `auth:wsse { +${indentString(`username: ${auth.wsse.username}`)} +${indentString(`password: ${auth.wsse.password}`)} +} + `; } diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.bru b/packages/bruno-lang/v2/tests/fixtures/collection.bru index 44a66c8dc..f11954ebf 100644 --- a/packages/bruno-lang/v2/tests/fixtures/collection.bru +++ b/packages/bruno-lang/v2/tests/fixtures/collection.bru @@ -17,6 +17,11 @@ auth:basic { password: secret } +auth:wsse { + username: john + password: secret +} + auth:bearer { token: 123 } diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.json b/packages/bruno-lang/v2/tests/fixtures/collection.json index 7bda2534d..102ee295c 100644 --- a/packages/bruno-lang/v2/tests/fixtures/collection.json +++ b/packages/bruno-lang/v2/tests/fixtures/collection.json @@ -31,6 +31,10 @@ "digest": { "username": "john", "password": "secret" + }, + "wsse": { + "username": "john", + "password": "secret" } }, "vars": { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index c4ff61558..1a3efeab7 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -40,6 +40,11 @@ auth:basic { password: secret } +auth:wsse { + username: john + password: secret +} + auth:bearer { token: 123 } diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index d0bd996f6..24997a90c 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -83,6 +83,10 @@ "scope": "read write", "state": "807061d5f0be", "pkce": false + }, + "wsse": { + "username": "john", + "password": "secret" } }, "body": { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 2934a60d8..11561c528 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -106,6 +106,13 @@ const authBasicSchema = Yup.object({ .noUnknown(true) .strict(); +const authWsseSchema = Yup.object({ + username: Yup.string().nullable(), + password: Yup.string().nullable() +}) + .noUnknown(true) + .strict(); + const authBearerSchema = Yup.object({ token: Yup.string().nullable() }) @@ -119,6 +126,14 @@ const authDigestSchema = Yup.object({ .noUnknown(true) .strict(); +const authApiKeySchema = Yup.object({ + key: Yup.string().nullable(), + value: Yup.string().nullable(), + placement: Yup.string().oneOf(['header', 'queryparams']).nullable() +}) + .noUnknown(true) + .strict(); + const oauth2Schema = Yup.object({ grantType: Yup.string() .oneOf(['client_credentials', 'password', 'authorization_code']) @@ -177,21 +192,16 @@ const oauth2Schema = Yup.object({ .noUnknown(true) .strict(); -const authApiKeySchema = Yup.object({ - key: Yup.string().nullable(), - value: Yup.string().nullable(), - placement: Yup.string().oneOf(['header', 'queryparams']).nullable() -}); - const authSchema = Yup.object({ mode: Yup.string() - .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'apikey']) + .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'wsse', 'apikey']) .required('mode is required'), awsv4: authAwsV4Schema.nullable(), basic: authBasicSchema.nullable(), bearer: authBearerSchema.nullable(), digest: authDigestSchema.nullable(), oauth2: oauth2Schema.nullable(), + wsse: authWsseSchema.nullable(), apikey: authApiKeySchema.nullable() }) .noUnknown(true) diff --git a/packages/bruno-tests/src/auth/index.js b/packages/bruno-tests/src/auth/index.js index 6d6ebfb55..e26a65529 100644 --- a/packages/bruno-tests/src/auth/index.js +++ b/packages/bruno-tests/src/auth/index.js @@ -3,6 +3,7 @@ const router = express.Router(); const authBearer = require('./bearer'); const authBasic = require('./basic'); +const authWsse = require('./wsse'); const authCookie = require('./cookie'); const authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials'); const authOAuth2AuthorizationCode = require('./oauth2/authorizationCode'); @@ -13,6 +14,7 @@ router.use('/oauth2/authorization_code', authOAuth2AuthorizationCode); router.use('/oauth2/client_credentials', authOAuth2ClientCredentials); router.use('/bearer', authBearer); router.use('/basic', authBasic); +router.use('/wsse', authWsse); router.use('/cookie', authCookie); module.exports = router; diff --git a/packages/bruno-tests/src/auth/wsse.js b/packages/bruno-tests/src/auth/wsse.js new file mode 100644 index 000000000..1af574a3d --- /dev/null +++ b/packages/bruno-tests/src/auth/wsse.js @@ -0,0 +1,70 @@ +'use strict'; + +const express = require('express'); +const router = express.Router(); +const crypto = require('crypto'); + +function sha256(data) { + return crypto.createHash('sha256').update(data).digest('base64'); +} + +function validateWSSE(req, res, next) { + const wsseHeader = req.headers['x-wsse']; + if (!wsseHeader) { + return unauthorized(res, 'WSSE header is missing'); + } + + const regex = /UsernameToken Username="(.+?)", PasswordDigest="(.+?)", (?:Nonce|nonce)="(.+?)", Created="(.+?)"/; + const matches = wsseHeader.match(regex); + + if (!matches) { + return unauthorized(res, 'Invalid WSSE header format'); + } + + const [_, username, passwordDigest, nonce, created] = matches; + const expectedPassword = 'bruno'; // Ideally store in a config or env variable + const expectedDigest = sha256(nonce + created + expectedPassword); + + if (passwordDigest !== expectedDigest) { + return unauthorized(res, 'Invalid credentials'); + } + + next(); +} + +// Helper to respond with an unauthorized SOAP fault +function unauthorized(res, message) { + const faultResponse = ` + + + + + soapenv:Client + ${message} + + + + `; + res.status(401).set('Content-Type', 'text/xml'); + res.send(faultResponse); +} + +const responses = { + success: ` + + + + + Success + + + + ` +}; + +router.post('/protected', validateWSSE, (req, res) => { + res.set('Content-Type', 'text/xml'); + res.send(responses.success); +}); + +module.exports = router; From 641f261733c89c59ef0ba1a8fef81ccf91828cbc Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Mon, 23 Sep 2024 17:50:55 +0530 Subject: [PATCH 08/27] chore: disabling smooth scroll in env vars view --- .../EnvironmentDetails/EnvironmentVariables/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index 05d9708b9..e45909c94 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -88,7 +88,9 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original useEffect(() => { if (formik.dirty) { - addButtonRef.current?.scrollIntoView({ behavior: 'smooth' }); + // Smooth scrolling to the changed parameter is temporarily disabled + // due to UX issues when editing the first row in a long list of environment variables. + // addButtonRef.current?.scrollIntoView({ behavior: 'smooth' }); } }, [formik.values, formik.dirty]); From 4ef5534d41ba030e8b34e5d20996414261cb957a Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:54:24 +0530 Subject: [PATCH 09/27] Enhancement: Accessibility issues on the Welcome page (#3173) * fix: accessibility issue in Welcome page - use button tag for collection instead of div - hide decorative image for assistive technology - give meaningful label to links in Links section * enhance: accessibility over the buttons on the welcome page * chore: fix translations --------- Co-authored-by: Shrilakshmi Shastry --- .../bruno-app/src/components/Welcome/index.js | 65 +++++++++++++------ 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/packages/bruno-app/src/components/Welcome/index.js b/packages/bruno-app/src/components/Welcome/index.js index 0ec4c1245..64100224c 100644 --- a/packages/bruno-app/src/components/Welcome/index.js +++ b/packages/bruno-app/src/components/Welcome/index.js @@ -21,9 +21,7 @@ const Welcome = () => { const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false); const handleOpenCollection = () => { - dispatch(openCollection()).catch( - (err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR')) - ); + dispatch(openCollection()).catch((err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR'))); }; const handleImportCollection = ({ collection, translationLog }) => { @@ -64,7 +62,7 @@ const Welcome = () => { /> ) : null} -
+
bruno
@@ -72,40 +70,69 @@ const Welcome = () => {
{t('COMMON.COLLECTIONS')}
-
setCreateCollectionModalOpen(true)}> - +
-
- + + +
-
setImportCollectionModalOpen(true)}> - + + +
+
+
{t('WELCOME.LINKS')}
From 3a58c6d3bd17800941a7f25da50b4542e05a1e4d Mon Sep 17 00:00:00 2001 From: anusreesubash <65728079+anusreesubash@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:34:14 +0530 Subject: [PATCH 10/27] Bugfix/openapi ensure url (#3183) * added validations for spec and ref * openapi import - cleanup url --------- Co-authored-by: Anusree Subash --- packages/bruno-app/src/utils/importers/openapi-collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js index 3bfe8394f..fd25b7f51 100644 --- a/packages/bruno-app/src/utils/importers/openapi-collection.js +++ b/packages/bruno-app/src/utils/importers/openapi-collection.js @@ -32,7 +32,7 @@ const readFile = (files) => { const ensureUrl = (url) => { // emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise - return url.replace(/(^\w+:|^)\/{2,}/, '$1/'); + return url.replace(/([^:])\/{2,}/g, '$1/'); }; const buildEmptyJsonBody = (bodySchema) => { From 5889e114d4f5aa9759e3bafb65b761eb6a22ef22 Mon Sep 17 00:00:00 2001 From: ajubin Date: Wed, 25 Sep 2024 07:08:41 +0200 Subject: [PATCH 11/27] feat(code-export): add support to grapqhl (#1288) Co-authored-by: Anoop M D --- .../GenerateCodeItem/CodeView/index.js | 5 ++++- .../Collections/Collection/CollectionItem/index.js | 2 +- packages/bruno-app/src/utils/codegenerator/har.js | 14 +++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js index 78977cabb..9d5648907 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js @@ -32,7 +32,10 @@ const CodeView = ({ language, item }) => { let snippet = ''; try { - snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client); + snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers, type: item.type })).convert( + target, + client + ); } catch (e) { console.error(e); snippet = 'Error generating code snippet'; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 6e5947e58..6efd00ceb 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -349,7 +349,7 @@ const CollectionItem = ({ item, collection, searchText }) => { Run
)} - {!isFolder && item.type === 'http-request' && ( + {!isFolder && (
{ diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index fa0738503..9bbd0eea9 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -31,6 +31,7 @@ const createHeaders = (request, headers) => { if (contentType !== '') { enabledHeaders.push({ name: 'content-type', value: contentType }); } + return enabledHeaders; }; @@ -43,7 +44,14 @@ const createQuery = (queryParams = []) => { })); }; -const createPostData = (body) => { +const createPostData = (body, type) => { + if (type === 'graphql-request') { + return { + mimeType: 'application/json', + text: JSON.stringify(body[body.mode]) + }; + } + const contentType = createContentType(body.mode); if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') { return { @@ -64,7 +72,7 @@ const createPostData = (body) => { } }; -export const buildHarRequest = ({ request, headers }) => { +export const buildHarRequest = ({ request, headers, type }) => { return { method: request.method, url: encodeURI(request.url), @@ -72,7 +80,7 @@ export const buildHarRequest = ({ request, headers }) => { cookies: [], headers: createHeaders(request, headers), queryString: createQuery(request.params), - postData: createPostData(request.body), + postData: createPostData(request.body, type), headersSize: 0, bodySize: 0 }; From 63d4757bfa24234045aaef4e621fa67ffcfe4ae1 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:10:49 +0530 Subject: [PATCH 12/27] Refactor CollectionItem component to conditionally render dropdown item for non-folder items with type 'http-request' or 'graphql-request' (#3190) --- .../Sidebar/Collections/Collection/CollectionItem/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 6efd00ceb..09b9552b5 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -349,7 +349,7 @@ const CollectionItem = ({ item, collection, searchText }) => { Run
)} - {!isFolder && ( + {!isFolder && (item.type === 'http-request' || item.type === 'graphql-request') && (
{ From 1f17d39a91f48430f745a5e4159a37db906e8c17 Mon Sep 17 00:00:00 2001 From: lohit Date: Wed, 25 Sep 2024 14:11:35 +0530 Subject: [PATCH 13/27] feat: remove pre-request var tooltip (#3188) --- .../src/components/CollectionSettings/Vars/VarsTable/index.js | 1 - .../src/components/FolderSettings/Vars/VarsTable/index.js | 1 - .../bruno-app/src/components/RequestPane/Vars/VarsTable/index.js | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js index 950076b60..ebc6a2fe7 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js @@ -83,7 +83,6 @@ const VarsTable = ({ collection, vars, varType }) => {
Value -
) : ( diff --git a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js index d0a77de44..17d79629e 100644 --- a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js @@ -82,7 +82,6 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
Value -
) : ( diff --git a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js index 84f040c6e..0f94f35bb 100644 --- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js @@ -83,7 +83,6 @@ const VarsTable = ({ item, collection, vars, varType }) => {
Value -
) : ( From a1719a33fc78e30f3542c0f00fdd929f2511bf32 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:15:09 +0530 Subject: [PATCH 14/27] Bugfix/rename request open tab not found (#3192) * Refactor: Normalize and resolve paths in renameItem action * Refactor: Normalize and resolve paths in renameItem action (handler side) * : --- packages/bruno-electron/src/ipc/collection.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 945c21559..3d1c15ef0 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -16,6 +16,7 @@ const { sanitizeDirectoryName, isWSLPath, normalizeWslPath, + normalizeAndResolvePath } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); @@ -329,12 +330,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => { try { // Normalize paths if they are WSL paths - if (isWSLPath(oldPath)) { - oldPath = normalizeWslPath(oldPath); - } - if (isWSLPath(newPath)) { - newPath = normalizeWslPath(newPath); - } + oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath); + newPath = isWSLPath(newPath) ? normalizeWslPath(newPath) : normalizeAndResolvePath(newPath); if (!fs.existsSync(oldPath)) { throw new Error(`path: ${oldPath} does not exist`); From 8e222189bc0966ce8f0a6ef7099e29b657e555fb Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:07:45 +0530 Subject: [PATCH 15/27] Bugfix/rename with same name (#3171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix rename with same name with different case * added `_temp` to the filename to change the request name (handle case insensitivity) on Win and macOS * chore: remove whitespaces and added path added path resolver * refactor: wslpath check * feat: safeToRename check added * refactor * refactor: code cleanup * chore: improved error message --------- Co-authored-by: Linhart Lukáš Co-authored-by: Anoop M D --- .../RenameCollectionItem/index.js | 7 +++-- .../ReduxStore/slices/collections/actions.js | 3 +- packages/bruno-electron/src/ipc/collection.js | 15 ++++++---- .../bruno-electron/src/utils/filesystem.js | 30 +++++++++++++++++-- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js index 5711cdbcd..6cf8cb21a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js @@ -28,9 +28,12 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { if (!isFolder && item.draft) { await dispatch(saveRequest(item.uid, collection.uid, true)); } + if (item.name === values.name) { + return; + } dispatch(renameItem(values.name, item.uid, collection.uid)) .then(() => { - toast.success('Request renamed!'); + toast.success('Request renamed'); onClose(); }) .catch((err) => { @@ -55,7 +58,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { handleConfirm={onSubmit} handleCancel={onClose} > - e.preventDefault()}> + e.preventDefault()}>
-
v1.30.1
+
v1.31.0
diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index 55b0bbdad..279b96a2c 100644 --- a/packages/bruno-app/src/providers/App/useTelemetry.js +++ b/packages/bruno-app/src/providers/App/useTelemetry.js @@ -60,7 +60,7 @@ const trackStart = () => { event: 'start', properties: { os: platformLib.os.family, - version: '1.30.1' + version: '1.31.0' } }); }; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index b158989aa..b2b382754 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -1,5 +1,5 @@ { - "version": "v1.30.1", + "version": "v1.31.0", "name": "bruno", "description": "Opensource API Client for Exploring and Testing APIs", "homepage": "https://www.usebruno.com", From 727fa26e44dd6461d41b4c4e27d48f02781337b4 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:18:02 +0530 Subject: [PATCH 20/27] Refactor CodeMirror styling to remove glow outline around folded JSON (#3208) * Refactor CodeMirror styling to remove glow outline around folded JSON * Improved font color for better legibility. * chore: used colot from theme for codemirror fold count --------- Co-authored-by: Anoop M D --- .../bruno-app/src/components/CodeEditor/StyledWrapper.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js index c77749cb8..4d47186c0 100644 --- a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js @@ -10,6 +10,12 @@ const StyledWrapper = styled.div` flex: 1 1 0; } + /* Removes the glow outline around the folded json */ + .CodeMirror-foldmarker { + text-shadow: none; + color: ${(props) => props.theme.textLink}; + } + .CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { background: #d2d7db; From f35b715c6f2af0b019e8afc295fc8b1d00bc26b9 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 30 Sep 2024 16:39:05 +0530 Subject: [PATCH 21/27] feat: restrict access to system process env vars (#3226) --- packages/bruno-electron/src/app/watcher.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 589cd29d8..b93b01a55 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -201,7 +201,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => { const payload = { collectionUid, processEnvVariables: { - ...process.env, ...jsonData } }; @@ -331,7 +330,6 @@ const change = async (win, pathname, collectionUid, collectionPath) => { const payload = { collectionUid, processEnvVariables: { - ...process.env, ...jsonData } }; From d448599a531ce29b9a8c0feb661b8cfa99557789 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 30 Sep 2024 16:51:49 +0530 Subject: [PATCH 22/27] feat: ui-state-snapshot (#3215) * wip: save env * feat: updates * feat: updates --- .../src/providers/App/useIpcEvents.js | 7 ++- .../ReduxStore/slices/collections/actions.js | 41 +++++++++++-- .../bruno-app/src/utils/collections/index.js | 4 ++ packages/bruno-electron/src/app/watcher.js | 9 +++ packages/bruno-electron/src/ipc/collection.js | 10 ++++ .../src/store/ui-state-snapshot.js | 60 +++++++++++++++++++ 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 packages/bruno-electron/src/store/ui-state-snapshot.js diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index f4a04030f..b885ad74d 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -19,7 +19,7 @@ import { runRequestEvent, scriptEnvironmentUpdateEvent } from 'providers/ReduxStore/slices/collections'; -import { collectionAddEnvFileEvent, openCollectionEvent } from 'providers/ReduxStore/slices/collections/actions'; +import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionsWithUiStateSnapshot } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import { isElectron } from 'utils/common/platform'; @@ -149,6 +149,10 @@ const useIpcEvents = () => { dispatch(updateCookies(val)); }); + const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => { + dispatch(hydrateCollectionsWithUiStateSnapshot(val)); + }) + return () => { removeCollectionTreeUpdateListener(); removeOpenCollectionListener(); @@ -165,6 +169,7 @@ const useIpcEvents = () => { removePreferencesUpdatesListener(); removeCookieUpdateListener(); removeSystemProxyEnvUpdatesListener(); + removeSnapshotHydrationListener(); }; }, [isElectron]); }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 777a194ab..e582b0f1f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -44,6 +44,7 @@ import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index'; import { name } from 'file-loader'; import slash from 'utils/common/slash'; +import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index'; export const renameCollection = (newName, collectionUid) => (dispatch, getState) => { const state = getState(); @@ -972,13 +973,15 @@ export const selectEnvironment = (environmentUid, collectionUid) => (dispatch, g const collectionCopy = cloneDeep(collection); if (environmentUid) { const environment = findEnvironmentInCollection(collectionCopy, environmentUid); - if (!environment) { + if (environment) { + ipcRenderer.invoke('renderer:update-ui-state-snapshot', { type: 'COLLECTION_ENVIRONMENT', data: { collectionPath: collection?.pathname, environmentName: environment?.name } }) + dispatch(_selectEnvironment({ environmentUid, collectionUid })); + resolve(); + } + else { return reject(new Error('Environment not found')); } } - - dispatch(_selectEnvironment({ environmentUid, collectionUid })); - resolve(); }); }; @@ -1141,3 +1144,33 @@ export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => ( .catch(reject); }); }; + + +export const hydrateCollectionsWithUiStateSnapshot = (payload) => (dispatch, getState) => { + const collectionSnapshotData = payload; + return new Promise((resolve, reject) => { + const state = getState(); + try { + if(!collectionSnapshotData) resolve(); + const { pathname, selectedEnvironment } = collectionSnapshotData; + const collection = findCollectionByPathname(state.collections.collections, pathname); + const collectionCopy = cloneDeep(collection); + const collectionUid = collectionCopy?.uid; + + // update selected environment + if (selectedEnvironment) { + const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment); + if (environment) { + dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid })); + } + } + + // todo: add any other redux state that you want to save + + resolve(); + } + catch(error) { + reject(error); + } + }); + }; \ No newline at end of file diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index ea8712be5..ddb98f531 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -132,6 +132,10 @@ export const findEnvironmentInCollection = (collection, envUid) => { return find(collection.environments, (e) => e.uid === envUid); }; +export const findEnvironmentInCollectionByName = (collection, name) => { + return find(collection.environments, (e) => e.name === name); +}; + export const moveCollectionItem = (collection, draggedItem, targetItem) => { let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid); diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index b93b01a55..82d116d81 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -12,6 +12,7 @@ const { decryptString } = require('../utils/encryption'); const { setDotEnvVars } = require('../store/process-env'); const { setBrunoConfig } = require('../store/bruno-config'); const EnvironmentSecretsStore = require('../store/env-secrets'); +const UiStateSnapshot = require('../store/ui-state-snapshot'); const environmentSecretsStore = new EnvironmentSecretsStore(); @@ -421,6 +422,13 @@ const unlinkDir = (win, pathname, collectionUid, collectionPath) => { win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory); }; +const onWatcherSetupComplete = (win, collectionPath) => { + const UiStateSnapshotStore = new UiStateSnapshot(); + const collectionsSnapshotState = UiStateSnapshotStore.getCollections(); + const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == collectionPath); + win.webContents.send('main:hydrate-app-with-ui-state-snapshot', collectionSnapshotState); +}; + class Watcher { constructor() { this.watchers = {}; @@ -456,6 +464,7 @@ class Watcher { let startedNewWatcher = false; watcher + .on('ready', () => onWatcherSetupComplete(win, watchPath)) .on('add', (pathname) => add(win, pathname, collectionUid, watchPath)) .on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath)) .on('change', (pathname) => change(win, pathname, collectionUid, watchPath)) diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 950902ece..0aa2058c2 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -25,9 +25,11 @@ const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids'); const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cookies'); const EnvironmentSecretsStore = require('../store/env-secrets'); const CollectionSecurityStore = require('../store/collection-security'); +const UiStateSnapshot = require('../store/ui-state-snapshot'); const environmentSecretsStore = new EnvironmentSecretsStore(); const collectionSecurityStore = new CollectionSecurityStore(); +const UiStateSnapshotStore = new UiStateSnapshot(); const envHasSecrets = (environment = {}) => { const secrets = _.filter(environment.variables, (v) => v.secret); @@ -695,6 +697,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection return Promise.reject(error); } }); + + ipcMain.handle('renderer:update-ui-state-snapshot', (event, { type, data }) => { + try { + UiStateSnapshotStore.update({ type, data }); + } catch (error) { + throw new Error(error.message); + } + }); }; const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { diff --git a/packages/bruno-electron/src/store/ui-state-snapshot.js b/packages/bruno-electron/src/store/ui-state-snapshot.js new file mode 100644 index 000000000..b3d1909b6 --- /dev/null +++ b/packages/bruno-electron/src/store/ui-state-snapshot.js @@ -0,0 +1,60 @@ +const Store = require('electron-store'); + +class UiStateSnapshot { + constructor() { + this.store = new Store({ + name: 'ui-state-snapshot', + clearInvalidConfig: true + }); + } + + getCollections() { + return this.store.get('collections') || []; + } + + saveCollections(collections) { + this.store.set('collections', collections); + } + + getCollectionByPathname({ pathname }) { + let collections = this.getCollections(); + + let collection = collections.find(c => c?.pathname === pathname); + if (!collection) { + collection = { pathname }; + collections.push(collection); + this.saveCollections(collections); + } + + return collection; + } + + setCollectionByPathname({ collection }) { + let collections = this.getCollections(); + + collections = collections.filter(c => c?.pathname !== collection.pathname); + collections.push({ ...collection }); + this.saveCollections(collections); + + return collection; + } + + updateCollectionEnvironment({ collectionPath, environmentName }) { + const collection = this.getCollectionByPathname({ pathname: collectionPath }); + collection.selectedEnvironment = environmentName; + this.setCollectionByPathname({ collection }); + } + + update({ type, data }) { + switch(type) { + case 'COLLECTION_ENVIRONMENT': + const { collectionPath, environmentName } = data; + this.updateCollectionEnvironment({ collectionPath, environmentName }); + break; + default: + break; + } + } +} + +module.exports = UiStateSnapshot; From e2baed6724ae49ea9ded78dc3918f86f3365a36a Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 30 Sep 2024 17:14:58 +0530 Subject: [PATCH 23/27] fix: interpolate json body for type object -- graphql variables (#3212) --- packages/bruno-electron/src/ipc/network/interpolate-vars.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 5ea2bf7f4..e60d52428 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -68,6 +68,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc if (request.data.length) { request.data = _interpolate(request.data); } + } else if (typeof request.data === 'object') { + try { + let parsed = JSON.stringify(request.data); + parsed = _interpolate(parsed); + request.data = JSON.parse(parsed); + } catch (err) {} } } else if (contentType === 'application/x-www-form-urlencoded') { if (typeof request.data === 'object') { From 02a82c5371d347a8148cac3be7b221b46593c4e2 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:19:18 +0530 Subject: [PATCH 24/27] refactor: `ReorderTable` component to use `useMemo` for rowsOrder (#3227) --- .../bruno-app/src/components/ReorderTable/index.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/bruno-app/src/components/ReorderTable/index.js b/packages/bruno-app/src/components/ReorderTable/index.js index 9d8c11088..b5ea369a2 100644 --- a/packages/bruno-app/src/components/ReorderTable/index.js +++ b/packages/bruno-app/src/components/ReorderTable/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState, useCallback } from 'react'; +import React, { useEffect, useRef, useState, useMemo } from 'react'; import { IconGripVertical, IconMinusVertical } from '@tabler/icons'; /** @@ -13,17 +13,17 @@ import { IconGripVertical, IconMinusVertical } from '@tabler/icons'; const ReorderTable = ({ children, updateReorderedItem }) => { const tbodyRef = useRef(); - const [rowsOrder, setRowsOrder] = useState(React.Children.toArray(children)); const [hoveredRow, setHoveredRow] = useState(null); const [dragStart, setDragStart] = useState(null); + const rowsOrder = useMemo(() => React.Children.toArray(children), [children]); + /** - * useEffect hook to update the rows order and handle row hover states + * useEffect hook to handle row hover states */ useEffect(() => { - setRowsOrder(React.Children.toArray(children)); handleRowHover(null, false); - }, [children, dragStart]); + }, [children]); const handleRowHover = (index, hoverstatus = true) => { setHoveredRow(hoverstatus ? index : null); @@ -48,7 +48,6 @@ const ReorderTable = ({ children, updateReorderedItem }) => { const updatedRowsOrder = [...rowsOrder]; const [movedRow] = updatedRowsOrder.splice(fromIndex, 1); updatedRowsOrder.splice(toIndex, 0, movedRow); - setRowsOrder(updatedRowsOrder); updateReorderedItem({ updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid']) From 95e56cd9c950ad6bca80760dcb8ea508764f0a86 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:20:46 +0530 Subject: [PATCH 25/27] Added Keybindings tab. (#3204) * Added Keybindings tab. * Minor Refactoring --- .../Preferences/Keybindings/StyledWrapper.js | 46 ++++++++++++++ .../Preferences/Keybindings/index.js | 45 ++++++++++++++ .../src/components/Preferences/index.js | 12 +++- .../bruno-app/src/providers/Hotkeys/index.js | 33 +++++----- .../src/providers/Hotkeys/keyMappings.js | 60 +++++++++++++++++++ 5 files changed, 179 insertions(+), 17 deletions(-) create mode 100644 packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Preferences/Keybindings/index.js create mode 100644 packages/bruno-app/src/providers/Hotkeys/keyMappings.js diff --git a/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js b/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js new file mode 100644 index 000000000..e12969388 --- /dev/null +++ b/packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js @@ -0,0 +1,46 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + table { + width: 100%; + border-collapse: collapse; + + thead, + td { + border: 2px solid ${(props) => props.theme.table.border}; + } + + thead { + color: ${(props) => props.theme.table.thead.color}; + font-size: 1rem; + user-select: none; + } + + td { + padding: 4px 8px; + } + + thead th { + font-weight: 600; + padding: 10px; + text-align: left; + } + } + + .table-container { + max-height: 400px; + overflow-y: scroll; + } + + .key-button { + display: inline-block; + color: ${(props) => props.theme.colors.text.white}; + border-radius: 4px; + padding: 1px 5px; + font-family: monospace; + margin-right: 8px; + border: 1px solid #ccc; + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Preferences/Keybindings/index.js b/packages/bruno-app/src/components/Preferences/Keybindings/index.js new file mode 100644 index 000000000..d2bc918aa --- /dev/null +++ b/packages/bruno-app/src/components/Preferences/Keybindings/index.js @@ -0,0 +1,45 @@ +import StyledWrapper from './StyledWrapper'; +import React from 'react'; +import { getKeyBindingsForOS } from 'providers/Hotkeys/keyMappings'; +import { isMacOS } from 'utils/common/platform'; + +const Keybindings = ({ close }) => { + const keyMapping = getKeyBindingsForOS(isMacOS() ? 'mac' : 'windows'); + + return ( + +
+ + + + + + + + + {keyMapping ? ( + Object.entries(keyMapping).map(([action, { name, keys }], index) => ( + + + + + )) + ) : ( + + + + )} + +
CommandKeybinding
{name} + {keys.split('+').map((key, i) => ( +
+ {key} +
+ ))} +
No key bindings available
+
+
+ ); +}; + +export default Keybindings; diff --git a/packages/bruno-app/src/components/Preferences/index.js b/packages/bruno-app/src/components/Preferences/index.js index 03b1d9ef8..3635ca5a9 100644 --- a/packages/bruno-app/src/components/Preferences/index.js +++ b/packages/bruno-app/src/components/Preferences/index.js @@ -1,11 +1,14 @@ import Modal from 'components/Modal/index'; import classnames from 'classnames'; import React, { useState } from 'react'; + import Support from './Support'; import General from './General'; import Proxy from './ProxySettings'; +import Display from './Display'; +import Keybindings from './Keybindings'; + import StyledWrapper from './StyledWrapper'; -import Display from './Display/index'; const Preferences = ({ onClose }) => { const [tab, setTab] = useState('general'); @@ -30,6 +33,10 @@ const Preferences = ({ onClose }) => { return ; } + case 'keybindings': { + return ; + } + case 'support': { return ; } @@ -50,6 +57,9 @@ const Preferences = ({ onClose }) => {
setTab('proxy')}> Proxy
+
setTab('keybindings')}> + Keybindings +
setTab('support')}> Support
diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js index 5b6bf1c00..41e71b4a2 100644 --- a/packages/bruno-app/src/providers/Hotkeys/index.js +++ b/packages/bruno-app/src/providers/Hotkeys/index.js @@ -10,6 +10,7 @@ import NewRequest from 'components/Sidebar/NewRequest'; import { sendRequest, saveRequest, saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; import { findCollectionByUid, findItemInCollection } from 'utils/collections'; import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs'; +import { getKeyBindingsForActionAllOS } from './keyMappings'; export const HotkeysContext = React.createContext(); @@ -43,7 +44,7 @@ export const HotkeysProvider = (props) => { // save hotkey useEffect(() => { - Mousetrap.bind(['command+s', 'ctrl+s'], (e) => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('save')], (e) => { if (isEnvironmentSettingsModalOpen) { console.log('todo: save environment settings'); } else { @@ -68,13 +69,13 @@ export const HotkeysProvider = (props) => { }); return () => { - Mousetrap.unbind(['command+s', 'ctrl+s']); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('save')]); }; }, [activeTabUid, tabs, saveRequest, collections, isEnvironmentSettingsModalOpen]); // send request (ctrl/cmd + enter) useEffect(() => { - Mousetrap.bind(['command+enter', 'ctrl+enter'], (e) => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('sendRequest')], (e) => { const activeTab = find(tabs, (t) => t.uid === activeTabUid); if (activeTab) { const collection = findCollectionByUid(collections, activeTab.collectionUid); @@ -95,13 +96,13 @@ export const HotkeysProvider = (props) => { }); return () => { - Mousetrap.unbind(['command+enter', 'ctrl+enter']); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('sendRequest')]); }; }, [activeTabUid, tabs, saveRequest, collections]); // edit environments (ctrl/cmd + e) useEffect(() => { - Mousetrap.bind(['command+e', 'ctrl+e'], (e) => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('editEnvironment')], (e) => { const activeTab = find(tabs, (t) => t.uid === activeTabUid); if (activeTab) { const collection = findCollectionByUid(collections, activeTab.collectionUid); @@ -115,13 +116,13 @@ export const HotkeysProvider = (props) => { }); return () => { - Mousetrap.unbind(['command+e', 'ctrl+e']); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('editEnvironment')]); }; }, [activeTabUid, tabs, collections, setShowEnvSettingsModal]); // new request (ctrl/cmd + b) useEffect(() => { - Mousetrap.bind(['command+b', 'ctrl+b'], (e) => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('newRequest')], (e) => { const activeTab = find(tabs, (t) => t.uid === activeTabUid); if (activeTab) { const collection = findCollectionByUid(collections, activeTab.collectionUid); @@ -135,13 +136,13 @@ export const HotkeysProvider = (props) => { }); return () => { - Mousetrap.unbind(['command+b', 'ctrl+b']); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('newRequest')]); }; }, [activeTabUid, tabs, collections, setShowNewRequestModal]); // close tab hotkey useEffect(() => { - Mousetrap.bind(['command+w', 'ctrl+w'], (e) => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('closeTab')], (e) => { dispatch( closeTabs({ tabUids: [activeTabUid] @@ -152,13 +153,13 @@ export const HotkeysProvider = (props) => { }); return () => { - Mousetrap.unbind(['command+w', 'ctrl+w']); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeTab')]); }; }, [activeTabUid]); // Switch to the previous tab useEffect(() => { - Mousetrap.bind(['command+pageup', 'ctrl+pageup'], (e) => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToPreviousTab')], (e) => { dispatch( switchTab({ direction: 'pageup' @@ -169,13 +170,13 @@ export const HotkeysProvider = (props) => { }); return () => { - Mousetrap.unbind(['command+pageup', 'ctrl+pageup']); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToPreviousTab')]); }; }, [dispatch]); // Switch to the next tab useEffect(() => { - Mousetrap.bind(['command+pagedown', 'ctrl+pagedown'], (e) => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToNextTab')], (e) => { dispatch( switchTab({ direction: 'pagedown' @@ -186,13 +187,13 @@ export const HotkeysProvider = (props) => { }); return () => { - Mousetrap.unbind(['command+pagedown', 'ctrl+pagedown']); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToNextTab')]); }; }, [dispatch]); // Close all tabs useEffect(() => { - Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => { + Mousetrap.bind([...getKeyBindingsForActionAllOS('closeAllTabs')], (e) => { const activeTab = find(tabs, (t) => t.uid === activeTabUid); if (activeTab) { const collection = findCollectionByUid(collections, activeTab.collectionUid); @@ -211,7 +212,7 @@ export const HotkeysProvider = (props) => { }); return () => { - Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']); + Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeAllTabs')]); }; }, [activeTabUid, tabs, collections, dispatch]); diff --git a/packages/bruno-app/src/providers/Hotkeys/keyMappings.js b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js new file mode 100644 index 000000000..05ad4531b --- /dev/null +++ b/packages/bruno-app/src/providers/Hotkeys/keyMappings.js @@ -0,0 +1,60 @@ +const KeyMapping = { + save: { mac: 'command+s', windows: 'ctrl+s', name: 'Save' }, + sendRequest: { mac: 'command+enter', windows: 'ctrl+enter', name: 'Send Request' }, + editEnvironment: { mac: 'command+e', windows: 'ctrl+e', name: 'Edit Environment' }, + newRequest: { mac: 'command+b', windows: 'ctrl+b', name: 'New Request' }, + closeTab: { mac: 'command+w', windows: 'ctrl+w', name: 'Close Tab' }, + openPreferences: { mac: 'command+,', windows: 'ctrl+,', name: 'Open Preferences' }, + minimizeWindow: { + mac: 'command+Shift+Q', + windows: 'control+Shift+Q', + name: 'Minimize Window' + }, + switchToPreviousTab: { + mac: 'command+pageup', + windows: 'ctrl+pageup', + name: 'Switch to Previous Tab' + }, + switchToNextTab: { + mac: 'command+pagedown', + windows: 'ctrl+pagedown', + name: 'Switch to Next Tab' + }, + closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' } +}; + +/** + * Retrieves the key bindings for a specific operating system. + * + * @param {string} os - The operating system (e.g., 'mac', 'windows'). + * @returns {Object} An object containing the key bindings for the specified OS. + */ +export const getKeyBindingsForOS = (os) => { + const keyBindings = {}; + for (const [action, { name, ...keys }] of Object.entries(KeyMapping)) { + if (keys[os]) { + keyBindings[action] = { + keys: keys[os], + name + }; + } + } + return keyBindings; +}; + +/** + * Retrieves the key bindings for a specific action across all operating systems. + * + * @param {string} action - The action for which to retrieve key bindings. + * @returns {Object|null} An object containing the key bindings for macOS, Windows, or null if the action is not found. + */ +export const getKeyBindingsForActionAllOS = (action) => { + const actionBindings = KeyMapping[action]; + + if (!actionBindings) { + console.warn(`Action "${action}" not found in KeyMapping.`); + return null; + } + + return [actionBindings.mac, actionBindings.windows]; +}; From 4797119657c4a315088494f85a3a93b2c48526b7 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 1 Oct 2024 18:24:03 +0530 Subject: [PATCH 26/27] fix: interpolate form-urlencoded and multipart formdata values individually (#3237) * fix: interpolate form-urlencoded and multipart values individually * feat: cli updates --- packages/bruno-cli/src/runner/interpolate-vars.js | 12 ++++++------ .../src/ipc/network/interpolate-vars.js | 12 ++++++------ .../collection/echo/echo form-url-encoded.bru | 13 ++++++++----- .../bruno-tests/collection/echo/echo multipart.bru | 6 +++++- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 0253c10cd..2b727e671 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -74,17 +74,17 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn } else if (contentType === 'application/x-www-form-urlencoded') { if (typeof request.data === 'object') { try { - let parsed = JSON.stringify(request.data); - parsed = _interpolate(parsed); - request.data = JSON.parse(parsed); + forOwn(request?.data, (value, key) => { + request.data[key] = _interpolate(value); + }); } catch (err) {} } } else if (contentType === 'multipart/form-data') { if (typeof request.data === 'object' && !(request?.data instanceof FormData)) { try { - let parsed = JSON.stringify(request.data); - parsed = _interpolate(parsed); - request.data = JSON.parse(parsed); + forOwn(request?.data, (value, key) => { + request.data[key] = _interpolate(value); + }); } catch (err) {} } } else { diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index e60d52428..99b0191cd 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -78,17 +78,17 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc } else if (contentType === 'application/x-www-form-urlencoded') { if (typeof request.data === 'object') { try { - let parsed = JSON.stringify(request.data); - parsed = _interpolate(parsed); - request.data = JSON.parse(parsed); + forOwn(request?.data, (value, key) => { + request.data[key] = _interpolate(value); + }); } catch (err) {} } } else if (contentType === 'multipart/form-data') { if (typeof request.data === 'object' && !(request.data instanceof FormData)) { try { - let parsed = JSON.stringify(request.data); - parsed = _interpolate(parsed); - request.data = JSON.parse(parsed); + forOwn(request?.data, (value, key) => { + request.data[key] = _interpolate(value); + }); } catch (err) {} } } else { diff --git a/packages/bruno-tests/collection/echo/echo form-url-encoded.bru b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru index a0d2f0afb..7c0ce77eb 100644 --- a/packages/bruno-tests/collection/echo/echo form-url-encoded.bru +++ b/packages/bruno-tests/collection/echo/echo form-url-encoded.bru @@ -12,12 +12,15 @@ post { body:form-urlencoded { form-data-key: {{form-data-key}} -} - -script:pre-request { - bru.setVar('form-data-key', 'form-data-value'); + form-data-stringified-object: {{form-data-stringified-object}} } assert { - res.body: eq form-data-key=form-data-value + res.body: eq form-data-key=form-data-value&form-data-stringified-object=%7B%22foo%22%3A123%7D +} + +script:pre-request { + let obj = JSON.stringify({foo:123}); + bru.setVar('form-data-key', 'form-data-value'); + bru.setVar('form-data-stringified-object', obj); } diff --git a/packages/bruno-tests/collection/echo/echo multipart.bru b/packages/bruno-tests/collection/echo/echo multipart.bru index b8fd8abf7..1edb2ca8a 100644 --- a/packages/bruno-tests/collection/echo/echo multipart.bru +++ b/packages/bruno-tests/collection/echo/echo multipart.bru @@ -11,14 +11,18 @@ post { } body:multipart-form { - foo: {{form-data-key}} + form-data-key: {{form-data-key}} + form-data-stringified-object: {{form-data-stringified-object}} file: @file(bruno.png) } assert { res.body: contains form-data-value + res.body: contains {"foo":123} } script:pre-request { + let obj = JSON.stringify({foo:123}); bru.setVar('form-data-key', 'form-data-value'); + bru.setVar('form-data-stringified-object', obj); } From 96e58f2f40a3973bfa56fc88432adfe905b71612 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Tue, 1 Oct 2024 20:35:26 +0530 Subject: [PATCH 27/27] release: v1.32.0 --- packages/bruno-app/src/components/Sidebar/index.js | 2 +- packages/bruno-app/src/providers/App/useTelemetry.js | 2 +- packages/bruno-electron/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index 2423bce68..fe02a8ae2 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -185,7 +185,7 @@ const Sidebar = () => { Star */}
-
v1.31.0
+
v1.32.0
diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index 279b96a2c..0e709abcb 100644 --- a/packages/bruno-app/src/providers/App/useTelemetry.js +++ b/packages/bruno-app/src/providers/App/useTelemetry.js @@ -60,7 +60,7 @@ const trackStart = () => { event: 'start', properties: { os: platformLib.os.family, - version: '1.31.0' + version: '1.32.0' } }); }; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index b2b382754..d71156a4a 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -1,5 +1,5 @@ { - "version": "v1.31.0", + "version": "v1.32.0", "name": "bruno", "description": "Opensource API Client for Exploring and Testing APIs", "homepage": "https://www.usebruno.com",