mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 09:28:33 +00:00
Compare commits
2 Commits
feature/pl
...
v1.40.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4fbca2759 | ||
|
|
7622a4aaae |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: helloanoop
|
||||
BIN
assets/images/sponsors/commit-company.png
Normal file
BIN
assets/images/sponsors/commit-company.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
assets/images/sponsors/samagata.png
Normal file
BIN
assets/images/sponsors/samagata.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
BIN
assets/images/sponsors/zuplo.png
Normal file
BIN
assets/images/sponsors/zuplo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -25025,7 +25025,7 @@
|
||||
},
|
||||
"packages/bruno-app": {
|
||||
"name": "@usebruno/app",
|
||||
"version": "2.0.0",
|
||||
"version": "1.39.0",
|
||||
"dependencies": {
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
@@ -26276,7 +26276,7 @@
|
||||
},
|
||||
"packages/bruno-electron": {
|
||||
"name": "bruno",
|
||||
"version": "2.0.0",
|
||||
"version": "v1.38.1",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
"build:electron:deb": "./scripts/build-electron.sh deb",
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"test:codegen": "npm run dev:web & node ./scripts/playwright-codegen.js",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "2.0.0",
|
||||
"version": "1.39.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "rsbuild dev",
|
||||
|
||||
@@ -95,7 +95,7 @@ const AuthMode = ({ collection }) => {
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
OAuth 2.0
|
||||
Oauth2
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
|
||||
@@ -13,4 +13,4 @@ const Wrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,120 @@
|
||||
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 { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { clearOauth2Cache } from 'utils/network/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
state,
|
||||
pkce,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePKCEToggle = (e) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
state,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
clearOauth2Cache(collection?.uid)
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-row w-full gap-4" key="pkce">
|
||||
<label className="block font-medium">Use PKCE</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
checked={Boolean(oAuth?.['pkce'])}
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
@@ -0,0 +1,33 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'callbackUrl',
|
||||
label: 'Callback URL'
|
||||
},
|
||||
{
|
||||
key: 'authorizationUrl',
|
||||
label: 'Authorization URL'
|
||||
},
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
label: 'State'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
@@ -0,0 +1,70 @@
|
||||
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 { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const OAuth2ClientCredentials = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2ClientCredentials;
|
||||
@@ -0,0 +1,21 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -0,0 +1,54 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.grant-type-mode-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grant-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,98 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeGrantType } from 'utils/collections';
|
||||
import { useEffect } from 'react';
|
||||
import { updateCollectionAuth, updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const GrantTypeSelector = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
|
||||
{humanizeGrantType(oAuth?.grantType)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onGrantTypeChange = (grantType) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// initialize redux state with a default oauth2 grant type
|
||||
// authorization_code - default option
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuthMode({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [oAuth]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<label className="block font-medium mb-2">Grant Type</label>
|
||||
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('password');
|
||||
}}
|
||||
>
|
||||
Password Credentials
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('authorization_code');
|
||||
}}
|
||||
>
|
||||
Authorization Code
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('client_credentials');
|
||||
}}
|
||||
>
|
||||
Client Credentials
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default GrantTypeSelector;
|
||||
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
@@ -0,0 +1,72 @@
|
||||
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 { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'password',
|
||||
accessTokenUrl,
|
||||
username,
|
||||
password,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
@@ -0,0 +1,29 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username'
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,33 +1,21 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
|
||||
const GrantTypeComponentMap = ({collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
};
|
||||
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', {});
|
||||
import GrantTypeSelector from './GrantTypeSelector/index';
|
||||
import OAuth2PasswordCredentials from './PasswordCredentials/index';
|
||||
import OAuth2AuthorizationCode from './AuthorizationCode/index';
|
||||
import OAuth2ClientCredentials from './ClientCredentials/index';
|
||||
|
||||
const grantTypeComponentMap = (grantType, collection) => {
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
return <OAuth2PasswordCredentials collection={collection} />;
|
||||
break;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
return <OAuth2AuthorizationCode collection={collection} />;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
return <OAuth2ClientCredentials collection={collection} />;
|
||||
break;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
@@ -36,12 +24,12 @@ const GrantTypeComponentMap = ({collection }) => {
|
||||
};
|
||||
|
||||
const OAuth2 = ({ collection }) => {
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<GrantTypeSelector request={request} updateAuth={updateCollectionAuth} collection={collection} />
|
||||
<GrantTypeComponentMap collection={collection} />
|
||||
<GrantTypeSelector collection={collection} />
|
||||
{grantTypeComponentMap(oAuth?.grantType, collection)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,10 +25,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
passphrase: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
domain: Yup.string()
|
||||
.required()
|
||||
.trim()
|
||||
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
|
||||
domain: Yup.string().required(),
|
||||
type: Yup.string().required().oneOf(['cert', 'pfx']),
|
||||
certFilePath: Yup.string().when('type', {
|
||||
is: (type) => type == 'cert',
|
||||
@@ -48,7 +45,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
let relevantValues = {};
|
||||
if (values.type === 'cert') {
|
||||
relevantValues = {
|
||||
domain: values.domain?.trim(),
|
||||
domain: values.domain,
|
||||
type: values.type,
|
||||
certFilePath: values.certFilePath,
|
||||
keyFilePath: values.keyFilePath,
|
||||
@@ -56,7 +53,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
};
|
||||
} else {
|
||||
relevantValues = {
|
||||
domain: values.domain?.trim(),
|
||||
domain: values.domain,
|
||||
type: values.type,
|
||||
pfxFilePath: values.pfxFilePath,
|
||||
passphrase: values.passphrase
|
||||
@@ -130,20 +127,15 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<label className="settings-label" htmlFor="domain">
|
||||
Domain
|
||||
</label>
|
||||
<div className="relative flex items-center">
|
||||
<div className="absolute left-0 pl-2 text-gray-400 pointer-events-none flex items-center h-full">
|
||||
https://
|
||||
</div>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="example.org"
|
||||
className="block textbox non-passphrase-input !pl-[60px]"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="*.example.org"
|
||||
className="block textbox non-passphrase-input"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
{formik.touched.domain && formik.errors.domain ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
|
||||
) : null}
|
||||
|
||||
@@ -119,6 +119,6 @@ This documentation supports Markdown formatting! You can use:
|
||||
- **Bold** and *italic* text
|
||||
- \`code blocks\` and syntax highlighting
|
||||
- Tables and lists
|
||||
- [Links](https://usebruno.com)
|
||||
- [Links](https://example.com)
|
||||
- And more!
|
||||
`;
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateFolderAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
import AuthMode from '../AuthMode';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
let request = get(folder, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
|
||||
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const Auth = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
let request = get(folder, 'root.request', {});
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'oauth2': {
|
||||
return (
|
||||
<>
|
||||
<GrantTypeSelector
|
||||
request={request}
|
||||
updateAuth={updateFolderAuth}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
<GrantTypeComponentMap collection={collection} folder={folder} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'none': {
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Configures authentication for the entire folder. This applies to all requests using the{' '}
|
||||
<span className="font-medium">Inherit</span> option in the <span className="font-medium">Auth</span> tab.
|
||||
</div>
|
||||
<div className="flex flex-grow justify-start items-center mb-4">
|
||||
<AuthMode collection={collection} folder={folder} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.auth-mode-selector {
|
||||
border: 1px solid ${({ theme }) => theme.colors.border};
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateFolderAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AuthMode = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateFolderAuthMode({
|
||||
mode: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthMode;
|
||||
@@ -8,9 +8,7 @@ import Tests from './Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars';
|
||||
import Documentation from './Documentation';
|
||||
import Auth from './Auth';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
@@ -39,9 +37,6 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
const responseVars = folderRoot?.request?.vars?.res || [];
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
|
||||
const auth = get(folderRoot, 'request.auth.mode');
|
||||
const hasAuth = auth && auth !== 'none';
|
||||
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
updatedFolderSettingsSelectedTab({
|
||||
@@ -66,9 +61,6 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation collection={collection} folder={folder} />;
|
||||
}
|
||||
@@ -101,10 +93,6 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
Vars
|
||||
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{hasAuth && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
|
||||
@@ -11,47 +11,6 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
.token-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
min-width: 100px;
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.token-placement-label {
|
||||
width: fit-content;
|
||||
// color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,63 +1,28 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import { clearOauth2Cache } from 'utils/network/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const {
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
state,
|
||||
pkce,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
const TokenPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const CredentialsPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleSave = () => { save(); };
|
||||
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
@@ -75,15 +40,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
state,
|
||||
scope,
|
||||
pkce,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -104,35 +61,30 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
clientSecret,
|
||||
state,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
autoFetchToken,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
clearOauth2Cache(collection?.uid)
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Configuration
|
||||
</span>
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
@@ -147,33 +99,8 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
|
||||
<label className="block min-w-[140px]">Add Credentials to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'body');
|
||||
}}
|
||||
>
|
||||
Request Body
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'basic_auth_header');
|
||||
}}
|
||||
>
|
||||
Basic Auth Header
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row w-full gap-4" key="pkce">
|
||||
<label className="block">Use PKCE</label>
|
||||
<label className="block font-medium">Use PKCE</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
@@ -181,154 +108,16 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Token
|
||||
</span>
|
||||
<div className="flex flex-row gap-4">
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['credentialsId'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('credentialsId', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<label className="block min-w-[140px]">Add token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'header');
|
||||
}}
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'url');
|
||||
}}
|
||||
>
|
||||
URL
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
tokenPlacement === 'header' ?
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex items-center gap-2.5 mt-4 mb-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Advanced Settings
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Refresh Token URL</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshTokenUrl", val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">Settings</span>
|
||||
</div>
|
||||
|
||||
{/* Automatically Fetch Token */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoFetchToken)}
|
||||
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
|
||||
className="cursor-pointer ml-1"
|
||||
/>
|
||||
<label className="block min-w-[140px]">Automatically fetch token if not found</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically fetch a new token when you try to access a resource and don't have one.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Refresh Token (With Refresh URL) */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoRefreshToken)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={isAutoRefreshDisabled}
|
||||
/>
|
||||
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically refresh your token using the refresh URL when it expires.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
export default OAuth2AuthorizationCode;
|
||||
|
||||
@@ -11,47 +11,6 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
.token-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
min-width: 100px;
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.token-placement-label {
|
||||
width: fit-content;
|
||||
// color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,61 +1,26 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
|
||||
const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const OAuth2ClientCredentials = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
|
||||
const {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
|
||||
const handleSave = () => { save(); };
|
||||
|
||||
const TokenPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const CredentialsPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
@@ -69,14 +34,6 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -85,21 +42,12 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Configuration
|
||||
</span>
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
@@ -114,190 +62,9 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
|
||||
<label className="block min-w-[140px]">Add Credentials to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'body');
|
||||
}}
|
||||
>
|
||||
Request Body
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'basic_auth_header');
|
||||
}}
|
||||
>
|
||||
Basic Auth Header
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Token
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['credentialsId'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('credentialsId', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<label className="block min-w-[140px]">Add token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'header');
|
||||
}}
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'url');
|
||||
}}
|
||||
>
|
||||
URL
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
tokenPlacement === 'header' ?
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex items-center gap-2.5 mt-4 mb-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Advanced Settings
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Refresh Token URL</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshTokenUrl", val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Auto-refresh token</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">Settings</span>
|
||||
</div>
|
||||
|
||||
{/* Automatically Fetch Token */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoFetchToken)}
|
||||
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
|
||||
className="cursor-pointer ml-1"
|
||||
/>
|
||||
<label className="block min-w-[140px]">Automatically fetch token if not found</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically fetch a new token when you try to access a resource and don't have one.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Refresh Token (With Refresh URL) */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoRefreshToken)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={isAutoRefreshDisabled}
|
||||
/>
|
||||
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically refresh your token using the refresh URL when it expires.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,20 +3,18 @@ import get from 'lodash/get';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconCaretDown, IconKey } from '@tabler/icons';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeGrantType } from 'utils/collections';
|
||||
import { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
const GrantTypeSelector = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const [valuesCache, setValuesCache] = useState({
|
||||
...oAuth
|
||||
});
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
|
||||
@@ -26,19 +24,13 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
});
|
||||
|
||||
const onGrantTypeChange = (grantType) => {
|
||||
let updatedValues = {
|
||||
...valuesCache,
|
||||
...oAuth,
|
||||
grantType
|
||||
};
|
||||
setValuesCache(updatedValues);
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
...updatedValues
|
||||
grantType
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -54,18 +46,7 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
accessTokenUrl: '',
|
||||
username: '',
|
||||
password: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
scope: '',
|
||||
credentialsPlacement: 'body',
|
||||
credentialsId: 'credentials',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
tokenQueryKey: 'access_token',
|
||||
grantType: 'authorization_code'
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -73,14 +54,7 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center gap-2.5 my-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Grant Type
|
||||
</span>
|
||||
</div>
|
||||
<label className="block font-medium mb-2">Grant Type</label>
|
||||
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
@@ -115,4 +89,4 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default GrantTypeSelector;
|
||||
export default GrantTypeSelector;
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import toast from 'react-hot-toast';
|
||||
import { cloneDeep, find } from 'lodash';
|
||||
import { IconLoader2 } from '@tabler/icons';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { getAllVariables } from "utils/collections/index";
|
||||
|
||||
const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, credentialsId }) => {
|
||||
const { uid: collectionUid } = collection;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [fetchingToken, toggleFetchingToken] = useState(false);
|
||||
const [refreshingToken, toggleRefreshingToken] = useState(false);
|
||||
|
||||
const interpolatedAccessTokenUrl = useMemo(() => {
|
||||
const variables = getAllVariables(collection, item);
|
||||
return interpolate(accessTokenUrl, variables);
|
||||
}, [collection, item, accessTokenUrl]);
|
||||
|
||||
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedAccessTokenUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const creds = credentialsData?.credentials || {};
|
||||
|
||||
const handleFetchOauth2Credentials = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleFetchingToken(true);
|
||||
try {
|
||||
const credentials = await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
|
||||
toggleFetchingToken(false);
|
||||
if (credentials?.access_token) {
|
||||
toast.success('token fetched successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occured while fetching token!');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('could not fetch the token!');
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error('An error occured while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshAccessToken = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleRefreshingToken(true);
|
||||
try {
|
||||
const credentials = await dispatch(refreshOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
|
||||
toggleRefreshingToken(false);
|
||||
if (credentials?.access_token) {
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
else {
|
||||
toast.error('An error occured while refreshing token!');
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error('An error occured while refreshing token!');
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-4 mt-4">
|
||||
<button onClick={handleFetchOauth2Credentials} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
|
||||
Get Access Token{fetchingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button>
|
||||
{creds?.refresh_token ? <button onClick={handleRefreshAccessToken} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
|
||||
Refresh Token{refreshingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button> : null}
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Oauth2ActionButtons;
|
||||
@@ -1,12 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
ol[role="tree"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
ol[role="group"] span {
|
||||
line-break: anywhere;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,173 +0,0 @@
|
||||
import { find } from "lodash";
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import { useState, useEffect } from "react";
|
||||
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
|
||||
const TokenSection = ({ title, token }) => {
|
||||
if (!token) return null;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [decodedToken, setDecodedToken] = useState(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
try {
|
||||
const parts = token.split('.');
|
||||
if (parts.length === 3) {
|
||||
const payload = JSON.parse(atob(parts[1]));
|
||||
setDecodedToken(payload);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error decoding token:', err);
|
||||
}
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const handleCopy = async (text) => {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2 border dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div
|
||||
className="flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center space-x-2 w-full">
|
||||
{isExpanded ?
|
||||
<IconChevronDown size={18} className="text-gray-500" /> :
|
||||
<IconChevronRight size={18} className="text-gray-500" />
|
||||
}
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<h3 className="text-sm font-medium">{title}</h3>
|
||||
{decodedToken?.exp && <ExpiryTimer expiresIn={decodedToken?.exp} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="p-3 text-sm">
|
||||
<div className="relative group">
|
||||
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => handleCopy(token)}
|
||||
className="p-1 bg-indigo-100 dark:hover:bg-indigo-200 rounded"
|
||||
title="Copy token"
|
||||
>
|
||||
{copied ?
|
||||
<IconCheck size={16} className="text-green-700" /> :
|
||||
<IconCopy size={16} className="text-gray-500" />
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<div className="font-mono text-xs bg-gray-50 dark:bg-gray-800 p-2 rounded break-all">
|
||||
{token}
|
||||
</div>
|
||||
</div>
|
||||
{decodedToken && (
|
||||
<div className="mt-3">
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Decoded Payload</div>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
|
||||
{Object.entries(decodedToken).map(([key, value]) => (
|
||||
<div key={key} className="overflow-hidden text-ellipsis">
|
||||
<span className="font-medium text-xs">{key}: </span>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-300">
|
||||
{typeof value === 'object' ? JSON.stringify(value) : value.toString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const formatExpiryTime = (seconds) => {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
||||
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
||||
};
|
||||
|
||||
const ExpiryTimer = ({ expiresIn }) => {
|
||||
if (!expiresIn) return null;
|
||||
|
||||
const calculateTimeLeft = () => Math.max(0, Math.floor(expiresIn - Date.now() / 1000));
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeLeft(calculateTimeLeft());
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [expiresIn]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`text-xs px-2 py-1 rounded-full min-w-[120px] text-center ${timeLeft <= 30
|
||||
? "bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400"
|
||||
: "bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"
|
||||
}`}
|
||||
>
|
||||
{timeLeft > 0 ? `Expires in ${formatExpiryTime(timeLeft)}` : `Expired`}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun }) => {
|
||||
const { uid: collectionUid } = collection;
|
||||
|
||||
const interpolatedUrl = useMemo(() => {
|
||||
const variables = getAllVariables(collection, item);
|
||||
return interpolate(url, variables);
|
||||
}, [collection, item, url]);
|
||||
|
||||
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const creds = credentialsData?.credentials || {};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="relative w-auto h-fit mt-2">
|
||||
{Object.keys(creds)?.length ? (
|
||||
creds?.error ? (
|
||||
<pre className="text-red-600 dark:text-red-400">Error fetching token. Check network logs for more details.</pre>
|
||||
) : (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-sm">
|
||||
<TokenSection title="Access Token" token={creds.access_token} />
|
||||
<TokenSection title="Refresh Token" token={creds.refresh_token} />
|
||||
<TokenSection title="ID Token" token={creds.id_token} />
|
||||
{(creds.token_type || creds.scope) ? <div className="mt-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg text-xs">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{creds.token_type ? <div className="flex items-center space-x-1">
|
||||
<span className="font-medium">Token Type:</span>
|
||||
<span className="text-gray-600 dark:text-gray-300">{creds.token_type}</span>
|
||||
</div> : null}
|
||||
{creds?.scope ? <div className="flex items-center space-x-1 min-w-0">
|
||||
<span className="font-medium flex-shrink-0">Scope:</span>
|
||||
<span className="text-gray-600 dark:text-gray-300 truncate" title={creds.scope}>
|
||||
{creds.scope}
|
||||
</span>
|
||||
</div> : null}
|
||||
</div>
|
||||
</div> : null}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">No token found</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Oauth2TokenViewer;
|
||||
@@ -11,47 +11,6 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
|
||||
.token-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
min-width: 100px;
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
min-width: 100px;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.token-placement-label {
|
||||
width: fit-content;
|
||||
// color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,62 +1,26 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
|
||||
const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
|
||||
const {
|
||||
accessTokenUrl,
|
||||
username,
|
||||
password,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleSave = () => { save(); }
|
||||
|
||||
const TokenPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const CredentialsPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
@@ -72,14 +36,6 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -88,21 +44,12 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Configuration
|
||||
</span>
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
@@ -117,190 +64,11 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
|
||||
<label className="block min-w-[140px]">Add Credentials to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'body');
|
||||
}}
|
||||
>
|
||||
Request Body
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('credentialsPlacement', 'basic_auth_header');
|
||||
}}
|
||||
>
|
||||
Basic Auth Header
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 mt-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Token
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
|
||||
<label className="block min-w-[140px]">Token ID</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['credentialsId'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('credentialsId', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
|
||||
<label className="block min-w-[140px]">Add token to</label>
|
||||
<div className="inline-flex items-center cursor-pointer token-placement-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'header');
|
||||
}}
|
||||
>
|
||||
Header
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleChange('tokenPlacement', 'url');
|
||||
}}
|
||||
>
|
||||
URL
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
tokenPlacement === 'header' ?
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
|
||||
<label className="block min-w-[140px]">Header Prefix</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenHeaderPrefix'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
|
||||
<label className="block font-medium min-w-[140px]">Query Param Key</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={oAuth['tokenQueryKey'] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange('tokenQueryKey', val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex items-center gap-2.5 mt-4 mb-2">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Advanced Settings
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Refresh Token URL</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<SingleLineEditor
|
||||
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange("refreshTokenUrl", val)}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-full mb-4">
|
||||
<label className="block min-w-[140px]">Auto-refresh token</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium">Settings</span>
|
||||
</div>
|
||||
|
||||
{/* Automatically Fetch Token */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoFetchToken)}
|
||||
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
|
||||
className="cursor-pointer ml-1"
|
||||
/>
|
||||
<label className="block min-w-[140px]">Automatically fetch token if not found</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically fetch a new token when you try to access a resource and don't have one.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Refresh Token (With Refresh URL) */}
|
||||
<div className="flex items-center gap-4 w-full">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(autoRefreshToken)}
|
||||
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
|
||||
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={isAutoRefreshDisabled}
|
||||
/>
|
||||
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative group cursor-pointer">
|
||||
<IconHelp size={16} className="text-gray-500" />
|
||||
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
|
||||
Automatically refresh your token using the refresh URL when it expires.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2PasswordCredentials;
|
||||
export default OAuth2AuthorizationCode;
|
||||
|
||||
@@ -5,34 +5,17 @@ import GrantTypeSelector from './GrantTypeSelector/index';
|
||||
import OAuth2PasswordCredentials from './PasswordCredentials/index';
|
||||
import OAuth2AuthorizationCode from './AuthorizationCode/index';
|
||||
import OAuth2ClientCredentials from './ClientCredentials/index';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const GrantTypeComponentMap = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
};
|
||||
|
||||
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
|
||||
const grantTypeComponentMap = (grantType, item, collection) => {
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
|
||||
return <OAuth2PasswordCredentials item={item} collection={collection} />;
|
||||
break;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
|
||||
return <OAuth2AuthorizationCode item={item} collection={collection} />;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
|
||||
return <OAuth2ClientCredentials item={item} collection={collection} />;
|
||||
break;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
@@ -41,12 +24,12 @@ const GrantTypeComponentMap = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
const OAuth2 = ({ item, collection }) => {
|
||||
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
|
||||
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<GrantTypeSelector item={item} request={request} updateAuth={updateAuth} collection={collection} />
|
||||
<GrantTypeComponentMap item={item} collection={collection} />
|
||||
<GrantTypeSelector item={item} collection={collection} />
|
||||
{grantTypeComponentMap(oAuth?.grantType, item, collection)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,51 +10,14 @@ import NTLMAuth from './NTLMAuth';
|
||||
|
||||
import ApiKeyAuth from './ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
import OAuth2 from './OAuth2/index';
|
||||
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _item?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const Auth = ({ item, collection }) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: i.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
@@ -83,21 +46,32 @@ const Auth = ({ item, collection }) => {
|
||||
return <ApiKeyAuth collection={collection} item={item} />;
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
{collectionAuth?.mode === 'oauth2' ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-1">
|
||||
<div>Collection level auth is: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
|
||||
</div>
|
||||
<div className="text-sm opacity-50">
|
||||
Note: You need to use scripting to set the access token in the request headers.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>Auth inherited from the Collection: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
|
||||
<StyledWrapper className="w-full mt-1">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
|
||||
@@ -152,7 +152,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
{(script.req || script.res) && (
|
||||
item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage ?
|
||||
item.preScriptResponseErrorMessage || item.postResponseScriptErrorMessage ?
|
||||
<ErrorIndicator /> :
|
||||
<ContentIndicator />
|
||||
)}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { clearRequestTimeline } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const ClearTimeline = ({ collection, item }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const clearResponse = () =>
|
||||
dispatch(
|
||||
clearRequestTimeline({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button onClick={clearResponse} className='text-link hover:underline' title="Clear Timeline">
|
||||
Clear Timeline
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearTimeline;
|
||||
@@ -13,7 +13,7 @@ import { useTheme } from 'providers/Theme/index';
|
||||
import { getEncoding, uuid } from 'utils/common/index';
|
||||
|
||||
const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
|
||||
if (data === undefined || !dataBuffer || !mode) {
|
||||
if (data === undefined || !dataBuffer) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ const formatErrorMessage = (error) => {
|
||||
|
||||
const remoteMethodError = "Error invoking remote method 'send-http-request':";
|
||||
|
||||
if (error?.includes(remoteMethodError)) {
|
||||
if (error.includes(remoteMethodError)) {
|
||||
const parts = error.split(remoteMethodError);
|
||||
return parts[1]?.trim() || error;
|
||||
}
|
||||
@@ -92,9 +92,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
// Always show raw
|
||||
const allowedPreviewModes = [{ mode: 'raw', name: 'Raw', uid: uuid() }];
|
||||
|
||||
if (!mode || !contentType) return allowedPreviewModes;
|
||||
|
||||
if (mode?.includes('html') && typeof data === 'string') {
|
||||
if (mode.includes('html') && typeof data === 'string') {
|
||||
allowedPreviewModes.unshift({ mode: 'preview-web', name: 'Web', uid: uuid() });
|
||||
} else if (mode.includes('image')) {
|
||||
allowedPreviewModes.unshift({ mode: 'preview-image', name: 'Image', uid: uuid() });
|
||||
@@ -142,7 +140,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="w-full h-full relative flex"
|
||||
className="w-full h-full relative"
|
||||
style={{ maxWidth: width }}
|
||||
queryFilterEnabled={queryFilterEnabled}
|
||||
>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.line {
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-family: Inter, sans-serif !important;
|
||||
|
||||
.arrow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.request {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.response {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RunnerTimeline = ({ request, response }) => {
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = typeof response.headers === 'object' ? Object.entries(response.headers) : [];
|
||||
|
||||
request = request || {};
|
||||
response = response || {};
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<div>
|
||||
<pre className="line request font-bold">
|
||||
<span className="arrow">{'>'}</span> {request.method} {request.url}
|
||||
</pre>
|
||||
{requestHeaders.map((h) => {
|
||||
return (
|
||||
<pre className="line request" key={h.name}>
|
||||
<span className="arrow">{'>'}</span> {h.name}: {h.value}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
|
||||
{requestData ? (
|
||||
<pre className="line request">
|
||||
<span className="arrow">{'>'}</span> data{' '}
|
||||
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<pre className="line response font-bold">
|
||||
<span className="arrow">{'<'}</span> {response.status} - {response.statusText}
|
||||
</pre>
|
||||
|
||||
{responseHeaders.map((h) => {
|
||||
return (
|
||||
<pre className="line response" key={h[0]}>
|
||||
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunnerTimeline;
|
||||
@@ -1,109 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.timeline-event {
|
||||
padding: 8px 0 0 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-event-content {
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.timeline-event-header {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.method-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-code {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.url-text {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.oauth-section {
|
||||
.oauth-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-weight: 600;
|
||||
|
||||
span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-switcher {
|
||||
border-bottom: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
margin-bottom: 16px;
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
padding: 8px 16px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color};
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: ${(props) => props.theme.tabs.active.border};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.network-logs {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.oauth-request-item-content {
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.collapsible-section {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.section-header {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-family: ${(props) => props.theme.font || 'Inter, sans-serif'} !important;
|
||||
font-family: Inter, sans-serif !important;
|
||||
|
||||
.arrow {
|
||||
opacity: 0.5;
|
||||
@@ -117,35 +19,6 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
|
||||
.request-label {
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
margin-left: 8px;
|
||||
background: ${(props) => props.theme.requestTabs.bg};
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import QueryResult from "components/ResponsePane/QueryResult/index";
|
||||
import { useState } from "react";
|
||||
|
||||
const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, width }) => {
|
||||
const [isBodyCollapsed, toggleBody] = useState(true);
|
||||
return (
|
||||
<div className="collapsible-section">
|
||||
<div className="section-header" onClick={() => toggleBody(!isBodyCollapsed)}>
|
||||
<pre className="flex flex-row items-center text-indigo-500/80 dark:text-indigo-500/80">
|
||||
<div className="opacity-70">{isBodyCollapsed ? '▼' : '▶'}</div> Body
|
||||
</pre>
|
||||
</div>
|
||||
{isBodyCollapsed && (
|
||||
<div className="mt-2">
|
||||
{data || dataBuffer ? (
|
||||
<div className="h-96 overflow-auto">
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
headers={headers}
|
||||
error={error}
|
||||
key={item?.uid}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500">No Body found</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BodyBlock;
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const HeadersBlock = ({ headers, type }) => {
|
||||
const [areHeadersCollapsed, toggleHeaders] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="collapsible-section mt-2">
|
||||
<div className="section-header" onClick={() => toggleHeaders(!areHeadersCollapsed)}>
|
||||
<pre className="flex flex-row items-center text-indigo-500/80 dark:text-indigo-500/80">
|
||||
<div className="opacity-70">{areHeadersCollapsed ? '▼' : '▶'}</div> Headers
|
||||
{headers && Object.keys(headers).length > 0 &&
|
||||
<div className="ml-1">({Object.keys(headers).length})</div>
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
{areHeadersCollapsed && (
|
||||
<div className="mt-1">
|
||||
{headers && Object.keys(headers).length > 0
|
||||
? <Headers headers={headers} type={type} />
|
||||
: <div className="text-gray-500">No Headers found</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
const Headers = ({ headers, type }) => {
|
||||
if (Array.isArray(headers)) {
|
||||
return (
|
||||
<div className="mt-1 text-sm">
|
||||
{headers.map((header, index) => (
|
||||
<pre key={index} className="mb-1 whitespace-pre-wrap">
|
||||
{type === 'request' ? '>' : '<'} <span className="opacity-60">{header?.name}:</span>
|
||||
<span className="whitespace-pre-wrap">{String(header?.value)}</span>
|
||||
</pre>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mt-1 text-sm">
|
||||
{Object.entries(headers).map(([key, value], index) => (
|
||||
<pre key={index} className="mb-1 whitespace-pre-wrap">
|
||||
{type === 'request' ? '>' : '<'} <span className="opacity-60">{key}:</span>
|
||||
<span>{String(value)}</span>
|
||||
</pre>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default HeadersBlock;
|
||||
@@ -1,19 +0,0 @@
|
||||
const Method = ({ method }) => {
|
||||
return (
|
||||
<span className={`${methodColors[method?.toUpperCase()] || 'text-white'} font-bold`}>
|
||||
{method?.toUpperCase()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const methodColors = {
|
||||
GET: 'text-green-500',
|
||||
POST: 'text-blue-500',
|
||||
PUT: 'text-yellow-500',
|
||||
DELETE: 'text-red-500',
|
||||
PATCH: 'text-purple-500',
|
||||
OPTIONS: 'text-gray-500',
|
||||
HEAD: 'text-gray-500',
|
||||
};
|
||||
|
||||
export default Method;
|
||||
@@ -1,26 +0,0 @@
|
||||
const Status = ({ statusCode, statusText }) => {
|
||||
return (
|
||||
<span
|
||||
className={`${
|
||||
statusColor(statusCode) || 'text-white'
|
||||
} font-bold`}
|
||||
>
|
||||
{statusCode}{' '}
|
||||
{statusText || ''}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const statusColor = (statusCode) => {
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
return 'text-green-500';
|
||||
} else if (statusCode >= 300 && statusCode < 400) {
|
||||
return 'text-yellow-500';
|
||||
} else if (statusCode >= 400 && statusCode < 600) {
|
||||
return 'text-red-500';
|
||||
} else {
|
||||
return 'text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
export default Status;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const getRelativeTime = (date) => {
|
||||
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
|
||||
const diff = (date - new Date()) / 1000;
|
||||
|
||||
const timeUnits = [
|
||||
{ unit: 'year', seconds: 31536000 },
|
||||
{ unit: 'month', seconds: 2592000 },
|
||||
{ unit: 'week', seconds: 604800 },
|
||||
{ unit: 'day', seconds: 86400 },
|
||||
{ unit: 'hour', seconds: 3600 },
|
||||
{ unit: 'minute', seconds: 60 },
|
||||
{ unit: 'second', seconds: 1 }
|
||||
];
|
||||
|
||||
for (const { unit, seconds } of timeUnits) {
|
||||
if (Math.abs(diff) >= seconds || unit === 'second') {
|
||||
return rtf.format(Math.round(diff / seconds), unit);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const RelativeTime = ({ timestamp }) => {
|
||||
const [relativeTime, setRelativeTime] = useState(getRelativeTime(new Date(timestamp)));
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setRelativeTime(getRelativeTime(new Date(timestamp)));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [timestamp]);
|
||||
|
||||
return <pre className="text-xs">{relativeTime}</pre>;
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
const Network = ({ logs }) => {
|
||||
return (
|
||||
<div className="bg-black/5 text-white network-logs rounded overflow-auto h-96">
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{logs.map((entry, index) => (
|
||||
<NetworkLogsEntry key={index} entry={entry} />
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const NetworkLogsEntry = ({ entry }) => {
|
||||
const { type, message } = entry;
|
||||
let className = '';
|
||||
|
||||
switch (type) {
|
||||
case 'request':
|
||||
className = 'text-blue-500';
|
||||
break;
|
||||
case 'response':
|
||||
className = 'text-green-500';
|
||||
break;
|
||||
case 'error':
|
||||
className = 'text-red-500';
|
||||
break;
|
||||
case 'tls':
|
||||
className = 'text-purple-500';
|
||||
break;
|
||||
case 'info':
|
||||
className = 'text-yellow-500';
|
||||
break;
|
||||
default:
|
||||
className = 'text-gray-400';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Network;
|
||||
@@ -1,41 +0,0 @@
|
||||
import Headers from "../Common/Headers/index";
|
||||
import BodyBlock from "../Common/Body/index";
|
||||
|
||||
const safeStringifyJSONIfNotString = (obj) => {
|
||||
if (obj === null || obj === undefined) return '';
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(obj);
|
||||
} catch (e) {
|
||||
return '[Unserializable Object]';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const Request = ({ collection, request, item, width }) => {
|
||||
let { url, headers, data, dataBuffer, error } = request || {};
|
||||
if (!dataBuffer) {
|
||||
dataBuffer = Buffer.from(safeStringifyJSONIfNotString(data))?.toString('base64');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Method and URL */}
|
||||
<div className="mb-1 flex gap-2">
|
||||
<pre className="whitespace-pre-wrap">{url}</pre>
|
||||
</div>
|
||||
|
||||
{/* Headers */}
|
||||
<Headers headers={headers} type={'request'} />
|
||||
|
||||
{/* Body */}
|
||||
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Request;
|
||||
@@ -1,43 +0,0 @@
|
||||
import BodyBlock from "../Common/Body/index";
|
||||
import Headers from "../Common/Headers/index";
|
||||
import Status from "../Common/Status/index";
|
||||
|
||||
const safeStringifyJSONIfNotString = (obj) => {
|
||||
if (obj === null || obj === undefined) return '';
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(obj);
|
||||
} catch (e) {
|
||||
return '[Unserializable Object]';
|
||||
}
|
||||
};
|
||||
|
||||
const Response = ({ collection, response, item, width }) => {
|
||||
let { status, statusCode, statusText, dataBuffer, headers, data, error } = response || {};
|
||||
if (!dataBuffer) {
|
||||
dataBuffer = Buffer.from(safeStringifyJSONIfNotString(data))?.toString('base64');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Status */}
|
||||
<div className="mb-1">
|
||||
<Status statusCode={status || statusCode} statusText={statusText} />
|
||||
{response.duration && <span className="text-sm text-gray-400 ml-2">{response.duration}ms</span>}
|
||||
{response.size && <span className="text-sm text-gray-400 ml-2">{response.size}B</span>}
|
||||
</div>
|
||||
|
||||
{/* Headers */}
|
||||
<Headers headers={headers} type={'response'} />
|
||||
|
||||
{/* Body */}
|
||||
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Response;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import Network from "./Network/index";
|
||||
import Request from "./Request/index";
|
||||
import Response from "./Response/index";
|
||||
import Method from "./Common/Method/index";
|
||||
import Status from "./Common/Status/index";
|
||||
import { RelativeTime } from "./Common/Time/index";
|
||||
|
||||
const TimelineItem = ({ timestamp, request, response, item, collection, width, isOauth2 }) => {
|
||||
const [isCollapsed, _toggleCollapse] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('request');
|
||||
const toggleCollapse = () => _toggleCollapse(prev => !prev);
|
||||
const { method, status, statusCode, statusText, url = '' } = request || {};
|
||||
const { status: responseStatus, statusCode: responseStatusCode, statusText: responseStatusText } = response || {};
|
||||
const showNetworkLogs = response.timeline && response.timeline.length > 0;
|
||||
|
||||
return (
|
||||
<div className={`border-b-2 ${isOauth2 ? 'border-indigo-700/50' : 'border-amber-700/50' } py-2`}>
|
||||
<div className="oauth-request-item-header cursor-pointer" onClick={toggleCollapse}>
|
||||
<div className="flex justify-between items-center min-w-0">
|
||||
<div className="flex items-center space-x-2 min-w-0">
|
||||
<Status statusCode={responseStatus || responseStatusCode} statusText={responseStatusText} />
|
||||
<Method method={method} />
|
||||
<Status statusCode={status || statusCode} statusText={statusText} />
|
||||
{isOauth2 ? <pre className="opacity-50">[oauth2.0]</pre> : null}
|
||||
<pre className="opacity-70">[{new Date(timestamp).toISOString()}]</pre>
|
||||
</div>
|
||||
<span className="text-sm text-gray-400 flex-shrink-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<RelativeTime timestamp={timestamp} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="truncate text-sm mt-1">{url}</div>
|
||||
</div>
|
||||
{isCollapsed && (<div className="text-sm overflow-hidden">
|
||||
{/* Tabs */}
|
||||
<div className="tabs-switcher flex mb-4">
|
||||
<button
|
||||
className={`mr-4 ${activeTab === 'request' ? 'active' : 'text-gray-400'}`}
|
||||
onClick={() => setActiveTab('request')}
|
||||
>
|
||||
Request
|
||||
</button>
|
||||
<button
|
||||
className={`mr-4 ${activeTab === 'response' ? 'active' : 'text-gray-400'}`}
|
||||
onClick={() => setActiveTab('response')}
|
||||
>
|
||||
Response
|
||||
</button>
|
||||
{showNetworkLogs && (
|
||||
<button
|
||||
className={`${activeTab === 'networkLogs' ? 'active' : 'text-gray-400'}`}
|
||||
onClick={() => setActiveTab('networkLogs')}
|
||||
>
|
||||
Network Logs
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="tab-content">
|
||||
{/* Request Tab */}
|
||||
{activeTab === 'request' && (
|
||||
<Request request={request} item={item} collection={collection} width={width} />
|
||||
)}
|
||||
|
||||
{/* Response Tab */}
|
||||
{activeTab === 'response' && (
|
||||
<Response response={response} item={item} collection={collection} width={width} />
|
||||
)}
|
||||
|
||||
{/* Network Logs Tab */}
|
||||
{activeTab === 'networkLogs' && showNetworkLogs && (
|
||||
<Network logs={response?.timeline} />
|
||||
)}
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineItem;
|
||||
@@ -1,123 +1,61 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
|
||||
import { get } from 'lodash';
|
||||
import TimelineItem from './TimelineItem/index';
|
||||
|
||||
const getEffectiveAuthSource = (collection, item) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
if (authMode !== 'inherit') return null;
|
||||
const Timeline = ({ request, response }) => {
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = typeof response.headers === 'object' ? Object.entries(response.headers) : [];
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
uid: collection.uid,
|
||||
auth: collectionAuth
|
||||
};
|
||||
request = request || {};
|
||||
response = response || {};
|
||||
|
||||
// Get path from collection to item
|
||||
let path = [];
|
||||
let currentItem = findItemInCollection(collection, item?.uid);
|
||||
while (currentItem) {
|
||||
path.unshift(currentItem);
|
||||
currentItem = findParentItemInCollection(collection, currentItem?.uid);
|
||||
}
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...path].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
uid: i.uid,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const Timeline = ({ collection, item, width }) => {
|
||||
// Get the effective auth source if auth mode is inherit
|
||||
const authSource = getEffectiveAuthSource(collection, item);
|
||||
|
||||
// Filter timeline entries based on new rules
|
||||
const combinedTimeline = ([...(collection.timeline || [])]).filter(obj => {
|
||||
// Always show entries for this item
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
|
||||
// For OAuth2 entries, also show if auth is inherited
|
||||
if (obj.type === 'oauth2' && authSource) {
|
||||
if (authSource.type === 'folder' && obj.folderUid === authSource.uid) return true;
|
||||
if (authSource.type === 'collection' && !obj.folderUid) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).sort((a, b) => b.timestamp - a.timestamp);
|
||||
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="pb-4 w-full flex flex-grow flex-col"
|
||||
style={{ maxWidth: width - 60, overflowWrap: 'break-word' }}
|
||||
>
|
||||
{combinedTimeline.map((event, index) => {
|
||||
if (event.type === 'request') {
|
||||
const { data, timestamp } = event;
|
||||
const { request, response } = data;
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<div>
|
||||
<pre className="line request font-bold">
|
||||
<span className="arrow">{'>'}</span> {request.method} {request.url}
|
||||
</pre>
|
||||
{requestHeaders.map((h) => {
|
||||
return (
|
||||
<div key={index} className="timeline-event mb-2">
|
||||
<TimelineItem
|
||||
timestamp={timestamp}
|
||||
request={request}
|
||||
response={response}
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
<pre className="line request" key={h.name}>
|
||||
<span className="arrow">{'>'}</span> {h.name}: {h.value}
|
||||
</pre>
|
||||
);
|
||||
} else if (event.type === 'oauth2') {
|
||||
const { data, timestamp } = event;
|
||||
const { debugInfo } = data;
|
||||
return (
|
||||
<div key={index} className="timeline-event">
|
||||
<div className="timeline-event-header cursor-pointer flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span className="font-bold">OAuth2.0 Calls</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{debugInfo && debugInfo.length > 0 ? (
|
||||
debugInfo.map((data, idx) => (
|
||||
<div className='ml-4'>
|
||||
<TimelineItem
|
||||
key={idx}
|
||||
timestamp={timestamp}
|
||||
request={data?.request}
|
||||
response={data?.response}
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width - 50}
|
||||
isOauth2={true}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>No debug information available.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
return null;
|
||||
})}
|
||||
{requestData ? (
|
||||
<pre className="line request">
|
||||
<span className="arrow">{'>'}</span> data{' '}
|
||||
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<pre className="line response font-bold">
|
||||
<span className="arrow">{'<'}</span> {response.status} - {response.statusText}
|
||||
</pre>
|
||||
|
||||
{responseHeaders.map((h) => {
|
||||
return (
|
||||
<pre className="line response" key={h[0]}>
|
||||
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
export default Timeline;
|
||||
|
||||
@@ -18,7 +18,6 @@ import ScriptErrorIcon from './ScriptErrorIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
|
||||
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -27,10 +26,6 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
const [showScriptErrorCard, setShowScriptErrorCard] = useState(false);
|
||||
|
||||
const requestTimeline = ([...(collection.timeline || [])]).filter(obj => {
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage) {
|
||||
setShowScriptErrorCard(true);
|
||||
@@ -68,7 +63,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline collection={collection} item={item} width={rightPaneWidth} />;
|
||||
return <Timeline request={item.requestSent} response={item.response} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;
|
||||
@@ -88,7 +83,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!item.response && !requestTimeline?.length) {
|
||||
if (!item.response) {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<Placeholder />
|
||||
@@ -139,17 +134,11 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
/>
|
||||
)}
|
||||
{focusedTab?.responsePaneTab === "timeline" ? (
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
) : item?.response ? (
|
||||
<>
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<StatusCode status={response.status} />
|
||||
<ResponseTime duration={response.duration} />
|
||||
<ResponseSize size={response.size} />
|
||||
</>
|
||||
) : null}
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<StatusCode status={response.status} />
|
||||
<ResponseTime duration={response.duration} />
|
||||
<ResponseSize size={response.size} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -163,17 +152,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
/>
|
||||
)}
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
|
||||
<Timeline
|
||||
collection={collection}
|
||||
item={item}
|
||||
width={rightPaneWidth}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<>{getTabPanel(focusedTab.responsePaneTab)}</>
|
||||
)}
|
||||
{getTabPanel(focusedTab.responsePaneTab)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,10 @@ import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
|
||||
import StatusCode from 'components/ResponsePane/StatusCode';
|
||||
import ResponseTime from 'components/ResponsePane/ResponseTime';
|
||||
import ResponseSize from 'components/ResponsePane/ResponseSize';
|
||||
import Timeline from 'components/ResponsePane/Timeline';
|
||||
import TestResults from 'components/ResponsePane/TestResults';
|
||||
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import RunnerTimeline from 'components/ResponsePane/RunnerTimeline';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
@@ -45,7 +45,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <ResponseHeaders headers={headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <RunnerTimeline request={requestSent} response={responseReceived} />;
|
||||
return <Timeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} assertionResults={assertionResults} />;
|
||||
|
||||
@@ -98,7 +98,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
};
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (event && event.detail != 1) return;
|
||||
if (event.detail != 1) return;
|
||||
//scroll to the active tab
|
||||
setTimeout(scrollToTheActiveTab, 50);
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { collectionAddOauth2CredentialsByUrl } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const useIpcEvents = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -161,17 +160,7 @@ const useIpcEvents = () => {
|
||||
|
||||
const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => {
|
||||
dispatch(hydrateCollectionWithUiStateSnapshot(val));
|
||||
});
|
||||
|
||||
const removeCollectionOauth2CredentialsUpdatesListener = ipcRenderer.on('main:credentials-update', (val) => {
|
||||
const payload = {
|
||||
...val,
|
||||
itemUid: val.itemUid || null,
|
||||
folderUid: val.folderUid || null,
|
||||
credentialsId: val.credentialsId || 'credentials'
|
||||
};
|
||||
dispatch(collectionAddOauth2CredentialsByUrl(payload));
|
||||
});
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeCollectionTreeUpdateListener();
|
||||
@@ -192,7 +181,6 @@ const useIpcEvents = () => {
|
||||
removeSystemProxyEnvUpdatesListener();
|
||||
removeGlobalEnvironmentsUpdatesListener();
|
||||
removeSnapshotHydrationListener();
|
||||
removeCollectionOauth2CredentialsUpdatesListener();
|
||||
};
|
||||
}, [isElectron]);
|
||||
};
|
||||
|
||||
@@ -37,9 +37,7 @@ import {
|
||||
resetRunResults,
|
||||
responseReceived,
|
||||
updateLastAction,
|
||||
setCollectionSecurityConfig,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl
|
||||
setCollectionSecurityConfig
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
@@ -50,7 +48,6 @@ import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'uti
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import { safeParseJSON, safeStringifyJSON } from 'utils/common/index';
|
||||
|
||||
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@@ -240,20 +237,11 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
|
||||
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
|
||||
.then((response) => {
|
||||
// Ensure any timestamps in the response are converted to numbers
|
||||
const serializedResponse = {
|
||||
...response,
|
||||
timeline: response.timeline?.map(entry => ({
|
||||
...entry,
|
||||
timestamp: entry.timestamp instanceof Date ? entry.timestamp.getTime() : entry.timestamp
|
||||
}))
|
||||
};
|
||||
|
||||
return dispatch(
|
||||
responseReceived({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collectionUid,
|
||||
response: serializedResponse
|
||||
response: response
|
||||
})
|
||||
);
|
||||
})
|
||||
@@ -1242,97 +1230,33 @@ export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (
|
||||
|
||||
|
||||
export const hydrateCollectionWithUiStateSnapshot = (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;
|
||||
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 }));
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchOauth2Credentials = (payload) => async (dispatch, getState) => {
|
||||
const { request, collection, itemUid, folderUid } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.ipcRenderer
|
||||
.invoke('renderer:fetch-oauth2-credentials', { itemUid, request, collection })
|
||||
.then(({ credentials, url, collectionUid, credentialsId, debugInfo }) => {
|
||||
dispatch(
|
||||
collectionAddOauth2CredentialsByUrl({
|
||||
credentials,
|
||||
url,
|
||||
collectionUid,
|
||||
credentialsId,
|
||||
debugInfo: safeParseJSON(safeStringifyJSON(debugInfo)),
|
||||
folderUid: folderUid || null,
|
||||
itemUid: !folderUid ? itemUid : null
|
||||
})
|
||||
);
|
||||
resolve(credentials);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const refreshOauth2Credentials = (payload) => async (dispatch, getState) => {
|
||||
const { request, collection, folderUid, itemUid } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.ipcRenderer
|
||||
.invoke('renderer:refresh-oauth2-credentials', { request, collection })
|
||||
.then(({ credentials, url, collectionUid, debugInfo, credentialsId }) => {
|
||||
dispatch(
|
||||
collectionAddOauth2CredentialsByUrl({
|
||||
credentials,
|
||||
url,
|
||||
collectionUid,
|
||||
credentialsId,
|
||||
debugInfo: safeParseJSON(safeStringifyJSON(debugInfo)),
|
||||
folderUid: folderUid || null,
|
||||
itemUid: !folderUid ? itemUid : null
|
||||
})
|
||||
);
|
||||
resolve(credentials);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
|
||||
const { collectionUid, url, credentialsId } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.ipcRenderer
|
||||
.invoke('clear-oauth2-cache', collectionUid, url, credentialsId)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
collectionClearOauth2CredentialsByUrl({
|
||||
url,
|
||||
collectionUid,
|
||||
})
|
||||
);
|
||||
// todo: add any other redux state that you want to save
|
||||
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
}
|
||||
catch(error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
@@ -281,36 +281,13 @@ export const collectionsSlice = createSlice({
|
||||
},
|
||||
responseReceived: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
item.requestState = 'received';
|
||||
item.response = action.payload.response;
|
||||
item.cancelTokenUid = null;
|
||||
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
}
|
||||
|
||||
// Ensure timestamp is a number (milliseconds since epoch)
|
||||
const timestamp = item?.requestSent?.timestamp instanceof Date
|
||||
? item.requestSent.timestamp.getTime()
|
||||
: item?.requestSent?.timestamp || Date.now();
|
||||
|
||||
// Append the new timeline entry with numeric timestamp
|
||||
collection.timeline.push({
|
||||
type: "request",
|
||||
collectionUid: collection.uid,
|
||||
folderUid: null,
|
||||
itemUid: item.uid,
|
||||
timestamp: timestamp,
|
||||
data: {
|
||||
request: item.requestSent || item.request,
|
||||
response: action.payload.response,
|
||||
timestamp: timestamp,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -324,23 +301,6 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
clearTimeline: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.timeline = [];
|
||||
}
|
||||
},
|
||||
clearRequestTimeline: (state, action) => {
|
||||
const { collectionUid, itemUid } = action.payload || {};
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
if (itemUid) {
|
||||
collection.timeline = collection?.timeline?.filter(t => t?.itemUid !== itemUid);
|
||||
}
|
||||
}
|
||||
},
|
||||
saveRequest: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -1579,23 +1539,6 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.request.tests', action.payload.tests);
|
||||
}
|
||||
},
|
||||
updateFolderAuth: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.itemUid) : null;
|
||||
if (!folder) return;
|
||||
|
||||
if (folder) {
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
switch (action.payload.mode) {
|
||||
case 'oauth2':
|
||||
set(folder, 'root.request.auth.oauth2', action.payload.content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
addCollectionHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -2076,97 +2019,7 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.docs', action.payload.docs);
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionAddOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
// Update oauth2Credentials (latest token)
|
||||
if (!collection.oauth2Credentials) {
|
||||
collection.oauth2Credentials = [];
|
||||
}
|
||||
let collectionOauth2Credentials = cloneDeep(collection.oauth2Credentials);
|
||||
|
||||
// Remove existing credentials for the same combination
|
||||
const filteredOauth2Credentials = filter(
|
||||
collectionOauth2Credentials,
|
||||
(creds) =>
|
||||
!(creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId)
|
||||
);
|
||||
|
||||
// Add the new credential with folderUid and itemUid
|
||||
filteredOauth2Credentials.push({
|
||||
collectionUid,
|
||||
folderUid,
|
||||
itemUid,
|
||||
url,
|
||||
credentials,
|
||||
credentialsId,
|
||||
debugInfo
|
||||
});
|
||||
|
||||
collection.oauth2Credentials = filteredOauth2Credentials;
|
||||
|
||||
if (!collection.timeline) {
|
||||
collection.timeline = [];
|
||||
}
|
||||
|
||||
if(debugInfo) {
|
||||
collection.timeline.push({
|
||||
type: "oauth2",
|
||||
collectionUid,
|
||||
folderUid,
|
||||
itemUid,
|
||||
timestamp: Date.now(),
|
||||
data: {
|
||||
collectionUid,
|
||||
folderUid,
|
||||
itemUid,
|
||||
url,
|
||||
credentials,
|
||||
credentialsId,
|
||||
debugInfo: debugInfo.data,
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
collectionClearOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, url, credentialsId } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
if (collection.oauth2Credentials) {
|
||||
let collectionOauth2Credentials = cloneDeep(collection.oauth2Credentials);
|
||||
const filteredOauth2Credentials = filter(
|
||||
collectionOauth2Credentials,
|
||||
(creds) =>
|
||||
!(creds.url === url && creds.collectionUid === collectionUid)
|
||||
);
|
||||
collection.oauth2Credentials = filteredOauth2Credentials;
|
||||
}
|
||||
},
|
||||
|
||||
collectionGetOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, url, credentialsId } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
const oauth2Credential = find(
|
||||
collection?.oauth2Credentials || [],
|
||||
(creds) =>
|
||||
creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId
|
||||
);
|
||||
return oauth2Credential;
|
||||
},
|
||||
updateFolderAuthMode: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
|
||||
if (folder) {
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2193,8 +2046,6 @@ export const {
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
responseCleared,
|
||||
clearTimeline,
|
||||
clearRequestTimeline,
|
||||
saveRequest,
|
||||
deleteRequestDraft,
|
||||
newEphemeralHttpRequest,
|
||||
@@ -2273,11 +2124,6 @@ export const {
|
||||
resetCollectionRunner,
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
updateFolderAuth,
|
||||
updateFolderAuthMode,
|
||||
moveCollection
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
|
||||
@@ -28,16 +28,12 @@ if (!SERVER_RENDERED) {
|
||||
undef: true,
|
||||
browser: true,
|
||||
devel: true,
|
||||
module: true,
|
||||
node: true,
|
||||
predef: {
|
||||
'bru': false,
|
||||
'req': false,
|
||||
'res': false,
|
||||
'test': false,
|
||||
'expect': false,
|
||||
'require': false,
|
||||
'module': false
|
||||
'expect': false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,19 +51,15 @@ if (!SERVER_RENDERED) {
|
||||
* Filter out errors due to top level awaits
|
||||
* See https://github.com/usebruno/bruno/issues/1214
|
||||
*
|
||||
* - E058: Missing semicolon at top level await
|
||||
* codemirror error: "Missing semicolon."
|
||||
* - W024: 'await' used as identifier (JSHint doesn't recognize top-level await syntax)
|
||||
* codemirror error: "Expected an identifier and instead saw 'await' (a reserved word)."
|
||||
*
|
||||
* Once JSHINT top level await support is added, this file can be removed
|
||||
* and we can use the default javascript-lint addon from codemirror
|
||||
*/
|
||||
errors = filter(errors, (error) => {
|
||||
if (error.code === 'E058' || error.code === 'W024') {
|
||||
if (error.code === 'E058') {
|
||||
if (
|
||||
error.evidence &&
|
||||
error.evidence.includes('await') &&
|
||||
error.reason === 'Missing semicolon.' &&
|
||||
error.scope === '(main)'
|
||||
) {
|
||||
return false;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
|
||||
import { uuid } from 'utils/common';
|
||||
import path from 'utils/common/path';
|
||||
import brunoCommon from '@usebruno/common';
|
||||
const { interpolate } = brunoCommon;
|
||||
|
||||
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
||||
if (!str || !str.length || !isString(str)) {
|
||||
@@ -383,19 +381,11 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
di.request.auth.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
|
||||
refreshTokenUrl: get(si.request, 'auth.oauth2.refreshTokenUrl', ''),
|
||||
username: get(si.request, 'auth.oauth2.username', ''),
|
||||
password: get(si.request, 'auth.oauth2.password', ''),
|
||||
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
||||
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
|
||||
scope: get(si.request, 'auth.oauth2.scope', ''),
|
||||
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
|
||||
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
scope: get(si.request, 'auth.oauth2.scope', '')
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@@ -404,35 +394,19 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
callbackUrl: get(si.request, 'auth.oauth2.callbackUrl', ''),
|
||||
authorizationUrl: get(si.request, 'auth.oauth2.authorizationUrl', ''),
|
||||
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
|
||||
refreshTokenUrl: get(si.request, 'auth.oauth2.refreshTokenUrl', ''),
|
||||
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
||||
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
|
||||
scope: get(si.request, 'auth.oauth2.scope', ''),
|
||||
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
|
||||
pkce: get(si.request, 'auth.oauth2.pkce', false),
|
||||
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
pkce: get(si.request, 'auth.oauth2.pkce', false)
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
di.request.auth.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
|
||||
refreshTokenUrl: get(si.request, 'auth.oauth2.refreshTokenUrl', ''),
|
||||
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
||||
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
|
||||
scope: get(si.request, 'auth.oauth2.scope', ''),
|
||||
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
|
||||
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
scope: get(si.request, 'auth.oauth2.scope', '')
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -979,15 +953,12 @@ export const getAllVariables = (collection, item) => {
|
||||
|
||||
const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])];
|
||||
|
||||
const oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials })
|
||||
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...oauth2CredentialVariables,
|
||||
...runtimeVariables,
|
||||
pathParams: {
|
||||
...pathParams
|
||||
@@ -1054,37 +1025,4 @@ const mergeVars = (collection, requestTreePath = []) => {
|
||||
folderVariables,
|
||||
requestVariables
|
||||
};
|
||||
};
|
||||
|
||||
export const getEnvVars = (environment = {}) => {
|
||||
const variables = environment.variables;
|
||||
if (!variables || !variables.length) {
|
||||
return {
|
||||
__name__: environment.name
|
||||
};
|
||||
}
|
||||
|
||||
const envVars = {};
|
||||
each(variables, (variable) => {
|
||||
if (variable.enabled) {
|
||||
envVars[variable.name] = variable.value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...envVars,
|
||||
__name__: environment.name
|
||||
};
|
||||
};
|
||||
|
||||
export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {
|
||||
let credentialsVariables = {};
|
||||
oauth2Credentials.forEach(({ credentialsId, credentials }) => {
|
||||
if (credentials) {
|
||||
Object.entries(credentials).forEach(([key, value]) => {
|
||||
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
|
||||
});
|
||||
}
|
||||
});
|
||||
return credentialsVariables;
|
||||
};
|
||||
};
|
||||
@@ -181,4 +181,4 @@ export const getEncoding = (headers) => {
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers?.['content-type'] || '');
|
||||
return charsetMatch?.[1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,66 +431,6 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
||||
value: authValues.value?.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it,
|
||||
placement: "header" //By default we are placing the apikey values in headers!
|
||||
}
|
||||
} else if (auth.type === 'oauth2'){
|
||||
const findValueUsingKey = (key) => {
|
||||
return auth?.oauth2?.find(v => v?.key == key)?.value || ''
|
||||
}
|
||||
const oauth2GrantTypeMaps = {
|
||||
'authorization_code_with_pkce': 'authorization_code',
|
||||
'authorization_code': 'authorization_code',
|
||||
'client_credentials': 'client_credentials',
|
||||
'password_credentials': 'password_credentials'
|
||||
}
|
||||
const grantType = oauth2GrantTypeMaps[findValueUsingKey('grant_type')] || 'authorization_code';
|
||||
if (grantType) {
|
||||
brunoRequestItem.request.auth.mode = 'oauth2';
|
||||
switch(grantType) {
|
||||
case 'authorization_code':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'authorization_code',
|
||||
authorizationUrl: findValueUsingKey('authUrl'),
|
||||
callbackUrl: findValueUsingKey('redirect_uri'),
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
pkce: Boolean(findValueUsingKey('grant_type') == 'authorization_code_with_pkce'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
case 'password_credentials':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'password',
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
username: findValueUsingKey('username'),
|
||||
password: findValueUsingKey('password'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
brunoRequestItem.request.auth.oauth2 = {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
|
||||
refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),
|
||||
clientId: findValueUsingKey('clientId'),
|
||||
clientSecret: findValueUsingKey('clientSecret'),
|
||||
scope: findValueUsingKey('scope'),
|
||||
state: findValueUsingKey('state'),
|
||||
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
|
||||
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
size: response.size,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
duration: response.duration,
|
||||
timeline: response.timeline
|
||||
duration: response.duration
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
@@ -37,7 +36,17 @@ const sendHttpRequest = async (item, collection, environment, runtimeVariables)
|
||||
export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
resolve({});
|
||||
ipcRenderer
|
||||
.invoke('send-collection-oauth2-request', collection, environment, runtimeVariables)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const clearOauth2Cache = async (uid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('clear-oauth2-cache', uid).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -147,13 +147,13 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
// todo: we have things happening in two places w.r.t basic auth
|
||||
// need to refactor this in the future
|
||||
// the request.auth (basic auth) object gets set inside the prepare-request.js file
|
||||
if (request.basicAuth) {
|
||||
const username = _interpolate(request.basicAuth.username) || '';
|
||||
const password = _interpolate(request.basicAuth.password) || '';
|
||||
if (request.auth) {
|
||||
const username = _interpolate(request.auth.username) || '';
|
||||
const password = _interpolate(request.auth.password) || '';
|
||||
|
||||
// use auth header based approach and delete the request.auth object
|
||||
request.headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
delete request.basicAuth;
|
||||
delete request.auth;
|
||||
}
|
||||
|
||||
if (request.awsv4config) {
|
||||
@@ -165,14 +165,12 @@ 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?.auth) delete request.auth;
|
||||
// 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;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
if (collectionAuth && request.auth?.mode === 'inherit') {
|
||||
if (collectionAuth.mode === 'basic') {
|
||||
axiosRequest.basicAuth = {
|
||||
axiosRequest.auth = {
|
||||
username: get(collectionAuth, 'basic.username'),
|
||||
password: get(collectionAuth, 'basic.password')
|
||||
};
|
||||
@@ -69,7 +69,7 @@ const prepareRequest = (item = {}, collection = {}) => {
|
||||
|
||||
if (request.auth && request.auth.mode !== 'inherit') {
|
||||
if (request.auth.mode === 'basic') {
|
||||
axiosRequest.basicAuth = {
|
||||
axiosRequest.auth = {
|
||||
username: get(request, 'auth.basic.username'),
|
||||
password: get(request, 'auth.basic.password')
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('prepare-request: prepareRequest', () => {
|
||||
|
||||
const result = prepareRequest(item, collection);
|
||||
const expected = { username: 'testUser', password: 'testPass123' };
|
||||
expect(result.basicAuth).toEqual(expected);
|
||||
expect(result.auth).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"version": "v1.38.1",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
|
||||
@@ -54,7 +54,6 @@ const jsonToCollectionBru = async (json, isFolder) => {
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
docs: _.get(json, 'docs', '')
|
||||
};
|
||||
|
||||
@@ -67,6 +66,10 @@ const jsonToCollectionBru = async (json, isFolder) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (!isFolder) {
|
||||
collectionBruJson.auth = _.get(json, 'request.auth', {});
|
||||
}
|
||||
|
||||
return _jsonToCollectionBru(collectionBruJson);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -24,7 +24,6 @@ const Watcher = require('./app/watcher');
|
||||
const { loadWindowState, saveBounds, saveMaximized } = require('./utils/window');
|
||||
const registerNotificationsIpc = require('./ipc/notifications');
|
||||
const registerGlobalEnvironmentsIpc = require('./ipc/global-environments');
|
||||
const { safeParseJSON, safeStringifyJSON } = require('./utils/common');
|
||||
|
||||
const lastOpenedCollections = new LastOpenedCollections();
|
||||
|
||||
@@ -161,16 +160,6 @@ app.on('ready', async () => {
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
let ogSend = mainWindow.webContents.send;
|
||||
mainWindow.webContents.send = function(channel, ...args) {
|
||||
return ogSend.apply(this, [channel, ...args?.map(_ => {
|
||||
// todo: replace this with @msgpack/msgpack encode/decode
|
||||
return safeParseJSON(safeStringifyJSON(_));
|
||||
})]);
|
||||
}
|
||||
});
|
||||
|
||||
// register all ipc handlers
|
||||
registerNetworkIpc(mainWindow);
|
||||
registerGlobalEnvironmentsIpc(mainWindow);
|
||||
|
||||
@@ -31,11 +31,6 @@ const { deleteCookiesForDomain, getDomainsWithCookies, addCookieForDomain, modif
|
||||
const EnvironmentSecretsStore = require('../store/env-secrets');
|
||||
const CollectionSecurityStore = require('../store/collection-security');
|
||||
const UiStateSnapshotStore = require('../store/ui-state-snapshot');
|
||||
const interpolateVars = require('./network/interpolate-vars');
|
||||
const { getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../utils/collection');
|
||||
const { getProcessEnvVars } = require('../store/process-env');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2');
|
||||
const { getCertsAndProxyConfig } = require('./network');
|
||||
const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
@@ -901,52 +896,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:fetch-oauth2-credentials', async (event, { itemUid, request, collection }) => {
|
||||
try {
|
||||
if (request.oauth2) {
|
||||
let requestCopy = _.cloneDeep(request);
|
||||
const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;
|
||||
const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const partialItem = { uid: itemUid };
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem);
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
mergeVars(collection, requestCopy, requestTreePath);
|
||||
}
|
||||
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
const certsAndProxyConfig = await getCertsAndProxyConfig({
|
||||
collectionUid,
|
||||
request: requestCopy,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
});
|
||||
const { oauth2: { grantType }} = requestCopy || {};
|
||||
let credentials, url, credentialsId;
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, forceFetch: true, certsAndProxyConfig }));
|
||||
break;
|
||||
case 'client_credentials':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, forceFetch: true, certsAndProxyConfig }));
|
||||
break;
|
||||
case 'password':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, forceFetch: true, certsAndProxyConfig }));
|
||||
break;
|
||||
}
|
||||
return { credentials, url, collectionUid, credentialsId, debugInfo };
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:load-request-via-worker', async (event, { collectionUid, pathname }) => {
|
||||
let fileStats;
|
||||
try {
|
||||
@@ -996,31 +945,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:refresh-oauth2-credentials', async (event, { request, collection }) => {
|
||||
try {
|
||||
if (request.oauth2) {
|
||||
let requestCopy = _.cloneDeep(request);
|
||||
const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;
|
||||
const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
const certsAndProxyConfig = await getCertsAndProxyConfig({
|
||||
collectionUid,
|
||||
request: requestCopy,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
});
|
||||
let { credentials, url, credentialsId, debugInfo } = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig });
|
||||
return { credentials, url, collectionUid, credentialsId, debugInfo };
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => {
|
||||
let fileStats;
|
||||
try {
|
||||
|
||||
@@ -8,15 +8,12 @@ const matchesCallbackUrl = (url, callbackUrl) => {
|
||||
const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let finalUrl = null;
|
||||
let debugInfo = {
|
||||
data: []
|
||||
};
|
||||
let currentMainRequest = null;
|
||||
|
||||
let allOpenWindows = BrowserWindow.getAllWindows();
|
||||
|
||||
// Close all windows except the main window (assumed to have id 1)
|
||||
let windowsExcludingMain = allOpenWindows.filter((w) => w.id !== 1);
|
||||
// main window id is '1'
|
||||
// get all other windows
|
||||
let windowsExcludingMain = allOpenWindows.filter((w) => w.id != 1);
|
||||
windowsExcludingMain.forEach((w) => {
|
||||
w.close();
|
||||
});
|
||||
@@ -30,104 +27,26 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
});
|
||||
window.on('ready-to-show', window.show.bind(window));
|
||||
|
||||
// Ensure the browser window complies with "SSL/TLS Certificate Verification" preference
|
||||
// We want browser window to comply with "SSL/TLS Certificate Verification" toggle in Preferences
|
||||
window.webContents.on('certificate-error', (event, url, error, certificate, callback) => {
|
||||
event.preventDefault();
|
||||
callback(!preferencesUtil.shouldVerifyTls());
|
||||
});
|
||||
|
||||
const { session: webSession } = window.webContents;
|
||||
|
||||
// Intercept request events and gather data
|
||||
webSession.webRequest.onBeforeRequest((details, callback) => {
|
||||
const { id: requestId, url, method, resourceType, frameId } = details;
|
||||
if (resourceType === 'mainFrame') {
|
||||
// This is a main frame request
|
||||
currentMainRequest = {
|
||||
requestId,
|
||||
resourceType,
|
||||
frameId,
|
||||
request: {
|
||||
url,
|
||||
method,
|
||||
headers: {},
|
||||
error: null
|
||||
},
|
||||
response: {
|
||||
headers: {},
|
||||
status: null,
|
||||
statusText: null,
|
||||
error: null
|
||||
},
|
||||
fromCache: false,
|
||||
completed: true,
|
||||
requests: [], // No sub-requests in this context
|
||||
};
|
||||
// Add to mainRequests
|
||||
|
||||
// pushing the currentMainRequest to debugInfo
|
||||
// the currentMainRequest will be further updated by object reference
|
||||
debugInfo.data.push(currentMainRequest);
|
||||
}
|
||||
|
||||
callback({ cancel: false });
|
||||
});
|
||||
|
||||
webSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
const { id: requestId, requestHeaders, method, url } = details;
|
||||
if (currentMainRequest?.requestId === requestId) {
|
||||
currentMainRequest.request = {
|
||||
url,
|
||||
headers: requestHeaders,
|
||||
method
|
||||
};
|
||||
}
|
||||
callback({ cancel: false, requestHeaders });
|
||||
});
|
||||
|
||||
webSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
const { id: requestId, url, statusCode, responseHeaders, method } = details;
|
||||
if (currentMainRequest?.requestId === requestId) {
|
||||
currentMainRequest.response = {
|
||||
url,
|
||||
method,
|
||||
status: statusCode,
|
||||
headers: responseHeaders
|
||||
};
|
||||
}
|
||||
callback({ cancel: false, responseHeaders });
|
||||
});
|
||||
|
||||
webSession.webRequest.onCompleted((details) => {
|
||||
const { id: requestId, fromCache } = details;
|
||||
if (currentMainRequest?.requestId === requestId) {
|
||||
currentMainRequest.completed = true;
|
||||
currentMainRequest.fromCache = fromCache;
|
||||
}
|
||||
});
|
||||
|
||||
webSession.webRequest.onErrorOccurred((details) => {
|
||||
const { id: requestId, error } = details;
|
||||
if (currentMainRequest?.requestId === requestId) {
|
||||
currentMainRequest.response.error = error;
|
||||
}
|
||||
});
|
||||
|
||||
function onWindowRedirect(url) {
|
||||
// Handle redirects as needed
|
||||
|
||||
// Check if redirect is to the callback URL and contains an authorization code
|
||||
// check if the redirect is to the callback URL and if it contains an authorization code
|
||||
if (matchesCallbackUrl(new URL(url), new URL(callbackUrl))) {
|
||||
if (!new URL(url).searchParams.has('code')) {
|
||||
reject(new Error('Invalid Callback URL: Does not contain an authorization code'));
|
||||
}
|
||||
finalUrl = url;
|
||||
window.close();
|
||||
}
|
||||
|
||||
// Handle OAuth error responses
|
||||
const urlObj = new URL(url);
|
||||
if (urlObj.searchParams.has('error')) {
|
||||
const error = urlObj.searchParams.get('error');
|
||||
const errorDescription = urlObj.searchParams.get('error_description');
|
||||
const errorUri = urlObj.searchParams.get('error_uri');
|
||||
if (url.match(/(error=).*/) || url.match(/(error_description=).*/) || url.match(/(error_uri=).*/)) {
|
||||
const _url = new URL(url);
|
||||
const error = _url.searchParams.get('error');
|
||||
const errorDescription = _url.searchParams.get('error_description');
|
||||
const errorUri = _url.searchParams.get('error_uri');
|
||||
let errorData = {
|
||||
message: 'Authorization Failed!',
|
||||
error,
|
||||
@@ -139,36 +58,13 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Update currentMainRequest when navigation occurs
|
||||
window.webContents.on('did-start-navigation', (event, url, isInPlace, isMainFrame) => {
|
||||
if (isMainFrame) {
|
||||
// Reset currentMainRequest since a new navigation is starting
|
||||
currentMainRequest = null;
|
||||
}
|
||||
});
|
||||
|
||||
window.webContents.on('did-navigate', (event, url) => {
|
||||
onWindowRedirect(url);
|
||||
});
|
||||
|
||||
window.webContents.on('will-redirect', (event, url) => {
|
||||
onWindowRedirect(url);
|
||||
});
|
||||
|
||||
window.on('close', () => {
|
||||
// Clean up listeners to prevent memory leaks
|
||||
window.webContents.removeAllListeners();
|
||||
webSession.webRequest.onBeforeRequest(null);
|
||||
webSession.webRequest.onBeforeSendHeaders(null);
|
||||
webSession.webRequest.onHeadersReceived(null);
|
||||
webSession.webRequest.onCompleted(null);
|
||||
webSession.webRequest.onErrorOccurred(null);
|
||||
|
||||
if (finalUrl) {
|
||||
try {
|
||||
const callbackUrlWithCode = new URL(finalUrl);
|
||||
const authorizationCode = callbackUrlWithCode.searchParams.get('code');
|
||||
return resolve({ authorizationCode, debugInfo });
|
||||
|
||||
return resolve({ authorizationCode });
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
@@ -177,10 +73,20 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// wait for the window to navigate to the callback url
|
||||
const didNavigateListener = (_, url) => {
|
||||
onWindowRedirect(url);
|
||||
};
|
||||
window.webContents.on('did-navigate', didNavigateListener);
|
||||
const willRedirectListener = (_, authorizeUrl) => {
|
||||
onWindowRedirect(authorizeUrl);
|
||||
};
|
||||
window.webContents.on('will-redirect', willRedirectListener);
|
||||
|
||||
try {
|
||||
await window.loadURL(authorizeUrl);
|
||||
} catch (error) {
|
||||
// Ignore ERR_ABORTED errors that occur during redirects
|
||||
// If browser redirects before load finished, loadURL throws an error with code ERR_ABORTED. This should be ignored.
|
||||
if (error.code === 'ERR_ABORTED') {
|
||||
console.debug('Ignoring ERR_ABORTED during authorizeUserInWindow');
|
||||
return;
|
||||
@@ -191,4 +97,4 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { authorizeUserInWindow, matchesCallbackUrl };
|
||||
module.exports = { authorizeUserInWindow, matchesCallbackUrl };
|
||||
|
||||
@@ -3,32 +3,11 @@ const Socket = require('net').Socket;
|
||||
const axios = require('axios');
|
||||
const connectionCache = new Map(); // Cache to store checkConnection() results
|
||||
const electronApp = require("electron");
|
||||
const { setupProxyAgents } = require('../../utils/proxy-util');
|
||||
const { addCookieToJar, getCookieStringForUrl } = require('../../utils/cookies');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { safeStringifyJSON } = require('../../utils/common');
|
||||
|
||||
const LOCAL_IPV6 = '::1';
|
||||
const LOCAL_IPV4 = '127.0.0.1';
|
||||
const LOCALHOST = 'localhost';
|
||||
const version = electronApp?.app?.getVersion()?.substring(1) ?? "";
|
||||
const redirectResponseCodes = [301, 302, 303, 307, 308];
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
let setCookieHeaders = [];
|
||||
if (headers['set-cookie']) {
|
||||
setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getTld = (hostname) => {
|
||||
if (!hostname) {
|
||||
@@ -70,16 +49,13 @@ const checkConnection = (host, port) =>
|
||||
* @see https://github.com/axios/axios/issues/695
|
||||
* @returns {axios.AxiosInstance}
|
||||
*/
|
||||
function makeAxiosInstance({
|
||||
proxyMode = 'off',
|
||||
proxyConfig = {},
|
||||
requestMaxRedirects = 5,
|
||||
httpsAgentRequestFields = {},
|
||||
interpolationOptions = {}
|
||||
} = {}) {
|
||||
function makeAxiosInstance() {
|
||||
/** @type {axios.AxiosInstance} */
|
||||
const instance = axios.create({
|
||||
transformRequest: function transformRequest(data, headers) {
|
||||
// doesn't apply the default transformRequest if the data is a string, so that axios doesn't add quotes see :
|
||||
// https://github.com/usebruno/bruno/issues/2043
|
||||
// https://github.com/axios/axios/issues/4034
|
||||
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
|
||||
const hasJSONContentType = contentType.includes('json');
|
||||
if (typeof data === 'string' && hasJSONContentType) {
|
||||
@@ -92,7 +68,6 @@ function makeAxiosInstance({
|
||||
return data;
|
||||
},
|
||||
proxy: false,
|
||||
maxRedirects: 0,
|
||||
headers: {
|
||||
"User-Agent": `bruno-runtime/${version}`
|
||||
}
|
||||
@@ -100,45 +75,6 @@ function makeAxiosInstance({
|
||||
|
||||
instance.interceptors.request.use(async (config) => {
|
||||
const url = URL.parse(config.url);
|
||||
config.metadata = config.metadata || {};
|
||||
config.metadata.startTime = new Date().getTime();
|
||||
const timeline = config.metadata.timeline || []
|
||||
|
||||
// Add initial request details to the timeline
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Preparing request to ${config.url}`,
|
||||
});
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Current time is ${new Date().toISOString()}`,
|
||||
});
|
||||
|
||||
// Add request method and headers
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'request',
|
||||
message: `${config.method.toUpperCase()} ${config.url}`,
|
||||
});
|
||||
Object.entries(config.headers).forEach(([key, value]) => {
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'requestHeader',
|
||||
message: `${key}: ${value}`,
|
||||
});
|
||||
});
|
||||
|
||||
// Add request data if available
|
||||
if (config.data) {
|
||||
let requestData = typeof config.data === 'string' ? config.data : JSON.stringify(config.data, null, 2);
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'requestData',
|
||||
message: requestData,
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve all *.localhost to localhost and check if it should use IPv6 or IPv4
|
||||
// RFC: 6761 section 6.3 (https://tools.ietf.org/html/rfc6761#section-6.3)
|
||||
@@ -155,244 +91,21 @@ function makeAxiosInstance({
|
||||
}
|
||||
|
||||
config.headers['request-start-time'] = Date.now();
|
||||
|
||||
const agentOptions = {
|
||||
...httpsAgentRequestFields,
|
||||
keepAlive: true,
|
||||
};
|
||||
|
||||
try {
|
||||
// Now call setupProxyAgents and pass the timeline
|
||||
setupProxyAgents({
|
||||
requestConfig: config,
|
||||
proxyMode: proxyMode, // 'on', 'off', or 'system', depending on your settings
|
||||
proxyConfig: proxyConfig,
|
||||
httpsAgentRequestFields: agentOptions,
|
||||
interpolationOptions: interpolationOptions, // Provide your interpolation options
|
||||
timeline,
|
||||
});
|
||||
}
|
||||
catch(err) {
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: err?.message,
|
||||
});
|
||||
}
|
||||
config.metadata.timeline = timeline;
|
||||
return config;
|
||||
});
|
||||
|
||||
let redirectCount = 0
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
let timeline;
|
||||
const end = Date.now();
|
||||
const start = response.config.headers['request-start-time'];
|
||||
response.headers['request-duration'] = end - start;
|
||||
redirectCount = 0;
|
||||
|
||||
const config = response.config;
|
||||
timeline = config?.metadata?.timeline || []
|
||||
const duration = end - config?.metadata.startTime;
|
||||
|
||||
const httpVersion = response?.request?.res?.httpVersion || response?.httpVersion;
|
||||
if (httpVersion?.startsWith('2')) {
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Using HTTP/2, server supports multiplexing`,
|
||||
});
|
||||
}
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'response',
|
||||
message: `HTTP/${httpVersion || '1.1'} ${response.status} ${response.statusText}`,
|
||||
});
|
||||
|
||||
Object.entries(response.headers).forEach(([key, value]) => {
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'responseHeader',
|
||||
message: `${key}: ${value}`,
|
||||
});
|
||||
});
|
||||
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Request completed in ${duration} ms`,
|
||||
});
|
||||
response.timeline = timeline;
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
const config = error.config;
|
||||
const timeline = config?.metadata?.timeline || [];
|
||||
timeline?.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: 'there was an error executing the request!'
|
||||
});
|
||||
if (error.response) {
|
||||
const end = Date.now();
|
||||
const start = error.config.headers['request-start-time'];
|
||||
error.response.headers['request-duration'] = end - start;
|
||||
const duration = end - config?.metadata?.startTime;
|
||||
if (error.response && redirectResponseCodes.includes(error.response.status)) {
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'response',
|
||||
message: `HTTP/${error.response.httpVersion || '1.1'} ${error.response.status} ${error.response.statusText}`,
|
||||
});
|
||||
Object.entries(error.response.headers).forEach(([key, value]) => {
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'responseHeader',
|
||||
message: `${key}: ${value}`,
|
||||
});
|
||||
});
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Request completed in ${duration} ms`,
|
||||
});
|
||||
|
||||
// Attach the timeline to the response
|
||||
error.response.timeline = timeline;
|
||||
|
||||
if (redirectCount >= requestMaxRedirects) {
|
||||
const errorResponseData = error.response.data;
|
||||
const dataBuffer = Buffer.isBuffer(errorResponseData) ? errorResponseData : Buffer.from(errorResponseData);
|
||||
timeline?.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: safeStringifyJSON(errorResponseData?.toString?.())
|
||||
});
|
||||
return {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: errorResponseData?.toString?.(),
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
duration: error.response.headers.get('request-duration') ?? 0,
|
||||
timeline: error.response.timeline
|
||||
};
|
||||
}
|
||||
|
||||
// Increase redirect count
|
||||
redirectCount++;
|
||||
|
||||
const locationHeader = error.response.headers.location;
|
||||
let redirectUrl = locationHeader;
|
||||
|
||||
// Handle relative URLs by resolving them against the original request URL
|
||||
if (locationHeader && !locationHeader.match(/^https?:\/\//i)) {
|
||||
// It's a relative URL, resolve it against the original URL
|
||||
redirectUrl = URL.resolve(error.config.url, locationHeader);
|
||||
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Resolving relative redirect URL: ${locationHeader} → ${redirectUrl}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
saveCookies(redirectUrl, error.response.headers);
|
||||
}
|
||||
|
||||
// Create a new request config for the redirect
|
||||
const requestConfig = {
|
||||
...error.config,
|
||||
url: redirectUrl,
|
||||
headers: {
|
||||
...error.config.headers,
|
||||
},
|
||||
};
|
||||
|
||||
if (preferencesUtil.shouldSendCookies()) {
|
||||
const cookieString = getCookieStringForUrl(redirectUrl);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
requestConfig.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
setupProxyAgents({
|
||||
requestConfig,
|
||||
proxyMode,
|
||||
proxyConfig,
|
||||
httpsAgentRequestFields,
|
||||
interpolationOptions,
|
||||
timeline
|
||||
});
|
||||
|
||||
requestConfig.metadata.timeline = timeline;
|
||||
// Make the redirected request
|
||||
return instance(requestConfig);
|
||||
}
|
||||
else {
|
||||
const errorResponseData = error.response.data;
|
||||
const dataBuffer = Buffer.isBuffer(errorResponseData) ? errorResponseData : Buffer.from(errorResponseData);
|
||||
Object.entries(error?.response?.headers || {}).forEach(([key, value]) => {
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'responseHeader',
|
||||
message: `${key}: ${value}`,
|
||||
});
|
||||
});
|
||||
timeline?.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: safeStringifyJSON(errorResponseData?.toString?.())
|
||||
});
|
||||
error?.cause && timeline?.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: safeStringifyJSON(error?.cause)
|
||||
});
|
||||
error?.errors && timeline?.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: safeStringifyJSON(error?.errors)
|
||||
});
|
||||
return {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: errorResponseData?.toString?.(),
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
duration: error.response.headers.get('request-duration') ?? 0,
|
||||
timeline
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (error?.code) {
|
||||
Object.entries(error?.response?.headers || {}).forEach(([key, value]) => {
|
||||
timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'responseHeader',
|
||||
message: `${key}: ${value}`,
|
||||
});
|
||||
});
|
||||
timeline?.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: safeStringifyJSON(error?.cause)
|
||||
});
|
||||
timeline?.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: safeStringifyJSON(error?.errors)
|
||||
});
|
||||
return {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
headers: error?.config?.headers,
|
||||
data: 'request failed, check timeline network logs',
|
||||
timeline
|
||||
};
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
76
packages/bruno-electron/src/ipc/network/helper.js
Normal file
76
packages/bruno-electron/src/ipc/network/helper.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const { each, filter } = require('lodash');
|
||||
|
||||
const sortCollection = (collection) => {
|
||||
const items = collection.items || [];
|
||||
let folderItems = filter(items, (item) => item.type === 'folder');
|
||||
let requestItems = filter(items, (item) => item.type !== 'folder');
|
||||
|
||||
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
|
||||
|
||||
collection.items = folderItems.concat(requestItems);
|
||||
|
||||
each(folderItems, (item) => {
|
||||
sortCollection(item);
|
||||
});
|
||||
};
|
||||
|
||||
const sortFolder = (folder = {}) => {
|
||||
const items = folder.items || [];
|
||||
let folderItems = filter(items, (item) => item.type === 'folder');
|
||||
let requestItems = filter(items, (item) => item.type !== 'folder');
|
||||
|
||||
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
|
||||
|
||||
folder.items = folderItems.concat(requestItems);
|
||||
|
||||
each(folderItems, (item) => {
|
||||
sortFolder(item);
|
||||
});
|
||||
|
||||
return folder;
|
||||
};
|
||||
|
||||
const findItemInCollection = (collection, itemId) => {
|
||||
let item = null;
|
||||
|
||||
if (collection.uid === itemId) {
|
||||
return collection;
|
||||
}
|
||||
|
||||
if (collection.items && collection.items.length) {
|
||||
collection.items.forEach((item) => {
|
||||
if (item.uid === itemId) {
|
||||
item = item;
|
||||
} else if (item.type === 'folder') {
|
||||
item = findItemInCollection(item, itemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
const getAllRequestsInFolderRecursively = (folder = {}) => {
|
||||
let requests = [];
|
||||
|
||||
if (folder.items && folder.items.length) {
|
||||
folder.items.forEach((item) => {
|
||||
if (item.type !== 'folder') {
|
||||
requests.push(item);
|
||||
} else {
|
||||
requests = requests.concat(getAllRequestsInFolderRecursively(item));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return requests;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sortCollection,
|
||||
sortFolder,
|
||||
findItemInCollection,
|
||||
getAllRequestsInFolderRecursively
|
||||
};
|
||||
@@ -1,35 +1,90 @@
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const qs = require('qs');
|
||||
const https = require('https');
|
||||
const tls = require('tls');
|
||||
const axios = require('axios');
|
||||
const path = require('path');
|
||||
const decomment = require('decomment');
|
||||
const fs = require('fs');
|
||||
const tls = require('tls');
|
||||
const contentDispositionParser = require('content-disposition');
|
||||
const mime = require('mime-types');
|
||||
const FormData = require('form-data');
|
||||
const { ipcMain } = require('electron');
|
||||
const { each, get, extend, cloneDeep } = require('lodash');
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
const { resolveAwsV4Credentials, addAwsV4Interceptor } = require('./awsv4auth-helper');
|
||||
const { addDigestInterceptor } = require('./digestauth-helper');
|
||||
const prepareRequest = require('./prepare-request');
|
||||
const prepareCollectionRequest = require('./prepare-collection-request');
|
||||
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
|
||||
const { prepareRequest } = require('./prepare-request');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
|
||||
const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse } = require('../../utils/common');
|
||||
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
|
||||
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
|
||||
const { createFormData } = require('../../utils/form-data');
|
||||
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('../../utils/oauth2');
|
||||
const { uuid } = require('../../utils/common');
|
||||
const interpolateVars = require('./interpolate-vars');
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
|
||||
const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
|
||||
const { addDigestInterceptor } = require('./digestauth-helper');
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
|
||||
const { chooseFileToSave, writeFile } = require('../../utils/filesystem');
|
||||
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
|
||||
const {
|
||||
resolveOAuth2AuthorizationCodeAccessToken,
|
||||
transformClientCredentialsRequest,
|
||||
transformPasswordCredentialsRequest
|
||||
} = require('./oauth2-helper');
|
||||
const Oauth2Store = require('../../store/oauth2');
|
||||
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 {
|
||||
return JSON.stringify(data);
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const safeParseJSON = (data) => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const getEnvVars = (environment = {}) => {
|
||||
const variables = environment.variables;
|
||||
if (!variables || !variables.length) {
|
||||
return {
|
||||
__name__: environment.name
|
||||
};
|
||||
}
|
||||
|
||||
const envVars = {};
|
||||
each(variables, (variable) => {
|
||||
if (variable.enabled) {
|
||||
envVars[variable.name] = variable.value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...envVars,
|
||||
__name__: environment.name
|
||||
};
|
||||
};
|
||||
|
||||
const getJsSandboxRuntime = (collection) => {
|
||||
const securityConfig = get(collection, 'securityConfig', {});
|
||||
return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
|
||||
};
|
||||
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
@@ -47,19 +102,18 @@ const saveCookies = (url, headers) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getJsSandboxRuntime = (collection) => {
|
||||
const securityConfig = get(collection, 'securityConfig', {});
|
||||
return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
|
||||
};
|
||||
|
||||
const getCertsAndProxyConfig = async ({
|
||||
const configureRequest = async (
|
||||
collectionUid,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
}) => {
|
||||
) => {
|
||||
if (!protocolRegex.test(request.url)) {
|
||||
request.url = `http://${request.url}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors
|
||||
* @see https://github.com/nodejs/node/pull/43522 keepAlive was changed to true globally on Node v19+
|
||||
@@ -149,106 +203,115 @@ const getCertsAndProxyConfig = async ({
|
||||
proxyConfig = preferencesUtil.getGlobalProxyConfig();
|
||||
proxyMode = get(proxyConfig, 'mode', 'off');
|
||||
}
|
||||
|
||||
return { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions };
|
||||
}
|
||||
|
||||
const configureRequest = async (
|
||||
collectionUid,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
) => {
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
if (!protocolRegex.test(request.url)) {
|
||||
request.url = `http://${request.url}`;
|
||||
if (proxyMode === 'on') {
|
||||
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
|
||||
if (shouldProxy) {
|
||||
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
|
||||
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
|
||||
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
|
||||
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
|
||||
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
||||
}
|
||||
if (socksEnabled) {
|
||||
request.httpsAgent = new SocksProxyAgent(
|
||||
proxyUri,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
request.httpAgent = new SocksProxyAgent(proxyUri);
|
||||
} else {
|
||||
request.httpsAgent = new PatchedHttpsProxyAgent(
|
||||
proxyUri,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
request.httpAgent = new HttpProxyAgent(proxyUri);
|
||||
}
|
||||
} else {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
} else if (proxyMode === 'system') {
|
||||
const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables();
|
||||
const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || '');
|
||||
if (shouldUseSystemProxy) {
|
||||
try {
|
||||
if (http_proxy?.length) {
|
||||
new URL(http_proxy);
|
||||
request.httpAgent = new HttpProxyAgent(http_proxy);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid system http_proxy');
|
||||
}
|
||||
try {
|
||||
if (https_proxy?.length) {
|
||||
new URL(https_proxy);
|
||||
request.httpsAgent = new PatchedHttpsProxyAgent(
|
||||
https_proxy,
|
||||
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid system https_proxy');
|
||||
}
|
||||
} else {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||
request.httpsAgent = new https.Agent({
|
||||
...httpsAgentRequestFields
|
||||
});
|
||||
}
|
||||
|
||||
const certsAndProxyConfig = await getCertsAndProxyConfig({
|
||||
collectionUid,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
});
|
||||
|
||||
let requestMaxRedirects = request.maxRedirects
|
||||
request.maxRedirects = 0
|
||||
let axiosInstance = makeAxiosInstance();
|
||||
|
||||
// Set default value for requestMaxRedirects if not explicitly set
|
||||
if (requestMaxRedirects === undefined) {
|
||||
requestMaxRedirects = 5; // Default to 5 redirects
|
||||
}
|
||||
|
||||
let { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
let axiosInstance = makeAxiosInstance({
|
||||
proxyMode,
|
||||
proxyConfig,
|
||||
requestMaxRedirects,
|
||||
httpsAgentRequestFields,
|
||||
interpolationOptions
|
||||
});
|
||||
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
|
||||
delete request.ntlmConfig;
|
||||
}
|
||||
|
||||
|
||||
if (request.oauth2) {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey } = {} } = requestCopy || {};
|
||||
let credentials, credentialsId;
|
||||
switch (grantType) {
|
||||
switch (request?.oauth2?.grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
}
|
||||
catch(error) {}
|
||||
}
|
||||
const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } =
|
||||
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy, collectionUid);
|
||||
request.method = 'POST';
|
||||
request.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
request.data = authorizationCodeData;
|
||||
request.url = authorizationCodeAccessTokenUrl;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
}
|
||||
catch(error) {}
|
||||
}
|
||||
const { data: clientCredentialsData, url: clientCredentialsAccessTokenUrl } =
|
||||
await transformClientCredentialsRequest(requestCopy);
|
||||
request.method = 'POST';
|
||||
request.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
request.data = clientCredentialsData;
|
||||
request.url = clientCredentialsAccessTokenUrl;
|
||||
break;
|
||||
case 'password':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
|
||||
request.url = url?.toString();
|
||||
}
|
||||
catch(error) {}
|
||||
}
|
||||
const { data: passwordData, url: passwordAccessTokenUrl } = await transformPasswordCredentialsRequest(
|
||||
requestCopy
|
||||
);
|
||||
request.method = 'POST';
|
||||
request.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
request.data = passwordData;
|
||||
request.url = passwordAccessTokenUrl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -317,6 +380,33 @@ const configureRequest = async (
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
|
||||
const charsetValue = charsetMatch?.[1];
|
||||
const dataBuffer = Buffer.from(response.data);
|
||||
// Overwrite the original data for backwards compatibility
|
||||
let data;
|
||||
if (iconv.encodingExists(charsetValue)) {
|
||||
data = iconv.decode(dataBuffer, charsetValue);
|
||||
} else {
|
||||
data = iconv.decode(dataBuffer, 'utf-8');
|
||||
}
|
||||
// Try to parse response to JSON, this can quietly fail
|
||||
try {
|
||||
// Filter out ZWNBSP character
|
||||
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
||||
data = data.replace(/^\uFEFF/, '');
|
||||
if (!disableParsingResponseJson) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return { data, dataBuffer };
|
||||
};
|
||||
|
||||
|
||||
const registerNetworkIpc = (mainWindow) => {
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
@@ -557,38 +647,22 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
);
|
||||
const requestData = request.mode == 'file'? "<request body redacted>": (typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data));
|
||||
let requestSent = {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
if (requestData) {
|
||||
requestSent.dataBuffer = Buffer.from(requestData);
|
||||
}
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'request-sent',
|
||||
requestSent,
|
||||
requestSent: {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
data: request.mode == 'file'? "<request body redacted>": safeParseJSON(safeStringifyJSON(request.data)) ,
|
||||
timestamp: Date.now()
|
||||
},
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
requestUid,
|
||||
cancelTokenUid
|
||||
});
|
||||
|
||||
if (request?.oauth2Credentials) {
|
||||
mainWindow.webContents.send('main:credentials-update', {
|
||||
credentials: request?.oauth2Credentials?.credentials,
|
||||
url: request?.oauth2Credentials?.url,
|
||||
collectionUid,
|
||||
credentialsId: request?.oauth2Credentials?.credentialsId,
|
||||
...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo,
|
||||
});
|
||||
}
|
||||
|
||||
let response, responseTime;
|
||||
try {
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
@@ -737,8 +811,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
data: response.data,
|
||||
dataBuffer: dataBuffer.toString('base64'),
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
duration: responseTime ?? 0,
|
||||
timeline: response.timeline
|
||||
duration: responseTime ?? 0
|
||||
};
|
||||
} catch (error) {
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
@@ -755,11 +828,86 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
return await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false });
|
||||
});
|
||||
|
||||
ipcMain.handle('clear-oauth2-cache', async (event, uid, url, credentialsId) => {
|
||||
ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, runtimeVariables) => {
|
||||
try {
|
||||
const collectionUid = collection.uid;
|
||||
const collectionPath = collection.pathname;
|
||||
const requestUid = uuid();
|
||||
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const _request = collectionRoot?.request;
|
||||
const request = prepareCollectionRequest(_request, collection, collectionPath);
|
||||
request.__bruno__executionMode = 'standalone';
|
||||
const envVars = getEnvVars(environment);
|
||||
const processEnvVars = getProcessEnvVars(collectionUid);
|
||||
const brunoConfig = getBrunoConfig(collectionUid);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
|
||||
await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
|
||||
interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars);
|
||||
const axiosInstance = await configureRequest(
|
||||
collection.uid,
|
||||
request,
|
||||
envVars,
|
||||
collection.runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
);
|
||||
|
||||
try {
|
||||
response = await axiosInstance(request);
|
||||
} catch (error) {
|
||||
if (error?.response) {
|
||||
response = error.response;
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
response.data = data;
|
||||
|
||||
await runPostResponse(
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('clear-oauth2-cache', async (event, uid) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const oauth2Store = new Oauth2Store();
|
||||
oauth2Store.clearSessionIdOfCollection({ collectionUid: uid, url, credentialsId });
|
||||
oauth2Store.clearSessionIdOfCollection(uid);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(new Error('Could not clear oauth2 cache'));
|
||||
@@ -992,23 +1140,17 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const requestData = request.mode == 'file'? "<request body redacted>": (typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data));
|
||||
let requestSent = {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
data: requestData
|
||||
}
|
||||
if (requestData) {
|
||||
requestSent.dataBuffer = Buffer.from(requestData);
|
||||
}
|
||||
|
||||
// todo:
|
||||
// i have no clue why electron can't send the request object
|
||||
// without safeParseJSON(safeStringifyJSON(request.data))
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'request-sent',
|
||||
requestSent,
|
||||
requestSent: {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
data: safeParseJSON(safeStringifyJSON(request.data))
|
||||
},
|
||||
...eventData
|
||||
});
|
||||
|
||||
@@ -1022,15 +1164,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
collectionPath
|
||||
);
|
||||
|
||||
if (request?.oauth2Credentials) {
|
||||
mainWindow.webContents.send('main:credentials-update', {
|
||||
credentials: request?.oauth2Credentials?.credentials,
|
||||
url: request?.oauth2Credentials?.url,
|
||||
collectionUid,
|
||||
credentialsId: request?.oauth2Credentials?.credentialsId
|
||||
});
|
||||
}
|
||||
|
||||
timeStart = Date.now();
|
||||
let response, responseTime;
|
||||
try {
|
||||
@@ -1311,4 +1444,3 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
module.exports = registerNetworkIpc;
|
||||
module.exports.configureRequest = configureRequest;
|
||||
module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig;
|
||||
|
||||
@@ -15,7 +15,6 @@ const getContentType = (headers = {}) => {
|
||||
|
||||
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
@@ -46,7 +45,6 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...oauth2CredentialVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
@@ -153,64 +151,62 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
// todo: we have things happening in two places w.r.t basic auth
|
||||
// need to refactor this in the future
|
||||
// the request.auth (basic auth) object gets set inside the prepare-request.js file
|
||||
if (request.basicAuth) {
|
||||
const username = _interpolate(request.basicAuth.username) || '';
|
||||
const password = _interpolate(request.basicAuth.password) || '';
|
||||
if (request.auth) {
|
||||
const username = _interpolate(request.auth.username) || '';
|
||||
const password = _interpolate(request.auth.password) || '';
|
||||
// use auth header based approach and delete the request.auth object
|
||||
request.headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
delete request.basicAuth;
|
||||
delete request.auth;
|
||||
}
|
||||
|
||||
if (request?.oauth2?.grantType) {
|
||||
let username, password, scope, clientId, clientSecret;
|
||||
switch (request.oauth2.grantType) {
|
||||
case 'password':
|
||||
username = _interpolate(request.oauth2.username) || '';
|
||||
password = _interpolate(request.oauth2.password) || '';
|
||||
clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
|
||||
request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';
|
||||
request.oauth2.username = _interpolate(request.oauth2.username) || '';
|
||||
request.oauth2.password = _interpolate(request.oauth2.password) || '';
|
||||
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
|
||||
request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';
|
||||
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
|
||||
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
|
||||
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
|
||||
request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);
|
||||
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
|
||||
request.oauth2.username = username;
|
||||
request.oauth2.password = password;
|
||||
request.oauth2.clientId = clientId;
|
||||
request.oauth2.clientSecret = clientSecret;
|
||||
request.oauth2.scope = scope;
|
||||
request.data = {
|
||||
grant_type: 'password',
|
||||
username,
|
||||
password,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || '';
|
||||
request.oauth2.authorizationUrl = _interpolate(request.oauth2.authorizationUrl) || '';
|
||||
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
|
||||
request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';
|
||||
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.state = _interpolate(request.oauth2.state) || '';
|
||||
request.oauth2.pkce = _interpolate(request.oauth2.pkce) || false;
|
||||
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
|
||||
request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';
|
||||
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
|
||||
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
|
||||
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
|
||||
request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);
|
||||
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
|
||||
break;
|
||||
case 'client_credentials':
|
||||
clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
|
||||
request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';
|
||||
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
|
||||
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
|
||||
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
|
||||
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
|
||||
request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';
|
||||
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
|
||||
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
|
||||
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
|
||||
request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);
|
||||
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
|
||||
request.oauth2.clientId = clientId;
|
||||
request.oauth2.clientSecret = clientSecret;
|
||||
request.oauth2.scope = scope;
|
||||
request.data = {
|
||||
grant_type: 'client_credentials',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
scope
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -247,8 +243,6 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || '';
|
||||
}
|
||||
|
||||
if(request?.auth) delete request.auth;
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
|
||||
129
packages/bruno-electron/src/ipc/network/oauth2-helper.js
Normal file
129
packages/bruno-electron/src/ipc/network/oauth2-helper.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const { get, cloneDeep } = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const { authorizeUserInWindow } = require('./authorize-user-in-window');
|
||||
const Oauth2Store = require('../../store/oauth2');
|
||||
|
||||
const generateCodeVerifier = () => {
|
||||
return crypto.randomBytes(22).toString('hex');
|
||||
};
|
||||
|
||||
const generateCodeChallenge = (codeVerifier) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(codeVerifier);
|
||||
const base64Hash = hash.digest('base64');
|
||||
return base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
};
|
||||
|
||||
// AUTHORIZATION CODE
|
||||
|
||||
const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) => {
|
||||
let codeVerifier = generateCodeVerifier();
|
||||
let codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
|
||||
let requestCopy = cloneDeep(request);
|
||||
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth;
|
||||
const data = {
|
||||
grant_type: 'authorization_code',
|
||||
code: authorizationCode,
|
||||
redirect_uri: callbackUrl,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
};
|
||||
if (pkce) {
|
||||
data['code_verifier'] = codeVerifier;
|
||||
}
|
||||
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
return {
|
||||
data,
|
||||
url
|
||||
};
|
||||
};
|
||||
|
||||
const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { oauth2 } = request;
|
||||
const { callbackUrl, clientId, authorizationUrl, scope, state, pkce } = oauth2;
|
||||
|
||||
const authorizationUrlWithQueryParams = new URL(authorizationUrl);
|
||||
authorizationUrlWithQueryParams.searchParams.append('response_type', 'code');
|
||||
authorizationUrlWithQueryParams.searchParams.append('client_id', clientId);
|
||||
if (callbackUrl) {
|
||||
authorizationUrlWithQueryParams.searchParams.append('redirect_uri', callbackUrl);
|
||||
}
|
||||
if (scope) {
|
||||
authorizationUrlWithQueryParams.searchParams.append('scope', scope);
|
||||
}
|
||||
if (pkce) {
|
||||
authorizationUrlWithQueryParams.searchParams.append('code_challenge', codeChallenge);
|
||||
authorizationUrlWithQueryParams.searchParams.append('code_challenge_method', 'S256');
|
||||
}
|
||||
if (state) {
|
||||
authorizationUrlWithQueryParams.searchParams.append('state', state);
|
||||
}
|
||||
try {
|
||||
const oauth2Store = new Oauth2Store();
|
||||
const { authorizationCode } = await authorizeUserInWindow({
|
||||
authorizeUrl: authorizationUrlWithQueryParams.toString(),
|
||||
callbackUrl,
|
||||
session: oauth2Store.getSessionIdOfCollection(collectionUid)
|
||||
});
|
||||
resolve({ authorizationCode });
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// CLIENT CREDENTIALS
|
||||
|
||||
const transformClientCredentialsRequest = async (request) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, scope } = oAuth;
|
||||
const data = {
|
||||
grant_type: 'client_credentials',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
};
|
||||
if (scope) {
|
||||
data.scope = scope;
|
||||
}
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
return {
|
||||
data,
|
||||
url
|
||||
};
|
||||
};
|
||||
|
||||
// PASSWORD CREDENTIALS
|
||||
|
||||
const transformPasswordCredentialsRequest = async (request) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { username, password, clientId, clientSecret, scope } = oAuth;
|
||||
const data = {
|
||||
grant_type: 'password',
|
||||
username,
|
||||
password,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
};
|
||||
if (scope) {
|
||||
data.scope = scope;
|
||||
}
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
return {
|
||||
data,
|
||||
url
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
resolveOAuth2AuthorizationCodeAccessToken,
|
||||
getOAuth2AuthorizationCode,
|
||||
transformClientCredentialsRequest,
|
||||
transformPasswordCredentialsRequest
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
const { get, each } = require('lodash');
|
||||
const { setAuthHeaders } = require('./prepare-request');
|
||||
|
||||
const prepareCollectionRequest = (request, collection) => {
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
let url = request.url;
|
||||
|
||||
// collection headers
|
||||
each(get(collectionRoot, 'request.headers', []), (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
each(request.headers, (h) => {
|
||||
if (h.enabled) {
|
||||
headers[h.name] = h.value;
|
||||
if (h.name.toLowerCase() === 'content-type') {
|
||||
contentTypeDefined = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let axiosRequest = {
|
||||
mode: request?.body?.mode,
|
||||
method: request.method,
|
||||
url,
|
||||
headers,
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
|
||||
|
||||
axiosRequest.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
|
||||
|
||||
if (request.script) {
|
||||
axiosRequest.script = request.script;
|
||||
}
|
||||
|
||||
axiosRequest.vars = request.vars;
|
||||
|
||||
axiosRequest.method = 'POST';
|
||||
|
||||
return axiosRequest;
|
||||
};
|
||||
|
||||
module.exports = prepareCollectionRequest;
|
||||
@@ -2,11 +2,12 @@ const { get, each, filter, find } = require('lodash');
|
||||
const decomment = require('decomment');
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs/promises');
|
||||
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials, mergeAuth } = require('../../utils/collection');
|
||||
const { buildFormUrlEncodedPayload } = require('../../utils/form-data');
|
||||
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection');
|
||||
const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
|
||||
const path = require('node:path');
|
||||
|
||||
const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
if (collectionAuth && request.auth.mode === 'inherit') {
|
||||
switch (collectionAuth.mode) {
|
||||
@@ -21,7 +22,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
};
|
||||
break;
|
||||
case 'basic':
|
||||
axiosRequest.basicAuth = {
|
||||
axiosRequest.auth = {
|
||||
username: get(collectionAuth, 'basic.username'),
|
||||
password: get(collectionAuth, 'basic.password')
|
||||
};
|
||||
@@ -41,7 +42,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
password: get(collectionAuth, 'ntlm.password'),
|
||||
domain: get(collectionAuth, 'ntlm.domain')
|
||||
};
|
||||
break;
|
||||
break;
|
||||
case 'wsse':
|
||||
const username = get(request, 'auth.wsse.username', '');
|
||||
const password = get(request, 'auth.wsse.password', '');
|
||||
@@ -68,68 +69,6 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;
|
||||
}
|
||||
break;
|
||||
case 'oauth2':
|
||||
const grantType = get(collectionAuth, 'oauth2.grantType');
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
|
||||
refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
|
||||
username: get(collectionAuth, 'oauth2.username'),
|
||||
password: get(collectionAuth, 'oauth2.password'),
|
||||
clientId: get(collectionAuth, 'oauth2.clientId'),
|
||||
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
|
||||
scope: get(collectionAuth, 'oauth2.scope'),
|
||||
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
|
||||
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
|
||||
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
callbackUrl: get(collectionAuth, 'oauth2.callbackUrl'),
|
||||
authorizationUrl: get(collectionAuth, 'oauth2.authorizationUrl'),
|
||||
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
|
||||
refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
|
||||
clientId: get(collectionAuth, 'oauth2.clientId'),
|
||||
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
|
||||
scope: get(collectionAuth, 'oauth2.scope'),
|
||||
state: get(collectionAuth, 'oauth2.state'),
|
||||
pkce: get(collectionAuth, 'oauth2.pkce'),
|
||||
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
|
||||
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
|
||||
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
|
||||
refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
|
||||
clientId: get(collectionAuth, 'oauth2.clientId'),
|
||||
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
|
||||
scope: get(collectionAuth, 'oauth2.scope'),
|
||||
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
|
||||
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
|
||||
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
|
||||
};
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +85,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
};
|
||||
break;
|
||||
case 'basic':
|
||||
axiosRequest.basicAuth = {
|
||||
axiosRequest.auth = {
|
||||
username: get(request, 'auth.basic.username'),
|
||||
password: get(request, 'auth.basic.password')
|
||||
};
|
||||
@@ -166,6 +105,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
password: get(request, 'auth.ntlm.password'),
|
||||
domain: get(request, 'auth.ntlm.domain')
|
||||
};
|
||||
break;
|
||||
case 'oauth2':
|
||||
const grantType = get(request, 'auth.oauth2.grantType');
|
||||
switch (grantType) {
|
||||
@@ -173,19 +113,11 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
|
||||
username: get(request, 'auth.oauth2.username'),
|
||||
password: get(request, 'auth.oauth2.password'),
|
||||
clientId: get(request, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret'),
|
||||
scope: get(request, 'auth.oauth2.scope'),
|
||||
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
|
||||
credentialsId: get(request, 'auth.oauth2.credentialsId'),
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
|
||||
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
|
||||
scope: get(request, 'auth.oauth2.scope')
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@@ -194,36 +126,20 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
callbackUrl: get(request, 'auth.oauth2.callbackUrl'),
|
||||
authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'),
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
|
||||
clientId: get(request, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret'),
|
||||
scope: get(request, 'auth.oauth2.scope'),
|
||||
state: get(request, 'auth.oauth2.state'),
|
||||
pkce: get(request, 'auth.oauth2.pkce'),
|
||||
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
|
||||
credentialsId: get(request, 'auth.oauth2.credentialsId'),
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
|
||||
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
|
||||
pkce: get(request, 'auth.oauth2.pkce')
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
axiosRequest.oauth2 = {
|
||||
grantType: grantType,
|
||||
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
|
||||
refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
|
||||
clientId: get(request, 'auth.oauth2.clientId'),
|
||||
clientSecret: get(request, 'auth.oauth2.clientSecret'),
|
||||
scope: get(request, 'auth.oauth2.scope'),
|
||||
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
|
||||
credentialsId: get(request, 'auth.oauth2.credentialsId'),
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
|
||||
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
|
||||
scope: get(request, 'auth.oauth2.scope')
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -262,7 +178,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
|
||||
const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
const collectionRoot = collection?.draft ? get(collection, 'draft', {}) : get(collection, 'root', {});
|
||||
const collectionRoot = get(collection, 'root', {});
|
||||
const collectionPath = collection?.pathname;
|
||||
const headers = {};
|
||||
let contentTypeDefined = false;
|
||||
@@ -281,9 +197,7 @@ const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
mergeHeaders(collection, request, requestTreePath);
|
||||
mergeScripts(collection, request, requestTreePath, scriptFlow);
|
||||
mergeVars(collection, request, requestTreePath);
|
||||
mergeAuth(collection, request, requestTreePath);
|
||||
request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
|
||||
request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });
|
||||
}
|
||||
|
||||
|
||||
@@ -405,14 +319,10 @@ const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
axiosRequest.folderVariables = request.folderVariables;
|
||||
axiosRequest.requestVariables = request.requestVariables;
|
||||
axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables;
|
||||
axiosRequest.oauth2CredentialVariables = request.oauth2CredentialVariables;
|
||||
axiosRequest.assertions = request.assertions;
|
||||
axiosRequest.oauth2Credentials = request.oauth2Credentials;
|
||||
|
||||
return axiosRequest;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
prepareRequest,
|
||||
setAuthHeaders
|
||||
}
|
||||
module.exports = prepareRequest;
|
||||
module.exports.setAuthHeaders = setAuthHeaders;
|
||||
|
||||
@@ -1,42 +1,24 @@
|
||||
const _ = require('lodash');
|
||||
const Store = require('electron-store');
|
||||
const { uuid, safeStringifyJSON, safeParseJSON } = require('../utils/common');
|
||||
const { encryptString, decryptString } = require('../utils/encryption');
|
||||
|
||||
/**
|
||||
* Sample secrets store file
|
||||
*
|
||||
* {
|
||||
* "collections": [{
|
||||
* "path": "/Users/anoop/Code/acme-acpi-collection",
|
||||
* "environments" : [{
|
||||
* "name": "Local",
|
||||
* "secrets": [{
|
||||
* "name": "token",
|
||||
* "value": "abracadabra"
|
||||
* }]
|
||||
* }]
|
||||
* }]
|
||||
* }
|
||||
*/
|
||||
const { uuid } = require('../utils/common');
|
||||
|
||||
class Oauth2Store {
|
||||
constructor() {
|
||||
this.store = new Store({
|
||||
name: 'oauth2',
|
||||
name: 'preferences',
|
||||
clearInvalidConfig: true
|
||||
});
|
||||
}
|
||||
|
||||
// Get oauth2 data for all collections
|
||||
getAllOauth2Data() {
|
||||
let oauth2Data = this.store.get('collections');
|
||||
let oauth2Data = this.store.get('oauth2');
|
||||
if (!Array.isArray(oauth2Data)) oauth2Data = [];
|
||||
return oauth2Data;
|
||||
}
|
||||
|
||||
// Get oauth2 data for a collection
|
||||
getOauth2DataOfCollection({ collectionUid, url }) {
|
||||
getOauth2DataOfCollection(collectionUid) {
|
||||
let oauth2Data = this.getAllOauth2Data();
|
||||
let oauth2DataForCollection = oauth2Data.find((d) => d?.collectionUid == collectionUid);
|
||||
|
||||
@@ -46,7 +28,7 @@ class Oauth2Store {
|
||||
collectionUid
|
||||
};
|
||||
let updatedOauth2Data = [...oauth2Data, newOauth2DataForCollection];
|
||||
this.store.set('collections', updatedOauth2Data);
|
||||
this.store.set('oauth2', updatedOauth2Data);
|
||||
|
||||
return newOauth2DataForCollection;
|
||||
}
|
||||
@@ -55,18 +37,18 @@ class Oauth2Store {
|
||||
}
|
||||
|
||||
// Update oauth2 data of a collection
|
||||
updateOauth2DataOfCollection({ collectionUid, url, data }) {
|
||||
updateOauth2DataOfCollection(collectionUid, data) {
|
||||
let oauth2Data = this.getAllOauth2Data();
|
||||
|
||||
let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);
|
||||
updatedOauth2Data.push({ ...data });
|
||||
|
||||
this.store.set('collections', updatedOauth2Data);
|
||||
this.store.set('oauth2', updatedOauth2Data);
|
||||
}
|
||||
|
||||
// Create a new oauth2 Session Id for a collection
|
||||
createNewOauth2SessionIdForCollection({ collectionUid, url }) {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
|
||||
// Create a new oauth2 Session ID for a collection
|
||||
createNewOauth2SessionIdForCollection(collectionUid) {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
|
||||
|
||||
let newSessionId = uuid();
|
||||
|
||||
@@ -75,21 +57,21 @@ class Oauth2Store {
|
||||
sessionId: newSessionId
|
||||
};
|
||||
|
||||
this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });
|
||||
this.updateOauth2DataOfCollection(collectionUid, newOauth2DataForCollection);
|
||||
|
||||
return newOauth2DataForCollection;
|
||||
}
|
||||
|
||||
// Get session id of a collection
|
||||
getSessionIdOfCollection({ collectionUid, url }) {
|
||||
getSessionIdOfCollection(collectionUid) {
|
||||
try {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
|
||||
|
||||
if (oauth2DataForCollection?.sessionId && typeof oauth2DataForCollection.sessionId === 'string') {
|
||||
return oauth2DataForCollection.sessionId;
|
||||
}
|
||||
|
||||
let newOauth2DataForCollection = this.createNewOauth2SessionIdForCollection({ collectionUid, url });
|
||||
let newOauth2DataForCollection = this.createNewOauth2SessionIdForCollection(collectionUid);
|
||||
return newOauth2DataForCollection?.sessionId;
|
||||
} catch (err) {
|
||||
console.log('error retrieving session id from cache', err);
|
||||
@@ -97,71 +79,21 @@ class Oauth2Store {
|
||||
}
|
||||
|
||||
// clear session id of a collection
|
||||
clearSessionIdOfCollection({ collectionUid, url }) {
|
||||
clearSessionIdOfCollection(collectionUid) {
|
||||
try {
|
||||
let oauth2Data = this.getAllOauth2Data();
|
||||
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
|
||||
delete oauth2DataForCollection.sessionId;
|
||||
delete oauth2DataForCollection.credentials;
|
||||
|
||||
let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);
|
||||
updatedOauth2Data.push({ ...oauth2DataForCollection });
|
||||
|
||||
this.store.set('collections', updatedOauth2Data);
|
||||
this.store.set('oauth2', updatedOauth2Data);
|
||||
} catch (err) {
|
||||
console.log('error while clearing the oauth2 session cache', err);
|
||||
}
|
||||
}
|
||||
|
||||
getCredentialsForCollection({ collectionUid, url, credentialsId }) {
|
||||
try {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
|
||||
let credentials = oauth2DataForCollection?.credentials?.find(c => (c?.url == url) && (c?.credentialsId == credentialsId));
|
||||
if (!credentials?.data) return null;
|
||||
let decryptedCredentialsData = safeParseJSON(decryptString(credentials?.data));
|
||||
return decryptedCredentialsData;
|
||||
} catch (err) {
|
||||
console.log('error retrieving oauth2 credentials from cache', err);
|
||||
}
|
||||
}
|
||||
|
||||
updateCredentialsForCollection({ collectionUid, url, credentialsId, credentials = {} }) {
|
||||
try {
|
||||
let encryptedCredentialsData = encryptString(safeStringifyJSON(credentials));
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
|
||||
let filteredCredentials = oauth2DataForCollection?.credentials?.filter(c => (c?.url !== url) || (c?.credentialsId !== credentialsId));
|
||||
if (!filteredCredentials) filteredCredentials = [];
|
||||
filteredCredentials.push({
|
||||
url,
|
||||
data: encryptedCredentialsData,
|
||||
credentialsId
|
||||
});
|
||||
let newOauth2DataForCollection = {
|
||||
...oauth2DataForCollection,
|
||||
credentials: filteredCredentials
|
||||
};
|
||||
this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });
|
||||
return newOauth2DataForCollection;
|
||||
} catch (err) {
|
||||
console.log('error updating oauth2 credentials from cache', err);
|
||||
}
|
||||
}
|
||||
|
||||
clearCredentialsForCollection({ collectionUid, url, credentialsId }) {
|
||||
try {
|
||||
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
|
||||
let filteredCredentials = oauth2DataForCollection?.credentials?.filter(c => (c?.url !== url) || (c?.credentialsId !== credentialsId));
|
||||
let newOauth2DataForCollection = {
|
||||
...oauth2DataForCollection,
|
||||
credentials: filteredCredentials
|
||||
};
|
||||
this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });
|
||||
return newOauth2DataForCollection;
|
||||
} catch (err) {
|
||||
console.log('error clearing oauth2 credentials from cache', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Oauth2Store;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { get, each, find, compact, filter } = require('lodash');
|
||||
const fs = require('fs');
|
||||
const { getRequestUid } = require('../cache/requestUids');
|
||||
const { uuid } = require('./common');
|
||||
|
||||
const { get, each, find, compact } = require('lodash');
|
||||
const os = require('os');
|
||||
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
@@ -263,137 +264,10 @@ const findItemInCollectionByPathname = (collection, pathname) => {
|
||||
return findItemByPathname(flattenedItems, pathname);
|
||||
};
|
||||
|
||||
const sortCollection = (collection) => {
|
||||
const items = collection.items || [];
|
||||
let folderItems = filter(items, (item) => item.type === 'folder');
|
||||
let requestItems = filter(items, (item) => item.type !== 'folder');
|
||||
|
||||
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
|
||||
|
||||
collection.items = folderItems.concat(requestItems);
|
||||
|
||||
each(folderItems, (item) => {
|
||||
sortCollection(item);
|
||||
});
|
||||
};
|
||||
|
||||
const sortFolder = (folder = {}) => {
|
||||
const items = folder.items || [];
|
||||
let folderItems = filter(items, (item) => item.type === 'folder');
|
||||
let requestItems = filter(items, (item) => item.type !== 'folder');
|
||||
|
||||
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
|
||||
|
||||
folder.items = folderItems.concat(requestItems);
|
||||
|
||||
each(folderItems, (item) => {
|
||||
sortFolder(item);
|
||||
});
|
||||
|
||||
return folder;
|
||||
};
|
||||
|
||||
const getAllRequestsInFolderRecursively = (folder = {}) => {
|
||||
let requests = [];
|
||||
|
||||
if (folder.items && folder.items.length) {
|
||||
folder.items.forEach((item) => {
|
||||
if (item.type !== 'folder') {
|
||||
requests.push(item);
|
||||
} else {
|
||||
requests = requests.concat(getAllRequestsInFolderRecursively(item));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return requests;
|
||||
};
|
||||
|
||||
const getEnvVars = (environment = {}) => {
|
||||
const variables = environment.variables;
|
||||
if (!variables || !variables.length) {
|
||||
return {
|
||||
__name__: environment.name
|
||||
};
|
||||
}
|
||||
|
||||
const envVars = {};
|
||||
each(variables, (variable) => {
|
||||
if (variable.enabled) {
|
||||
envVars[variable.name] = variable.value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...envVars,
|
||||
__name__: environment.name
|
||||
};
|
||||
};
|
||||
|
||||
const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {
|
||||
let credentialsVariables = {};
|
||||
oauth2Credentials.forEach(({ credentialsId, credentials }) => {
|
||||
if (credentials) {
|
||||
Object.entries(credentials).forEach(([key, value]) => {
|
||||
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
|
||||
});
|
||||
}
|
||||
});
|
||||
return credentialsVariables;
|
||||
};
|
||||
|
||||
const mergeAuth = (collection, request, requestTreePath) => {
|
||||
// Start with collection level auth (always consider collection auth as base)
|
||||
let collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
let effectiveAuth = collectionAuth;
|
||||
let lastFolderWithAuth = null;
|
||||
|
||||
// Traverse through the path to find the closest auth configuration
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
// Only consider folders that have a valid auth mode
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveAuth = folderAuth;
|
||||
lastFolderWithAuth = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If request is set to inherit, use the effective auth from collection/folders
|
||||
if (request.auth.mode === 'inherit') {
|
||||
request.auth = effectiveAuth;
|
||||
|
||||
// For OAuth2, we need to handle credentials properly
|
||||
if (effectiveAuth.mode === 'oauth2') {
|
||||
if (lastFolderWithAuth) {
|
||||
// If auth is from folder, add folderUid and clear itemUid
|
||||
request.oauth2Credentials = {
|
||||
...request.oauth2Credentials,
|
||||
folderUid: lastFolderWithAuth.uid,
|
||||
itemUid: null,
|
||||
mode: request.auth.mode
|
||||
};
|
||||
} else {
|
||||
// If auth is from collection, ensure no folderUid and no itemUid
|
||||
request.oauth2Credentials = {
|
||||
...request.oauth2Credentials,
|
||||
folderUid: null,
|
||||
itemUid: null,
|
||||
mode: request.auth.mode
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
mergeHeaders,
|
||||
mergeVars,
|
||||
mergeScripts,
|
||||
mergeAuth,
|
||||
getTreePathFromCollectionToItem,
|
||||
flattenItems,
|
||||
findItem,
|
||||
@@ -402,10 +276,5 @@ module.exports = {
|
||||
findItemInCollectionByPathname,
|
||||
findParentItemInCollection,
|
||||
parseBruFileMeta,
|
||||
sortCollection,
|
||||
sortFolder,
|
||||
getAllRequestsInFolderRecursively,
|
||||
getEnvVars,
|
||||
getFormattedCollectionOauth2Credentials,
|
||||
hydrateRequestWithUuid
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
const { customAlphabet } = require('nanoid');
|
||||
const iconv = require('iconv-lite');
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
const uuid = () => {
|
||||
@@ -86,32 +85,6 @@ const flattenDataForDotNotation = (data) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const parseDataFromResponse = (response, disableParsingResponseJson = false) => {
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || '');
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals
|
||||
const charsetValue = charsetMatch?.[1];
|
||||
const dataBuffer = Buffer.from(response.data);
|
||||
// Overwrite the original data for backwards compatibility
|
||||
let data;
|
||||
if (iconv.encodingExists(charsetValue)) {
|
||||
data = iconv.decode(dataBuffer, charsetValue);
|
||||
} else {
|
||||
data = iconv.decode(dataBuffer, 'utf-8');
|
||||
}
|
||||
// Try to parse response to JSON, this can quietly fail
|
||||
try {
|
||||
// Filter out ZWNBSP character
|
||||
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
||||
data = data.replace(/^\uFEFF/, '');
|
||||
if (!disableParsingResponseJson) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return { data, dataBuffer };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
uuid,
|
||||
stringifyJson,
|
||||
@@ -120,6 +93,5 @@ module.exports = {
|
||||
safeParseJSON,
|
||||
simpleHash,
|
||||
generateUidBasedOnHash,
|
||||
flattenDataForDotNotation,
|
||||
parseDataFromResponse
|
||||
flattenDataForDotNotation
|
||||
};
|
||||
|
||||
@@ -1,806 +0,0 @@
|
||||
const { get, cloneDeep } = require('lodash');
|
||||
const crypto = require('crypto');
|
||||
const { authorizeUserInWindow } = require('../ipc/network/authorize-user-in-window');
|
||||
const Oauth2Store = require('../store/oauth2');
|
||||
const { makeAxiosInstance } = require('../ipc/network/axios-instance');
|
||||
const { safeParseJSON, safeStringifyJSON } = require('./common');
|
||||
const qs = require('qs');
|
||||
|
||||
const oauth2Store = new Oauth2Store();
|
||||
|
||||
const persistOauth2Credentials = ({ collectionUid, url, credentials, credentialsId }) => {
|
||||
if (credentials?.error || !credentials?.access_token) return;
|
||||
const enhancedCredentials = {
|
||||
...credentials,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
oauth2Store.updateCredentialsForCollection({ collectionUid, url, credentials: enhancedCredentials, credentialsId });
|
||||
};
|
||||
|
||||
const clearOauth2Credentials = ({ collectionUid, url, credentialsId }) => {
|
||||
oauth2Store.clearCredentialsForCollection({ collectionUid, url, credentialsId });
|
||||
};
|
||||
|
||||
const getStoredOauth2Credentials = ({ collectionUid, url, credentialsId }) => {
|
||||
try {
|
||||
const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId });
|
||||
return credentials;
|
||||
}
|
||||
catch(error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isTokenExpired = (credentials) => {
|
||||
if (!credentials?.access_token) {
|
||||
return true;
|
||||
}
|
||||
if (!credentials?.expires_in || !credentials.created_at) {
|
||||
return false;
|
||||
}
|
||||
const expiryTime = credentials.created_at + credentials.expires_in * 1000;
|
||||
return Date.now() > expiryTime;
|
||||
};
|
||||
|
||||
// AUTHORIZATION CODE
|
||||
|
||||
const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfig }) => {
|
||||
let codeVerifier = generateCodeVerifier();
|
||||
let codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const {
|
||||
clientId,
|
||||
clientSecret,
|
||||
callbackUrl,
|
||||
scope,
|
||||
pkce,
|
||||
credentialsPlacement,
|
||||
authorizationUrl,
|
||||
credentialsId,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
} = oAuth;
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
if (!forceFetch) {
|
||||
const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
|
||||
if (storedCredentials) {
|
||||
// Token exists
|
||||
if (!isTokenExpired(storedCredentials)) {
|
||||
// Token is valid, use it
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
} else {
|
||||
// Token is expired
|
||||
if (autoRefreshToken && storedCredentials.refresh_token) {
|
||||
// Try to refresh token
|
||||
try {
|
||||
const refreshedCredentialsData = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig });
|
||||
return { collectionUid, url, credentials: refreshedCredentialsData.credentials, credentialsId };
|
||||
} catch (error) {
|
||||
// Refresh failed
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
if (autoFetchToken) {
|
||||
// Proceed to fetch new token
|
||||
} else {
|
||||
// Proceed with expired token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
}
|
||||
} else if (autoRefreshToken && !storedCredentials.refresh_token) {
|
||||
// Cannot refresh; try autoFetchToken
|
||||
if (autoFetchToken) {
|
||||
// Proceed to fetch new token
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
} else {
|
||||
// Proceed with expired token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
} else if (!autoRefreshToken && autoFetchToken) {
|
||||
// Proceed to fetch new token
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
} else {
|
||||
// Proceed with expired token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No stored credentials
|
||||
if (autoFetchToken && !storedCredentials) {
|
||||
// Proceed to fetch new token
|
||||
} else {
|
||||
// Proceed without token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new token process
|
||||
const { authorizationCode, debugInfo } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
|
||||
|
||||
requestCopy.method = 'POST';
|
||||
requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
requestCopy.headers['Accept'] = 'application/json';
|
||||
if (credentialsPlacement === "basic_auth_header") {
|
||||
requestCopy.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
|
||||
}
|
||||
const data = {
|
||||
grant_type: 'authorization_code',
|
||||
code: authorizationCode,
|
||||
redirect_uri: callbackUrl,
|
||||
client_id: clientId,
|
||||
};
|
||||
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
if (pkce) {
|
||||
data['code_verifier'] = codeVerifier;
|
||||
}
|
||||
if (scope) {
|
||||
data.scope = scope;
|
||||
}
|
||||
requestCopy.data = qs.stringify(data);
|
||||
requestCopy.url = url;
|
||||
requestCopy.responseType = 'arraybuffer';
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
|
||||
try {
|
||||
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
|
||||
// Interceptor to capture request data
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
url: response?.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: response?.timeline
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
url: error?.response?.url,
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: error?.response?.timeline,
|
||||
error: 'fetching access token failed! check timeline network logs'
|
||||
};
|
||||
}
|
||||
else if(error?.code) {
|
||||
axiosResponseInfo = {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
headers: error?.config?.headers,
|
||||
data: safeStringifyJSON(error?.errors),
|
||||
timeline: error?.response?.timeline
|
||||
};
|
||||
}
|
||||
return axiosResponseInfo;
|
||||
});
|
||||
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const parsedResponseData = safeParseJSON(
|
||||
Buffer.isBuffer(response.data) ? response.data?.toString() : response.data
|
||||
);
|
||||
// Ensure debugInfo.data is initialized
|
||||
if (!debugInfo) {
|
||||
debugInfo = { data: [] };
|
||||
} else if (!debugInfo.data) {
|
||||
debugInfo.data = [];
|
||||
}
|
||||
|
||||
// Add the axios request and response info as a main request in debugInfo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
request: {
|
||||
url: axiosRequestInfo?.url,
|
||||
method: axiosRequestInfo?.method,
|
||||
headers: axiosRequestInfo?.headers || {},
|
||||
data: axiosRequestInfo?.data,
|
||||
error: null
|
||||
},
|
||||
response: {
|
||||
url: axiosResponseInfo?.url,
|
||||
headers: axiosResponseInfo?.headers,
|
||||
data: parsedResponseData,
|
||||
status: axiosResponseInfo?.status,
|
||||
statusText: axiosResponseInfo?.statusText,
|
||||
error: axiosResponseInfo?.error,
|
||||
timeline: axiosResponseInfo?.timeline
|
||||
},
|
||||
fromCache: false,
|
||||
completed: true,
|
||||
requests: [], // No sub-requests in this context
|
||||
};
|
||||
|
||||
debugInfo.data.push(axiosMainRequest);
|
||||
|
||||
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
}
|
||||
};
|
||||
|
||||
const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const { oauth2 } = request;
|
||||
const { callbackUrl, clientId, authorizationUrl, scope, state, pkce, accessTokenUrl } = oauth2;
|
||||
|
||||
const authorizationUrlWithQueryParams = new URL(authorizationUrl);
|
||||
authorizationUrlWithQueryParams.searchParams.append('response_type', 'code');
|
||||
authorizationUrlWithQueryParams.searchParams.append('client_id', clientId);
|
||||
if (callbackUrl) {
|
||||
authorizationUrlWithQueryParams.searchParams.append('redirect_uri', callbackUrl);
|
||||
}
|
||||
if (scope) {
|
||||
authorizationUrlWithQueryParams.searchParams.append('scope', scope);
|
||||
}
|
||||
if (pkce) {
|
||||
authorizationUrlWithQueryParams.searchParams.append('code_challenge', codeChallenge);
|
||||
authorizationUrlWithQueryParams.searchParams.append('code_challenge_method', 'S256');
|
||||
}
|
||||
if (state) {
|
||||
authorizationUrlWithQueryParams.searchParams.append('state', state);
|
||||
}
|
||||
try {
|
||||
const authorizeUrl = authorizationUrlWithQueryParams.toString();
|
||||
const { authorizationCode, debugInfo } = await authorizeUserInWindow({
|
||||
authorizeUrl,
|
||||
callbackUrl,
|
||||
session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: accessTokenUrl })
|
||||
});
|
||||
resolve({ authorizationCode, debugInfo });
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// CLIENT CREDENTIALS
|
||||
|
||||
const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfig }) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
} = oAuth;
|
||||
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
|
||||
if (!forceFetch) {
|
||||
const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
|
||||
if (storedCredentials) {
|
||||
// Token exists
|
||||
if (!isTokenExpired(storedCredentials)) {
|
||||
// Token is valid, use it
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
} else {
|
||||
// Token is expired
|
||||
if (autoRefreshToken && storedCredentials.refresh_token) {
|
||||
// Try to refresh token
|
||||
try {
|
||||
const refreshedCredentialsData = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig });
|
||||
return { collectionUid, url, credentials: refreshedCredentialsData.credentials, credentialsId };
|
||||
} catch (error) {
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
if (autoFetchToken) {
|
||||
// Proceed to fetch new token
|
||||
} else {
|
||||
// Proceed with expired token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
}
|
||||
} else if (autoRefreshToken && !storedCredentials.refresh_token) {
|
||||
if (autoFetchToken) {
|
||||
// Proceed to fetch new token
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
} else {
|
||||
// Proceed with expired token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
} else if (!autoRefreshToken && autoFetchToken) {
|
||||
// Proceed to fetch new token
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
} else {
|
||||
// Proceed with expired token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No stored credentials
|
||||
if (autoFetchToken && !storedCredentials) {
|
||||
// Proceed to fetch new token
|
||||
} else {
|
||||
// Proceed without token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new token process
|
||||
requestCopy.method = 'POST';
|
||||
requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
requestCopy.headers['Accept'] = 'application/json';
|
||||
if (credentialsPlacement === "basic_auth_header") {
|
||||
requestCopy.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
|
||||
}
|
||||
const data = {
|
||||
grant_type: 'client_credentials',
|
||||
client_id: clientId,
|
||||
};
|
||||
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
if (scope) {
|
||||
data.scope = scope;
|
||||
}
|
||||
requestCopy.data = qs.stringify(data);
|
||||
requestCopy.url = url;
|
||||
requestCopy.responseType = 'arraybuffer';
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
let debugInfo = { data: [] };
|
||||
|
||||
try {
|
||||
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
url: response?.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: response?.timeline
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
url: error?.response?.url,
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: error?.response?.timeline,
|
||||
error: 'fetching access token failed! check timeline network logs'
|
||||
};
|
||||
}
|
||||
else if(error?.code) {
|
||||
axiosResponseInfo = {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
headers: error?.config?.headers,
|
||||
data: safeStringifyJSON(error?.errors),
|
||||
timeline: error?.response?.timeline
|
||||
};
|
||||
}
|
||||
return axiosResponseInfo;
|
||||
});
|
||||
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const parsedResponseData = safeParseJSON(
|
||||
Buffer.isBuffer(response.data) ? response.data.toString() : response.data
|
||||
);
|
||||
|
||||
// Add the axios request and response info as a main request in debugInfo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
request: {
|
||||
url: axiosRequestInfo?.url,
|
||||
method: axiosRequestInfo?.method,
|
||||
headers: axiosRequestInfo?.headers || {},
|
||||
data: axiosRequestInfo?.data,
|
||||
error: null
|
||||
},
|
||||
response: {
|
||||
url: axiosResponseInfo.url,
|
||||
headers: axiosResponseInfo?.headers,
|
||||
data: parsedResponseData,
|
||||
status: axiosResponseInfo?.status,
|
||||
statusText: axiosResponseInfo?.statusText,
|
||||
timeline: axiosResponseInfo?.timeline,
|
||||
error: null
|
||||
},
|
||||
fromCache: false,
|
||||
completed: true,
|
||||
requests: [], // No sub-requests in this context
|
||||
};
|
||||
|
||||
debugInfo.data.push(axiosMainRequest);
|
||||
|
||||
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
}
|
||||
};
|
||||
|
||||
// PASSWORD CREDENTIALS
|
||||
|
||||
const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfig }) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const {
|
||||
username,
|
||||
password,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
} = oAuth;
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
|
||||
if (!forceFetch) {
|
||||
const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
|
||||
if (storedCredentials) {
|
||||
// Token exists
|
||||
if (!isTokenExpired(storedCredentials)) {
|
||||
// Token is valid, use it
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
} else {
|
||||
// Token is expired
|
||||
if (autoRefreshToken && storedCredentials.refresh_token) {
|
||||
// Try to refresh token
|
||||
try {
|
||||
const refreshedCredentialsData = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig });
|
||||
return { collectionUid, url, credentials: refreshedCredentialsData.credentials, credentialsId };
|
||||
} catch (error) {
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
if (autoFetchToken) {
|
||||
// Proceed to fetch new token
|
||||
} else {
|
||||
// Proceed with expired token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
}
|
||||
} else if (autoRefreshToken && !storedCredentials.refresh_token) {
|
||||
// Cannot refresh; try autoFetchToken
|
||||
if (autoFetchToken) {
|
||||
// Proceed to fetch new token
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
} else {
|
||||
// Proceed with expired token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
} else if (!autoRefreshToken && autoFetchToken) {
|
||||
// Proceed to fetch new token
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
} else {
|
||||
// Proceed with expired token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No stored credentials
|
||||
if (autoFetchToken && !storedCredentials) {
|
||||
// Proceed to fetch new token
|
||||
} else {
|
||||
// Proceed without token
|
||||
return { collectionUid, url, credentials: storedCredentials, credentialsId };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new token process
|
||||
requestCopy.method = 'POST';
|
||||
requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
requestCopy.headers['Accept'] = 'application/json';
|
||||
if (credentialsPlacement === "basic_auth_header") {
|
||||
requestCopy.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
|
||||
}
|
||||
const data = {
|
||||
grant_type: 'password',
|
||||
username,
|
||||
password,
|
||||
client_id: clientId,
|
||||
};
|
||||
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
if (scope) {
|
||||
data.scope = scope;
|
||||
}
|
||||
requestCopy.data = qs.stringify(data);
|
||||
requestCopy.url = url;
|
||||
requestCopy.responseType = 'arraybuffer';
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
let debugInfo = { data: [] };
|
||||
|
||||
try {
|
||||
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
url: response?.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: response?.timeline
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
url: error?.response?.url,
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: error?.response?.timeline,
|
||||
error: 'fetching access token failed! check timeline network logs'
|
||||
};
|
||||
}
|
||||
else if(error?.code) {
|
||||
axiosResponseInfo = {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
headers: error?.config?.headers,
|
||||
data: safeStringifyJSON(error?.errors),
|
||||
timeline: error?.response?.timeline
|
||||
};
|
||||
}
|
||||
return axiosResponseInfo;
|
||||
});
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const parsedResponseData = safeParseJSON(
|
||||
Buffer.isBuffer(response.data) ? response.data.toString() : response.data
|
||||
);
|
||||
|
||||
// Add the axios request and response info as a main request in debugInfo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
request: {
|
||||
url: axiosRequestInfo?.url,
|
||||
method: axiosRequestInfo?.method,
|
||||
headers: axiosRequestInfo?.headers || {},
|
||||
data: axiosRequestInfo?.data,
|
||||
error: null
|
||||
},
|
||||
response: {
|
||||
url: axiosResponseInfo?.url,
|
||||
headers: axiosResponseInfo?.headers,
|
||||
data: parsedResponseData,
|
||||
status: axiosResponseInfo?.status,
|
||||
statusText: axiosResponseInfo?.statusText,
|
||||
timeline: axiosResponseInfo?.timeline,
|
||||
error: null
|
||||
},
|
||||
fromCache: false,
|
||||
completed: true,
|
||||
requests: [], // No sub-requests in this context
|
||||
};
|
||||
|
||||
debugInfo.data.push(axiosMainRequest);
|
||||
|
||||
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
}
|
||||
};
|
||||
|
||||
const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyConfig }) => {
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, credentialsId } = oAuth;
|
||||
const url = oAuth.refreshTokenUrl ? oAuth.refreshTokenUrl : oAuth.accessTokenUrl;
|
||||
|
||||
const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
if (!credentials?.refresh_token) {
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
// Proceed without token
|
||||
return { collectionUid, url, credentials: null, credentialsId };
|
||||
} else {
|
||||
const data = {
|
||||
grant_type: 'refresh_token',
|
||||
client_id: clientId,
|
||||
refresh_token: credentials.refresh_token,
|
||||
};
|
||||
if (clientSecret) {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
requestCopy.method = 'POST';
|
||||
requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
requestCopy.headers['Accept'] = 'application/json';
|
||||
requestCopy.data = qs.stringify(data);
|
||||
requestCopy.url = url;
|
||||
requestCopy.responseType = 'arraybuffer';
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
let debugInfo = { data: [] };
|
||||
|
||||
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: requestData,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
url: response?.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: response?.timeline
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
url: error?.response?.url,
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
timestamp: Date.now(),
|
||||
timeline: error?.response?.timeline,
|
||||
error: 'fetching access token failed! check timeline network logs'
|
||||
};
|
||||
}
|
||||
else if(error?.code) {
|
||||
axiosResponseInfo = {
|
||||
status: '-',
|
||||
statusText: error.code,
|
||||
headers: error?.config?.headers,
|
||||
data: safeStringifyJSON(error?.errors),
|
||||
timeline: error?.response?.timeline
|
||||
};
|
||||
}
|
||||
return axiosResponseInfo;
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const parsedResponseData = safeParseJSON(
|
||||
Buffer.isBuffer(response.data) ? response.data.toString() : response.data
|
||||
);
|
||||
|
||||
// Add the axios request and response info as a main request in debugInfo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
request: {
|
||||
url: axiosRequestInfo?.url,
|
||||
method: axiosRequestInfo?.method,
|
||||
headers: axiosRequestInfo?.headers || {},
|
||||
data: axiosRequestInfo?.data,
|
||||
error: null
|
||||
},
|
||||
response: {
|
||||
url: axiosResponseInfo?.url,
|
||||
headers: axiosResponseInfo?.headers,
|
||||
data: parsedResponseData,
|
||||
status: axiosResponseInfo?.status,
|
||||
statusText: axiosResponseInfo?.statusText,
|
||||
timeline: axiosResponseInfo?.timeline,
|
||||
error: null
|
||||
},
|
||||
fromCache: false,
|
||||
completed: true,
|
||||
requests: [], // No sub-requests in this context
|
||||
};
|
||||
|
||||
debugInfo.data.push(axiosMainRequest);
|
||||
|
||||
if (parsedResponseData?.error) {
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
return { collectionUid, url, credentials: null, credentialsId, debugInfo };
|
||||
}
|
||||
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
// Proceed without token
|
||||
return { collectionUid, url, credentials: null, credentialsId, debugInfo };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// HELPER FUNCTIONS
|
||||
|
||||
const generateCodeVerifier = () => {
|
||||
return crypto.randomBytes(22).toString('hex');
|
||||
};
|
||||
|
||||
const generateCodeChallenge = (codeVerifier) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(codeVerifier);
|
||||
const base64Hash = hash.digest('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
return base64Hash;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getOAuth2TokenUsingAuthorizationCode,
|
||||
getOAuth2AuthorizationCode,
|
||||
getOAuth2TokenUsingClientCredentials,
|
||||
getOAuth2TokenUsingPasswordCredentials,
|
||||
refreshOauth2Token
|
||||
};
|
||||
@@ -1,11 +1,6 @@
|
||||
const parseUrl = require('url').parse;
|
||||
const https = require('https');
|
||||
const { isEmpty } = require('lodash');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { interpolateString } = require('../ipc/network/interpolate-string');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { preferencesUtil } = require('../store/preferences');
|
||||
const { isEmpty, get, isUndefined, isNull } = require('lodash');
|
||||
|
||||
const DEFAULT_PORTS = {
|
||||
ftp: 21,
|
||||
@@ -84,292 +79,7 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent {
|
||||
}
|
||||
}
|
||||
|
||||
function createTimelineAgentClass(BaseAgentClass) {
|
||||
return class extends BaseAgentClass {
|
||||
constructor(options, timeline) {
|
||||
// For proxy agents, the first argument is the proxy URI and the second is options
|
||||
if (options?.proxy) {
|
||||
const { proxy: proxyUri, ...agentOptions } = options;
|
||||
// Ensure TLS options are properly set
|
||||
const tlsOptions = {
|
||||
...agentOptions,
|
||||
rejectUnauthorized: agentOptions.rejectUnauthorized ?? true,
|
||||
};
|
||||
super(proxyUri, tlsOptions);
|
||||
this.timeline = Array.isArray(timeline) ? timeline : [];
|
||||
this.alpnProtocols = tlsOptions.ALPNProtocols || ['h2', 'http/1.1'];
|
||||
this.caProvided = !!tlsOptions.ca;
|
||||
|
||||
// Log TLS verification status
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `SSL validation: ${tlsOptions.rejectUnauthorized ? 'enabled' : 'disabled'}`,
|
||||
});
|
||||
|
||||
// Log the proxy details
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Using proxy: ${proxyUri}`,
|
||||
});
|
||||
} else {
|
||||
// This is a regular HTTPS agent case
|
||||
const tlsOptions = {
|
||||
...options,
|
||||
rejectUnauthorized: options.rejectUnauthorized ?? true,
|
||||
};
|
||||
super(tlsOptions);
|
||||
this.timeline = Array.isArray(timeline) ? timeline : [];
|
||||
this.alpnProtocols = options.ALPNProtocols || ['h2', 'http/1.1'];
|
||||
this.caProvided = !!options.ca;
|
||||
|
||||
// Log TLS verification status
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `SSL validation: ${tlsOptions.rejectUnauthorized ? 'enabled' : 'disabled'}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
createConnection(options, callback) {
|
||||
const { host, port } = options;
|
||||
|
||||
// Log ALPN protocols offered
|
||||
if (this.alpnProtocols && this.alpnProtocols.length > 0) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: `ALPN: offers ${this.alpnProtocols.join(', ')}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Log CAfile and CApath (if possible)
|
||||
if (this.caProvided) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: `CA certificates provided`,
|
||||
});
|
||||
} else {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: `Using system default CA certificates`,
|
||||
});
|
||||
}
|
||||
|
||||
// Log "Trying host:port..."
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Trying ${host}:${port}...`,
|
||||
});
|
||||
|
||||
const socket = super.createConnection(options, callback);
|
||||
|
||||
// Attach event listeners to the socket
|
||||
socket.on('lookup', (err, address, family, host) => {
|
||||
if (err) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `DNS lookup error for ${host}: ${err.message}`,
|
||||
});
|
||||
} else {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `DNS lookup: ${host} -> ${address}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
const address = socket.remoteAddress || host;
|
||||
const remotePort = socket.remotePort || port;
|
||||
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Connected to ${host} (${address}) port ${remotePort}`,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('secureConnect', () => {
|
||||
const protocol = socket.getProtocol() || 'SSL/TLS';
|
||||
const cipher = socket.getCipher();
|
||||
const cipherSuite = cipher ? `${cipher.name} (${cipher.version})` : 'Unknown cipher';
|
||||
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: `SSL connection using ${protocol} / ${cipherSuite}`,
|
||||
});
|
||||
|
||||
// ALPN protocol
|
||||
const alpnProtocol = socket.alpnProtocol || 'None';
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: `ALPN: server accepted ${alpnProtocol}`,
|
||||
});
|
||||
|
||||
// Server certificate
|
||||
const cert = socket.getPeerCertificate(true);
|
||||
if (cert) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: `Server certificate:`,
|
||||
});
|
||||
if (cert.subject) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: ` subject: ${Object.entries(cert.subject).map(([k, v]) => `${k}=${v}`).join(', ')}`,
|
||||
});
|
||||
}
|
||||
if (cert.valid_from) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: ` start date: ${cert.valid_from}`,
|
||||
});
|
||||
}
|
||||
if (cert.valid_to) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: ` expire date: ${cert.valid_to}`,
|
||||
});
|
||||
}
|
||||
if (cert.subjectaltname) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: ` subjectAltName: ${cert.subjectaltname}`,
|
||||
});
|
||||
}
|
||||
if (cert.issuer) {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: ` issuer: ${Object.entries(cert.issuer).map(([k, v]) => `${k}=${v}`).join(', ')}`,
|
||||
});
|
||||
}
|
||||
|
||||
// SSL certificate verify ok
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'tls',
|
||||
message: `SSL certificate verify ok.`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'error',
|
||||
message: `Socket error: ${err.message}`,
|
||||
});
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setupProxyAgents({
|
||||
requestConfig,
|
||||
proxyMode = 'off',
|
||||
proxyConfig,
|
||||
httpsAgentRequestFields,
|
||||
interpolationOptions,
|
||||
timeline,
|
||||
}) {
|
||||
// Ensure TLS options are properly set
|
||||
const tlsOptions = {
|
||||
...httpsAgentRequestFields,
|
||||
rejectUnauthorized: httpsAgentRequestFields.rejectUnauthorized !== undefined ? httpsAgentRequestFields.rejectUnauthorized : true,
|
||||
};
|
||||
|
||||
if (proxyMode === 'on') {
|
||||
const shouldProxy = shouldUseProxy(requestConfig.url, get(proxyConfig, 'bypassProxy', ''));
|
||||
if (shouldProxy) {
|
||||
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
|
||||
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
|
||||
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
|
||||
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
|
||||
const socksEnabled = proxyProtocol.includes('socks');
|
||||
|
||||
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
|
||||
let proxyUri;
|
||||
if (proxyAuthEnabled) {
|
||||
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
|
||||
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
|
||||
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
|
||||
} else {
|
||||
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
|
||||
}
|
||||
|
||||
if (socksEnabled) {
|
||||
const TimelineSocksProxyAgent = createTimelineAgentClass(SocksProxyAgent);
|
||||
requestConfig.httpAgent = new TimelineSocksProxyAgent({ proxy: proxyUri }, timeline);
|
||||
requestConfig.httpsAgent = new TimelineSocksProxyAgent({ proxy: proxyUri, ...tlsOptions }, timeline);
|
||||
} else {
|
||||
const TimelineHttpsProxyAgent = createTimelineAgentClass(PatchedHttpsProxyAgent);
|
||||
requestConfig.httpAgent = new HttpProxyAgent(proxyUri); // For http, no need for timeline
|
||||
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
|
||||
{ proxy: proxyUri, ...tlsOptions },
|
||||
timeline
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If proxy should not be used, set default HTTPS agent
|
||||
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
|
||||
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
|
||||
}
|
||||
} else if (proxyMode === 'system') {
|
||||
const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables();
|
||||
const shouldUseSystemProxy = shouldUseProxy(requestConfig.url, no_proxy || '');
|
||||
if (shouldUseSystemProxy) {
|
||||
try {
|
||||
if (http_proxy?.length) {
|
||||
new URL(http_proxy);
|
||||
requestConfig.httpAgent = new HttpProxyAgent(http_proxy);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid system http_proxy');
|
||||
}
|
||||
try {
|
||||
if (https_proxy?.length) {
|
||||
new URL(https_proxy);
|
||||
const TimelineHttpsProxyAgent = createTimelineAgentClass(PatchedHttpsProxyAgent);
|
||||
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
|
||||
{ proxy: https_proxy,...tlsOptions },
|
||||
timeline
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid system https_proxy');
|
||||
}
|
||||
} else {
|
||||
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
|
||||
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
|
||||
}
|
||||
} else {
|
||||
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
|
||||
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
shouldUseProxy,
|
||||
PatchedHttpsProxyAgent,
|
||||
setupProxyAgents
|
||||
PatchedHttpsProxyAgent
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
const { prepareRequest } = require('../../src/ipc/network/prepare-request');
|
||||
const prepareRequest = require('../../src/ipc/network/prepare-request');
|
||||
const { buildFormUrlEncodedPayload } = require('../../src/utils/form-data');
|
||||
|
||||
describe('prepare-request: prepareRequest', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@ const { interpolate } = require('@usebruno/common');
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
class Bru {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables) {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables) {
|
||||
this.envVariables = envVariables || {};
|
||||
this.runtimeVariables = runtimeVariables || {};
|
||||
this.processEnvVars = cloneDeep(processEnvVars || {});
|
||||
@@ -12,7 +12,6 @@ class Bru {
|
||||
this.folderVariables = folderVariables || {};
|
||||
this.requestVariables = requestVariables || {};
|
||||
this.globalEnvironmentVariables = globalEnvironmentVariables || {};
|
||||
this.oauth2CredentialVariables = oauth2CredentialVariables || {};
|
||||
this.collectionPath = collectionPath;
|
||||
this.runner = {
|
||||
skipRequest: () => {
|
||||
@@ -38,7 +37,6 @@ class Bru {
|
||||
...this.envVariables,
|
||||
...this.folderVariables,
|
||||
...this.requestVariables,
|
||||
...this.oauth2CredentialVariables,
|
||||
...this.runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
@@ -94,10 +92,6 @@ class Bru {
|
||||
this.globalEnvironmentVariables[key] = value;
|
||||
}
|
||||
|
||||
getOauth2CredentialVar(key) {
|
||||
return this._interpolate(this.oauth2CredentialVariables[key]);
|
||||
}
|
||||
|
||||
hasVar(key) {
|
||||
return Object.hasOwn(this.runtimeVariables, key);
|
||||
}
|
||||
|
||||
@@ -246,7 +246,6 @@ class AssertRuntime {
|
||||
|
||||
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
@@ -280,7 +279,6 @@ class AssertRuntime {
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...oauth2CredentialVariables,
|
||||
...runtimeVariables,
|
||||
...processEnvVars,
|
||||
...bruContext
|
||||
|
||||
@@ -51,11 +51,10 @@ class ScriptRuntime {
|
||||
runRequestByItemPathname
|
||||
) {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
||||
@@ -123,7 +122,6 @@ class ScriptRuntime {
|
||||
sandbox: context,
|
||||
require: {
|
||||
context: 'sandbox',
|
||||
builtin: [ "*" ],
|
||||
external: true,
|
||||
root: [collectionPath, ...additionalContextRootsAbsolute],
|
||||
mock: {
|
||||
@@ -184,11 +182,10 @@ class ScriptRuntime {
|
||||
runRequestByItemPathname
|
||||
) {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
@@ -253,7 +250,6 @@ class ScriptRuntime {
|
||||
sandbox: context,
|
||||
require: {
|
||||
context: 'sandbox',
|
||||
builtin: [ "*" ],
|
||||
external: true,
|
||||
root: [collectionPath],
|
||||
mock: {
|
||||
|
||||
@@ -53,12 +53,6 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'getGlobalEnvVar', getGlobalEnvVar);
|
||||
getGlobalEnvVar.dispose();
|
||||
|
||||
let getOauth2CredentialVar = vm.newFunction('getOauth2CredentialVar', function (key) {
|
||||
return marshallToVm(bru.getOauth2CredentialVar(vm.dump(key)), vm);
|
||||
});
|
||||
vm.setProp(bruObject, 'getOauth2CredentialVar', getOauth2CredentialVar);
|
||||
getOauth2CredentialVar.dispose();
|
||||
|
||||
let setGlobalEnvVar = vm.newFunction('setGlobalEnvVar', function (key, value) {
|
||||
bru.setGlobalEnvVar(vm.dump(key), vm.dump(value));
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const ohm = require('ohm-js');
|
||||
const _ = require('lodash');
|
||||
const { safeParseJson, outdentString } = require('./utils');
|
||||
const { outdentString } = require('../../v1/src/utils');
|
||||
|
||||
/**
|
||||
* A Bru file is made up of blocks.
|
||||
@@ -514,19 +514,11 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
const callbackUrlKey = _.find(auth, { name: 'callback_url' });
|
||||
const authorizationUrlKey = _.find(auth, { name: 'authorization_url' });
|
||||
const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' });
|
||||
const refreshTokenUrlKey = _.find(auth, { name: 'refresh_token_url' });
|
||||
const clientIdKey = _.find(auth, { name: 'client_id' });
|
||||
const clientSecretKey = _.find(auth, { name: 'client_secret' });
|
||||
const scopeKey = _.find(auth, { name: 'scope' });
|
||||
const stateKey = _.find(auth, { name: 'state' });
|
||||
const pkceKey = _.find(auth, { name: 'pkce' });
|
||||
const credentialsPlacementKey = _.find(auth, { name: 'credentials_placement' });
|
||||
const credentialsIdKey = _.find(auth, { name: 'credentials_id' });
|
||||
const tokenPlacementKey = _.find(auth, { name: 'token_placement' });
|
||||
const tokenHeaderPrefixKey = _.find(auth, { name: 'token_header_prefix' });
|
||||
const tokenQueryKeyKey = _.find(auth, { name: 'token_query_key' });
|
||||
const autoFetchTokenKey = _.find(auth, { name: 'auto_fetch_token' });
|
||||
const autoRefreshTokenKey = _.find(auth, { name: 'auto_refresh_token' });
|
||||
return {
|
||||
auth: {
|
||||
oauth2:
|
||||
@@ -534,19 +526,11 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
? {
|
||||
grantType: grantTypeKey ? grantTypeKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',
|
||||
username: usernameKey ? usernameKey.value : '',
|
||||
password: passwordKey ? passwordKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : '',
|
||||
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
|
||||
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false
|
||||
scope: scopeKey ? scopeKey.value : ''
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
|
||||
? {
|
||||
@@ -554,35 +538,19 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',
|
||||
authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : '',
|
||||
state: stateKey ? stateKey.value : '',
|
||||
pkce: pkceKey ? safeParseJson(pkceKey?.value) ?? false : false,
|
||||
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
|
||||
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false
|
||||
pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
|
||||
? {
|
||||
grantType: grantTypeKey ? grantTypeKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : '',
|
||||
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
|
||||
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false
|
||||
scope: scopeKey ? scopeKey.value : ''
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const ohm = require('ohm-js');
|
||||
const _ = require('lodash');
|
||||
const { safeParseJson, outdentString } = require('./utils');
|
||||
const { outdentString } = require('../../v1/src/utils');
|
||||
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
|
||||
@@ -274,19 +274,11 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
const callbackUrlKey = _.find(auth, { name: 'callback_url' });
|
||||
const authorizationUrlKey = _.find(auth, { name: 'authorization_url' });
|
||||
const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' });
|
||||
const refreshTokenUrlKey = _.find(auth, { name: 'refresh_token_url' });
|
||||
const clientIdKey = _.find(auth, { name: 'client_id' });
|
||||
const clientSecretKey = _.find(auth, { name: 'client_secret' });
|
||||
const scopeKey = _.find(auth, { name: 'scope' });
|
||||
const stateKey = _.find(auth, { name: 'state' });
|
||||
const pkceKey = _.find(auth, { name: 'pkce' });
|
||||
const credentialsPlacementKey = _.find(auth, { name: 'credentials_placement' });
|
||||
const credentialsIdKey = _.find(auth, { name: 'credentials_id' });
|
||||
const tokenPlacementKey = _.find(auth, { name: 'token_placement' });
|
||||
const tokenHeaderPrefixKey = _.find(auth, { name: 'token_header_prefix' });
|
||||
const tokenQueryKeyKey = _.find(auth, { name: 'token_query_key' });
|
||||
const autoFetchTokenKey = _.find(auth, { name: 'auto_fetch_token' });
|
||||
const autoRefreshTokenKey = _.find(auth, { name: 'auto_refresh_token' });
|
||||
return {
|
||||
auth: {
|
||||
oauth2:
|
||||
@@ -294,19 +286,11 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
? {
|
||||
grantType: grantTypeKey ? grantTypeKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',
|
||||
username: usernameKey ? usernameKey.value : '',
|
||||
password: passwordKey ? passwordKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : '',
|
||||
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
|
||||
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false
|
||||
scope: scopeKey ? scopeKey.value : ''
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
|
||||
? {
|
||||
@@ -314,35 +298,19 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',
|
||||
authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : '',
|
||||
state: stateKey ? stateKey.value : '',
|
||||
pkce: pkceKey ? safeParseJson(pkceKey?.value) ?? false : false,
|
||||
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
|
||||
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false
|
||||
pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
|
||||
? {
|
||||
grantType: grantTypeKey ? grantTypeKey.value : '',
|
||||
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
|
||||
refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',
|
||||
clientId: clientIdKey ? clientIdKey.value : '',
|
||||
clientSecret: clientSecretKey ? clientSecretKey.value : '',
|
||||
scope: scopeKey ? scopeKey.value : '',
|
||||
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
|
||||
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false
|
||||
scope: scopeKey ? scopeKey.value : ''
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { indentString } = require('./utils');
|
||||
const { indentString } = require('../../v1/src/utils');
|
||||
|
||||
const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]);
|
||||
const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]);
|
||||
@@ -183,21 +183,11 @@ ${indentString(`domain: ${auth?.ntlm?.domain || ''}`)}
|
||||
bru += `auth:oauth2 {
|
||||
${indentString(`grant_type: password`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}
|
||||
${indentString(`username: ${auth?.oauth2?.username || ''}`)}
|
||||
${indentString(`password: ${auth?.oauth2?.password || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -208,21 +198,11 @@ ${indentString(`grant_type: authorization_code`)}
|
||||
${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}
|
||||
${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`state: ${auth?.oauth2?.state || ''}`)}
|
||||
${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}
|
||||
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -231,19 +211,9 @@ ${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).
|
||||
bru += `auth:oauth2 {
|
||||
${indentString(`grant_type: client_credentials`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const { indentString } = require('./utils');
|
||||
const { indentString } = require('../../v1/src/utils');
|
||||
|
||||
const enabled = (items = []) => items.filter((item) => item.enabled);
|
||||
const disabled = (items = []) => items.filter((item) => !item.enabled);
|
||||
@@ -149,21 +149,11 @@ ${indentString(`placement: ${auth?.apikey?.placement || ''}`)}
|
||||
bru += `auth:oauth2 {
|
||||
${indentString(`grant_type: password`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}
|
||||
${indentString(`username: ${auth?.oauth2?.username || ''}`)}
|
||||
${indentString(`password: ${auth?.oauth2?.password || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -174,21 +164,11 @@ ${indentString(`grant_type: authorization_code`)}
|
||||
${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}
|
||||
${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`state: ${auth?.oauth2?.state || ''}`)}
|
||||
${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}
|
||||
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -197,19 +177,9 @@ ${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).
|
||||
bru += `auth:oauth2 {
|
||||
${indentString(`grant_type: client_credentials`)}
|
||||
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
|
||||
${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}
|
||||
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
|
||||
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
|
||||
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
|
||||
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
|
||||
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
|
||||
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// safely parse json
|
||||
const safeParseJson = (json) => {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const indentString = (str) => {
|
||||
if (!str || !str.length) {
|
||||
return str || '';
|
||||
}
|
||||
|
||||
return str
|
||||
.split('\n')
|
||||
.map((line) => ' ' + line)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const outdentString = (str) => {
|
||||
if (!str || !str.length) {
|
||||
return str || '';
|
||||
}
|
||||
|
||||
return str
|
||||
.split('\n')
|
||||
.map((line) => line.replace(/^ /, ''))
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
safeParseJson,
|
||||
indentString,
|
||||
outdentString
|
||||
};
|
||||
@@ -59,18 +59,11 @@ auth:oauth2 {
|
||||
callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback
|
||||
authorization_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
|
||||
access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token
|
||||
refresh_token_url:
|
||||
client_id: client_id_1
|
||||
client_secret: client_secret_1
|
||||
scope: read write
|
||||
state: 807061d5f0be
|
||||
pkce: false
|
||||
credentials_placement: body
|
||||
credentials_id: credentials
|
||||
token_placement: header
|
||||
token_header_prefix: Bearer
|
||||
auto_fetch_token: true
|
||||
auto_refresh_token: true
|
||||
}
|
||||
|
||||
body:json {
|
||||
|
||||
@@ -74,23 +74,15 @@
|
||||
"password": "secret"
|
||||
},
|
||||
"oauth2": {
|
||||
"accessTokenUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/token",
|
||||
"authorizationUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize",
|
||||
"autoFetchToken": true,
|
||||
"autoRefreshToken": true,
|
||||
"callbackUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/callback",
|
||||
"grantType": "authorization_code",
|
||||
"clientId": "client_id_1",
|
||||
"clientSecret": "client_secret_1",
|
||||
"credentialsId": "credentials",
|
||||
"credentialsPlacement": "body",
|
||||
"grantType": "authorization_code",
|
||||
"pkce": false,
|
||||
"refreshTokenUrl": "",
|
||||
"authorizationUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize",
|
||||
"callbackUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/callback",
|
||||
"accessTokenUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/token",
|
||||
"scope": "read write",
|
||||
"state": "807061d5f0be",
|
||||
"tokenHeaderPrefix": "Bearer",
|
||||
"tokenPlacement": "header",
|
||||
"tokenQueryKey": "access_token"
|
||||
"pkce": false
|
||||
},
|
||||
"wsse": {
|
||||
"username": "john",
|
||||
|
||||
@@ -210,48 +210,6 @@ const oauth2Schema = Yup.object({
|
||||
is: (val) => ['authorization_code'].includes(val),
|
||||
then: Yup.boolean().default(false),
|
||||
otherwise: Yup.boolean()
|
||||
}),
|
||||
credentialsPlacement: Yup.string().when('grantType', {
|
||||
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
credentialsId: Yup.string().when('grantType', {
|
||||
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
tokenPlacement: Yup.string().when('grantType', {
|
||||
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
tokenHeaderPrefix: Yup.string().when(['grantType', 'tokenPlacement'], {
|
||||
is: (grantType, tokenPlacement) =>
|
||||
['client_credentials', 'password', 'authorization_code'].includes(grantType) && tokenPlacement === 'header',
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
tokenQueryKey: Yup.string().when(['grantType', 'tokenPlacement'], {
|
||||
is: (grantType, tokenPlacement) =>
|
||||
['client_credentials', 'password', 'authorization_code'].includes(grantType) && tokenPlacement === 'url',
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
refreshTokenUrl: Yup.string().when('grantType', {
|
||||
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
autoRefreshToken: Yup.boolean().when('grantType', {
|
||||
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
|
||||
then: Yup.boolean().default(false),
|
||||
otherwise: Yup.boolean()
|
||||
}),
|
||||
autoFetchToken: Yup.boolean().when('grantType', {
|
||||
is: (val) => ['authorization_code'].includes(val),
|
||||
then: Yup.boolean().default(true),
|
||||
otherwise: Yup.boolean()
|
||||
})
|
||||
})
|
||||
.noUnknown(true)
|
||||
|
||||
@@ -43,5 +43,6 @@ tests {
|
||||
expect(res.getBody()).to.eql({
|
||||
"hello": "bruno"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user