mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 13:45:52 +00:00
Oauth2 folder (#4105)
This commit is contained in:
@@ -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,86 @@
|
||||
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}
|
||||
folder={folder}
|
||||
/>
|
||||
{grantTypeComponentMap(collection, 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;
|
||||
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
@@ -0,0 +1,62 @@
|
||||
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,7 +8,9 @@ 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 (
|
||||
@@ -37,6 +39,9 @@ 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({
|
||||
@@ -61,6 +66,9 @@ 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} />;
|
||||
}
|
||||
@@ -93,6 +101,10 @@ 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>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, forwardRef, useState, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { clearOauth2Cache, fetchOauth2Credentials, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -13,7 +13,7 @@ import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import { cloneDeep, find } from 'lodash';
|
||||
import { interpolateStringUsingCollectionAndItem } from 'utils/collections/index';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
@@ -23,29 +23,33 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
const [showRefreshButton, setShowRefreshButton] = useState(false);
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const {
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
state,
|
||||
pkce,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
reuseToken,
|
||||
const {
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
state,
|
||||
pkce,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshUrl,
|
||||
autoRefresh
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const refreshUrlAvailable = refreshUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshUrlAvailable;
|
||||
|
||||
|
||||
const TokenPlacementIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
@@ -54,7 +58,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
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'}
|
||||
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
|
||||
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
@@ -67,11 +71,16 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
requestCopy.headers = {};
|
||||
toggleFetchingToken(true);
|
||||
try {
|
||||
await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
|
||||
await dispatch(fetchOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
request: requestCopy,
|
||||
collection,
|
||||
folderUid: folder?.uid || null
|
||||
}));
|
||||
toggleFetchingToken(false);
|
||||
toast.success('token fetched successfully!');
|
||||
}
|
||||
catch(error) {
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error('An error occured while fetching token!');
|
||||
@@ -88,14 +97,14 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
toggleRefreshingToken(false);
|
||||
toast.success('token refreshed successfully!');
|
||||
}
|
||||
catch(error) {
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
toggleRefreshingToken(false);
|
||||
toast.error('An error occured while refreshing token!');
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {save();};
|
||||
const handleSave = () => { save(); };
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
@@ -118,10 +127,10 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
reuseToken,
|
||||
refreshUrl,
|
||||
autoRefresh,
|
||||
[key]: value
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value,
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -147,7 +156,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
reuseToken,
|
||||
autoFetchToken,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
@@ -165,10 +174,10 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
});
|
||||
};
|
||||
|
||||
const { uid: collectionUid } = collection;
|
||||
const interpolatedUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
|
||||
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const creds = credentialsData?.credentials || {};
|
||||
const { uid: collectionUid } = collection;
|
||||
const interpolatedUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
|
||||
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
|
||||
const creds = credentialsData?.credentials || {};
|
||||
|
||||
useEffect(() => {
|
||||
// Update visibility whenever credentials change
|
||||
@@ -302,7 +311,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
/>
|
||||
</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">
|
||||
@@ -340,24 +349,58 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
</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.autoRefresh', false)}
|
||||
onChange={(e) => handleChange('autoRefresh', e.target.checked)}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
|
||||
<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>
|
||||
<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} /> : ""}
|
||||
Get Access Token{fetchingToken ? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button>
|
||||
{showRefreshButton && (
|
||||
<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} /> : ""}
|
||||
Refresh Token{refreshingToken ? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, forwardRef, useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -23,7 +23,23 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
|
||||
const { accessTokenUrl, clientId, clientSecret, scope, credentialsPlacement, credentialsId, tokenPlacement, tokenHeaderPrefix, tokenQueryKey, reuseToken, refreshUrl, autoRefresh } = oAuth;
|
||||
const {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
refreshUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const refreshUrlAvailable = refreshUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshUrlAvailable;
|
||||
|
||||
const handleFetchOauth2Credentials = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
@@ -97,9 +113,9 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
reuseToken,
|
||||
refreshUrl,
|
||||
autoRefresh,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -278,12 +294,58 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefresh', false)}
|
||||
onChange={(e) => handleChange('autoRefresh', e.target.checked)}
|
||||
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>
|
||||
|
||||
<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} /> : ""}
|
||||
|
||||
@@ -66,7 +66,6 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
tokenQueryKey: 'access_token',
|
||||
reuseToken: false
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/
|
||||
|
||||
const TokenSection = ({ title, token }) => {
|
||||
if (!token) return null;
|
||||
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [decodedToken, setDecodedToken] = useState(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -33,18 +33,18 @@ const TokenSection = ({ title, token }) => {
|
||||
|
||||
return (
|
||||
<div className="mb-2 border dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div
|
||||
<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" /> :
|
||||
{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}/>}
|
||||
<h3 className="text-sm font-medium">{title}</h3>
|
||||
{decodedToken?.exp && <ExpiryTimer expiresIn={decodedToken?.exp} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,8 +57,8 @@ const TokenSection = ({ title, token }) => {
|
||||
className="p-1 bg-indigo-100 dark:hover:bg-indigo-200 rounded"
|
||||
title="Copy token"
|
||||
>
|
||||
{copied ?
|
||||
<IconCheck size={16} className="text-green-700" /> :
|
||||
{copied ?
|
||||
<IconCheck size={16} className="text-green-700" /> :
|
||||
<IconCopy size={16} className="text-gray-500" />
|
||||
}
|
||||
</button>
|
||||
@@ -113,11 +113,10 @@ const ExpiryTimer = ({ expiresIn }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`text-xs px-2 py-1 rounded-full ${
|
||||
timeLeft <= 30
|
||||
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>
|
||||
@@ -138,16 +137,17 @@ const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun })
|
||||
<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">
|
||||
<span className="font-medium">Scope:</span>
|
||||
<span className="text-gray-600 dark:text-gray-300">{creds.scope}</span>
|
||||
{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}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, forwardRef, useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -35,11 +35,14 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
reuseToken,
|
||||
refreshUrl,
|
||||
autoRefresh
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
} = oAuth;
|
||||
|
||||
const refreshUrlAvailable = refreshUrl?.trim() !== '';
|
||||
const isAutoRefreshDisabled = !refreshUrlAvailable;
|
||||
|
||||
const handleFetchOauth2Credentials = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
@@ -113,9 +116,9 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
tokenPlacement,
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
reuseToken,
|
||||
refreshUrl,
|
||||
autoRefresh,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -294,12 +297,58 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
<input
|
||||
type="checkbox"
|
||||
className="cursor-pointer w-4 h-4 accent-indigo-600"
|
||||
checked={get(request, 'auth.oauth2.autoRefresh', false)}
|
||||
onChange={(e) => handleChange('autoRefresh', e.target.checked)}
|
||||
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>
|
||||
|
||||
<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} /> : ""}
|
||||
|
||||
@@ -12,12 +12,49 @@ import ApiKeyAuth from './ApiKeyAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
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 collectionRoot = get(collection, 'root', {});
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
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 getAuthView = () => {
|
||||
switch (authMode) {
|
||||
@@ -46,11 +83,12 @@ 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 the Collection: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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;
|
||||
@@ -0,0 +1,61 @@
|
||||
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,11 +1,115 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.timeline-event {
|
||||
border-bottom: 1px solid ${(props) => props.theme.colors.text.muted};
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timeline-event-content {
|
||||
background: ${(props) => props.theme.requestTabs.bg};
|
||||
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;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.oauth-request-item-content {
|
||||
background: ${(props) => props.theme.requestTabs.bg};
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.collapsible-section {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.section-header {
|
||||
cursor: pointer;
|
||||
padding: 8px 0;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-family: Inter, sans-serif !important;
|
||||
font-family: ${(props) => props.theme.font || 'Inter, sans-serif'} !important;
|
||||
|
||||
.arrow {
|
||||
opacity: 0.5;
|
||||
@@ -19,6 +123,35 @@ 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,61 +1,615 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import React, { useState } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import QueryResult from '../QueryResult/index';
|
||||
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
|
||||
import { get } from 'lodash';
|
||||
const iconv = require('iconv-lite');
|
||||
|
||||
const Timeline = ({ request, response }) => {
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = typeof response.headers === 'object' ? Object.entries(response.headers) : [];
|
||||
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',
|
||||
};
|
||||
|
||||
request = request || {};
|
||||
response = response || {};
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
});
|
||||
const getEffectiveAuthSource = (collection, item) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
uid: collection.uid,
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Get path from collection to item
|
||||
let path = [];
|
||||
let currentItem = findItemInCollection(collection, item?.uid);
|
||||
while (currentItem) {
|
||||
path.unshift(currentItem);
|
||||
currentItem = findParentItemInCollection(collection, currentItem?.uid);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
const [openSections, setOpenSections] = useState(() =>
|
||||
combinedTimeline.map((_, index) => index === 0)
|
||||
);
|
||||
|
||||
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) => {
|
||||
<StyledWrapper
|
||||
className="pb-4 w-full"
|
||||
style={{ maxWidth: width - 40, overflowWrap: 'break-word' }}
|
||||
>
|
||||
{combinedTimeline.map((event, index) => {
|
||||
const isOpen = openSections[index];
|
||||
const toggleOpen = () => {
|
||||
setOpenSections((prevState) => {
|
||||
const newState = [...prevState];
|
||||
newState[index] = !newState[index];
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
if (event.type === 'request') {
|
||||
const { request, response, timestamp } = event.data;
|
||||
return (
|
||||
<pre className="line request" key={h.name}>
|
||||
<span className="arrow">{'>'}</span> {h.name}: {h.value}
|
||||
</pre>
|
||||
<div key={index} className="timeline-event border-b border-gray-700 py-2">
|
||||
<div
|
||||
className="timeline-event-header cursor-pointer"
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
<div className="flex justify-between items-center min-w-0 gap-2">
|
||||
<div className="flex items-center space-x-2 min-w-0 flex-1">
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<span
|
||||
className={`${
|
||||
methodColors[request.method?.toUpperCase()] || 'text-white'
|
||||
} font-bold`}
|
||||
>
|
||||
{request.method?.toUpperCase()}
|
||||
</span>{' '}
|
||||
</div>
|
||||
<span
|
||||
className={`${
|
||||
statusColor(response.status || request.statusCode) || 'text-white'
|
||||
} font-bold`}
|
||||
>
|
||||
{response.status || request.statusCode}{' '}
|
||||
{response.statusText || ''}
|
||||
</span>
|
||||
<div className="flex items-center flex-shrink-0 space-x-2">
|
||||
{response.duration && (
|
||||
<span className="text-sm text-gray-400">
|
||||
{response.duration}ms
|
||||
</span>
|
||||
)}
|
||||
{response.size && (
|
||||
<span className="text-sm text-gray-400">
|
||||
{response.size}B
|
||||
</span>
|
||||
)}
|
||||
{response.state && (
|
||||
<span className="text-sm text-gray-400">
|
||||
{response.state}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-400 flex-shrink-0 overflow-hidden text-ellipsis whitespace-nowrap" style={{ minWidth: '120px', maxWidth: '200px' }}>
|
||||
{new Date(timestamp).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="truncate text-sm text-[#9b9b9b] mt-1">{request.url}</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="timeline-event-content ml-4 mt-2">
|
||||
<RenderRequestResponse
|
||||
request={request}
|
||||
response={response}
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{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) => {
|
||||
} else if (event.type === 'oauth2') {
|
||||
const { data } = event;
|
||||
const { debugInfo, fetchedAt } = data;
|
||||
const flattenedRequests = flattenRequests(debugInfo);
|
||||
return (
|
||||
<pre className="line response" key={h[0]}>
|
||||
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
|
||||
</pre>
|
||||
<div key={index} className="timeline-event border-b border-gray-700 py-2">
|
||||
<div
|
||||
className="timeline-event-header cursor-pointer flex items-center"
|
||||
onClick={toggleOpen}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>{isOpen ? '▼' : '▶'}</span>
|
||||
<span className="ml-2 font-bold">OAuth2.0 Calls</span>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="ml-4 mt-2">
|
||||
{flattenedRequests && flattenedRequests.length > 0 ? (
|
||||
flattenedRequests.map((data, idx) => (
|
||||
<OAuthRequestItem
|
||||
key={idx}
|
||||
request={data}
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width - 50}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div>No debug information available.</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
const flattenRequests = (requests, level = 0) => {
|
||||
let flatList = [];
|
||||
requests.forEach((request) => {
|
||||
flatList.push({ ...request, isSubRequest: level > 0 });
|
||||
if (request.requests && request.requests.length > 0) {
|
||||
flatList = flatList.concat(flattenRequests(request.requests, level + 1));
|
||||
}
|
||||
});
|
||||
return flatList;
|
||||
};
|
||||
|
||||
// Helper function to process dataBuffer
|
||||
const processDataBuffer = (dataBuffer) => {
|
||||
if (dataBuffer) {
|
||||
try {
|
||||
let buffer;
|
||||
if (Buffer.isBuffer(dataBuffer)) {
|
||||
buffer = dataBuffer;
|
||||
} else if (typeof dataBuffer === 'string') {
|
||||
buffer = Buffer.from(dataBuffer, 'base64');
|
||||
} else if (dataBuffer instanceof Uint8Array || Array.isArray(dataBuffer)) {
|
||||
buffer = Buffer.from(dataBuffer);
|
||||
} else {
|
||||
return JSON.stringify(dataBuffer);
|
||||
}
|
||||
const dataRaw = iconv.decode(buffer, 'utf-8');
|
||||
const formattedData = dataRaw.replace(/^\uFEFF/, '');
|
||||
return formattedData;
|
||||
} catch (error) {
|
||||
console.error('Error processing dataBuffer:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const RenderRequestResponse = ({ request, response, item, collection, width }) => {
|
||||
const [activeTab, setActiveTab] = useState('request');
|
||||
const [isRequestHeadersOpen, setIsRequestHeadersOpen] = useState(false);
|
||||
const [isResponseHeadersOpen, setIsResponseHeadersOpen] = useState(false);
|
||||
const [isRequestCookiesOpen, setIsRequestCookiesOpen] = useState(false);
|
||||
const [isResponseCookiesOpen, setIsResponseCookiesOpen] = useState(false);
|
||||
const [isRequestBodyOpen, setIsRequestBodyOpen] = useState(false);
|
||||
const [isResponseBodyOpen, setIsResponseBodyOpen] = useState(false);
|
||||
|
||||
const requestHeaders = request.headers || request.requestHeaders || {};
|
||||
const responseHeaders = response.headers || response.responseHeaders || {};
|
||||
|
||||
const {
|
||||
cookies: requestCookies,
|
||||
headers: filteredRequestHeaders,
|
||||
} = separateCookiesAndHeaders(requestHeaders);
|
||||
const {
|
||||
cookies: responseCookies,
|
||||
headers: filteredResponseHeaders,
|
||||
} = separateCookiesAndHeaders(responseHeaders);
|
||||
|
||||
const showNetworkLogs = response.timeline && response.timeline.length > 0;
|
||||
|
||||
return (
|
||||
<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' && (
|
||||
<div>
|
||||
{/* Method and URL */}
|
||||
<div className="mb-4">
|
||||
<span className={`${methodColors[request.method.toUpperCase()] || 'text-white'} font-bold`}>
|
||||
{request.method.toUpperCase()}
|
||||
</span>{' '}
|
||||
<span>{request.url}</span>
|
||||
</div>
|
||||
|
||||
{/* Headers */}
|
||||
<div className="collapsible-section">
|
||||
<div className="section-header" onClick={() => setIsRequestHeadersOpen(!isRequestHeadersOpen)}>
|
||||
<span className="font-bold">
|
||||
{isRequestHeadersOpen ? '▼' : '▶'} Headers
|
||||
{filteredRequestHeaders && Object.keys(filteredRequestHeaders).length > 0 &&
|
||||
<sup className="ml-1 font-medium">({Object.keys(filteredRequestHeaders).length})</sup>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
{isRequestHeadersOpen && (
|
||||
<div className="mt-2">
|
||||
{filteredRequestHeaders && Object.keys(filteredRequestHeaders).length > 0
|
||||
? renderHeaders(filteredRequestHeaders)
|
||||
: <div className="text-gray-500">No Headers found</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cookies */}
|
||||
<div className="collapsible-section">
|
||||
<div className="section-header" onClick={() => setIsRequestCookiesOpen(!isRequestCookiesOpen)}>
|
||||
<span className="font-bold">
|
||||
{isRequestCookiesOpen ? '▼' : '▶'} Cookies
|
||||
{requestCookies && Object.keys(requestCookies).length > 0 &&
|
||||
<sup className="ml-1 font-medium">({Object.keys(requestCookies).length})</sup>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
{isRequestCookiesOpen && (
|
||||
<div className="mt-2">
|
||||
{requestCookies && Object.keys(requestCookies).length > 0
|
||||
? renderHeaders(requestCookies)
|
||||
: <div className="text-gray-500">No Cookies found</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="collapsible-section">
|
||||
<div className="section-header" onClick={() => setIsRequestBodyOpen(!isRequestBodyOpen)}>
|
||||
<span className="font-bold">
|
||||
{isRequestBodyOpen ? '▼' : '▶'} Body
|
||||
</span>
|
||||
</div>
|
||||
{isRequestBodyOpen && (
|
||||
<div className="mt-2">
|
||||
{request.data || request.dataBuffer ? (
|
||||
<div className="h-96 overflow-auto">
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
data={request.data}
|
||||
dataBuffer={request.dataBuffer}
|
||||
headers={request.headers}
|
||||
error={request.error}
|
||||
key={item.filename}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500">No Body found</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Response Tab */}
|
||||
{activeTab === 'response' && (
|
||||
<div>
|
||||
{/* Status */}
|
||||
<div className="mb-4">
|
||||
<span className={`${statusColor(response.status || request.statusCode) || 'text-white'} font-bold`}>
|
||||
{response.status || request.statusCode}
|
||||
</span>{' '}
|
||||
<span>{response.statusText || request.statusText || ''}</span>
|
||||
{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 */}
|
||||
<div className="collapsible-section">
|
||||
<div className="section-header" onClick={() => setIsResponseHeadersOpen(!isResponseHeadersOpen)}>
|
||||
<span className="font-bold">
|
||||
{isResponseHeadersOpen ? '▼' : '▶'} Headers
|
||||
{filteredResponseHeaders && Object.keys(filteredResponseHeaders).length > 0 &&
|
||||
<sup className="ml-1 font-medium">({Object.keys(filteredResponseHeaders).length})</sup>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
{isResponseHeadersOpen && (
|
||||
<div className="mt-2">
|
||||
{filteredResponseHeaders && Object.keys(filteredResponseHeaders).length > 0
|
||||
? renderHeaders(filteredResponseHeaders)
|
||||
: <div className="text-gray-500">No Headers found</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cookies */}
|
||||
<div className="collapsible-section">
|
||||
<div className="section-header" onClick={() => setIsResponseCookiesOpen(!isResponseCookiesOpen)}>
|
||||
<span className="font-bold">
|
||||
{isResponseCookiesOpen ? '▼' : '▶'} Cookies
|
||||
{responseCookies && Object.keys(responseCookies).length > 0 &&
|
||||
<sup className="ml-1 font-medium">({Object.keys(responseCookies).length})</sup>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
{isResponseCookiesOpen && (
|
||||
<div className="mt-2">
|
||||
{responseCookies && Object.keys(responseCookies).length > 0
|
||||
? renderHeaders(responseCookies)
|
||||
: <div className="text-gray-500">No Cookies found</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="collapsible-section">
|
||||
<div className="section-header" onClick={() => setIsResponseBodyOpen(!isResponseBodyOpen)}>
|
||||
<span className="font-bold">
|
||||
{isResponseBodyOpen ? '▼' : '▶'} Body
|
||||
</span>
|
||||
</div>
|
||||
{isResponseBodyOpen && (
|
||||
<div className="mt-2">
|
||||
{response.data || response.dataBuffer ? (
|
||||
<div className="h-96 overflow-auto">
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
data={response.data}
|
||||
dataBuffer={response.dataBuffer}
|
||||
headers={response.headers}
|
||||
error={response.error}
|
||||
key={item.filename}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500">No Body found</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Network Logs Tab */}
|
||||
{activeTab === 'networkLogs' && showNetworkLogs && (
|
||||
<div className="bg-black/5 text-white p-2 network-logs rounded overflow-auto h-96">
|
||||
<pre className="whitespace-pre-wrap">
|
||||
{response.timeline.map((entry, index) => (
|
||||
<NetworkLogsEntry key={index} entry={entry} />
|
||||
))}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
const renderHeaders = (data) => {
|
||||
if (Array.isArray(data)) {
|
||||
return (
|
||||
<div className="mt-2 text-sm">
|
||||
{data.map((header, index) => (
|
||||
<div key={index} className="flex mb-2">
|
||||
<div className="w-1/3 font-bold">{header.name}:</div>
|
||||
<div className="w-2/3">{String(header.value)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mt-2 text-sm">
|
||||
{Object.entries(data).map(([key, value], index) => (
|
||||
<div key={index} className="flex mb-2">
|
||||
<div className="w-1/3 font-bold">{key}:</div>
|
||||
<div className="w-2/3">{String(value)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const separateCookiesAndHeaders = (headers) => {
|
||||
const cookies = {};
|
||||
let filteredHeaders = {};
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
filteredHeaders = headers.filter((header) => header.enabled !== false);
|
||||
filteredHeaders.forEach((header) => {
|
||||
if (
|
||||
header.name.toLowerCase() === 'cookie' ||
|
||||
header.name.toLowerCase() === 'set-cookie'
|
||||
) {
|
||||
cookies[header.name] = header.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (key.toLowerCase() === 'cookie' || key.toLowerCase() === 'set-cookie') {
|
||||
cookies[key] = value;
|
||||
} else {
|
||||
filteredHeaders[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { cookies, headers: filteredHeaders };
|
||||
};
|
||||
|
||||
const OAuthRequestItem = ({ request, item, collection, width }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggleOpen = () => {
|
||||
setIsOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
const { isSubRequest } = request;
|
||||
const url = request.url || '';
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
|
||||
const isImage = imageExtensions.some((ext) => url.toLowerCase().endsWith(ext));
|
||||
|
||||
return (
|
||||
<div className="border-b border-gray-700 py-2">
|
||||
<div className="oauth-request-item-header cursor-pointer" onClick={toggleOpen}>
|
||||
<div className="flex justify-between items-center min-w-0">
|
||||
<div className="flex items-center space-x-2 min-w-0">
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<span className={`${methodColors[request.method?.toUpperCase()] || 'text-white'} font-bold`}>
|
||||
{request.method?.toUpperCase()}
|
||||
</span>{' '}
|
||||
</div>
|
||||
<span className={`${statusColor(request.statusCode) || 'text-white'} font-bold`}>
|
||||
{request.statusCode}
|
||||
{request.statusText || ''}
|
||||
</span>
|
||||
<div className="flex items-center flex-shrink-0 space-x-2">
|
||||
{isSubRequest && <span className="request-label">API Request</span>}
|
||||
{isImage && <span className="request-label">Image</span>}
|
||||
{request.duration && <span className="text-sm text-gray-400">{request.duration}ms</span>}
|
||||
{request.size && <span className="text-sm text-gray-400">{request.size}B</span>}
|
||||
{request.state && <span className="text-sm text-gray-400">{request.state}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="truncate text-sm text-[#9b9b9b] mt-1">{request.url}</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="oauth-request-item-content mt-2 text-sm">
|
||||
<RenderRequestResponse
|
||||
request={request}
|
||||
response={request}
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
@@ -54,7 +54,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={item.requestSent} response={item.response} />;
|
||||
return <Timeline collection={collection} item={item} width={rightPaneWidth} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;
|
||||
|
||||
@@ -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 <Timeline request={requestSent} response={responseReceived} />;
|
||||
return <RunnerTimeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} assertionResults={assertionResults} />;
|
||||
|
||||
@@ -164,7 +164,13 @@ const useIpcEvents = () => {
|
||||
});
|
||||
|
||||
const removeCollectionOauth2CredentialsUpdatesListener = ipcRenderer.on('main:credentials-update', (val) => {
|
||||
dispatch(collectionAddOauth2CredentialsByUrl(val));
|
||||
const payload = {
|
||||
...val,
|
||||
itemUid: val.itemUid || null,
|
||||
folderUid: val.folderUid || null,
|
||||
credentialsId: val.credentialsId || 'credentials'
|
||||
};
|
||||
dispatch(collectionAddOauth2CredentialsByUrl(payload));
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -238,11 +238,20 @@ 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: response
|
||||
response: serializedResponse
|
||||
})
|
||||
);
|
||||
})
|
||||
@@ -1195,39 +1204,61 @@ export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getS
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchOauth2Credentials = (payload) => async (dispatch, getState) => {
|
||||
const { request, collection, itemUid } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:fetch-oauth2-credentials', { itemUid, request, collection })
|
||||
.then(({ credentials, url, collectionUid, credentialsId }) => {
|
||||
dispatch(collectionAddOauth2CredentialsByUrl({ credentials, url, collectionUid, credentialsId }));
|
||||
resolve(credentials);
|
||||
})
|
||||
.catch(reject);
|
||||
})
|
||||
}
|
||||
|
||||
export const refreshOauth2Credentials = (payload) => async (dispatch, getState) => {
|
||||
const { request, collection } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:refresh-oauth2-credentials', { request, collection })
|
||||
.then(({ credentials, url, collectionUid, credentialsId }) => {
|
||||
dispatch(collectionAddOauth2CredentialsByUrl({ credentials, url, collectionUid, credentialsId }));
|
||||
resolve(credentials);
|
||||
})
|
||||
.catch(reject);
|
||||
})
|
||||
}
|
||||
|
||||
export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
|
||||
const { collectionUid, url, credentialsId } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer.invoke('clear-oauth2-cache', collectionUid, url, credentialsId).then(() => {
|
||||
dispatch(collectionClearOauth2CredentialsByUrl({ collectionUid, url, credentialsId }));
|
||||
resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
};
|
||||
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,
|
||||
folderUid: folderUid || null,
|
||||
itemUid: !folderUid ? itemUid : null
|
||||
})
|
||||
);
|
||||
resolve(credentials);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const refreshOauth2Credentials = (payload) => async (dispatch, getState) => {
|
||||
const { request, collection, folderUid, itemId } = payload;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.ipcRenderer
|
||||
.invoke('renderer:refresh-oauth2-credentials', { request, collection })
|
||||
.then(({ credentials, url, collectionUid, debugInfo }) => {
|
||||
dispatch(
|
||||
collectionAddOauth2CredentialsByUrl({
|
||||
credentials,
|
||||
url,
|
||||
collectionUid,
|
||||
debugInfo,
|
||||
folderUid: folderUid || null,
|
||||
itemId: !folderUid ? itemId : 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(() => {
|
||||
// We do not dispatch any action to modify the Redux store,
|
||||
// since we are only clearing the session on the main process side.
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
@@ -260,13 +260,36 @@ 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,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1444,6 +1467,26 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.request.tests', action.payload.tests);
|
||||
}
|
||||
},
|
||||
updateFolderAuth: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
console.log('action.payload.content inside bro', action.payload);
|
||||
if (!collection) return;
|
||||
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.itemUid) : null;
|
||||
console.log('folder inside bro', folder);
|
||||
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);
|
||||
|
||||
@@ -1886,34 +1929,83 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
collectionAddOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, url, credentials, credentialsId } = action.payload;
|
||||
const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (!collection) return;
|
||||
if (!collection?.oauth2Credentials) {
|
||||
|
||||
// Update oauth2Credentials (latest token)
|
||||
if (!collection.oauth2Credentials) {
|
||||
collection.oauth2Credentials = [];
|
||||
}
|
||||
let collectionOauth2Credentials = cloneDeep(collection?.oauth2Credentials);
|
||||
const filterdOauth2Credentials = filter(collectionOauth2Credentials, creds => !(creds?.url == url && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId));
|
||||
filterdOauth2Credentials.push({ collectionUid, url, credentials, credentialsId });
|
||||
collection.oauth2Credentials = filterdOauth2Credentials;
|
||||
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) {
|
||||
collection.oauth2Credentials = [];
|
||||
}
|
||||
let collectionOauth2Credentials = cloneDeep(collection?.oauth2Credentials);
|
||||
const filterdOauth2Credentials = filter(collectionOauth2Credentials, creds => !(creds?.url == url && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId));
|
||||
collection.oauth2Credentials = filterdOauth2Credentials;
|
||||
// Since we don't want to remove tokens from oauth2Credentials or timeline,
|
||||
},
|
||||
|
||||
collectionGetOauth2CredentialsByUrl: (state, action) => {
|
||||
const { collectionUid, url, credentialsId } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
const oauth2Credentials = find(collection?.oauth2Credentials || [], creds => (creds?.url == url && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId));
|
||||
return oauth2Credentials;
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2016,7 +2108,9 @@ export const {
|
||||
updateFolderDocs,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
updateFolderAuth,
|
||||
updateFolderAuthMode
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -367,7 +367,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
reuseToken: get(si.request, 'auth.oauth2.reuseToken', false)
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@@ -386,7 +387,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
reuseToken: get(si.request, 'auth.oauth2.reuseToken', false)
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
@@ -402,7 +404,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
||||
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
||||
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
||||
reuseToken: get(si.request, 'auth.oauth2.reuseToken', false)
|
||||
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
||||
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -1050,9 +1053,11 @@ export const getEnvVars = (environment = {}) => {
|
||||
export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {
|
||||
let credentialsVariables = {};
|
||||
oauth2Credentials.forEach(({ credentialsId, credentials }) => {
|
||||
Object.entries(credentials).forEach(([key, value]) => {
|
||||
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
|
||||
});
|
||||
if (credentials) {
|
||||
Object.entries(credentials).forEach(([key, value]) => {
|
||||
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
|
||||
});
|
||||
}
|
||||
});
|
||||
return credentialsVariables;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,7 +14,8 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
size: response.size,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
duration: response.duration
|
||||
duration: response.duration,
|
||||
timeline: response.timeline
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
|
||||
@@ -51,6 +51,7 @@ const jsonToCollectionBru = (json, isFolder) => {
|
||||
res: _.get(json, 'request.vars.res', [])
|
||||
},
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
docs: _.get(json, 'docs', '')
|
||||
};
|
||||
|
||||
@@ -63,10 +64,6 @@ const jsonToCollectionBru = (json, isFolder) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (!isFolder) {
|
||||
collectionBruJson.auth = _.get(json, 'request.auth', {});
|
||||
}
|
||||
|
||||
return _jsonToCollectionBru(collectionBruJson);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
|
||||
@@ -808,7 +808,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
}
|
||||
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
requestCopy = await configureRequestWithCertsAndProxy({
|
||||
const {newRequest} = await configureRequestWithCertsAndProxy({
|
||||
collectionUid,
|
||||
request: requestCopy,
|
||||
envVars,
|
||||
@@ -816,23 +816,24 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
});
|
||||
requestCopy = newRequest
|
||||
const { oauth2: { grantType }} = requestCopy || {};
|
||||
let credentials, url, credentialsId;
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url, credentialsId } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, forceFetch: true }));
|
||||
({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, forceFetch: true }));
|
||||
break;
|
||||
case 'client_credentials':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url, credentialsId } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, forceFetch: true }));
|
||||
({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, forceFetch: true }));
|
||||
break;
|
||||
case 'password':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url, credentialsId } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, forceFetch: true }));
|
||||
({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, forceFetch: true }));
|
||||
break;
|
||||
}
|
||||
return { credentials, url, collectionUid, credentialsId };
|
||||
return { credentials, url, collectionUid, credentialsId, debugInfo };
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
@@ -843,11 +844,20 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
||||
try {
|
||||
if (request.oauth2) {
|
||||
let requestCopy = _.cloneDeep(request);
|
||||
const { uid: collectionUid, runtimeVariables, environments = [], activeEnvironmentUid } = collection;
|
||||
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 {newRequest} = await configureRequestWithCertsAndProxy({
|
||||
collectionUid,
|
||||
request: requestCopy,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath
|
||||
});
|
||||
requestCopy = newRequest
|
||||
let { credentials, url, credentialsId } = await refreshOauth2Token(requestCopy, collectionUid);
|
||||
return { credentials, url, collectionUid, credentialsId };
|
||||
}
|
||||
|
||||
@@ -8,12 +8,15 @@ 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();
|
||||
|
||||
// main window id is '1'
|
||||
// get all other windows
|
||||
let windowsExcludingMain = allOpenWindows.filter((w) => w.id != 1);
|
||||
// Close all windows except the main window (assumed to have id 1)
|
||||
let windowsExcludingMain = allOpenWindows.filter((w) => w.id !== 1);
|
||||
windowsExcludingMain.forEach((w) => {
|
||||
w.close();
|
||||
});
|
||||
@@ -27,26 +30,140 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
});
|
||||
window.on('ready-to-show', window.show.bind(window));
|
||||
|
||||
// We want browser window to comply with "SSL/TLS Certificate Verification" toggle in Preferences
|
||||
// Ensure the browser window complies with "SSL/TLS Certificate Verification" preference
|
||||
window.webContents.on('certificate-error', (event, url, error, certificate, callback) => {
|
||||
event.preventDefault();
|
||||
callback(!preferencesUtil.shouldVerifyTls());
|
||||
});
|
||||
|
||||
function onWindowRedirect(url) {
|
||||
// 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'));
|
||||
const { session: webSession } = window.webContents;
|
||||
|
||||
// Map to store request data using requestId as the key
|
||||
const requestMap = {};
|
||||
|
||||
// Intercept request events and gather data
|
||||
webSession.webRequest.onBeforeRequest((details, callback) => {
|
||||
const { id: requestId, url, method, resourceType, frameId } = details;
|
||||
|
||||
const request = {
|
||||
requestId,
|
||||
url,
|
||||
method,
|
||||
resourceType,
|
||||
frameId,
|
||||
timestamp: Date.now(),
|
||||
requestHeaders: {},
|
||||
responseHeaders: {},
|
||||
statusCode: null,
|
||||
error: null,
|
||||
fromCache: null,
|
||||
completed: false,
|
||||
};
|
||||
|
||||
requestMap[requestId] = request;
|
||||
|
||||
if (resourceType === 'mainFrame') {
|
||||
// This is a main frame request
|
||||
currentMainRequest = {
|
||||
requestId,
|
||||
url,
|
||||
method,
|
||||
timestamp: request.timestamp,
|
||||
requestHeaders: {},
|
||||
responseHeaders: {},
|
||||
statusCode: null,
|
||||
error: null,
|
||||
fromCache: null,
|
||||
completed: false,
|
||||
requests: [], // To hold sub-resource requests
|
||||
};
|
||||
// Add to mainRequests
|
||||
debugInfo.data.push(currentMainRequest);
|
||||
} else if (currentMainRequest) {
|
||||
// Associate sub-resource request with current main request
|
||||
currentMainRequest.requests.push(request);
|
||||
}
|
||||
|
||||
callback({ cancel: false });
|
||||
});
|
||||
|
||||
webSession.webRequest.onBeforeSendHeaders((details, callback) => {
|
||||
const { id: requestId, requestHeaders } = details;
|
||||
if (requestMap[requestId]) {
|
||||
requestMap[requestId].requestHeaders = requestHeaders;
|
||||
}
|
||||
|
||||
if (requestMap[requestId]?.resourceType === 'mainFrame') {
|
||||
if (currentMainRequest?.requestId === requestId) {
|
||||
currentMainRequest.requestHeaders = requestHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
callback({ cancel: false, requestHeaders });
|
||||
});
|
||||
|
||||
webSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
const { id: requestId, statusCode, responseHeaders } = details;
|
||||
if (requestMap[requestId]) {
|
||||
requestMap[requestId].statusCode = statusCode;
|
||||
requestMap[requestId].responseHeaders = responseHeaders;
|
||||
}
|
||||
|
||||
if (requestMap[requestId]?.resourceType === 'mainFrame') {
|
||||
if (currentMainRequest?.requestId === requestId) {
|
||||
currentMainRequest.statusCode = statusCode;
|
||||
currentMainRequest.responseHeaders = responseHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
callback({ cancel: false, responseHeaders });
|
||||
});
|
||||
|
||||
webSession.webRequest.onCompleted((details) => {
|
||||
const { id: requestId, fromCache } = details;
|
||||
if (requestMap[requestId]) {
|
||||
requestMap[requestId].completed = true;
|
||||
requestMap[requestId].fromCache = fromCache;
|
||||
}
|
||||
|
||||
// If this is a mainFrame request, update currentMainRequest
|
||||
if (requestMap[requestId]?.resourceType === 'mainFrame') {
|
||||
if (currentMainRequest?.requestId === requestId) {
|
||||
currentMainRequest.completed = true;
|
||||
currentMainRequest.fromCache = fromCache;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
webSession.webRequest.onErrorOccurred((details) => {
|
||||
const { id: requestId, error } = details;
|
||||
if (requestMap[requestId]) {
|
||||
requestMap[requestId].error = error;
|
||||
}
|
||||
|
||||
// If this is a mainFrame request, update currentMainRequest
|
||||
if (requestMap[requestId]?.resourceType === 'mainFrame') {
|
||||
if (currentMainRequest?.requestId === requestId) {
|
||||
currentMainRequest.error = error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function onWindowRedirect(url) {
|
||||
// Handle redirects as needed
|
||||
|
||||
// Check if redirect is to the callback URL and contains an authorization code
|
||||
if (matchesCallbackUrl(new URL(url), new URL(callbackUrl))) {
|
||||
finalUrl = url;
|
||||
window.close();
|
||||
}
|
||||
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');
|
||||
|
||||
// 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');
|
||||
let errorData = {
|
||||
message: 'Authorization Failed!',
|
||||
error,
|
||||
@@ -58,13 +175,37 @@ 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 });
|
||||
return resolve({ authorizationCode, debugInfo });
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
@@ -73,20 +214,10 @@ 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) {
|
||||
// If browser redirects before load finished, loadURL throws an error with code ERR_ABORTED. This should be ignored.
|
||||
// Ignore ERR_ABORTED errors that occur during redirects
|
||||
if (error.code === 'ERR_ABORTED') {
|
||||
console.debug('Ignoring ERR_ABORTED during authorizeUserInWindow');
|
||||
return;
|
||||
@@ -97,4 +228,4 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { authorizeUserInWindow, matchesCallbackUrl };
|
||||
module.exports = { authorizeUserInWindow, matchesCallbackUrl };
|
||||
@@ -277,7 +277,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
credentials: request?.oauth2Credentials?.credentials,
|
||||
url: request?.oauth2Credentials?.url,
|
||||
collectionUid,
|
||||
credentialsId: request?.oauth2Credentials?.credentialsId
|
||||
credentialsId: request?.oauth2Credentials?.credentialsId,
|
||||
...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),
|
||||
debugInfo: request?.oauth2Credentials?.debugInfo,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -407,7 +409,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
data: response.data,
|
||||
dataBuffer: dataBuffer.toString('base64'),
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
duration: responseTime ?? 0
|
||||
duration: responseTime ?? 0,
|
||||
timeline: response.timeline
|
||||
};
|
||||
} catch (error) {
|
||||
deleteCancelToken(cancelTokenUid);
|
||||
|
||||
@@ -173,7 +173,8 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
|
||||
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
|
||||
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
|
||||
request.oauth2.reuseToken = _interpolate(request.oauth2.reuseToken) || false;
|
||||
request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);
|
||||
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
|
||||
break;
|
||||
case 'authorization_code':
|
||||
request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || '';
|
||||
@@ -190,7 +191,8 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
|
||||
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
|
||||
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
|
||||
request.oauth2.reuseToken = _interpolate(request.oauth2.reuseToken) || false;
|
||||
request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);
|
||||
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
|
||||
break;
|
||||
case 'client_credentials':
|
||||
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
|
||||
@@ -203,7 +205,8 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
|
||||
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
|
||||
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
|
||||
request.oauth2.reuseToken = _interpolate(request.oauth2.reuseToken) || false;
|
||||
request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);
|
||||
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -3,11 +3,31 @@ 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('./proxy-util');
|
||||
const { addCookieToJar, getCookieStringForUrl } = require('./cookies');
|
||||
const { preferencesUtil } = require('../store/preferences');
|
||||
|
||||
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) {
|
||||
@@ -49,13 +69,16 @@ const checkConnection = (host, port) =>
|
||||
* @see https://github.com/axios/axios/issues/695
|
||||
* @returns {axios.AxiosInstance}
|
||||
*/
|
||||
function makeAxiosInstance() {
|
||||
function makeAxiosInstance({
|
||||
proxyMode = 'off',
|
||||
proxyConfig = {},
|
||||
requestMaxRedirects = 0,
|
||||
httpsAgentRequestFields = {},
|
||||
interpolationOptions = {}
|
||||
} = {}) {
|
||||
/** @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) {
|
||||
@@ -75,6 +98,51 @@ function makeAxiosInstance() {
|
||||
|
||||
instance.interceptors.request.use(async (config) => {
|
||||
const url = URL.parse(config.url);
|
||||
|
||||
config.metadata = config.metadata || {};
|
||||
config.metadata.startTime = new Date().getTime();
|
||||
config.metadata.timeline = config.metadata.timeline || [];
|
||||
|
||||
// Add initial request details to the timeline
|
||||
config.metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Preparing request to ${config.url}`,
|
||||
});
|
||||
config.metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Current time is ${new Date().toISOString()}`,
|
||||
});
|
||||
|
||||
// Add request method and headers
|
||||
config.metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'request',
|
||||
message: `${config.method.toUpperCase()} ${config.url}`,
|
||||
});
|
||||
Object.entries(config.headers).forEach(([key, value]) => {
|
||||
config.metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'requestHeader',
|
||||
message: `${key}: ${value}`,
|
||||
});
|
||||
});
|
||||
|
||||
// Add request data if available
|
||||
if (config.data) {
|
||||
let requestData;
|
||||
try {
|
||||
requestData = typeof config.data === 'string' ? config.data : JSON.stringify(config.data, null, 2);
|
||||
} catch (err) {
|
||||
requestData = config.data.toString();
|
||||
}
|
||||
config.metadata.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)
|
||||
@@ -91,14 +159,73 @@ function makeAxiosInstance() {
|
||||
}
|
||||
|
||||
config.headers['request-start-time'] = Date.now();
|
||||
|
||||
const agentOptions = {
|
||||
...httpsAgentRequestFields,
|
||||
keepAlive: true,
|
||||
};
|
||||
|
||||
// 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: config.metadata.timeline,
|
||||
});
|
||||
return config;
|
||||
});
|
||||
|
||||
let redirectCount = 0
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
const end = Date.now();
|
||||
const start = response.config.headers['request-start-time'];
|
||||
response.headers['request-duration'] = end - start;
|
||||
redirectCount = 0;
|
||||
|
||||
const config = response.config;
|
||||
const metadata = config.metadata;
|
||||
const duration = end - metadata.startTime;
|
||||
|
||||
const httpVersion = response.request?.res?.httpVersion || '1.1';
|
||||
metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'response',
|
||||
message: `HTTP/${httpVersion} ${response.status} ${response.statusText}`,
|
||||
});
|
||||
|
||||
if (httpVersion.startsWith('2')) {
|
||||
metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Using HTTP/2, server supports multiplexing`,
|
||||
});
|
||||
}
|
||||
|
||||
metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'response',
|
||||
message: `HTTP/${response.httpVersion || '1.1'} ${response.status} ${response.statusText}`,
|
||||
});
|
||||
Object.entries(response.headers).forEach(([key, value]) => {
|
||||
metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'responseHeader',
|
||||
message: `${key}: ${value}`,
|
||||
});
|
||||
});
|
||||
metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Request completed in ${duration} ms`,
|
||||
});
|
||||
|
||||
// Attach the timeline to the response
|
||||
response.timeline = metadata.timeline;
|
||||
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
@@ -106,6 +233,86 @@ function makeAxiosInstance() {
|
||||
const end = Date.now();
|
||||
const start = error.config.headers['request-start-time'];
|
||||
error.response.headers['request-duration'] = end - start;
|
||||
const config = error.config;
|
||||
const metadata = config.metadata;
|
||||
const duration = end - metadata.startTime;
|
||||
|
||||
if (error.response && redirectResponseCodes.includes(error.response.status)) {
|
||||
|
||||
|
||||
metadata.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]) => {
|
||||
metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'responseHeader',
|
||||
message: `${key}: ${value}`,
|
||||
});
|
||||
});
|
||||
metadata.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Request completed in ${duration} ms`,
|
||||
});
|
||||
|
||||
// Attach the timeline to the response
|
||||
error.response.timeline = metadata.timeline;
|
||||
|
||||
if (redirectCount >= requestMaxRedirects) {
|
||||
const dataBuffer = Buffer.from(error.response.data);
|
||||
|
||||
return {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
dataBuffer: dataBuffer.toString('base64'),
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
duration: error.response.headers.get('request-duration') ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// Increase redirect count
|
||||
redirectCount++;
|
||||
|
||||
const redirectUrl = error.response.headers.location;
|
||||
|
||||
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(error.response.headers.location);
|
||||
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
|
||||
requestConfig.headers['cookie'] = cookieString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setupProxyAgents({
|
||||
requestConfig,
|
||||
proxyMode,
|
||||
proxyConfig,
|
||||
httpsAgentRequestFields,
|
||||
interpolationOptions,
|
||||
timeline: metadata.timeline
|
||||
});
|
||||
|
||||
// Make the redirected request
|
||||
return instance(requestConfig);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -293,18 +293,65 @@ const getEnvVars = (environment = {}) => {
|
||||
const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {
|
||||
let credentialsVariables = {};
|
||||
oauth2Credentials.forEach(({ credentialsId, credentials }) => {
|
||||
Object.entries(credentials).forEach(([key, value]) => {
|
||||
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
|
||||
});
|
||||
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,
|
||||
slash,
|
||||
findItemByPathname,
|
||||
|
||||
@@ -7,21 +7,30 @@ const { safeParseJSON, safeStringifyJSON } = require('./common');
|
||||
|
||||
const oauth2Store = new Oauth2Store();
|
||||
|
||||
// temp: this should be removed when more complex scenarios for fetching tokens are handled (refershing, automatic fetch, ...)
|
||||
const ALWAYS_REUSE_ACCESS_TOKEN____UNLESS_FETCHED_MANUALLY = true;
|
||||
|
||||
const persistOauth2Credentials = ({ collectionUid, url, credentials, credentialsId }) => {
|
||||
oauth2Store.updateCredentialsForCollection({ collectionUid, url, credentials, credentialsId });
|
||||
}
|
||||
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 }) => {
|
||||
const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId });
|
||||
return credentials;
|
||||
}
|
||||
};
|
||||
|
||||
const isTokenExpired = (credentials) => {
|
||||
if (!credentials || !credentials.expires_in || !credentials.created_at) {
|
||||
return true; // Assume expired if missing data
|
||||
}
|
||||
const expiryTime = credentials.created_at + credentials.expires_in * 1000;
|
||||
return Date.now() > expiryTime;
|
||||
};
|
||||
|
||||
// AUTHORIZATION CODE
|
||||
|
||||
@@ -31,19 +40,79 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
|
||||
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, callbackUrl, scope, pkce, credentialsPlacement, authorizationUrl, credentialsId, reuseToken } = oAuth;
|
||||
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 ((reuseToken || ALWAYS_REUSE_ACCESS_TOKEN____UNLESS_FETCHED_MANUALLY) && !forceFetch) {
|
||||
const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }) || {};
|
||||
return { collectionUid, url, credentials, 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);
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
|
||||
|
||||
// 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") {
|
||||
if (credentialsPlacement === "basic_auth_header") {
|
||||
requestCopy.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
|
||||
}
|
||||
const data = {
|
||||
@@ -51,22 +120,95 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
|
||||
code: authorizationCode,
|
||||
redirect_uri: callbackUrl,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
};
|
||||
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
if (pkce) {
|
||||
data['code_verifier'] = codeVerifier;
|
||||
}
|
||||
if (scope) {
|
||||
data.scope = scope;
|
||||
}
|
||||
requestCopy.data = data;
|
||||
requestCopy.url = url;
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
|
||||
try {
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
// Interceptor to capture request data
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: config.data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const responseData = Buffer.isBuffer(response.data) ? response.data?.toString() : response.data;
|
||||
const parsedResponseData = safeParseJSON(responseData);
|
||||
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(), // Generate a unique requestId
|
||||
url: axiosRequestInfo.url,
|
||||
method: axiosRequestInfo.method,
|
||||
timestamp: axiosRequestInfo.timestamp,
|
||||
requestHeaders: axiosRequestInfo.headers || {},
|
||||
requestBody: axiosRequestInfo.data,
|
||||
responseHeaders: axiosResponseInfo.headers || {},
|
||||
responseBody: parsedResponseData,
|
||||
statusCode: axiosResponseInfo.status || null,
|
||||
statusMessage: axiosResponseInfo.statusText || null,
|
||||
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 };
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
}
|
||||
};
|
||||
@@ -94,12 +236,12 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
|
||||
}
|
||||
try {
|
||||
const authorizeUrl = authorizationUrlWithQueryParams.toString();
|
||||
const { authorizationCode } = await authorizeUserInWindow({
|
||||
const { authorizationCode, debugInfo } = await authorizeUserInWindow({
|
||||
authorizeUrl,
|
||||
callbackUrl,
|
||||
session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: accessTokenUrl })
|
||||
});
|
||||
resolve({ authorizationCode });
|
||||
resolve({ authorizationCode, debugInfo });
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
@@ -111,42 +253,158 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
|
||||
const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, forceFetch = false }) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, scope, credentialsPlacement, credentialsId, reuseToken } = oAuth;
|
||||
const {
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
} = oAuth;
|
||||
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
|
||||
if ((reuseToken || ALWAYS_REUSE_ACCESS_TOKEN____UNLESS_FETCHED_MANUALLY) && !forceFetch) {
|
||||
const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }) || {};
|
||||
return { collectionUid, url, credentials, credentialsId };
|
||||
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);
|
||||
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") {
|
||||
if (credentialsPlacement === "basic_auth_header") {
|
||||
requestCopy.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
|
||||
}
|
||||
const data = {
|
||||
grant_type: 'client_credentials',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
};
|
||||
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
if (scope) {
|
||||
data.scope = scope;
|
||||
}
|
||||
requestCopy.data = data;
|
||||
requestCopy.url = url;
|
||||
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
let debugInfo = { data: [] };
|
||||
|
||||
try {
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
// Interceptor to capture request data
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: config.data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const responseData = Buffer.isBuffer(response.data) ? response.data?.toString() : response.data;
|
||||
const responseData = Buffer.isBuffer(response.data) ? response.data.toString() : response.data;
|
||||
const parsedResponseData = safeParseJSON(responseData);
|
||||
|
||||
// Add the axios request and response info as a main request in debugInfo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
url: axiosRequestInfo.url,
|
||||
method: axiosRequestInfo.method,
|
||||
timestamp: axiosRequestInfo.timestamp,
|
||||
requestHeaders: axiosRequestInfo.headers || {},
|
||||
requestBody: axiosRequestInfo.data,
|
||||
responseHeaders: axiosResponseInfo.headers || {},
|
||||
responseBody: parsedResponseData,
|
||||
statusCode: axiosResponseInfo.status || null,
|
||||
statusMessage: axiosResponseInfo.statusText || null,
|
||||
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 };
|
||||
}
|
||||
catch (error) {
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
}
|
||||
};
|
||||
@@ -156,18 +414,76 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
|
||||
const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, forceFetch = false }) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { username, password, clientId, clientSecret, scope, credentialsPlacement, credentialsId, reuseToken } = oAuth;
|
||||
const {
|
||||
username,
|
||||
password,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
credentialsPlacement,
|
||||
credentialsId,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
} = oAuth;
|
||||
const url = requestCopy?.oauth2?.accessTokenUrl;
|
||||
|
||||
if ((reuseToken || ALWAYS_REUSE_ACCESS_TOKEN____UNLESS_FETCHED_MANUALLY) && !forceFetch) {
|
||||
const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId }) || {};
|
||||
return { collectionUid, url, credentials, credentialsId };
|
||||
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);
|
||||
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") {
|
||||
if (credentialsPlacement === "basic_auth_header") {
|
||||
requestCopy.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
|
||||
}
|
||||
const data = {
|
||||
@@ -175,46 +491,108 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
|
||||
username,
|
||||
password,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret
|
||||
};
|
||||
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
|
||||
data.client_secret = clientSecret;
|
||||
}
|
||||
if (scope) {
|
||||
data.scope = scope;
|
||||
}
|
||||
requestCopy.data = data;
|
||||
requestCopy.url = url;
|
||||
|
||||
// Initialize variables to hold request and response data for debugging
|
||||
let axiosRequestInfo = null;
|
||||
let axiosResponseInfo = null;
|
||||
let debugInfo = { data: [] };
|
||||
|
||||
try {
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
// Interceptor to capture request data
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
axiosRequestInfo = {
|
||||
method: config.method.toUpperCase(),
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
data: config.data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return config;
|
||||
});
|
||||
|
||||
// Interceptor to capture response data
|
||||
axiosInstance.interceptors.response.use((response) => {
|
||||
axiosResponseInfo = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return response;
|
||||
}, (error) => {
|
||||
if (error.response) {
|
||||
axiosResponseInfo = {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
headers: error.response.headers,
|
||||
data: error.response.data,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const responseData = Buffer.isBuffer(response.data) ? response.data?.toString() : response.data;
|
||||
const responseData = Buffer.isBuffer(response.data) ? response.data.toString() : response.data;
|
||||
const parsedResponseData = safeParseJSON(responseData);
|
||||
|
||||
// Add the axios request and response info as a main request in debugInfo
|
||||
const axiosMainRequest = {
|
||||
requestId: Date.now().toString(),
|
||||
url: axiosRequestInfo.url,
|
||||
method: axiosRequestInfo.method,
|
||||
timestamp: axiosRequestInfo.timestamp,
|
||||
requestHeaders: axiosRequestInfo.headers || {},
|
||||
requestBody: axiosRequestInfo.data,
|
||||
responseHeaders: axiosResponseInfo.headers || {},
|
||||
responseBody: parsedResponseData,
|
||||
statusCode: axiosResponseInfo.status || null,
|
||||
statusMessage: axiosResponseInfo.statusText || null,
|
||||
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 };
|
||||
}
|
||||
catch (error) {
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
|
||||
} catch (error) {
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
}
|
||||
};
|
||||
|
||||
const refreshOauth2Token = async (request, collectionUid) => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
const refreshOauth2Token = async (requestCopy, collectionUid) => {
|
||||
const oAuth = get(requestCopy, 'oauth2', {});
|
||||
const { clientId, clientSecret, credentialsId } = oAuth;
|
||||
const url = requestCopy?.oauth2?.refreshUrl ? requestCopy?.oauth2?.refreshUrl : requestCopy?.oauth2?.accessTokenUrl;
|
||||
const url = oAuth.refreshUrl ? oAuth.refreshUrl : oAuth.accessTokenUrl;
|
||||
|
||||
const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
if (!credentials?.refresh_token) {
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
let error = new Error('no refresh token found');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
else {
|
||||
// Proceed without token
|
||||
return { collectionUid, url, credentials: null, credentialsId };
|
||||
} else {
|
||||
const data = {
|
||||
grant_type: 'refresh_token',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
refresh_token: credentials?.refresh_token
|
||||
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';
|
||||
@@ -224,18 +602,17 @@ const refreshOauth2Token = async (request, collectionUid) => {
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
try {
|
||||
const response = await axiosInstance(requestCopy);
|
||||
const responseData = Buffer.isBuffer(response.data) ? response.data?.toString() : response.data;
|
||||
const responseData = Buffer.isBuffer(response.data) ? response.data.toString() : response.data;
|
||||
const parsedResponseData = safeParseJSON(responseData);
|
||||
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
|
||||
return { collectionUid, url, credentials: parsedResponseData, credentialsId };
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
clearOauth2Credentials({ collectionUid, url, credentialsId });
|
||||
return Promise.reject(safeStringifyJSON(error?.response?.data));
|
||||
// Proceed without token
|
||||
return { collectionUid, url, credentials: null, credentialsId };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// HELPER FUNCTIONS
|
||||
|
||||
@@ -246,8 +623,11 @@ const generateCodeVerifier = () => {
|
||||
const generateCodeChallenge = (codeVerifier) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(codeVerifier);
|
||||
const base64Hash = hash.digest('base64');
|
||||
return base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
const base64Hash = hash.digest('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
return base64Hash;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
@@ -256,4 +636,4 @@ module.exports = {
|
||||
getOAuth2TokenUsingClientCredentials,
|
||||
getOAuth2TokenUsingPasswordCredentials,
|
||||
refreshOauth2Token
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,11 @@
|
||||
const parseUrl = require('url').parse;
|
||||
const { isEmpty } = require('lodash');
|
||||
const https = require('https');
|
||||
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,
|
||||
@@ -79,7 +84,254 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent {
|
||||
}
|
||||
}
|
||||
|
||||
function createTimelineAgentClass(BaseAgentClass) {
|
||||
return class extends BaseAgentClass {
|
||||
constructor(options, timeline) {
|
||||
super(options);
|
||||
this.timeline = timeline;
|
||||
this.alpnProtocols = options.ALPNProtocols || ['h2', 'http/1.1'];
|
||||
this.caProvided = !!options.ca;
|
||||
}
|
||||
|
||||
createConnection(options, callback) {
|
||||
const { host, port } = options;
|
||||
|
||||
// Log SSL validation
|
||||
this.timeline.push({
|
||||
timestamp: new Date(),
|
||||
type: 'info',
|
||||
message: `Enable SSL validation`,
|
||||
});
|
||||
|
||||
// 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,
|
||||
}) {
|
||||
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, ...httpsAgentRequestFields }, timeline);
|
||||
} else {
|
||||
const TimelineHttpsProxyAgent = createTimelineAgentClass(HttpsProxyAgent);
|
||||
requestConfig.httpAgent = new HttpProxyAgent(proxyUri); // For http, no need for timeline
|
||||
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
|
||||
proxyUri,
|
||||
{ ...httpsAgentRequestFields },
|
||||
timeline
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If proxy should not be used, set default HTTPS agent
|
||||
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
|
||||
requestConfig.httpsAgent = new TimelineHttpsAgent(httpsAgentRequestFields, 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(HttpsProxyAgent);
|
||||
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
|
||||
https_proxy,
|
||||
{ ...httpsAgentRequestFields },
|
||||
timeline
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid system https_proxy');
|
||||
}
|
||||
} else {
|
||||
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
|
||||
requestConfig.httpsAgent = new TimelineHttpsAgent(httpsAgentRequestFields, timeline);
|
||||
}
|
||||
} else {
|
||||
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
|
||||
requestConfig.httpsAgent = new TimelineHttpsAgent(httpsAgentRequestFields, timeline);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
shouldUseProxy,
|
||||
PatchedHttpsProxyAgent
|
||||
PatchedHttpsProxyAgent,
|
||||
setupProxyAgents
|
||||
};
|
||||
|
||||
@@ -10,9 +10,9 @@ const { HttpProxyAgent } = require('http-proxy-agent');
|
||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
const iconv = require('iconv-lite');
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials } = require('./collection');
|
||||
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials, mergeAuth } = require('./collection');
|
||||
const { buildFormUrlEncodedPayload } = require('./form-data');
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('./proxy-util');
|
||||
const { setupProxyAgents } = require('./proxy-util');
|
||||
const { makeAxiosInstance } = require('./axios-instance');
|
||||
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('./oauth2');
|
||||
const { resolveAwsV4Credentials, addAwsV4Interceptor, addDigestInterceptor } = require('./auth');
|
||||
@@ -103,7 +103,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
|
||||
reuseToken: get(collectionAuth, 'oauth2.reuseToken')
|
||||
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@@ -123,7 +124,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
|
||||
reuseToken: get(collectionAuth, 'oauth2.reuseToken')
|
||||
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
@@ -139,7 +141,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
|
||||
reuseToken: get(collectionAuth, 'oauth2.reuseToken')
|
||||
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -198,7 +201,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
|
||||
reuseToken: get(request, 'auth.oauth2.reuseToken')
|
||||
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
|
||||
};
|
||||
break;
|
||||
case 'authorization_code':
|
||||
@@ -218,7 +222,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
|
||||
reuseToken: get(request, 'auth.oauth2.reuseToken')
|
||||
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
|
||||
};
|
||||
break;
|
||||
case 'client_credentials':
|
||||
@@ -234,7 +239,8 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
|
||||
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
|
||||
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
|
||||
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
|
||||
reuseToken: get(request, 'auth.oauth2.reuseToken')
|
||||
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
|
||||
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -292,6 +298,7 @@ const prepareRequest = (item, collection) => {
|
||||
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 });
|
||||
}
|
||||
@@ -392,6 +399,7 @@ const prepareRequest = (item, collection) => {
|
||||
axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables;
|
||||
axiosRequest.oauth2CredentialVariables = request.oauth2CredentialVariables;
|
||||
axiosRequest.assertions = request.assertions;
|
||||
axiosRequest.oauth2Credentials = request.oauth2Credentials;
|
||||
|
||||
return axiosRequest;
|
||||
};
|
||||
@@ -536,77 +544,15 @@ const configureRequestWithCertsAndProxy = async ({
|
||||
proxyMode = get(proxyConfig, 'mode', 'off');
|
||||
}
|
||||
|
||||
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);
|
||||
setupProxyAgents({
|
||||
requestConfig: request,
|
||||
proxyMode,
|
||||
proxyConfig,
|
||||
httpsAgentRequestFields,
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
return request;
|
||||
return {proxyMode, newRequest: request, proxyConfig, httpsAgentRequestFields, interpolationOptions};
|
||||
}
|
||||
|
||||
const configureRequest = async (
|
||||
@@ -622,7 +568,7 @@ const configureRequest = async (
|
||||
request.url = `http://${request.url}`;
|
||||
}
|
||||
|
||||
request = await configureRequestWithCertsAndProxy({
|
||||
const {proxyMode, newRequest, proxyConfig, httpsAgentRequestFields, interpolationOptions} = await configureRequestWithCertsAndProxy({
|
||||
collectionUid,
|
||||
request,
|
||||
envVars,
|
||||
@@ -631,7 +577,17 @@ const configureRequest = async (
|
||||
collectionPath
|
||||
});
|
||||
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
request = newRequest
|
||||
let requestMaxRedirects = request.maxRedirects
|
||||
request.maxRedirects = 0
|
||||
|
||||
let axiosInstance = makeAxiosInstance({
|
||||
proxyMode,
|
||||
proxyConfig,
|
||||
requestMaxRedirects,
|
||||
httpsAgentRequestFields,
|
||||
interpolationOptions
|
||||
});
|
||||
|
||||
if (request.ntlmConfig) {
|
||||
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
|
||||
@@ -645,8 +601,8 @@ const configureRequest = async (
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId };
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
}
|
||||
@@ -661,8 +617,8 @@ const configureRequest = async (
|
||||
break;
|
||||
case 'client_credentials':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId };
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
}
|
||||
@@ -677,8 +633,8 @@ const configureRequest = async (
|
||||
break;
|
||||
case 'password':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
({ credentials, url: oauth2Url, credentialsId } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId };
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
|
||||
}
|
||||
|
||||
@@ -490,7 +490,8 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
const tokenPlacementKey = _.find(auth, { name: 'token_placement' });
|
||||
const tokenHeaderPrefixKey = _.find(auth, { name: 'token_header_prefix' });
|
||||
const tokenQueryKeyKey = _.find(auth, { name: 'token_query_key' });
|
||||
const reuseTokenKey = _.find(auth, { name: 'reuse_token' });
|
||||
const autoFetchTokenKey = _.find(auth, { name: 'auto_fetch_token' });
|
||||
const autoRefreshTokenKey = _.find(auth, { name: 'auto_refresh_token' });
|
||||
return {
|
||||
auth: {
|
||||
oauth2:
|
||||
@@ -509,7 +510,8 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false
|
||||
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
|
||||
? {
|
||||
@@ -528,7 +530,8 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false
|
||||
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
|
||||
? {
|
||||
@@ -543,7 +546,8 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false
|
||||
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
||||
@@ -285,7 +285,8 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
const tokenPlacementKey = _.find(auth, { name: 'token_placement' });
|
||||
const tokenHeaderPrefixKey = _.find(auth, { name: 'token_header_prefix' });
|
||||
const tokenQueryKeyKey = _.find(auth, { name: 'token_query_key' });
|
||||
const reuseTokenKey = _.find(auth, { name: 'reuseToken' });
|
||||
const autoFetchTokenKey = _.find(auth, { name: 'auto_fetch_token' });
|
||||
const autoRefreshTokenKey = _.find(auth, { name: 'auto_refresh_token' });
|
||||
return {
|
||||
auth: {
|
||||
oauth2:
|
||||
@@ -304,7 +305,8 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false
|
||||
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
|
||||
? {
|
||||
@@ -323,7 +325,8 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false
|
||||
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
|
||||
}
|
||||
: grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
|
||||
? {
|
||||
@@ -338,7 +341,8 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
|
||||
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
|
||||
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
|
||||
reuseToken: reuseTokenKey?.value ? JSON.parse(reuseTokenKey?.value || false) : false
|
||||
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
|
||||
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,8 @@ ${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`reuse_token: ${auth?.oauth2?.reuseToken || ''}`)}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -220,7 +221,8 @@ ${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`reuse_token: ${auth?.oauth2?.reuseToken || ''}`)}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -240,7 +242,8 @@ ${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`reuse_token: ${auth?.oauth2?.reuseToken || ''}`)}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@@ -162,7 +162,8 @@ ${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`reuse_token: ${auth?.oauth2?.reuseToken || ''}`)}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -186,7 +187,8 @@ ${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`reuse_token: ${auth?.oauth2?.reuseToken || ''}`)}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
@@ -206,7 +208,8 @@ ${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
|
||||
}${
|
||||
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
|
||||
}
|
||||
${indentString(`reuse_token: ${auth?.oauth2?.reuseToken || ''}`)}
|
||||
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken).toString()}`)}
|
||||
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken).toString()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@@ -227,20 +227,20 @@ const oauth2Schema = Yup.object({
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
reuseToken: Yup.boolean().when('grantType', {
|
||||
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
|
||||
then: Yup.boolean().default(false),
|
||||
otherwise: Yup.boolean()
|
||||
}),
|
||||
refreshUrl: Yup.string().when('grantType', {
|
||||
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
|
||||
then: Yup.string().nullable(),
|
||||
otherwise: Yup.string().nullable().strip()
|
||||
}),
|
||||
autoRefresh: Yup.boolean().when('grantType', {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user