diff --git a/package-lock.json b/package-lock.json index abf5d7b578..98b2208634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8184,6 +8184,29 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-ntlm": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/axios-ntlm/-/axios-ntlm-1.4.3.tgz", + "integrity": "sha512-CS6WE8chZpEDKxv4IFwr5zcG7InMC6Ek0aj2n2tHauBh+8KiYVC4qMn3N2arjR5tnyILQuTGlI0mc83hgWxS4Q==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.9", + "des.js": "^1.1.0", + "dev-null": "^0.1.1", + "js-md4": "^0.3.2" + } + }, + "node_modules/axios-ntlm/node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -10928,7 +10951,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", @@ -10979,6 +11001,12 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/dev-null": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz", + "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -15466,6 +15494,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16722,7 +16756,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, "license": "ISC" }, "node_modules/minimalistic-crypto-utils": { @@ -24115,6 +24148,7 @@ "@usebruno/vm2": "^3.9.13", "aws4-axios": "^3.3.0", "axios": "1.7.5", + "axios-ntlm": "^1.4.2", "chai": "^4.3.7", "chalk": "^3.0.0", "decomment": "^0.9.5", @@ -24165,7 +24199,7 @@ }, "packages/bruno-electron": { "name": "bruno", - "version": "v1.36.0", + "version": "v1.38.1", "dependencies": { "@aws-sdk/credential-providers": "3.658.1", "@usebruno/common": "0.1.0", @@ -24177,6 +24211,7 @@ "about-window": "^1.15.2", "aws4-axios": "^3.3.0", "axios": "1.7.5", + "axios-ntlm": "^1.4.2", "chai": "^4.3.7", "chokidar": "^3.5.3", "content-disposition": "^0.5.4", diff --git a/packages/bruno-app/rsbuild.config.mjs b/packages/bruno-app/rsbuild.config.mjs index 704ea4d090..25f5b7ae7e 100644 --- a/packages/bruno-app/rsbuild.config.mjs +++ b/packages/bruno-app/rsbuild.config.mjs @@ -24,4 +24,16 @@ export default defineConfig({ html: { title: 'Bruno' }, + tools: { + rspack: { + module: { + parser: { + javascript: { + // This loads the JavaScript contents from a library along with the main JavaScript bundle. + dynamicImportMode: "eager", + }, + }, + }, + }, + } }); diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 168a3b02be..398007a4ab 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -7,12 +7,12 @@ import React from 'react'; import { isEqual, escapeRegExp } from 'lodash'; -import { getEnvironmentVariables } from 'utils/collections'; import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror'; import StyledWrapper from './StyledWrapper'; import * as jsonlint from '@prantlf/jsonlint'; import { JSHINT } from 'jshint'; import stripJsonComments from 'strip-json-comments'; +import { getAllVariables } from 'utils/collections'; let CodeMirror; const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; @@ -293,7 +293,7 @@ export default class CodeEditor extends React.Component { } if (this.editor) { - let variables = getEnvironmentVariables(this.props.collection); + let variables = getAllVariables(this.props.collection, this.props.item); if (!isEqual(variables, this.variables)) { this.addOverlay(); } @@ -333,7 +333,7 @@ export default class CodeEditor extends React.Component { addOverlay = () => { const mode = this.props.mode || 'application/ld+json'; - let variables = getEnvironmentVariables(this.props.collection); + let variables = getAllVariables(this.props.collection, this.props.item); this.variables = variables; defineCodeMirrorBrunoVariablesMode(variables, mode); 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 7dabb4c71a..2c541cc289 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -79,6 +79,15 @@ const AuthMode = ({ collection }) => { > Digest Auth +
{ + dropdownTippyRef.current.hide(); + onModeChange('ntlm'); + }} + > + NTLM Auth +
{ diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/StyledWrapper.js new file mode 100644 index 0000000000..316d3a7c5f --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/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/CollectionSettings/Auth/NTLMAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js new file mode 100644 index 0000000000..341c805dcf --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js @@ -0,0 +1,110 @@ +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 NTLMAuth = ({ collection }) => { + + + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const ntlmAuth = get(collection, 'root.request.auth.ntlm', {}); + + const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); + + + const handleUsernameChange = (username) => { + dispatch( + updateCollectionAuth({ + mode: 'ntlm', + collectionUid: collection.uid, + content: { + username: username, + password: ntlmAuth.password, + domain: ntlmAuth.domain + + } + }) + ); + }; + + const handlePasswordChange = (password) => { + dispatch( + updateCollectionAuth({ + mode: 'ntlm', + collectionUid: collection.uid, + content: { + username: ntlmAuth.username, + password: password, + domain: ntlmAuth.domain + } + }) + ); + }; + + const handleDomainChange = (domain) => { + dispatch( + updateCollectionAuth({ + mode: 'ntlm', + collectionUid: collection.uid, + content: { + username: ntlmAuth.username, + password: ntlmAuth.password, + domain: domain + } + }) + ); + }; + + + + + return ( + + +
+ handleUsernameChange(val)} + collection={collection} + /> +
+ + +
+ handlePasswordChange(val)} + collection={collection} + isSecret={true} + /> +
+ + +
+ handleDomainChange(val)} + collection={collection} + /> +
+
+ ); +}; + +export default NTLMAuth; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js index 05efc17b23..c19ae98738 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js @@ -11,6 +11,8 @@ import ApiKeyAuth from './ApiKeyAuth/'; import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import OAuth2 from './OAuth2'; +import NTLMAuth from './NTLMAuth'; + const Auth = ({ collection }) => { const authMode = get(collection, 'root.request.auth.mode'); @@ -32,6 +34,9 @@ const Auth = ({ collection }) => { case 'digest': { return ; } + case 'ntlm': { + return ; + } case 'oauth2': { return ; } diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js index f0ffee808e..262f068e78 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js @@ -2,9 +2,6 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` div.CodeMirror { - /* todo: find a better way */ - height: calc(100vh - 240px); - .CodeMirror-scroll { padding-bottom: 0px; } diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js index 18a1aca1dc..23dbe9e701 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js @@ -32,22 +32,27 @@ const Docs = ({ collection }) => { const onSave = () => dispatch(saveCollectionRoot(collection.uid)); return ( - -
+ +
{isEditing ? 'Preview' : 'Edit'}
{isEditing ? ( - +
+ + +
) : ( )} diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js index c2dfb3bdf2..848048c134 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js @@ -78,7 +78,10 @@ const EnvironmentSelector = ({ collection }) => { No Environment
-
+
{ + handleSettingsIconClick(); + dropdownTippyRef.current.hide(); + }}>
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 20ac3ee928..ab2ea7691b 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 @@ -1,8 +1,9 @@ import React, { useRef, useEffect } from 'react'; import cloneDeep from 'lodash/cloneDeep'; -import { IconTrash, IconAlertCircle } from '@tabler/icons'; +import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; +import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import SingleLineEditor from 'components/SingleLineEditor'; import StyledWrapper from './StyledWrapper'; import { uuid } from 'utils/common'; @@ -13,7 +14,7 @@ import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions import toast from 'react-hot-toast'; import { Tooltip } from 'react-tooltip'; -const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables }) => { +const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const addButtonRef = useRef(null); @@ -84,6 +85,19 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original formik.setFieldValue(formik.values.length, newVariable, false); }; + const onActivate = () => { + dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid)) + .then(() => { + if (environment) { + toast.success(`Environment changed to ${environment.name}`); + onClose(); + } else { + toast.success(`No Environments are active now`); + } + }) + .catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment')); + }; + const handleRemoveVar = (id) => { formik.setValues(formik.values.filter((variable) => variable.uid !== id)); }; @@ -183,13 +197,19 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
-
- - +
); diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js index f9fca74ec8..17c0bbcf09 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js @@ -5,7 +5,7 @@ import DeleteEnvironment from '../../DeleteEnvironment'; import RenameEnvironment from '../../RenameEnvironment'; import EnvironmentVariables from './EnvironmentVariables'; -const EnvironmentDetails = ({ environment, collection, setIsModified }) => { +const EnvironmentDetails = ({ environment, collection, setIsModified, onClose }) => { const [openEditModal, setOpenEditModal] = useState(false); const [openDeleteModal, setOpenDeleteModal] = useState(false); const [openCopyModal, setOpenCopyModal] = useState(false); @@ -38,7 +38,7 @@ const EnvironmentDetails = ({ environment, collection, setIsModified }) => {
- +
); diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js index 079b578917..278a7f25d3 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js @@ -9,8 +9,9 @@ import ManageSecrets from '../ManageSecrets'; import StyledWrapper from './StyledWrapper'; import ConfirmSwitchEnv from './ConfirmSwitchEnv'; import ToolHint from 'components/ToolHint'; +import { isEqual } from 'lodash'; -const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified }) => { +const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose }) => { const { environments } = collection; const [openCreateModal, setOpenCreateModal] = useState(false); const [openImportModal, setOpenImportModal] = useState(false); @@ -24,6 +25,11 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti useEffect(() => { if (selectedEnvironment) { + const _selectedEnvironment = environments?.find(env => env?.uid === selectedEnvironment?.uid); + const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment); + if (hasSelectedEnvironmentChanged) { + setSelectedEnvironment(_selectedEnvironment); + } setOriginalEnvironmentVariables(selectedEnvironment.variables); return; } @@ -135,6 +141,7 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti collection={collection} setIsModified={setIsModified} originalEnvironmentVariables={originalEnvironmentVariables} + onClose={onClose} /> diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js index 3a17e2ecd0..81c663caf9 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js @@ -72,6 +72,7 @@ const EnvironmentSettings = ({ collection, onClose }) => { collection={collection} isModified={isModified} setIsModified={setIsModified} + onClose={onClose} /> ); diff --git a/packages/bruno-app/src/components/FolderSettings/Documentation/index.js b/packages/bruno-app/src/components/FolderSettings/Documentation/index.js index 81a3ccd014..964afdece6 100644 --- a/packages/bruno-app/src/components/FolderSettings/Documentation/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Documentation/index.js @@ -37,22 +37,25 @@ const Documentation = ({ collection, folder }) => { } return ( - -
+ +
{isEditing ? 'Preview' : 'Edit'}
{isEditing ? ( - +
+ + +
) : ( )} diff --git a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js index 85be8f137e..fa1269e146 100644 --- a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js +++ b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js @@ -55,7 +55,7 @@ const StyledMarkdownBodyWrapper = styled.div` height: 1px; padding: 0; margin: 24px 0; - background-color: var(--color-border-default); + background-color: var(--color-sidebar-collection-item-active-indent-border); border: 0; } diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index 3ee08cbb90..0b44b928bd 100644 --- a/packages/bruno-app/src/components/Modal/index.js +++ b/packages/bruno-app/src/components/Modal/index.js @@ -62,7 +62,7 @@ const Modal = ({ confirmText, cancelText, handleCancel, - handleConfirm, + handleConfirm = () => {}, children, confirmDisabled, hideCancel, @@ -92,7 +92,7 @@ const Modal = ({ }; useFocusTrap(modalRef); - + const closeModal = (args) => { setIsClosing(true); setTimeout(() => handleCancel(args), closeModalFadeTimeout); @@ -103,7 +103,7 @@ const Modal = ({ return () => { document.removeEventListener('keydown', handleKeydown); }; - }, [disableEscapeKey, document]); + }, [disableEscapeKey, document, handleConfirm]); let classes = 'bruno-modal'; if (isClosing) { diff --git a/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js b/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js index 2473463db3..b449ffce6a 100644 --- a/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js @@ -19,9 +19,8 @@ const StyledWrapper = styled.div` opacity: 0.5; } - .CodeMirror-scroll { - overflow: hidden !important; - ${'' /* padding-bottom: 50px !important; */} + .CodeMirror-scroll { + overflow: visible !important; position: relative; display: block; margin: 0px; diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js index c79f2c8ede..9f332d3cde 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js @@ -146,19 +146,8 @@ const AssertionRow = ({ const { operator, value } = parseAssertionOperator(assertion.value); return ( - - - handleAssertionChange(e, assertion, 'name')} - /> - + <> + handleRemoveAssertion(assertion)} />
- + ); }; diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js index 14e21e1c60..efb860893c 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js @@ -4,6 +4,7 @@ const Wrapper = styled.div` table { width: 100%; border-collapse: collapse; + font-weight: 600; table-layout: fixed; thead, @@ -15,24 +16,15 @@ const Wrapper = styled.div` color: ${(props) => props.theme.table.thead.color}; font-size: 0.8125rem; user-select: none; - font-weight: 600; } td { padding: 6px 10px; - - &:nth-child(2) { - width: 130px; - } - - &:nth-child(4) { - width: 70px; } - select { + select { background-color: transparent; } } - } .btn-add-assertion { font-size: 0.8125rem; @@ -42,7 +34,8 @@ const Wrapper = styled.div` width: 100%; border: solid 1px transparent; outline: none !important; - background-color: inherit; + color: ${(props) => props.theme.table.input.color}; + background: transparent; &:focus { outline: none !important; diff --git a/packages/bruno-app/src/components/RequestPane/Assertions/index.js b/packages/bruno-app/src/components/RequestPane/Assertions/index.js index 1805a632e1..7d173f3c5a 100644 --- a/packages/bruno-app/src/components/RequestPane/Assertions/index.js +++ b/packages/bruno-app/src/components/RequestPane/Assertions/index.js @@ -6,6 +6,9 @@ import { addAssertion, updateAssertion, deleteAssertion } from 'providers/ReduxS import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import AssertionRow from './AssertionRow'; import StyledWrapper from './StyledWrapper'; +import Table from 'components/Table/index'; +import ReorderTable from 'components/ReorderTable/index'; +import { moveAssertion } from 'providers/ReduxStore/slices/collections/index'; const Assertions = ({ item, collection }) => { const dispatch = useDispatch(); @@ -57,21 +60,43 @@ const Assertions = ({ item, collection }) => { ); }; + const handleAssertionDrag = ({ updateReorderedItem }) => { + dispatch( + moveAssertion({ + collectionUid: collection.uid, + itemUid: item.uid, + updateReorderedItem + }) + ); + }; + return ( - - - - - - - - - - +
ExprOperatorValue
+ {assertions && assertions.length ? assertions.map((assertion) => { - return ( + return ( + + { onSave={onSave} handleRun={handleRun} /> - ); - }) + + ); + }) : null} - -
+ handleAssertionChange(e, assertion, 'name')} + /> +
+ + 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 dfbaba7fa0..1e3bedc2f3 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -70,6 +70,15 @@ const AuthMode = ({ item, collection }) => { > Digest Auth +
{ + dropdownTippyRef?.current?.hide(); + onModeChange('ntlm'); + }} + > + NTLM Auth +
{ diff --git a/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/StyledWrapper.js new file mode 100644 index 0000000000..316d3a7c5f --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/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/NTLMAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js new file mode 100644 index 0000000000..65e7560418 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js @@ -0,0 +1,110 @@ +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 NTLMAuth = ({ item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const ntlmAuth = item.draft ? get(item, 'draft.request.auth.ntlm', {}) : get(item, 'request.auth.ntlm', {}); + + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleUsernameChange = (username) => { + dispatch( + updateAuth({ + mode: 'ntlm', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + username: username, + password: ntlmAuth.password, + domain: ntlmAuth.domain + + } + }) + ); + }; + + const handlePasswordChange = (password) => { + dispatch( + updateAuth({ + mode: 'ntlm', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + username: ntlmAuth.username, + password: password, + domain: ntlmAuth.domain + } + }) + ); + }; + + const handleDomainChange = (domain) => { + dispatch( + updateAuth({ + mode: 'ntlm', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + username: ntlmAuth.username, + password: ntlmAuth.password, + domain: domain + } + }) + ); + }; + + return ( + + +
+ handleUsernameChange(val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+ + +
+ handlePasswordChange(val)} + onRun={handleRun} + collection={collection} + item={item} + isSecret={true} + /> +
+ + +
+ handleDomainChange(val)} + onRun={handleRun} + collection={collection} + item={item} + /> +
+
+ ); +}; + +export default NTLMAuth; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index 1515e5224f..743d23267e 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -6,6 +6,8 @@ import BearerAuth from './BearerAuth'; import BasicAuth from './BasicAuth'; import DigestAuth from './DigestAuth'; import WsseAuth from './WsseAuth'; +import NTLMAuth from './NTLMAuth'; + import ApiKeyAuth from './ApiKeyAuth'; import StyledWrapper from './StyledWrapper'; import { humanizeRequestAuthMode } from 'utils/collections/index'; @@ -31,6 +33,9 @@ const Auth = ({ item, collection }) => { case 'digest': { return ; } + case 'ntlm': { + return ; + } case 'oauth2': { return ; } diff --git a/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/StyledWrapper.js index f04a30be0f..517415935f 100644 --- a/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/StyledWrapper.js @@ -19,16 +19,8 @@ const Wrapper = styled.div` } td { padding: 6px 10px; - - &:nth-child(1) { - width: 30%; - } - - &:nth-child(3) { - width: 70px; } } - } .btn-add-param { font-size: 0.8125rem; diff --git a/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js b/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js index 4cb2726065..5b3a208f0f 100644 --- a/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js @@ -6,12 +6,15 @@ import { useTheme } from 'providers/Theme'; import { addFormUrlEncodedParam, updateFormUrlEncodedParam, - deleteFormUrlEncodedParam + deleteFormUrlEncodedParam, + moveFormUrlEncodedParam } from 'providers/ReduxStore/slices/collections'; import MultiLineEditor from 'components/MultiLineEditor'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import RemoveButton from 'components/RemoveButton/index'; +import ReorderTable from 'components/ReorderTable/index'; +import Table from 'components/Table/index'; const FormUrlEncodedParams = ({ item, collection }) => { const dispatch = useDispatch(); @@ -64,55 +67,64 @@ const FormUrlEncodedParams = ({ item, collection }) => { ); }; + const handleParamDrag = ({ updateReorderedItem }) => { + dispatch( + moveFormUrlEncodedParam({ + collectionUid: collection.uid, + itemUid: item.uid, + updateReorderedItem + }) + ); + }; + return ( - - - - - - - - - +
KeyValue
+ {params && params.length ? params.map((param, index) => { - return ( - - - + return ( + + + -
- handleParamChange(e, param, 'name')} - /> - - - handleParamChange( - { - target: { - value: newValue - } - }, - param, - 'value' - ) - } - allowNewlines={true} - onRun={handleRun} - collection={collection} - item={item} - /> -
+ handleParamChange(e, param, 'name')} + /> + + + handleParamChange( + { + target: { + value: newValue + } + }, + param, + 'value' + ) + } + allowNewlines={true} + onRun={handleRun} + collection={collection} + item={item} + /> +
{ ); }) : null} -
+ + diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js index 49c45f21dc..f7b6e5d132 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js @@ -19,23 +19,7 @@ const Wrapper = styled.div` } td { padding: 6px 10px; - - &:nth-child(1) { - width: 30%; - } - - &:nth-child(2) { - width: 45%; - } - - &:nth-child(3) { - width: 25%; - } - - &:nth-child(4) { - width: 70px; } - } } .btn-add-param { diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js index ff02bdb713..39a51cee81 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js @@ -6,13 +6,16 @@ import { useTheme } from 'providers/Theme'; import { addMultipartFormParam, updateMultipartFormParam, - deleteMultipartFormParam + deleteMultipartFormParam, + moveMultipartFormParam } from 'providers/ReduxStore/slices/collections'; import MultiLineEditor from 'components/MultiLineEditor'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import FilePickerEditor from 'components/FilePickerEditor'; import RemoveButton from 'components/RemoveButton'; +import Table from 'components/Table/index'; +import ReorderTable from 'components/ReorderTable/index'; const MultipartFormParams = ({ item, collection }) => { const dispatch = useDispatch(); @@ -82,80 +85,65 @@ const MultipartFormParams = ({ item, collection }) => { ); }; + const handleParamDrag = ({ updateReorderedItem }) => { + dispatch( + moveMultipartFormParam({ + collectionUid: collection.uid, + itemUid: item.uid, + updateReorderedItem + }) + ); + }; + return ( - - - - - - - - - - +
KeyValueContent-Type
+ {params && params.length ? params.map((param, index) => { - return ( - - + + - - + )} + + -
- handleParamChange(e, param, 'name')} + return ( +
+ handleParamChange(e, param, 'name')} + /> + + {param.type === 'file' ? ( + + handleParamChange( + { + target: { + value: newValue + } + }, + param, + 'value' + ) + } + collection={collection} /> - - {param.type === 'file' ? ( - - handleParamChange( - { - target: { - value: newValue - } - }, - param, - 'value' - ) - } - collection={collection} - /> - ) : ( - - handleParamChange( - { - target: { - value: newValue - } - }, - param, - 'value' - ) - } - onRun={handleRun} - allowNewlines={true} - collection={collection} - item={item} - /> - )} - + ) : ( handleParamChange( { @@ -164,13 +152,37 @@ const MultipartFormParams = ({ item, collection }) => { } }, param, - 'contentType' + 'value' ) } onRun={handleRun} + allowNewlines={true} collection={collection} + item={item} /> - + + handleParamChange( + { + target: { + value: newValue + } + }, + param, + 'contentType' + ) + } + onRun={handleRun} + collection={collection} + /> +
{ ); }) : null} -
+ +
diff --git a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/StyledWrapper.js index efacc82885..c8ec78387f 100644 --- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/StyledWrapper.js @@ -19,16 +19,8 @@ const Wrapper = styled.div` } td { padding: 6px 10px; - - &:nth-child(1) { - width: 30%; - } - - &:nth-child(3) { - width: 70px; } } - } .btn-add-var { font-size: 0.8125rem; @@ -38,7 +30,8 @@ const Wrapper = styled.div` width: 100%; border: solid 1px transparent; outline: none !important; - background-color: inherit; + color: ${(props) => props.theme.table.input.color}; + background: transparent; &:focus { outline: none !important; 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 2e66be0de0..fe4b48732e 100644 --- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js @@ -2,7 +2,7 @@ import React from 'react'; import cloneDeep from 'lodash/cloneDeep'; import { useDispatch } from 'react-redux'; import { useTheme } from 'providers/Theme'; -import { addVar, updateVar, deleteVar } from 'providers/ReduxStore/slices/collections'; +import { addVar, updateVar, deleteVar, moveVar } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import SingleLineEditor from 'components/SingleLineEditor'; import InfoTip from 'components/InfoTip'; @@ -10,6 +10,8 @@ import StyledWrapper from './StyledWrapper'; import toast from 'react-hot-toast'; import { variableNameRegex } from 'utils/common/regex'; import RemoveButton from 'components/RemoveButton/index'; +import Table from 'components/Table/index'; +import ReorderTable from 'components/ReorderTable/index'; const VarsTable = ({ item, collection, vars, varType }) => { const dispatch = useDispatch(); @@ -73,35 +75,41 @@ const VarsTable = ({ item, collection, vars, varType }) => { ); }; + const handleVarDrag = ({ updateReorderedItem }) => { + dispatch( + moveVar({ + type: varType, + collectionUid: collection.uid, + itemUid: item.uid, + updateReorderedItem + }) + ); + }; + return ( - - - - - {varType === 'request' ? ( - - ) : ( - - )} - - - - - {vars && vars.length +
Name -
- Value -
-
-
- Expr - -
-
+ Value + + ) : ( +
+ Expr + +
+ ), accessor: 'value', width: '46%' }, + { name: '', accessor: '', width: '14%' } + ]} + > + + {vars && vars.length ? vars.map((_var) => { return ( - - + -
+
{ ); }) : null} -
+ + diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 61dd8b6ed7..3b814a7e5e 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -152,6 +152,8 @@ const Collection = ({ collection, searchText }) => {
{ style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }} onClick={handleClick} /> - diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js index 6c40317292..47f0f553e1 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js @@ -68,7 +68,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => { ); }; return ( - +

Select the type of your existing collection :

diff --git a/packages/bruno-app/src/components/VariablesEditor/index.js b/packages/bruno-app/src/components/VariablesEditor/index.js index 08f415113a..a06b6a1fff 100644 --- a/packages/bruno-app/src/components/VariablesEditor/index.js +++ b/packages/bruno-app/src/components/VariablesEditor/index.js @@ -15,7 +15,7 @@ const KeyValueExplorer = ({ data = [], theme }) => { setShowSecret(!showSecret)} /> - {data.map((envVar) => ( + {data.toSorted((a, b) => a.name.localeCompare(b.name)).map((envVar) => (
{envVar.name} diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index f89b1343ee..6b64e1279e 100644 --- a/packages/bruno-app/src/providers/App/useTelemetry.js +++ b/packages/bruno-app/src/providers/App/useTelemetry.js @@ -58,7 +58,7 @@ const trackStart = () => { event: 'start', properties: { os: platformLib.os.family, - version: '1.36.0' + version: '1.38.1' } }); }; 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 10f552ec57..11f12026f7 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -473,6 +473,10 @@ export const collectionsSlice = createSlice({ item.draft.request.auth.mode = 'digest'; item.draft.request.auth.digest = action.payload.content; break; + case 'ntlm': + item.draft.request.auth.mode = 'ntlm'; + item.draft.request.auth.ntlm = action.payload.content; + break; case 'oauth2': item.draft.request.auth.mode = 'oauth2'; item.draft.request.auth.oauth2 = action.payload.content; @@ -528,11 +532,16 @@ export const collectionsSlice = createSlice({ const { updateReorderedItem } = action.payload; const params = item.draft.request.params; - item.draft.request.params = updateReorderedItem.map((uid) => { - return params.find((param) => param.uid === uid); + const queryParams = params.filter((param) => param.type === 'query'); + const pathParams = params.filter((param) => param.type === 'path'); + + // Reorder only query params based on updateReorderedItem + const reorderedQueryParams = updateReorderedItem.map((uid) => { + return queryParams.find((param) => param.uid === uid); }); - - // update request url + item.draft.request.params = [...reorderedQueryParams, ...pathParams]; + + // Update request URL const parts = splitOnFirst(item.draft.request.url, '?'); const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')); if (query && query.length) { @@ -690,6 +699,28 @@ export const collectionsSlice = createSlice({ } } }, + moveRequestHeader: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + // Ensure item.draft is a deep clone of item if not already present + if (!item.draft) { + item.draft = cloneDeep(item); + } + + // Extract payload data + const { updateReorderedItem } = action.payload; + const params = item.draft.request.headers; + + item.draft.request.headers = updateReorderedItem.map((uid) => { + return params.find((param) => param.uid === uid); + }); + } + } + }, addFormUrlEncodedParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -748,6 +779,28 @@ export const collectionsSlice = createSlice({ } } }, + moveFormUrlEncodedParam: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + // Ensure item.draft is a deep clone of item if not already present + if (!item.draft) { + item.draft = cloneDeep(item); + } + + // Extract payload data + const { updateReorderedItem } = action.payload; + const params = item.draft.request.body.formUrlEncoded; + + item.draft.request.body.formUrlEncoded = updateReorderedItem.map((uid) => { + return params.find((param) => param.uid === uid); + }); + } + } + }, addMultipartFormParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -810,6 +863,28 @@ export const collectionsSlice = createSlice({ } } }, + moveMultipartFormParam: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + // Ensure item.draft is a deep clone of item if not already present + if (!item.draft) { + item.draft = cloneDeep(item); + } + + // Extract payload data + const { updateReorderedItem } = action.payload; + const params = item.draft.request.body.multipartForm; + + item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => { + return params.find((param) => param.uid === uid); + }); + } + } + }, updateRequestAuthMode: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1023,6 +1098,28 @@ export const collectionsSlice = createSlice({ } } }, + moveAssertion: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + // Ensure item.draft is a deep clone of item if not already present + if (!item.draft) { + item.draft = cloneDeep(item); + } + + // Extract payload data + const { updateReorderedItem } = action.payload; + const params = item.draft.request.assertions; + + item.draft.request.assertions = updateReorderedItem.map((uid) => { + return params.find((param) => param.uid === uid); + }); + } + } + }, addVar: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const type = action.payload.type; @@ -1117,6 +1214,37 @@ export const collectionsSlice = createSlice({ } } }, + moveVar: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + const type = action.payload.type; + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + // Ensure item.draft is a deep clone of item if not already present + if (!item.draft) { + item.draft = cloneDeep(item); + } + + // Extract payload data + const { updateReorderedItem } = action.payload; + if(type == "request"){ + const params = item.draft.request.vars.req; + + item.draft.request.vars.req = updateReorderedItem.map((uid) => { + return params.find((param) => param.uid === uid); + }); + } else if (type === 'response') { + const params = item.draft.request.vars.res; + + item.draft.request.vars.res = updateReorderedItem.map((uid) => { + return params.find((param) => param.uid === uid); + }); + } + } + } + }, updateCollectionAuthMode: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); @@ -1144,6 +1272,9 @@ export const collectionsSlice = createSlice({ case 'digest': set(collection, 'root.request.auth.digest', action.payload.content); break; + case 'ntlm': + set(collection, 'root.request.auth.ntlm', action.payload.content); + break; case 'oauth2': set(collection, 'root.request.auth.oauth2', action.payload.content); break; @@ -1794,12 +1925,15 @@ export const { addRequestHeader, updateRequestHeader, deleteRequestHeader, + moveRequestHeader, addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam, + moveFormUrlEncodedParam, addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam, + moveMultipartFormParam, updateRequestAuthMode, updateRequestBodyMode, updateRequestBody, @@ -1812,9 +1946,11 @@ export const { addAssertion, updateAssertion, deleteAssertion, + moveAssertion, addVar, updateVar, deleteVar, + moveVar, addFolderHeader, updateFolderHeader, deleteFolderHeader, diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index d770c60a3c..bc6c731f4d 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -340,6 +340,13 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} password: get(si.request, 'auth.digest.password', '') }; break; + case 'ntlm': + di.request.auth.ntlm = { + username: get(si.request, 'auth.ntlm.username', ''), + password: get(si.request, 'auth.ntlm.password', ''), + domain: get(si.request, 'auth.ntlm.domain', '') + }; + break; case 'oauth2': let grantType = get(si.request, 'auth.oauth2.grantType', ''); switch (grantType) { @@ -680,6 +687,10 @@ export const humanizeRequestAuthMode = (mode) => { label = 'Digest Auth'; break; } + case 'ntlm': { + label = 'NTLM'; + break; + } case 'oauth2': { label = 'OAuth 2.0'; break; diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index adfc936589..8353e8ba83 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -50,8 +50,10 @@ "@usebruno/common": "0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", + "@usebruno/vm2": "^3.9.13", "aws4-axios": "^3.3.0", "axios": "1.7.5", + "axios-ntlm": "^1.4.2", "chai": "^4.3.7", "chalk": "^3.0.0", "decomment": "^0.9.5", diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 8e0ff300c5..88cee00cb5 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -165,6 +165,13 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.awsv4config.profileName = _interpolate(request.awsv4config.profileName) || ''; } + // interpolate vars for ntlmConfig auth + if (request.ntlmConfig) { + request.ntlmConfig.username = _interpolate(request.ntlmConfig.username) || ''; + request.ntlmConfig.password = _interpolate(request.ntlmConfig.password) || ''; + request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || ''; + } + if (request) return request; }; diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 0ed8667609..11963bd296 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -36,7 +36,7 @@ const prepareRequest = (item = {}, collection = {}) => { }; const collectionAuth = get(collection, 'root.request.auth'); - if (collectionAuth && request.auth.mode === 'inherit') { + if (collectionAuth && request.auth?.mode === 'inherit') { if (collectionAuth.mode === 'basic') { axiosRequest.auth = { username: get(collectionAuth, 'basic.username'), @@ -47,9 +47,27 @@ const prepareRequest = (item = {}, collection = {}) => { if (collectionAuth.mode === 'bearer') { axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`; } + + if (collectionAuth.mode === 'apikey') { + if (collectionAuth.apikey?.placement === 'header') { + axiosRequest.headers[collectionAuth.apikey?.key] = collectionAuth.apikey?.value; + } + + if (collectionAuth.apikey?.placement === 'queryparams') { + if (axiosRequest.url && collectionAuth.apikey?.key) { + try { + const urlObj = new URL(request.url); + urlObj.searchParams.set(collectionAuth.apikey?.key, collectionAuth.apikey?.value); + axiosRequest.url = urlObj.toString(); + } catch (error) { + console.error('Invalid URL:', request.url, error); + } + } + } + } } - if (request.auth) { + if (request.auth && request.auth.mode !== 'inherit') { if (request.auth.mode === 'basic') { axiosRequest.auth = { username: get(request, 'auth.basic.username'), @@ -68,6 +86,14 @@ const prepareRequest = (item = {}, collection = {}) => { }; } + if (request.auth.mode === 'ntlm') { + axiosRequest.ntlmConfig = { + username: get(request, 'auth.ntlm.username'), + password: get(request, 'auth.ntlm.password'), + domain: get(request, 'auth.ntlm.domain') + }; + } + if (request.auth.mode === 'bearer') { axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; } diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 29ed749b94..b2bbc3795d 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -23,6 +23,8 @@ const { parseDataFromResponse } = require('../utils/common'); const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies'); const { createFormData } = require('../utils/form-data'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; +const { NtlmClient } = require('axios-ntlm'); + const onConsoleLog = (type, args) => { console[type](...args); @@ -250,8 +252,13 @@ const runSingleRequest = async function ( let response, responseTime; try { - // run request - const axiosInstance = makeAxiosInstance(); + + let axiosInstance = makeAxiosInstance(); + if (request.ntlmConfig) { + axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults) + delete request.ntlmConfig; + } + if (request.awsv4config) { // todo: make this happen in prepare-request.js diff --git a/packages/bruno-cli/tests/runner/prepare-request.spec.js b/packages/bruno-cli/tests/runner/prepare-request.spec.js index 37d3e34d30..ffc9986dfd 100644 --- a/packages/bruno-cli/tests/runner/prepare-request.spec.js +++ b/packages/bruno-cli/tests/runner/prepare-request.spec.js @@ -1,5 +1,4 @@ -const { describe, it, expect } = require('@jest/globals'); - +const { describe, it, expect, beforeEach } = require('@jest/globals'); const prepareRequest = require('../../src/runner/prepare-request'); describe('prepare-request: prepareRequest', () => { @@ -22,4 +21,144 @@ describe('prepare-request: prepareRequest', () => { expect(result.data).toEqual(expected); }); }); + + describe('Properly maps inherited auth from collectionRoot', () => { + // Initialize Test Fixtures + let collection, item; + + beforeEach(() => { + collection = { + name: 'Test Collection', + root: { + request: { + auth: {} + } + } + }; + + item = { + name: 'Test Request', + type: 'http-request', + request: { + method: 'GET', + headers: [], + params: [], + url: 'https://usebruno.com', + auth: { + mode: 'inherit' + }, + script: { + req: 'console.log("Pre Request")', + res: 'console.log("Post Response")' + } + } + }; + }); + + describe('API Key Authentication', () => { + it('If collection auth is apikey in header', () => { + collection.root.request.auth = { + mode: "apikey", + apikey: { + key: "x-api-key", + value: "{{apiKey}}", + placement: "header" + } + }; + + const result = prepareRequest(item, collection); + expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}'); + }); + + it('If collection auth is apikey in header and request has existing headers', () => { + collection.root.request.auth = { + mode: "apikey", + apikey: { + key: "x-api-key", + value: "{{apiKey}}", + placement: "header" + } + }; + + item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true }); + const result = prepareRequest(item, collection); + expect(result.headers).toHaveProperty('Content-Type', 'application/json'); + expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}'); + }); + + it('If collection auth is apikey in query parameters', () => { + collection.root.request.auth = { + mode: "apikey", + apikey: { + key: "x-api-key", + value: "{{apiKey}}", + placement: "queryparams" + } + }; + + const urlObj = new URL(item.request.url); + urlObj.searchParams.set(collection.root.request.auth.apikey.key, collection.root.request.auth.apikey.value); + + const expected = urlObj.toString(); + const result = prepareRequest(item, collection); + expect(result.url).toEqual(expected); + }); + }); + + describe('Basic Authentication', () => { + it('If collection auth is basic auth', () => { + collection.root.request.auth = { + mode: 'basic', + basic: { + username: 'testUser', + password: 'testPass123' + } + }; + + const result = prepareRequest(item, collection); + const expected = { username: 'testUser', password: 'testPass123' }; + expect(result.auth).toEqual(expected); + }); + }); + + describe('Bearer Token Authentication', () => { + it('If collection auth is bearer token', () => { + collection.root.request.auth = { + mode: 'bearer', + bearer: { + token: 'token' + } + }; + + const result = prepareRequest(item, collection); + expect(result.headers).toHaveProperty('Authorization', 'Bearer token'); + }); + + it('If collection auth is bearer token and request has existing headers', () => { + collection.root.request.auth = { + mode: 'bearer', + bearer: { + token: 'token' + } + }; + + item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true }); + + const result = prepareRequest(item, collection); + expect(result.headers).toHaveProperty('Authorization', 'Bearer token'); + expect(result.headers).toHaveProperty('Content-Type', 'application/json'); + }); + }); + + describe('No Authentication', () => { + it('If request does not have auth configured', () => { + delete item.request.auth; + let result; + expect(() => { + result = prepareRequest(item, collection); + }).not.toThrow(); + expect(result).toBeDefined(); + }); + }); + }); }); diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index 7e8e255649..16a032b3a8 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -1,5 +1,5 @@ { - "version": "v1.36.0", + "version": "v1.38.1", "name": "bruno", "description": "Opensource API Client for Exploring and Testing APIs", "homepage": "https://www.usebruno.com", @@ -19,7 +19,9 @@ "test": "node --experimental-vm-modules $(npx which jest)" }, "jest": { - "modulePaths": ["node_modules"] + "modulePaths": [ + "node_modules" + ] }, "dependencies": { "@aws-sdk/credential-providers": "3.658.1", @@ -28,9 +30,11 @@ "@usebruno/lang": "0.12.0", "@usebruno/node-machine-id": "^2.0.0", "@usebruno/schema": "0.7.0", + "@usebruno/vm2": "^3.9.13", "about-window": "^1.15.2", "aws4-axios": "^3.3.0", "axios": "1.7.5", + "axios-ntlm": "^1.4.2", "chai": "^4.3.7", "chokidar": "^3.5.3", "content-disposition": "^0.5.4", @@ -55,7 +59,6 @@ "socks-proxy-agent": "^8.0.2", "tough-cookie": "^4.1.3", "uuid": "^9.0.0", - "@usebruno/vm2": "^3.9.13", "yup": "^0.32.11" }, "optionalDependencies": { diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 782878c59a..218fadf380 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -40,6 +40,7 @@ const iconv = require('iconv-lite'); const FormData = require('form-data'); const { createFormData } = require('../../utils/form-data'); const { findItemInCollectionByPathname } = require('../../utils/collection'); +const { NtlmClient } = require('axios-ntlm'); const safeStringifyJSON = (data) => { try { @@ -272,7 +273,15 @@ const configureRequest = async ( ...httpsAgentRequestFields }); } - const axiosInstance = makeAxiosInstance(); + + + let axiosInstance = makeAxiosInstance(); + + if (request.ntlmConfig) { + axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults) + delete request.ntlmConfig; + } + if (request.oauth2) { let requestCopy = cloneDeep(request); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index d9833f59a0..2b46327afb 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -231,6 +231,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc request.wsse.password = _interpolate(request.wsse.password) || ''; } + + // interpolate vars for ntlmConfig auth + if (request.ntlmConfig) { + request.ntlmConfig.username = _interpolate(request.ntlmConfig.username) || ''; + request.ntlmConfig.password = _interpolate(request.ntlmConfig.password) || ''; + request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || ''; + } + 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 69825bc4e1..6c7672e7d6 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -5,6 +5,7 @@ const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data'); const setAuthHeaders = (axiosRequest, request, collectionRoot) => { + const collectionAuth = get(collectionRoot, 'request.auth'); if (collectionAuth && request.auth.mode === 'inherit') { switch (collectionAuth.mode) { @@ -33,6 +34,13 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { password: get(collectionAuth, 'digest.password') }; break; + case 'ntlm': + axiosRequest.ntlmConfig = { + username: get(collectionAuth, 'ntlm.username'), + password: get(collectionAuth, 'ntlm.password'), + domain: get(collectionAuth, 'ntlm.domain') + }; + break; case 'wsse': const username = get(request, 'auth.wsse.username', ''); const password = get(request, 'auth.wsse.password', ''); @@ -89,6 +97,13 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { password: get(request, 'auth.digest.password') }; break; + case 'ntlm': + axiosRequest.ntlmConfig = { + username: get(request, 'auth.ntlm.username'), + password: get(request, 'auth.ntlm.password'), + domain: get(request, 'auth.ntlm.domain') + }; + break; case 'oauth2': const grantType = get(request, 'auth.oauth2.grantType'); switch (grantType) { diff --git a/packages/bruno-js/src/sandbox/quickjs/index.js b/packages/bruno-js/src/sandbox/quickjs/index.js index d5fe5e8f3b..58ccd885d0 100644 --- a/packages/bruno-js/src/sandbox/quickjs/index.js +++ b/packages/bruno-js/src/sandbox/quickjs/index.js @@ -22,12 +22,6 @@ const toNumber = (value) => { return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value); }; -const removeQuotes = (str) => { - if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) { - return str.slice(1, -1); - } - return str; -}; const executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => { if (!externalScript?.length || typeof externalScript !== 'string') { @@ -44,7 +38,6 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc if (externalScript === 'null') return null; if (externalScript === 'undefined') return undefined; - externalScript = removeQuotes(externalScript); const vm = QuickJSSyncContext; @@ -94,7 +87,6 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external if (externalScript === 'null') return null; if (externalScript === 'undefined') return undefined; - externalScript = removeQuotes(externalScript); try { const module = await newQuickJSWASMModule(); diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 55b454d02a..6b5ecacb57 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -85,14 +85,6 @@ const evaluateJsTemplateLiteral = (templateLiteral, context) => { return undefined; } - if (templateLiteral.startsWith('"') && templateLiteral.endsWith('"')) { - return templateLiteral.slice(1, -1); - } - - if (templateLiteral.startsWith("'") && templateLiteral.endsWith("'")) { - return templateLiteral.slice(1, -1); - } - if (!isNaN(templateLiteral)) { const number = Number(templateLiteral); // Check if the number is too high. Too high number might get altered, see #1000 diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 228691c1b2..2fe5fb472a 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 | authwsse | authapikey + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart params = paramspath | paramsquery @@ -87,6 +87,7 @@ const grammar = ohm.grammar(`Bru { authbasic = "auth:basic" dictionary authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary + authNTLM = "auth:ntlm" dictionary authOAuth2 = "auth:oauth2" dictionary authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary @@ -450,6 +451,26 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authNTLM(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const usernameKey = _.find(auth, { name: 'username' }); + const passwordKey = _.find(auth, { name: 'password' }); + const domainKey = _.find(auth, { name: 'domain' }); + + const username = usernameKey ? usernameKey.value : ''; + const password = passwordKey ? passwordKey.value : ''; + const domain = passwordKey ? domainKey.value : ''; + + return { + auth: { + ntlm: { + username, + password, + domain + } + } + }; + }, authOAuth2(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); const grantTypeKey = _.find(auth, { name: 'grant_type' }); diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 5180f0193d..61d373d91e 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 | authwsse | authapikey + auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM |authOAuth2 | authwsse | authapikey nl = "\\r"? "\\n" st = " " | "\\t" @@ -42,6 +42,7 @@ const grammar = ohm.grammar(`Bru { authbasic = "auth:basic" dictionary authbearer = "auth:bearer" dictionary authdigest = "auth:digest" dictionary + authNTLM = "auth:ntlm" dictionary authOAuth2 = "auth:oauth2" dictionary authwsse = "auth:wsse" dictionary authapikey = "auth:apikey" dictionary @@ -245,6 +246,26 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + authNTLM(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const usernameKey = _.find(auth, { name: 'username' }); + const passwordKey = _.find(auth, { name: 'password' }); + const domainKey = _.find(auth, { name: 'domain' }); + + const username = usernameKey ? usernameKey.value : ''; + const password = passwordKey ? passwordKey.value : ''; + const domain = domainKey ? domainKey.value : ''; + + return { + auth: { + ntlm: { + username, + password, + domain + } + } + }; + }, authOAuth2(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); const grantTypeKey = _.find(auth, { name: 'grant_type' }); diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 5c8a573b62..62b31c2f99 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -165,6 +165,18 @@ ${indentString(`password: ${auth?.digest?.password || ''}`)} `; } + + if (auth && auth.ntlm) { + bru += `auth:ntlm { +${indentString(`username: ${auth?.ntlm?.username || ''}`)} +${indentString(`password: ${auth?.ntlm?.password || ''}`)} +${indentString(`domain: ${auth?.ntlm?.domain || ''}`)} + +} + +`; + } + if (auth && auth.oauth2) { switch (auth?.oauth2?.grantType) { case 'password': diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index 8b162b7a6f..c2a843dc6e 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -120,6 +120,17 @@ ${indentString(`username: ${auth.digest.username}`)} ${indentString(`password: ${auth.digest.password}`)} } +`; + } + +if (auth && auth.ntlm) { + bru += `auth:ntlm { +${indentString(`username: ${auth.ntlm.username}`)} +${indentString(`password: ${auth.ntlm.password}`)} +${indentString(`domain: ${auth.ntlm.domain}`)} + +} + `; } diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 3d5959f15d..b6e044ae47 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -127,6 +127,17 @@ const authDigestSchema = Yup.object({ .noUnknown(true) .strict(); + + + const authNTLMSchema = Yup.object({ + username: Yup.string().nullable(), + password: Yup.string().nullable(), + domain: Yup.string().nullable() + + }) + .noUnknown(true) + .strict(); + const authApiKeySchema = Yup.object({ key: Yup.string().nullable(), value: Yup.string().nullable(), @@ -195,11 +206,12 @@ const oauth2Schema = Yup.object({ const authSchema = Yup.object({ mode: Yup.string() - .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2', 'wsse', 'apikey']) + .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey']) .required('mode is required'), awsv4: authAwsV4Schema.nullable(), basic: authBasicSchema.nullable(), bearer: authBearerSchema.nullable(), + ntlm: authNTLMSchema.nullable(), digest: authDigestSchema.nullable(), oauth2: oauth2Schema.nullable(), wsse: authWsseSchema.nullable(),