diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/StyledWrapper.js index ebb8ee46a..cc8980abe 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/StyledWrapper.js @@ -2,13 +2,42 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` .tabs { - .active { - border-bottom: solid 1px ${(props) => props.theme.input.border}; + .tab { + cursor: pointer; + padding: 4px 8px !important; + font-size: 12px; + border-radius: 4px; + + &:hover { + background-color: ${(props) => props.theme.mode === 'dark' ? 'rgba(99, 102, 241, 0.1)' : 'rgba(99, 102, 241, 0.1)'}; + } + + &.active { + background-color: ${(props) => props.theme.mode === 'dark' ? 'rgba(99, 102, 241, 0.2)' : 'rgba(99, 102, 241, 0.1)'}; + color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'}; + font-weight: 500; + } } } + .additional-parameter-sends-in-selector { select { height: 32px; + width: 100%; + border: 1px solid ${(props) => props.theme.input.border}; + border-radius: 4px; + padding: 0 8px; + + &:focus { + outline: none; + border-color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'}; + } + } + } + + .add-additional-param-actions { + &:hover { + color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'}; } } ` diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js index 1e13b676e..b9b6f66bc 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js @@ -2,7 +2,7 @@ import { useDispatch } from "react-redux"; import React, { forwardRef, useState } from 'react'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; -import { IconPlus, IconCaretDown, IconTrash } from '@tabler/icons'; +import { IconPlus, IconCaretDown, IconTrash, IconAdjustmentsHorizontal } from '@tabler/icons'; import { cloneDeep } from "lodash"; import SingleLineEditor from "components/SingleLineEditor/index"; import StyledWrapper from "./StyledWrapper"; @@ -19,7 +19,33 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection }) => { additionalParameters = {} } = oAuth; + const isEmptyParam = (param) => { + return !param.name.trim() && !param.value.trim(); + }; + + const hasEmptyRow = () => { + const tabParams = additionalParameters[activeTab] || []; + return tabParams.some(isEmptyParam); + }; + const updateAdditionalParams = ({ updatedAdditionalParams }) => { + const filteredParams = cloneDeep(updatedAdditionalParams); + + Object.keys(filteredParams).forEach(paramType => { + if (filteredParams[paramType]?.length) { + filteredParams[paramType] = filteredParams[paramType].filter(param => + param.name.trim() || param.value.trim() + ); + + if (filteredParams[paramType].length === 0) { + delete filteredParams[paramType]; + } + } else if (Array.isArray(filteredParams[paramType]) && filteredParams[paramType].length === 0) { + // Remove empty arrays + delete filteredParams[paramType]; + } + }); + dispatch( updateAuth({ mode: 'oauth2', @@ -27,7 +53,7 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection }) => { itemUid: item.uid, content: { ...oAuth, - additionalParameters: updatedAdditionalParams, + additionalParameters: Object.keys(filteredParams).length > 0 ? filteredParams : undefined } }) ); @@ -35,24 +61,56 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection }) => { const handleUpdateAdditionalParam = ({ paramType, key, paramIndex, value }) => { const updatedAdditionalParams = cloneDeep(additionalParameters); + + if (!updatedAdditionalParams[paramType]) { + updatedAdditionalParams[paramType] = []; + } + + if (!updatedAdditionalParams[paramType][paramIndex]) { + updatedAdditionalParams[paramType][paramIndex] = { + name: '', + value: '', + sendIn: 'headers', + enabled: true + }; + } + updatedAdditionalParams[paramType][paramIndex][key] = value; + + // Only filter when updating a parameter updateAdditionalParams({ updatedAdditionalParams }); } const handleDeleteAdditionalParam = ({ paramType, paramIndex }) => { const updatedAdditionalParams = cloneDeep(additionalParameters); - updatedAdditionalParams[paramType] = updatedAdditionalParams[paramType]?.filter((_, index) => index !== paramIndex); + + if (updatedAdditionalParams[paramType]?.length) { + updatedAdditionalParams[paramType] = updatedAdditionalParams[paramType].filter((_, index) => index !== paramIndex); + + // If the array is now empty, ensure we're not sending empty arrays + if (updatedAdditionalParams[paramType].length === 0) { + delete updatedAdditionalParams[paramType]; + } + } + updateAdditionalParams({ updatedAdditionalParams }); } const handleAddNewAdditionalParam = () => { - const paramType = activeTab; - const updatedAdditionalParams = cloneDeep(additionalParameters); - if (!updatedAdditionalParams?.[paramType]) { - updatedAdditionalParams[paramType] = []; + // Prevent adding multiple empty rows + if (hasEmptyRow()) { + return; } - updatedAdditionalParams[paramType] = [ - ...updatedAdditionalParams[paramType], + + const paramType = activeTab; + const localAdditionalParameters = cloneDeep(additionalParameters); + + if (!localAdditionalParameters[paramType]) { + localAdditionalParameters[paramType] = []; + } + + localAdditionalParameters[paramType] = [ + ...localAdditionalParameters[paramType], { name: '', value: '', @@ -60,10 +118,36 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection }) => { enabled: true } ]; - updateAdditionalParams({ updatedAdditionalParams }); + + // Don't filter here to allow the empty row to display in UI + // But don't permanently store it in state until it has values + dispatch( + updateAuth({ + mode: 'oauth2', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + ...oAuth, + additionalParameters: localAdditionalParameters, + } + }) + ); } + + // Add a class to the Add Parameter button if it's disabled + const addButtonDisabled = hasEmptyRow(); + return ( +
+
+ +
+ + Additional Parameters + +
+
setActiveTab('authorization')}>Authorization
setActiveTab('token')}>Token
@@ -73,18 +157,17 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection }) => { headers={[ { name: 'Key', accessor: 'name', width: '30%' }, { name: 'Value', accessor: 'value', width: '30%' }, - { name: 'Sends In', accessor: 'sendIn', width: '150px' }, + { name: 'Send In', accessor: 'sendIn', width: '150px' }, { name: '', accessor: '', width: '15%' } ]} > - {additionalParameters?.[activeTab]?.map((param, index) => - + {(additionalParameters?.[activeTab] || []).map((param, index) => + handleUpdateAdditionalParam({ paramType: activeTab, key: 'name', @@ -96,9 +179,8 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection }) => { handleUpdateAdditionalParam({ paramType: activeTab, key: 'value', @@ -111,7 +193,7 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection }) => {
{ @@ -163,8 +245,12 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection }) => { )} -
- +
+ + Add Parameter
) @@ -187,7 +273,19 @@ const Icon = forwardRef((props, ref) => { }); const sendInOptionsMap = { - 'authorization_code': ['headers', 'queryparams'], - 'password': ['headers', 'queryparams', 'body'], - 'client_credentials': ['headers', 'queryparams', 'body'] + 'authorization_code': { + 'authorization': ['headers', 'queryparams'], + 'token': ['headers', 'queryparams', 'body'], + 'refresh': ['headers', 'queryparams', 'body'] + }, + 'password': { + 'authorization': ['headers', 'queryparams'], + 'token': ['headers', 'queryparams', 'body'], + 'refresh': ['headers', 'queryparams', 'body'] + }, + 'client_credentials': { + 'authorization': ['headers', 'queryparams'], + 'token': ['headers', 'queryparams', 'body'], + 'refresh': ['headers', 'queryparams', 'body'] + } } \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js index 5aadcd156..d61bbf013 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js @@ -330,7 +330,12 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
- +
); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js index f4b01135b..f9fc99973 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js @@ -298,7 +298,12 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu - + diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js index 086e10335..f12f50c92 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js @@ -301,7 +301,12 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update - + ); diff --git a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js index 7d2e23abc..cb2903d7c 100644 --- a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js +++ b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js @@ -5,7 +5,7 @@ const matchesCallbackUrl = (url, callbackUrl) => { return url ? url.href.startsWith(callbackUrl.href) : false; }; -const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => { +const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session, additionalHeaders = {} }) => { return new Promise(async (resolve, reject) => { let finalUrl = null; let debugInfo = { @@ -75,6 +75,14 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => { webSession.webRequest.onBeforeSendHeaders((details, callback) => { const { id: requestId, requestHeaders, method, url } = details; + + if (details.resourceType === 'mainFrame' && Object.keys(additionalHeaders).length > 0) { + // Add our custom headers + for (const [name, value] of Object.entries(additionalHeaders)) { + requestHeaders[name] = value; + } + } + if (currentMainRequest?.requestId === requestId) { currentMainRequest.request = { url, diff --git a/packages/bruno-electron/src/utils/oauth2.js b/packages/bruno-electron/src/utils/oauth2.js index 882f39767..50b1f73ed 100644 --- a/packages/bruno-electron/src/utils/oauth2.js +++ b/packages/bruno-electron/src/utils/oauth2.js @@ -61,6 +61,7 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo credentialsId, autoRefreshToken, autoFetchToken, + additionalParameters, } = oAuth; const url = requestCopy?.oauth2?.accessTokenUrl; if (!forceFetch) { @@ -140,6 +141,12 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo if (scope) { data.scope = scope; } + + // Apply additional parameters to token request + if (additionalParameters?.token?.length) { + applyAdditionalParameters(requestCopy, data, additionalParameters.token); + } + requestCopy.data = qs.stringify(data); requestCopy.url = url; requestCopy.responseType = 'arraybuffer'; @@ -249,7 +256,7 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { return new Promise(async (resolve, reject) => { const { oauth2 } = request; - const { callbackUrl, clientId, authorizationUrl, scope, state, pkce, accessTokenUrl } = oauth2; + const { callbackUrl, clientId, authorizationUrl, scope, state, pkce, accessTokenUrl, additionalParameters } = oauth2; const authorizationUrlWithQueryParams = new URL(authorizationUrl); authorizationUrlWithQueryParams.searchParams.append('response_type', 'code'); @@ -267,12 +274,23 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { if (state) { authorizationUrlWithQueryParams.searchParams.append('state', state); } + if (additionalParameters?.authorization?.length) { + additionalParameters.authorization.forEach(param => { + if (param.enabled && param.name) { + if (param.sendIn === 'queryparams') { + authorizationUrlWithQueryParams.searchParams.append(param.name, param.value || ''); + } + } + }); + } + try { const authorizeUrl = authorizationUrlWithQueryParams.toString(); const { authorizationCode, debugInfo } = await authorizeUserInWindow({ authorizeUrl, callbackUrl, - session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: accessTokenUrl }) + session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: accessTokenUrl }), + additionalHeaders: getAdditionalHeaders(additionalParameters?.authorization) }); resolve({ authorizationCode, debugInfo }); } catch (err) { @@ -281,6 +299,21 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => { }); }; +const getAdditionalHeaders = (params) => { + if (!params || !params.length) { + return {}; + } + + const headers = {}; + params.forEach(param => { + if (param.enabled && param.name && param.sendIn === 'headers') { + headers[param.name] = param.value || ''; + } + }); + + return headers; +}; + // CLIENT CREDENTIALS const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfig }) => { @@ -294,6 +327,7 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo credentialsId, autoRefreshToken, autoFetchToken, + additionalParameters, } = oAuth; const url = requestCopy?.oauth2?.accessTokenUrl; @@ -366,6 +400,10 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo if (scope) { data.scope = scope; } + if (additionalParameters?.token?.length) { + applyAdditionalParameters(requestCopy, data, additionalParameters.token); + } + requestCopy.data = qs.stringify(data); requestCopy.url = url; requestCopy.responseType = 'arraybuffer'; @@ -480,6 +518,7 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, credentialsId, autoRefreshToken, autoFetchToken, + additionalParameters, } = oAuth; const url = requestCopy?.oauth2?.accessTokenUrl; @@ -554,6 +593,10 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, if (scope) { data.scope = scope; } + if (additionalParameters?.token?.length) { + applyAdditionalParameters(requestCopy, data, additionalParameters.token); + } + requestCopy.data = qs.stringify(data); requestCopy.url = url; requestCopy.responseType = 'arraybuffer'; @@ -671,6 +714,10 @@ const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyCon if (clientSecret) { data.client_secret = clientSecret; } + if (oAuth.additionalParameters?.refresh?.length) { + applyAdditionalParameters(requestCopy, data, oAuth.additionalParameters.refresh); + } + requestCopy.method = 'POST'; requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded'; requestCopy.headers['Accept'] = 'application/json'; @@ -678,7 +725,7 @@ const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyCon requestCopy.url = url; requestCopy.responseType = 'arraybuffer'; - // Initialize variables to hold request and response data for debugging + // Initialize variables to hold request and response data for debugging let axiosRequestInfo = null; let axiosResponseInfo = null; let debugInfo = { data: [] }; @@ -797,6 +844,38 @@ const generateCodeChallenge = (codeVerifier) => { return base64Hash; }; +// Apply additional parameters to a request +const applyAdditionalParameters = (requestCopy, data, params) => { + if (!params || !params.length) { + return; + } + + params.forEach(param => { + if (!param.enabled || !param.name) { + return; + } + + switch (param.sendIn) { + case 'headers': + requestCopy.headers[param.name] = param.value || ''; + break; + case 'queryparams': + // For query params, add to URL + if (!requestCopy.url.includes('?')) { + requestCopy.url += '?'; + } else if (!requestCopy.url.endsWith('&') && !requestCopy.url.endsWith('?')) { + requestCopy.url += '&'; + } + requestCopy.url += `${encodeURIComponent(param.name)}=${encodeURIComponent(param.value || '')}`; + break; + case 'body': + // For body, add to data object + data[param.name] = param.value || ''; + break; + } + }); +}; + module.exports = { getOAuth2TokenUsingAuthorizationCode, getOAuth2AuthorizationCode, diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 7e61afe45..9db992d9e 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -103,9 +103,9 @@ const grammar = ohm.grammar(`Bru { oAuth2TokenHeaders = "auth:oauth2:token_headers" dictionary oAuth2TokenQueryParams = "auth:oauth2:token_queryparams" dictionary oAuth2TokenBodyValues = "auth:oauth2:token_bodyvalues" dictionary - oAuth2RefreshHeaders = "auth:oauth2:authorization_headers" dictionary - oAuth2RefreshQueryParams = "auth:oauth2:authorization_queryparams" dictionary - oAuth2RefreshBodyValues = "auth:oauth2:authorization_bodyvalues" dictionary + oAuth2RefreshHeaders = "auth:oauth2:refresh_headers" dictionary + oAuth2RefreshQueryParams = "auth:oauth2:refresh_queryparams" dictionary + oAuth2RefreshBodyValues = "auth:oauth2:refresh_bodyvalues" dictionary body = "body" st* "{" nl* textblock tagend bodyjson = "body:json" st* "{" nl* textblock tagend diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index 029aa337b..178afd87a 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -41,9 +41,9 @@ const grammar = ohm.grammar(`Bru { oAuth2TokenHeaders = "auth:oauth2:token_headers" dictionary oAuth2TokenQueryParams = "auth:oauth2:token_queryparams" dictionary oAuth2TokenBodyValues = "auth:oauth2:token_bodyvalues" dictionary - oAuth2RefreshHeaders = "auth:oauth2:authorization_headers" dictionary - oAuth2RefreshQueryParams = "auth:oauth2:authorization_queryparams" dictionary - oAuth2RefreshBodyValues = "auth:oauth2:authorization_bodyvalues" dictionary + oAuth2RefreshHeaders = "auth:oauth2:refresh_headers" dictionary + oAuth2RefreshQueryParams = "auth:oauth2:refresh_queryparams" dictionary + oAuth2RefreshBodyValues = "auth:oauth2:refresh_bodyvalues" dictionary headers = "headers" dictionary diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js index d09c197aa..64d377aee 100644 --- a/packages/bruno-lang/v2/src/utils.js +++ b/packages/bruno-lang/v2/src/utils.js @@ -32,7 +32,7 @@ const outdentString = (str) => { const mergeOauth2AdditionalParameters = (ast) => { let additionalParameters = {}; const authorizationHeaders = ast?.oauth2_additional_parameters_authorization_headers; - const authorizationQueryParams = ast?.oauth2_additional_parameters_authorization_headers; + const authorizationQueryParams = ast?.oauth2_additional_parameters_authorization_queryparams; const tokenHeaders = ast?.oauth2_additional_parameters_token_headers; const tokenQueryParams = ast?.oauth2_additional_parameters_token_queryparams; const tokenBodyValues = ast?.oauth2_additional_parameters_token_bodyvalues; @@ -50,6 +50,7 @@ const mergeOauth2AdditionalParameters = (ast) => { } if (authorizationQueryParams?.length) { additionalParameters['authorization'] = [ + ...additionalParameters['authorization'] || [], ...authorizationQueryParams?.map(_ => ({ ..._, sendIn: 'queryparams' })) ] } @@ -64,11 +65,13 @@ const mergeOauth2AdditionalParameters = (ast) => { } if (tokenQueryParams?.length) { additionalParameters['token'] = [ + ...additionalParameters['token'] || [], ...tokenQueryParams?.map(_ => ({ ..._, sendIn: 'queryparams' })) ] } if (tokenBodyValues?.length) { additionalParameters['token'] = [ + ...additionalParameters['token'] || [], ...tokenBodyValues?.map(_ => ({ ..._, sendIn: 'body' })) ] } @@ -77,17 +80,19 @@ const mergeOauth2AdditionalParameters = (ast) => { additionalParameters['refresh'] = [] } if (refreshHeaders?.length) { - additionalParameters['token'] = [ + additionalParameters['refresh'] = [ ...refreshHeaders?.map(_ => ({ ..._, sendIn: 'headers' })) ] } if (refreshQueryParams?.length) { - additionalParameters['token'] = [ + additionalParameters['refresh'] = [ + ...additionalParameters['refresh'] || [], ...refreshQueryParams?.map(_ => ({ ..._, sendIn: 'queryparams' })) ] } if (refreshBodyValues?.length) { - additionalParameters['token'] = [ + additionalParameters['refresh'] = [ + ...additionalParameters['refresh'] || [], ...refreshBodyValues?.map(_ => ({ ..._, sendIn: 'body' })) ] }