Merge branch 'main' into feature/theme-ux-overhaul

This commit is contained in:
Anoop M D
2025-12-27 09:08:33 +05:30
committed by GitHub
31 changed files with 1577 additions and 531 deletions

10
package-lock.json generated
View File

@@ -29,7 +29,6 @@
"@eslint/compat": "^1.3.2",
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@opencollection/types": "0.3.0",
"@playwright/test": "^1.51.1",
"@rollup/plugin-json": "^6.1.0",
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
@@ -6100,9 +6099,9 @@
}
},
"node_modules/@opencollection/types": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.3.0.tgz",
"integrity": "sha512-kw+co3sM4ATDQI85lgy5UmOilEHFVdNYvOjZXmnSw6PUDUTAGBiaZNdwdSXwp//o4IwKbTQb8/0I3UdjLKh+qA==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.5.0.tgz",
"integrity": "sha512-9rpu5agMrMLcMVU2UgyV+PYV3Zf/sHBJDHMQoq8XiMEUH8lt9f7yGtlerm/9dS3SHMpGX4A8ik0OFtc0dX4r1Q==",
"dev": true,
"license": "MIT"
},
@@ -33627,7 +33626,7 @@
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@opencollection/types": "0.3.0",
"@opencollection/types": "~0.5.0",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
@@ -35273,6 +35272,7 @@
"devDependencies": {
"@babel/preset-env": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
"@opencollection/types": "~0.5.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",

View File

@@ -23,7 +23,6 @@
"@eslint/compat": "^1.3.2",
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@opencollection/types": "0.3.0",
"@playwright/test": "^1.51.1",
"@rollup/plugin-json": "^6.1.0",
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",

View File

@@ -12,7 +12,19 @@ import Button from 'ui/Button';
const ProxySettings = ({ collection }) => {
const dispatch = useDispatch();
const initialProxyConfig = { enabled: 'global', protocol: 'http', hostname: '', port: '', auth: { enabled: false, username: '', password: '' }, bypassProxy: '' };
const initialProxyConfig = {
inherit: true,
config: {
protocol: 'http',
hostname: '',
port: '',
auth: {
username: '',
password: ''
},
bypassProxy: ''
}
};
// Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig
const currentProxyConfig = collection.draft?.brunoConfig
@@ -82,34 +94,57 @@ const ProxySettings = ({ collection }) => {
const handleEnabledChange = (e) => {
const value = e.target.value;
// Convert string to boolean or keep as 'global'
const enabled = value === 'true' ? true : value === 'false' ? false : 'global';
updateProxy({ enabled });
// Map UI values to new format
if (value === 'inherit') {
updateProxy({ disabled: false, inherit: true });
} else if (value === 'true') {
updateProxy({ disabled: false, inherit: false });
} else {
updateProxy({ disabled: true, inherit: false });
}
};
const handleProtocolChange = (e) => {
updateProxy({ protocol: e.target.value });
updateProxy({
config: {
...currentProxyConfig.config,
protocol: e.target.value
}
});
};
const handleHostnameChange = (e) => {
const hostname = e.target.value;
if (validateHostnameOnChange(hostname)) {
updateProxy({ hostname });
updateProxy({
config: {
...currentProxyConfig.config,
hostname
}
});
}
};
const handlePortChange = (e) => {
const port = e.target.value ? Number(e.target.value) : '';
if (validatePortOnChange(port)) {
updateProxy({ port });
updateProxy({
config: {
...currentProxyConfig.config,
port
}
});
}
};
const handleAuthEnabledChange = (e) => {
updateProxy({
auth: {
...currentProxyConfig.auth,
enabled: e.target.checked
config: {
...currentProxyConfig.config,
auth: {
...currentProxyConfig.config.auth,
disabled: !e.target.checked
}
}
});
};
@@ -118,9 +153,12 @@ const ProxySettings = ({ collection }) => {
const username = e.target.value;
if (validateAuthUsernameOnChange(username)) {
updateProxy({
auth: {
...currentProxyConfig.auth,
username
config: {
...currentProxyConfig.config,
auth: {
...currentProxyConfig.config.auth,
username
}
}
});
}
@@ -130,9 +168,12 @@ const ProxySettings = ({ collection }) => {
const password = e.target.value;
if (validateAuthPasswordOnChange(password)) {
updateProxy({
auth: {
...currentProxyConfig.auth,
password
config: {
...currentProxyConfig.config,
auth: {
...currentProxyConfig.config.auth,
password
}
}
});
}
@@ -141,11 +182,19 @@ const ProxySettings = ({ collection }) => {
const handleBypassProxyChange = (e) => {
const bypassProxy = e.target.value;
if (validateBypassProxyOnChange(bypassProxy)) {
updateProxy({ bypassProxy });
updateProxy({
config: {
...currentProxyConfig.config,
bypassProxy
}
});
}
};
const enabledValue = currentProxyConfig.enabled === true ? 'true' : currentProxyConfig.enabled === false ? 'false' : 'global';
// Map new format to UI values
const disabled = currentProxyConfig.disabled || false;
const inherit = currentProxyConfig.inherit !== undefined ? currentProxyConfig.inherit : true;
const enabledValue = disabled ? 'false' : (inherit ? 'inherit' : 'true');
return (
<StyledWrapper className="h-full w-full">
@@ -157,9 +206,9 @@ const ProxySettings = ({ collection }) => {
<InfoTip infotipId="request-var">
<div>
<ul>
<li><span style={{ width: '50px', display: 'inline-block' }}>global</span> - use global proxy config</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection proxy config</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>disable</span> - disable proxy</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>inherit</span> - inherit from global preferences</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection-specific proxy config</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>disabled</span> - disable proxy for this collection</li>
</ul>
</div>
</InfoTip>
@@ -169,12 +218,12 @@ const ProxySettings = ({ collection }) => {
<input
type="radio"
name="enabled"
value="global"
checked={enabledValue === 'global'}
value="inherit"
checked={enabledValue === 'inherit'}
onChange={handleEnabledChange}
className="mr-1"
/>
global
inherit
</label>
<label className="flex items-center ml-4">
<input
@@ -200,164 +249,168 @@ const ProxySettings = ({ collection }) => {
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center">
{enabledValue === 'true' && (
<>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
Protocol
</label>
<div className="flex items-center">
<label className="flex items-center">
<input
type="radio"
name="protocol"
value="http"
checked={(currentProxyConfig.config?.protocol || 'http') === 'http'}
onChange={handleProtocolChange}
className="mr-1"
/>
HTTP
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="https"
checked={(currentProxyConfig.config?.protocol || 'http') === 'https'}
onChange={handleProtocolChange}
className="mr-1"
/>
HTTPS
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks4"
checked={(currentProxyConfig.config?.protocol || 'http') === 'socks4'}
onChange={handleProtocolChange}
className="mr-1"
/>
SOCKS4
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={(currentProxyConfig.config?.protocol || 'http') === 'socks5'}
onChange={handleProtocolChange}
className="mr-1"
/>
SOCKS5
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
type="radio"
name="protocol"
value="http"
checked={(currentProxyConfig.protocol || 'http') === 'http'}
onChange={handleProtocolChange}
className="mr-1"
/>
HTTP
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="https"
checked={(currentProxyConfig.protocol || 'http') === 'https'}
onChange={handleProtocolChange}
className="mr-1"
/>
HTTPS
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks4"
checked={(currentProxyConfig.protocol || 'http') === 'socks4'}
onChange={handleProtocolChange}
className="mr-1"
/>
SOCKS4
</label>
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
value="socks5"
checked={(currentProxyConfig.protocol || 'http') === 'socks5'}
onChange={handleProtocolChange}
className="mr-1"
/>
SOCKS5
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
Hostname
</label>
<input
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={handleHostnameChange}
value={currentProxyConfig.hostname || ''}
/>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={handlePortChange}
value={currentProxyConfig.port || ''}
/>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={currentProxyConfig.auth?.enabled || false}
onChange={handleAuthEnabledChange}
/>
</div>
<div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={currentProxyConfig.auth?.username || ''}
onChange={handleAuthUsernameChange}
/>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
<input
id="auth.password"
type={passwordVisible ? 'text' : 'password'}
name="auth.password"
className="outline-none bg-transparent w-[10.5rem]"
id="hostname"
type="text"
name="hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={currentProxyConfig.auth?.password || ''}
onChange={handleAuthPasswordChange}
onChange={handleHostnameChange}
value={currentProxyConfig.config?.hostname || ''}
/>
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
</button>
</div>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="bypassProxy">
Proxy Bypass
</label>
<input
id="bypassProxy"
type="text"
name="bypassProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={handleBypassProxyChange}
value={currentProxyConfig.bypassProxy || ''}
/>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
Port
</label>
<input
id="port"
type="number"
name="port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={handlePortChange}
value={currentProxyConfig.config?.port || ''}
/>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.disabled">
Auth
</label>
<input
type="checkbox"
name="auth.disabled"
checked={!currentProxyConfig.config?.auth?.disabled}
onChange={handleAuthEnabledChange}
/>
</div>
<div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
Username
</label>
<input
id="auth.username"
type="text"
name="auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={currentProxyConfig.config?.auth?.username || ''}
onChange={handleAuthUsernameChange}
/>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
Password
</label>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
<input
id="auth.password"
type={passwordVisible ? 'text' : 'password'}
name="auth.password"
className="outline-none bg-transparent w-[10.5rem]"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={currentProxyConfig.config?.auth?.password || ''}
onChange={handleAuthPasswordChange}
/>
<button
type="button"
className="btn btn-sm absolute right-0"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
</button>
</div>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="bypassProxy">
Proxy Bypass
</label>
<input
id="bypassProxy"
type="text"
name="bypassProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={handleBypassProxyChange}
value={currentProxyConfig.config?.bypassProxy || ''}
/>
</div>
</>
)}
<div className="mt-6">
<Button type="submit" size="sm" onClick={handleSave}>
Save

View File

@@ -162,17 +162,18 @@ const CollectionProperties = ({ onClose }) => {
<IconCookieOff size={48} strokeWidth={1.5} className="text-gray-500" />
<h2 className="text-lg font-medium mt-4">No cookies found</h2>
<p className="text-gray-500 mt-2">Add cookies to get started</p>
<button
<Button
type="submit"
className="submit btn btn-sm btn-secondary flex items-center gap-1 mt-8"
size="sm"
className="mt-8"
icon={<IconCirclePlus strokeWidth={1.5} size={16} />}
onClick={(e) => {
e.stopPropagation();
handleAddCookie();
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
<span>Add Cookie</span>
</button>
Add Cookie
</Button>
</div>
) : cookies.length && !filteredCookies.length ? (
// No search results

View File

@@ -6,6 +6,7 @@ import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import Button from 'ui/Button';
const ExportEnvironmentModal = ({ onClose, environments = [], environmentType }) => {
const dispatch = useDispatch();
@@ -242,22 +243,26 @@ const ExportEnvironmentModal = ({ onClose, environments = [], environmentType })
{/* Export Actions */}
<div className="flex justify-end gap-2 mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
<button
<Button
type="button"
className="btn btn-sm btn-cancel mt-2 flex items-center"
size="sm"
color="secondary"
variant="ghost"
onClick={onClose}
disabled={isExporting}
className="mt-2 mr-2"
>
Cancel
</button>
<button
</Button>
<Button
type="button"
className="btn btn-sm btn-secondary mt-2 flex items-center"
size="sm"
onClick={handleExport}
disabled={isExporting || selectedCount === 0}
className="mt-2"
>
{isExporting ? 'Exporting...' : `Export ${selectedCount || ''} Environment${selectedCount !== 1 ? 's' : ''}`}
</button>
</Button>
</div>
</div>
</Modal>

View File

@@ -18,56 +18,41 @@ const ProxySettings = ({ close }) => {
console.log(preferences);
const proxySchema = Yup.object({
mode: Yup.string().oneOf(['off', 'on', 'system']),
protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string()
.when('enabled', {
is: 'on',
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable()
})
.max(1024),
port: Yup.number()
.min(1)
.max(65535)
.typeError('Specify port between 1 and 65535')
.nullable()
.transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object()
.when('enabled', {
is: 'on',
then: Yup.object({
enabled: Yup.boolean(),
username: Yup.string()
.when(['enabled'], {
is: true,
then: (username) => username.required('Specify username for proxy authentication.')
})
.max(1024),
password: Yup.string()
.when('enabled', {
is: true,
then: (password) => password.required('Specify password for proxy authentication.')
})
.max(1024)
})
})
.optional(),
bypassProxy: Yup.string().optional().max(1024)
disabled: Yup.boolean().optional(),
inherit: Yup.boolean().required(),
config: Yup.object({
protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string().max(1024),
port: Yup.number()
.min(1)
.max(65535)
.typeError('Specify port between 1 and 65535')
.nullable()
.transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object({
disabled: Yup.boolean().optional(),
username: Yup.string().max(1024),
password: Yup.string().max(1024)
}).optional(),
bypassProxy: Yup.string().optional().max(1024)
}).required()
});
const formik = useFormik({
initialValues: {
mode: preferences.proxy.mode,
protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || 0,
auth: {
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
},
bypassProxy: preferences.proxy.bypassProxy || ''
disabled: preferences.proxy.disabled || false,
inherit: preferences.proxy.inherit || false,
config: {
protocol: preferences.proxy.config?.protocol || 'http',
hostname: preferences.proxy.config?.hostname || '',
port: preferences.proxy.config?.port || 0,
auth: {
disabled: preferences.proxy.config?.auth?.disabled || false,
username: preferences.proxy.config?.auth?.username || '',
password: preferences.proxy.config?.auth?.password || ''
},
bypassProxy: preferences.proxy.config?.bypassProxy || ''
}
},
validationSchema: proxySchema,
onSubmit: (values) => {
@@ -103,16 +88,19 @@ const ProxySettings = ({ close }) => {
useEffect(() => {
formik.setValues({
mode: preferences.proxy.mode,
protocol: preferences.proxy.protocol || 'http',
hostname: preferences.proxy.hostname || '',
port: preferences.proxy.port || '',
auth: {
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
},
bypassProxy: preferences.proxy.bypassProxy || ''
disabled: preferences.proxy.disabled || false,
inherit: preferences.proxy.inherit || false,
config: {
protocol: preferences.proxy.config?.protocol || 'http',
hostname: preferences.proxy.config?.hostname || '',
port: preferences.proxy.config?.port || '',
auth: {
disabled: preferences.proxy.config?.auth?.disabled || false,
username: preferences.proxy.config?.auth?.username || '',
password: preferences.proxy.config?.auth?.password || ''
},
bypassProxy: preferences.proxy.config?.bypassProxy || ''
}
});
}, [preferences]);
@@ -137,10 +125,11 @@ const ProxySettings = ({ close }) => {
<input
type="radio"
name="mode"
value="false"
checked={formik.values.mode === 'off'}
value="off"
checked={formik.values.disabled === true}
onChange={(e) => {
formik.setFieldValue('mode', 'off');
formik.setFieldValue('disabled', true);
formik.setFieldValue('inherit', false);
}}
className="mr-1 cursor-pointer"
/>
@@ -150,10 +139,11 @@ const ProxySettings = ({ close }) => {
<input
type="radio"
name="mode"
value="true"
checked={formik.values.mode === 'on'}
value="on"
checked={formik.values.disabled === false && formik.values.inherit === false}
onChange={(e) => {
formik.setFieldValue('mode', 'on');
formik.setFieldValue('disabled', false);
formik.setFieldValue('inherit', false);
}}
className="mr-1 cursor-pointer"
/>
@@ -164,15 +154,18 @@ const ProxySettings = ({ close }) => {
type="radio"
name="mode"
value="system"
checked={formik.values.mode === 'system'}
onChange={formik.handleChange}
checked={formik.values.disabled === false && formik.values.inherit === true}
onChange={(e) => {
formik.setFieldValue('disabled', false);
formik.setFieldValue('inherit', true);
}}
className="mr-1 cursor-pointer"
/>
System Proxy
</label>
</div>
</div>
{formik?.values?.mode === 'system' ? (
{formik.values.disabled === false && formik.values.inherit === true ? (
<div className="mb-3 pt-1 text-muted system-proxy-settings">
<small>
Below values are sourced from your system environment variables and cannot be directly updated in Bruno.<br />
@@ -200,7 +193,7 @@ const ProxySettings = ({ close }) => {
</div>
</div>
) : null}
{formik?.values?.mode === 'on' ? (
{formik.values.disabled === false && formik.values.inherit === false ? (
<>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="protocol">
@@ -210,9 +203,9 @@ const ProxySettings = ({ close }) => {
<label className="flex items-center">
<input
type="radio"
name="protocol"
name="config.protocol"
value="http"
checked={formik.values.protocol === 'http'}
checked={formik.values.config.protocol === 'http'}
onChange={formik.handleChange}
className="mr-1"
/>
@@ -221,9 +214,9 @@ const ProxySettings = ({ close }) => {
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
name="config.protocol"
value="https"
checked={formik.values.protocol === 'https'}
checked={formik.values.config.protocol === 'https'}
onChange={formik.handleChange}
className="mr-1"
/>
@@ -232,9 +225,9 @@ const ProxySettings = ({ close }) => {
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
name="config.protocol"
value="socks4"
checked={formik.values.protocol === 'socks4'}
checked={formik.values.config.protocol === 'socks4'}
onChange={formik.handleChange}
className="mr-1"
/>
@@ -243,9 +236,9 @@ const ProxySettings = ({ close }) => {
<label className="flex items-center ml-4">
<input
type="radio"
name="protocol"
name="config.protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
checked={formik.values.config.protocol === 'socks5'}
onChange={formik.handleChange}
className="mr-1"
/>
@@ -254,92 +247,94 @@ const ProxySettings = ({ close }) => {
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="hostname">
<label className="settings-label" htmlFor="config.hostname">
Hostname
</label>
<input
id="hostname"
id="config.hostname"
type="text"
name="hostname"
name="config.hostname"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.hostname || ''}
value={formik.values.config.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
{formik.touched.config?.hostname && formik.errors.config?.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.config.hostname}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
<label className="settings-label" htmlFor="config.port">
Port
</label>
<input
id="port"
id="config.port"
type="number"
name="port"
name="config.port"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
value={formik.values.config.port}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
{formik.touched.config?.port && formik.errors.config?.port ? (
<div className="ml-3 text-red-500">{formik.errors.config.port}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
<label className="settings-label" htmlFor="config.auth.disabled">
Auth
</label>
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
name="config.auth.disabled"
checked={!formik.values.config.auth.disabled}
onChange={(e) => {
formik.setFieldValue('config.auth.disabled', !e.target.checked);
}}
/>
</div>
<div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.username">
<label className="settings-label" htmlFor="config.auth.username">
Username
</label>
<input
id="auth.username"
id="config.auth.username"
type="text"
name="auth.username"
name="config.auth.username"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
value={formik.values.config.auth.username}
onChange={formik.handleChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
{formik.touched.config?.auth?.username && formik.errors.config?.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.config.auth.username}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
<label className="settings-label" htmlFor="config.auth.password">
Password
</label>
<div className="textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative">
<input
id="auth.password"
id="config.auth.password"
type={passwordVisible ? `text` : 'password'}
name="auth.password"
name="config.auth.password"
className="outline-none w-[10.5rem] bg-transparent"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
value={formik.values.config.auth.password}
onChange={formik.handleChange}
/>
<button
@@ -350,29 +345,29 @@ const ProxySettings = ({ close }) => {
{passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}
</button>
</div>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
{formik.touched.config?.auth?.password && formik.errors.config?.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.config.auth.password}</div>
) : null}
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="bypassProxy">
<label className="settings-label" htmlFor="config.bypassProxy">
Proxy Bypass
</label>
<input
id="bypassProxy"
id="config.bypassProxy"
type="text"
name="bypassProxy"
name="config.bypassProxy"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.bypassProxy || ''}
value={formik.values.config.bypassProxy || ''}
/>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
{formik.touched.config?.bypassProxy && formik.errors.config?.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.config.bypassProxy}</div>
) : null}
</div>
</>

View File

@@ -6,6 +6,7 @@ import toast from 'react-hot-toast';
import get from 'lodash/get';
import Modal from 'components/Modal/index';
import CodeEditor from 'components/CodeEditor';
import Button from 'ui/Button';
const GrpcurlModal = ({ isOpen, onClose, command }) => {
const { displayedTheme } = useTheme();
@@ -39,12 +40,12 @@ const GrpcurlModal = ({ isOpen, onClose, command }) => {
<div className="flex w-full min-h-[400px]">
<div className="flex-grow relative">
<div className="absolute top-2 right-2 z-10">
<button
<Button
size="sm"
variant="ghost"
onClick={handleCopy}
className="btn btn-sm btn-secondary flex items-center gap-2"
>
{copied ? <IconCheck size={20} /> : <IconCopy size={20} />}
</button>
icon={copied ? <IconCheck size={20} /> : <IconCopy size={20} />}
/>
</div>
<CodeEditor
value={command}

View File

@@ -14,6 +14,7 @@ import path from 'utils/common/path';
import Portal from 'components/Portal';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
import Button from 'ui/Button';
const CloneCollectionItem = ({ collectionUid, item, onClose }) => {
const dispatch = useDispatch();
@@ -198,19 +199,12 @@ const CloneCollectionItem = ({ collectionUid, item, onClose }) => {
</Dropdown>
</div>
<div className="flex justify-end">
<span className="mr-2">
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Clone
</button>
</span>
<Button type="button" color="secondary" variant="ghost" onClick={onClose} className="mr-2">
Cancel
</Button>
<Button type="submit">
Clone
</Button>
</div>
</div>
</form>

View File

@@ -15,6 +15,7 @@ import PathDisplay from 'components/PathDisplay';
import Portal from 'components/Portal';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
import Button from 'ui/Button';
const RenameCollectionItem = ({ collectionUid, item, onClose }) => {
const dispatch = useDispatch();
@@ -216,19 +217,12 @@ const RenameCollectionItem = ({ collectionUid, item, onClose }) => {
</Dropdown>
</div>
<div className="flex justify-end">
<span className="mr-2">
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Rename
</button>
</span>
<Button type="button" color="secondary" variant="ghost" onClick={onClose} className="mr-2">
Cancel
</Button>
<Button type="submit">
Rename
</Button>
</div>
</div>
</form>

View File

@@ -10,6 +10,7 @@ import StyledWrapper from './StyledWrapper';
import { areItemsLoading } from 'utils/collections';
import RunnerTags from 'components/RunnerResults/RunnerTags/index';
import { getRequestItemsForCollectionRun } from 'utils/collections/index';
import Button from 'ui/Button';
const RunCollectionItem = ({ collectionUid, item, onClose }) => {
const dispatch = useDispatch();
@@ -80,32 +81,24 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
<RunnerTags collectionUid={collection.uid} className="mb-6" />
<div className="flex justify-end bruno-modal-footer">
<span className="mr-3">
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<Button type="button" color="secondary" variant="ghost" onClick={onClose} className="mr-3">
Cancel
</Button>
{
isCollectionRunInProgress
? (
<span>
<button type="submit" className="submit btn btn-md btn-secondary mr-3" onClick={handleViewRunner}>
View Run
</button>
</span>
<Button type="submit" onClick={handleViewRunner}>
View Run
</Button>
)
: (
<>
<span>
<button type="submit" disabled={shouldDisableRecursiveFolderRun} className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
Recursive Run
</button>
</span>
<span>
<button type="submit" disabled={shouldDisableFolderRun} className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
Run
</button>
</span>
<Button type="submit" disabled={shouldDisableRecursiveFolderRun} onClick={() => onSubmit(true)} className="mr-3">
Recursive Run
</Button>
<Button type="submit" disabled={shouldDisableFolderRun} onClick={() => onSubmit(false)}>
Run
</Button>
</>
)
}

View File

@@ -256,12 +256,12 @@ const RemoveCollectionsModal = ({ collectionUids, onClose }) => {
Collections will be removed from the current workspace but will still be available in the file system and can be re-opened later.
</div>
<div className="flex justify-end mt-6">
<button className="btn btn-close btn-sm mr-2" data-testid="modal-close-button" onClick={handleCancel}>
<Button size="sm" color="secondary" variant="ghost" onClick={handleCancel} className="mr-2" data-testid="modal-close-button">
Cancel
</button>
<button className="btn btn-secondary btn-sm" onClick={handleCloseAllCollections}>
</Button>
<Button size="sm" onClick={handleCloseAllCollections}>
{hasMultipleCollections ? 'Close All' : 'Close'}
</button>
</Button>
</div>
</>
)}

View File

@@ -13,6 +13,7 @@ import Help from 'components/Help';
import Dropdown from 'components/Dropdown';
import { IconCaretDown } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import Button from 'ui/Button';
const NewFolder = ({ collectionUid, item, onClose }) => {
const dispatch = useDispatch();
@@ -181,19 +182,12 @@ const NewFolder = ({ collectionUid, item, onClose }) => {
</Dropdown>
</div>
<div className="flex justify-end">
<span className="mr-2">
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
>
Create
</button>
</span>
<Button type="button" color="secondary" variant="ghost" onClick={onClose} className="mr-2">
Cancel
</Button>
<Button type="submit">
Create
</Button>
</div>
</div>
</form>

View File

@@ -22,6 +22,7 @@ import Help from 'components/Help';
import StyledWrapper from './StyledWrapper';
import SingleLineEditor from 'components/SingleLineEditor/index';
import { useTheme } from 'styled-components';
import Button from 'ui/Button';
const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
@@ -594,16 +595,12 @@ const NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {
</Dropdown>
</div>
<div className="flex justify-end">
<span className="mr-2">
<button type="button" onClick={onClose} className="btn btn-md btn-close">
Cancel
</button>
</span>
<span>
<button type="submit" className="submit btn btn-md btn-secondary">
Create
</button>
</span>
<Button type="button" color="secondary" variant="ghost" onClick={onClose} className="mr-2">
Cancel
</Button>
<Button type="submit">
Create
</Button>
</div>
</div>
</form>

View File

@@ -28,12 +28,12 @@
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@opencollection/types": "0.3.0",
"@usebruno/schema-types": "0.0.1",
"@opencollection/types": "~0.5.0",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"@usebruno/schema-types": "0.0.1",
"@web/rollup-plugin-copy": "^0.5.1",
"babel-jest": "^29.7.0",
"rimraf": "^5.0.7",

View File

@@ -32,27 +32,12 @@ const toOpenCollectionConfig = (brunoConfig: BrunoConfig | undefined): Collectio
}
}
if (brunoConfig.proxy?.enabled) {
if (brunoConfig.proxy.enabled === 'global') {
config.proxy = 'inherit';
} else {
config.proxy = {
protocol: brunoConfig.proxy.protocol || 'http',
hostname: brunoConfig.proxy.hostname || '',
port: brunoConfig.proxy.port || 0
};
if (brunoConfig.proxy.auth?.enabled) {
config.proxy.auth = {
username: brunoConfig.proxy.auth.username || '',
password: brunoConfig.proxy.auth.password || ''
};
}
if (brunoConfig.proxy.bypassProxy) {
config.proxy.bypassProxy = brunoConfig.proxy.bypassProxy;
}
}
if (brunoConfig.proxy) {
config.proxy = {
disabled: brunoConfig.proxy.disabled,
inherit: brunoConfig.proxy.inherit,
config: brunoConfig.proxy.config
};
}
if (brunoConfig.clientCertificates?.certs?.length) {

View File

@@ -124,7 +124,7 @@ const fromOpenCollectionOAuth2 = (auth: AuthOAuth2): BrunoAuth => {
clientId: auth.credentials?.clientId || null,
clientSecret: auth.credentials?.clientSecret || null,
scope: auth.scope || null,
pkce: auth.pkce?.enabled || null,
pkce: (auth.pkce && !auth.pkce.disabled) || null,
credentialsPlacement: getCredentialsPlacement(auth.credentials),
credentialsId: auth.tokenConfig?.id || 'credentials',
tokenPlacement: getTokenPlacement(auth.tokenConfig),
@@ -357,7 +357,7 @@ const toOpenCollectionOAuth2 = (oauth2: BrunoOAuth2 | null | undefined): AuthOAu
placement: oauth2.credentialsPlacement === 'basic_auth_header' ? 'basic_auth_header' : 'body'
},
scope: oauth2.scope || '',
pkce: oauth2.pkce ? { enabled: true, method: 'S256' } : undefined,
pkce: oauth2.pkce ? { method: 'S256' } : undefined,
tokenConfig: {
id: oauth2.credentialsId || 'credentials',
placement: oauth2.tokenPlacement === 'query'

View File

@@ -36,30 +36,12 @@ const fromOpenCollectionConfig = (oc: OpenCollection): BrunoConfig => {
};
}
if (config.proxy && typeof config.proxy !== 'boolean') {
if (config.proxy === 'inherit') {
brunoConfig.proxy = { enabled: 'global' };
} else {
const proxyConfig = config.proxy;
brunoConfig.proxy = {
enabled: true,
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || 0
};
if (proxyConfig.auth) {
brunoConfig.proxy.auth = {
enabled: true,
username: proxyConfig.auth.username || '',
password: proxyConfig.auth.password || ''
};
}
if (proxyConfig.bypassProxy) {
brunoConfig.proxy.bypassProxy = proxyConfig.bypassProxy;
}
}
if (config.proxy) {
brunoConfig.proxy = {
disabled: config.proxy.disabled,
inherit: config.proxy.inherit,
config: config.proxy.config
};
}
if (config.clientCertificates?.length) {

View File

@@ -66,9 +66,10 @@ export type {
} from '@opencollection/types/requests/websocket';
// OpenCollection config types
export type { Environment, CollectionConfig } from '@opencollection/types/config/environments';
export type { Environment } from '@opencollection/types/config/environments';
export type { CollectionConfig } from '@opencollection/types/config/collection';
export type { Protobuf, ProtoFileItem, ProtoFileImportPath } from '@opencollection/types/config/protobuf';
export type { Proxy, ProxyAuth } from '@opencollection/types/config/proxy';
export type { Proxy, ProxyConnectionConfig, ProxyConnectionAuth } from '@opencollection/types/config/proxy';
export type { ClientCertificate, PemCertificate, Pkcs12Certificate } from '@opencollection/types/config/certificates';
// OpenCollection common types
@@ -175,16 +176,19 @@ export interface BrunoConfig {
importPaths?: { path: string; disabled?: boolean }[];
};
proxy?: {
enabled?: boolean | 'global';
protocol?: string;
hostname?: string;
port?: number;
auth?: {
enabled?: boolean;
username?: string;
password?: string;
disabled?: boolean;
inherit?: boolean;
config?: {
protocol?: string;
hostname?: string;
port?: number;
auth?: {
disabled?: boolean;
username?: string;
password?: string;
};
bypassProxy?: string;
};
bypassProxy?: string;
};
clientCertificates?: {
certs?: Array<{

View File

@@ -27,7 +27,7 @@ const EnvironmentSecretsStore = require('../store/env-secrets');
const UiStateSnapshot = require('../store/ui-state-snapshot');
const { parseFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
const { parseLargeRequestWithRedaction } = require('../utils/parse');
const { transformBrunoConfigAfterRead } = require('../utils/transfomBrunoConfig');
const { transformBrunoConfigAfterRead } = require('../utils/transformBrunoConfig');
const MAX_FILE_SIZE = 2.5 * 1024 * 1024;

View File

@@ -4,7 +4,7 @@ const { dialog, ipcMain } = require('electron');
const Yup = require('yup');
const { isDirectory, getCollectionStats, normalizeAndResolvePath } = require('../utils/filesystem');
const { generateUidBasedOnHash } = require('../utils/common');
const { transformBrunoConfigAfterRead } = require('../utils/transfomBrunoConfig');
const { transformBrunoConfigAfterRead } = require('../utils/transformBrunoConfig');
const { parseCollection } = require('@usebruno/filestore');
// todo: bruno.json config schema validation errors must be propagated to the UI

View File

@@ -62,7 +62,7 @@ const { getProcessEnvVars } = require('../store/process-env');
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, refreshOauth2Token } = require('../utils/oauth2');
const { getCertsAndProxyConfig } = require('./network/cert-utils');
const collectionWatcher = require('../app/collection-watcher');
const { transformBrunoConfigBeforeSave } = require('../utils/transfomBrunoConfig');
const { transformBrunoConfigBeforeSave } = require('../utils/transformBrunoConfig');
const { REQUEST_TYPES } = require('../utils/constants');
const { cancelOAuth2AuthorizationRequest, isOauth2AuthorizationRequestInProgress } = require('../utils/oauth2-protocol-handler');

View File

@@ -101,12 +101,14 @@ const getCertsAndProxyConfig = async ({
/**
* Proxy configuration
*
* Preferences proxyMode has three possible values: on, off, system
* Collection proxyMode has three possible values: true, false, global
* New format:
* - disabled: boolean (optional, defaults to false)
* - inherit: boolean (required)
* - config: { protocol, hostname, port, auth, bypassProxy }
*
* When collection proxyMode is true, it overrides the app-level proxy settings
* When collection proxyMode is false, it ignores the app-level proxy settings
* When collection proxyMode is global, it uses the app-level proxy settings
* When collection proxy has inherit=false and disabled=false, use collection-specific proxy
* When collection proxy has inherit=true, inherit from global preferences
* When disabled=true, proxy is disabled
*
* Below logic calculates the proxyMode and proxyConfig to be used for the request
*/
@@ -114,14 +116,32 @@ const getCertsAndProxyConfig = async ({
let proxyConfig = {};
const collectionProxyConfig = get(brunoConfig, 'proxy', {});
const collectionProxyEnabled = get(collectionProxyConfig, 'enabled', 'global');
if (collectionProxyEnabled === true) {
proxyConfig = collectionProxyConfig;
const collectionProxyDisabled = get(collectionProxyConfig, 'disabled', false);
const collectionProxyInherit = get(collectionProxyConfig, 'inherit', true);
const collectionProxyConfigData = get(collectionProxyConfig, 'config', collectionProxyConfig);
if (!collectionProxyDisabled && !collectionProxyInherit) {
// Use collection-specific proxy
proxyConfig = collectionProxyConfigData;
proxyMode = 'on';
} else if (collectionProxyEnabled === 'global') {
proxyConfig = preferencesUtil.getGlobalProxyConfig();
proxyMode = get(proxyConfig, 'mode', 'off');
} else if (!collectionProxyDisabled && collectionProxyInherit) {
// Inherit from global preferences
const globalProxy = preferencesUtil.getGlobalProxyConfig();
const globalDisabled = get(globalProxy, 'disabled', false);
const globalInherit = get(globalProxy, 'inherit', false);
const globalProxyConfigData = get(globalProxy, 'config', globalProxy);
if (!globalDisabled && !globalInherit) {
// Use global custom proxy
proxyConfig = globalProxyConfigData;
proxyMode = 'on';
} else if (!globalDisabled && globalInherit) {
// Use system proxy
proxyMode = 'system';
}
// else: global proxy is disabled, proxyMode stays 'off'
}
// else: collection proxy is disabled, proxyMode stays 'off'
return { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions };
};

View File

@@ -30,16 +30,17 @@ const defaultPreferences = {
codeFontSize: 13
},
proxy: {
mode: 'off',
protocol: 'http',
hostname: '',
port: null,
auth: {
enabled: false,
username: '',
password: ''
},
bypassProxy: ''
inherit: true,
config: {
protocol: 'http',
hostname: '',
port: null,
auth: {
username: '',
password: ''
},
bypassProxy: ''
}
},
layout: {
responsePaneOrientation: 'horizontal'
@@ -79,16 +80,19 @@ const preferencesSchema = Yup.object().shape({
codeFontSize: Yup.number().min(1).max(32).nullable()
}),
proxy: Yup.object({
mode: Yup.string().oneOf(['off', 'on', 'system']),
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string().max(1024),
port: Yup.number().min(1).max(65535).nullable(),
auth: Yup.object({
enabled: Yup.boolean(),
username: Yup.string().max(1024),
password: Yup.string().max(1024)
}).optional(),
bypassProxy: Yup.string().optional().max(1024)
disabled: Yup.boolean().optional(),
inherit: Yup.boolean().required(),
config: Yup.object({
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string().max(1024),
port: Yup.number().min(1).max(65535).nullable(),
auth: Yup.object({
disabled: Yup.boolean().optional(),
username: Yup.string().max(1024),
password: Yup.string().max(1024)
}).optional(),
bypassProxy: Yup.string().optional().max(1024)
}).required()
}),
layout: Yup.object({
responsePaneOrientation: Yup.string().oneOf(['horizontal', 'vertical'])
@@ -118,17 +122,73 @@ class PreferencesStore {
getPreferences() {
let preferences = this.store.get('preferences', {});
// This to support the old preferences format
// In the old format, we had a proxy.enabled flag
// In the new format, this maps to proxy.mode = 'on'
if (preferences?.proxy?.enabled) {
preferences.proxy.mode = 'on';
}
// Migrate proxy configuration from old formats to new format
const proxyMigrated = get(preferences, '_migrations.proxyConfigFormat', false);
if (!proxyMigrated && preferences?.proxy) {
const proxy = preferences.proxy || {};
// Delete the proxy.enabled property if it exists, regardless of its value
// This is a part of migration to the new preferences format
if (preferences?.proxy && 'enabled' in preferences.proxy) {
delete preferences.proxy.enabled;
// Check if this is an old format that needs migration
const hasOldFormat = proxy.hasOwnProperty('enabled') || proxy.hasOwnProperty('mode');
if (hasOldFormat) {
let newProxy = {
inherit: true,
config: {
protocol: proxy.protocol || 'http',
hostname: proxy.hostname || '',
port: proxy.port || null,
auth: {
username: get(proxy, 'auth.username', ''),
password: get(proxy, 'auth.password', '')
},
bypassProxy: proxy.bypassProxy || ''
}
};
// Handle old format 1: enabled (boolean)
if (proxy.hasOwnProperty('enabled') && typeof proxy.enabled === 'boolean') {
newProxy.disabled = !proxy.enabled;
newProxy.inherit = false;
} else if (proxy.hasOwnProperty('mode')) {
// Handle old format 2: mode ('off' | 'on' | 'system')
if (proxy.mode === 'off') {
newProxy.disabled = true;
newProxy.inherit = false;
} else if (proxy.mode === 'on') {
newProxy.disabled = false;
newProxy.inherit = false;
} else if (proxy.mode === 'system') {
newProxy.disabled = false;
newProxy.inherit = true;
}
}
// Migrate auth.enabled to auth.disabled
if (get(proxy, 'auth.enabled') === false) {
newProxy.config.auth.disabled = true;
}
// If auth.enabled is true or undefined, omit disabled (defaults to false)
// Omit disabled: false at top level (optional field)
if (newProxy.disabled === false) {
delete newProxy.disabled;
}
// Omit auth.disabled: false (optional field)
if (newProxy.config.auth.disabled === false) {
delete newProxy.config.auth.disabled;
}
preferences.proxy = newProxy;
// Mark migration as complete // ?
// if (!preferences._migrations) {
// preferences._migrations = {};
// }
// preferences._migrations.proxyConfigFormat = true;
// Save the migrated preferences back to the store
// this.store.set('preferences', preferences);
}
}
// Migrate font size from 14px to 13px for existing users

View File

@@ -318,7 +318,8 @@ function setupProxyAgents({
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 proxyAuthDisabled = get(proxyConfig, 'auth.disabled', false);
const proxyAuthEnabled = !proxyAuthDisabled;
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;

View File

@@ -1,5 +1,6 @@
const path = require('path');
const { isFile, isDirectory } = require('./filesystem');
const { get } = require('lodash');
function transformBrunoConfigBeforeSave(brunoConfig) {
// remove exists from importPaths and protoFiles
@@ -16,6 +17,18 @@ function transformBrunoConfigBeforeSave(brunoConfig) {
});
}
// Clean up proxy config before saving
if (brunoConfig.proxy) {
// Remove disabled: false (optional field)
if (brunoConfig.proxy.disabled === false) {
delete brunoConfig.proxy.disabled;
}
// Remove auth.disabled: false (optional field)
if (brunoConfig.proxy.config?.auth?.disabled === false) {
delete brunoConfig.proxy.config.auth.disabled;
}
}
return brunoConfig;
}
@@ -61,6 +74,59 @@ async function transformBrunoConfigAfterRead(brunoConfig, collectionPathname) {
}));
}
// Migrate proxy configuration from old format to new format
if (brunoConfig.proxy) {
const proxy = brunoConfig.proxy || {};
// Check if this is an old format (has 'enabled' property)
if (proxy.hasOwnProperty('enabled')) {
const enabled = proxy.enabled;
let newProxy = {
inherit: true,
config: {
protocol: proxy.protocol || 'http',
hostname: proxy.hostname || '',
port: proxy.port || null,
auth: {
username: get(proxy, 'auth.username', ''),
password: get(proxy, 'auth.password', '')
},
bypassProxy: proxy.bypassProxy || ''
}
};
// Handle old format: enabled (true | false | 'global')
if (enabled === true) {
newProxy.disabled = false;
newProxy.inherit = false;
} else if (enabled === false) {
newProxy.disabled = true;
newProxy.inherit = false;
} else if (enabled === 'global') {
newProxy.disabled = false;
newProxy.inherit = true;
}
// Migrate auth.enabled to auth.disabled
if (get(proxy, 'auth.enabled') === false) {
newProxy.config.auth.disabled = true;
}
// If auth.enabled is true or undefined, omit disabled (defaults to false)
// Omit disabled: false at top level (optional field)
if (newProxy.disabled === false) {
delete newProxy.disabled;
}
// Omit auth.disabled: false (optional field)
if (newProxy.config.auth.disabled === false) {
delete newProxy.config.auth.disabled;
}
brunoConfig.proxy = newProxy;
}
}
return brunoConfig;
}

View File

@@ -0,0 +1,297 @@
let mockStoreData = {};
jest.mock('electron-store', () => {
return jest.fn().mockImplementation((opts = {}) => {
return {
get: (key, fallback) => (key in mockStoreData ? mockStoreData[key] : fallback),
set: (key, value) => {
mockStoreData[key] = value;
}
};
});
});
const { getPreferences, savePreferences } = require('../../src/store/preferences');
describe('Proxy Preferences Migration', () => {
beforeEach(() => {
// Reset mock store data before each test
mockStoreData = {};
});
describe('New Format (no migration needed)', () => {
it('should handle new format with inherit: false', () => {
const newFormatProxy = {
proxy: {
inherit: false,
config: {
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
username: 'user',
password: 'pass'
},
bypassProxy: 'localhost'
}
}
};
mockStoreData['preferences'] = newFormatProxy;
const preferences = getPreferences();
// Verify key fields are preserved from stored preferences
expect(preferences.proxy.inherit).toBe(false);
expect(preferences.proxy.config.protocol).toBe('http');
expect(preferences.proxy.config.hostname).toBe('proxy.example.com');
expect(preferences.proxy.config.port).toBe(8080);
expect(preferences.proxy.config.auth.username).toBe('user');
expect(preferences.proxy.config.auth.password).toBe('pass');
expect(preferences.proxy.config.bypassProxy).toBe('localhost');
});
it('should handle new format with inherit: true', () => {
const newFormatProxy = {
proxy: {
inherit: true,
config: {
protocol: 'http',
hostname: '',
port: null,
auth: {
username: '',
password: ''
},
bypassProxy: ''
}
}
};
mockStoreData['preferences'] = newFormatProxy;
const preferences = getPreferences();
expect(preferences.proxy.inherit).toBe(true);
expect(preferences.proxy.config).toBeDefined();
});
it('should handle new format with disabled: true', () => {
const newFormatProxy = {
proxy: {
disabled: true,
inherit: false,
config: {
protocol: 'http',
hostname: '',
port: null,
auth: {
username: '',
password: ''
},
bypassProxy: ''
}
}
};
mockStoreData['preferences'] = newFormatProxy;
const preferences = getPreferences();
// disabled: true is preserved from stored preferences
expect(preferences.proxy.disabled).toBe(true);
expect(preferences.proxy.inherit).toBe(false);
expect(preferences.proxy.config).toBeDefined();
});
it('should handle new format with auth.disabled: true', () => {
const newFormatProxy = {
proxy: {
inherit: false,
config: {
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
disabled: true,
username: 'user',
password: 'pass'
},
bypassProxy: ''
}
}
};
mockStoreData['preferences'] = newFormatProxy;
const preferences = getPreferences();
// auth.disabled: true is preserved from stored preferences
expect(preferences.proxy.config.auth.disabled).toBe(true);
expect(preferences.proxy.config.auth.username).toBe('user');
expect(preferences.proxy.config.auth.password).toBe('pass');
});
});
describe('Old Format 1: enabled (boolean)', () => {
it('should migrate enabled: true to disabled: false, inherit: false', () => {
const oldFormatProxy = {
proxy: {
enabled: true,
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
enabled: true,
username: 'user',
password: 'pass'
},
bypassProxy: 'localhost'
}
};
mockStoreData['preferences'] = oldFormatProxy;
const preferences = getPreferences();
// After migration, inherit should be false (old enabled: true maps to inherit: false)
expect(preferences.proxy.inherit).toBe(false);
// Values are preserved from stored preferences
expect(preferences.proxy.config.protocol).toBe('http');
expect(preferences.proxy.config.hostname).toBe('proxy.example.com');
expect(preferences.proxy.config.port).toBe(8080);
expect(preferences.proxy.config.auth.username).toBe('user');
expect(preferences.proxy.config.auth.password).toBe('pass');
expect(preferences.proxy.config.bypassProxy).toBe('localhost');
expect(preferences.proxy.disabled).toBeUndefined(); // disabled: false is omitted
});
it('should migrate enabled: false to disabled: true, inherit: false', () => {
const oldFormatProxy = {
proxy: {
enabled: false,
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
enabled: false,
username: '',
password: ''
},
bypassProxy: ''
}
};
mockStoreData['preferences'] = oldFormatProxy;
const preferences = getPreferences();
// After migration, enabled: false becomes disabled: true, inherit: false
expect(preferences.proxy.disabled).toBe(true);
expect(preferences.proxy.inherit).toBe(false);
});
it('should migrate auth.enabled: false to auth.disabled: true', () => {
const oldFormatProxy = {
proxy: {
enabled: true,
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
enabled: false,
username: 'user',
password: 'pass'
},
bypassProxy: ''
}
};
mockStoreData['preferences'] = oldFormatProxy;
const preferences = getPreferences();
// auth.disabled: true is preserved from stored preferences
expect(preferences.proxy.config.auth.disabled).toBe(true);
});
});
describe('Old Format 2: mode (string)', () => {
it('should migrate mode: "off" to disabled: true, inherit: false', () => {
const oldFormatProxy = {
proxy: {
mode: 'off',
protocol: 'http',
hostname: '',
port: null,
auth: {
enabled: false,
username: '',
password: ''
},
bypassProxy: ''
}
};
mockStoreData['preferences'] = oldFormatProxy;
const preferences = getPreferences();
// disabled: true is preserved from migration
expect(preferences.proxy.disabled).toBe(true);
expect(preferences.proxy.inherit).toBe(false);
});
it('should migrate mode: "on" to disabled: false, inherit: false', () => {
const oldFormatProxy = {
proxy: {
mode: 'on',
protocol: 'https',
hostname: 'proxy.example.com',
port: 8443,
auth: {
enabled: true,
username: 'user',
password: 'pass'
},
bypassProxy: '*.local'
}
};
mockStoreData['preferences'] = oldFormatProxy;
const preferences = getPreferences();
expect(preferences.proxy.disabled).toBeUndefined(); // disabled: false is omitted
expect(preferences.proxy.inherit).toBe(false);
// Values are preserved from stored preferences
expect(preferences.proxy.config.protocol).toBe('https');
expect(preferences.proxy.config.hostname).toBe('proxy.example.com');
expect(preferences.proxy.config.port).toBe(8443);
});
it('should migrate mode: "system" to disabled: false, inherit: true', () => {
const oldFormatProxy = {
proxy: {
mode: 'system',
protocol: 'http',
hostname: '',
port: null,
auth: {
enabled: false,
username: '',
password: ''
},
bypassProxy: ''
}
};
mockStoreData['preferences'] = oldFormatProxy;
const preferences = getPreferences();
expect(preferences.proxy.disabled).toBeUndefined(); // disabled: false is omitted
expect(preferences.proxy.inherit).toBe(true);
});
});
});

View File

@@ -0,0 +1,570 @@
const { transformBrunoConfigBeforeSave, transformBrunoConfigAfterRead } = require('../../src/utils/transformBrunoConfig');
describe('BrunoConfig Proxy Transform', () => {
describe('transformBrunoConfigAfterRead - Migration from old to new format', () => {
describe('Old Format: enabled (true | false | "global")', () => {
test('should migrate enabled: true to disabled: false, inherit: false', async () => {
const oldConfig = {
name: 'Test Collection',
proxy: {
enabled: true,
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
enabled: true,
username: 'user',
password: 'pass'
},
bypassProxy: 'localhost'
}
};
const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
expect(result.proxy).toEqual({
inherit: false,
config: {
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
username: 'user',
password: 'pass'
},
bypassProxy: 'localhost'
}
});
expect(result.proxy.disabled).toBeUndefined(); // disabled: false is omitted
});
test('should migrate enabled: false to disabled: true, inherit: false', async () => {
const oldConfig = {
name: 'Test Collection',
proxy: {
enabled: false,
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
enabled: false,
username: '',
password: ''
},
bypassProxy: ''
}
};
const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
expect(result.proxy.disabled).toBe(true);
expect(result.proxy.inherit).toBe(false);
});
test('should migrate enabled: "global" to disabled: false, inherit: true', async () => {
const oldConfig = {
name: 'Test Collection',
proxy: {
enabled: 'global',
protocol: 'http',
hostname: '',
port: null,
auth: {
enabled: false,
username: '',
password: ''
},
bypassProxy: ''
}
};
const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
expect(result.proxy.disabled).toBeUndefined(); // disabled: false is omitted
expect(result.proxy.inherit).toBe(true);
});
test('should migrate auth.enabled: false to auth.disabled: true', async () => {
const oldConfig = {
name: 'Test Collection',
proxy: {
enabled: true,
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
enabled: false,
username: 'user',
password: 'pass'
},
bypassProxy: ''
}
};
const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
expect(result.proxy.config.auth.disabled).toBe(true);
expect(result.proxy.config.auth.username).toBe('user');
expect(result.proxy.config.auth.password).toBe('pass');
});
test('should omit auth.disabled when auth.enabled: true', async () => {
const oldConfig = {
name: 'Test Collection',
proxy: {
enabled: true,
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
enabled: true,
username: 'user',
password: 'pass'
},
bypassProxy: ''
}
};
const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
expect(result.proxy.config.auth.disabled).toBeUndefined();
expect(result.proxy.config.auth.username).toBe('user');
expect(result.proxy.config.auth.password).toBe('pass');
});
});
describe('New Format (no migration)', () => {
test('should not modify new format with inherit: false', async () => {
const newConfig = {
name: 'Test Collection',
proxy: {
inherit: false,
config: {
protocol: 'https',
hostname: 'proxy.example.com',
port: 8443,
auth: {
username: 'user',
password: 'pass'
},
bypassProxy: '*.local'
}
}
};
const result = await transformBrunoConfigAfterRead(newConfig, '/test/path');
expect(result.proxy).toEqual(newConfig.proxy);
});
test('should not modify new format with inherit: true', async () => {
const newConfig = {
name: 'Test Collection',
proxy: {
inherit: true,
config: {
protocol: 'http',
hostname: '',
port: null,
auth: {
username: '',
password: ''
},
bypassProxy: ''
}
}
};
const result = await transformBrunoConfigAfterRead(newConfig, '/test/path');
expect(result.proxy).toEqual(newConfig.proxy);
});
test('should not modify new format with disabled: true', async () => {
const newConfig = {
name: 'Test Collection',
proxy: {
disabled: true,
inherit: false,
config: {
protocol: 'http',
hostname: '',
port: null,
auth: {
username: '',
password: ''
},
bypassProxy: ''
}
}
};
const result = await transformBrunoConfigAfterRead(newConfig, '/test/path');
expect(result.proxy).toEqual(newConfig.proxy);
});
test('should not modify new format with auth.disabled: true', async () => {
const newConfig = {
name: 'Test Collection',
proxy: {
inherit: false,
config: {
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
disabled: true,
username: 'user',
password: 'pass'
},
bypassProxy: ''
}
}
};
const result = await transformBrunoConfigAfterRead(newConfig, '/test/path');
expect(result.proxy).toEqual(newConfig.proxy);
});
});
describe('Edge Cases', () => {
test('should handle missing proxy config', async () => {
const config = {
name: 'Test Collection'
};
const result = await transformBrunoConfigAfterRead(config, '/test/path');
expect(result.proxy).toBeUndefined();
});
test('should handle null port values', async () => {
const oldConfig = {
name: 'Test Collection',
proxy: {
enabled: true,
protocol: 'http',
hostname: 'proxy.example.com',
port: null,
auth: {
enabled: false,
username: '',
password: ''
},
bypassProxy: ''
}
};
const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
expect(result.proxy.config.port).toBeNull();
});
test('should handle SOCKS protocols', async () => {
const oldConfig = {
name: 'Test Collection',
proxy: {
enabled: true,
protocol: 'socks5',
hostname: 'socks.example.com',
port: 1080,
auth: {
enabled: true,
username: 'socksuser',
password: 'sockspass'
},
bypassProxy: ''
}
};
const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
expect(result.proxy.config.protocol).toBe('socks5');
expect(result.proxy.config.hostname).toBe('socks.example.com');
expect(result.proxy.config.port).toBe(1080);
});
test('should handle missing auth object', async () => {
const oldConfig = {
name: 'Test Collection',
proxy: {
enabled: true,
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
bypassProxy: ''
}
};
const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
expect(result.proxy.config.auth).toEqual({
username: '',
password: ''
});
});
test('should preserve protobuf config during proxy migration', async () => {
const oldConfig = {
name: 'Test Collection',
protobuf: {
protoFiles: [{ path: 'test.proto' }],
importPaths: [{ path: 'imports/' }]
},
proxy: {
enabled: 'global',
protocol: 'http',
hostname: '',
port: null,
auth: {
enabled: false,
username: '',
password: ''
},
bypassProxy: ''
}
};
const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
expect(result.protobuf).toBeDefined();
expect(result.protobuf.protoFiles).toHaveLength(1);
expect(result.proxy.inherit).toBe(true);
});
});
});
describe('transformBrunoConfigBeforeSave - Cleanup optional fields', () => {
test('should remove disabled: false from proxy config', () => {
const config = {
name: 'Test Collection',
proxy: {
disabled: false,
inherit: false,
config: {
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
username: 'user',
password: 'pass'
},
bypassProxy: ''
}
}
};
const result = transformBrunoConfigBeforeSave(config);
expect(result.proxy.disabled).toBeUndefined();
expect(result.proxy.inherit).toBe(false);
});
test('should keep disabled: true in proxy config', () => {
const config = {
name: 'Test Collection',
proxy: {
disabled: true,
inherit: false,
config: {
protocol: 'http',
hostname: '',
port: null,
auth: {
username: '',
password: ''
},
bypassProxy: ''
}
}
};
const result = transformBrunoConfigBeforeSave(config);
expect(result.proxy.disabled).toBe(true);
});
test('should remove auth.disabled: false from proxy config', () => {
const config = {
name: 'Test Collection',
proxy: {
inherit: false,
config: {
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
disabled: false,
username: 'user',
password: 'pass'
},
bypassProxy: ''
}
}
};
const result = transformBrunoConfigBeforeSave(config);
expect(result.proxy.config.auth.disabled).toBeUndefined();
expect(result.proxy.config.auth.username).toBe('user');
});
test('should keep auth.disabled: true in proxy config', () => {
const config = {
name: 'Test Collection',
proxy: {
inherit: false,
config: {
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
disabled: true,
username: 'user',
password: 'pass'
},
bypassProxy: ''
}
}
};
const result = transformBrunoConfigBeforeSave(config);
expect(result.proxy.config.auth.disabled).toBe(true);
});
test('should handle missing proxy config', () => {
const config = {
name: 'Test Collection'
};
const result = transformBrunoConfigBeforeSave(config);
expect(result.proxy).toBeUndefined();
});
test('should preserve protobuf config cleanup', () => {
const config = {
name: 'Test Collection',
protobuf: {
protoFiles: [{ path: 'test.proto', exists: true }],
importPaths: [{ path: 'imports/', exists: false }]
},
proxy: {
disabled: false,
inherit: false,
config: {
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
disabled: false,
username: 'user',
password: 'pass'
},
bypassProxy: ''
}
}
};
const result = transformBrunoConfigBeforeSave(config);
// Protobuf exists fields should be removed
expect(result.protobuf.protoFiles[0].exists).toBeUndefined();
expect(result.protobuf.importPaths[0].exists).toBeUndefined();
// Proxy optional fields should be removed
expect(result.proxy.disabled).toBeUndefined();
expect(result.proxy.config.auth.disabled).toBeUndefined();
});
test('should not modify config without optional fields', () => {
const config = {
name: 'Test Collection',
proxy: {
inherit: true,
config: {
protocol: 'http',
hostname: '',
port: null,
auth: {
username: '',
password: ''
},
bypassProxy: ''
}
}
};
const result = transformBrunoConfigBeforeSave(config);
expect(result.proxy).toEqual(config.proxy);
});
});
describe('Round-trip transformation', () => {
test('should handle read -> save -> read cycle for old format', async () => {
const oldConfig = {
name: 'Test Collection',
proxy: {
enabled: true,
protocol: 'http',
hostname: 'proxy.example.com',
port: 8080,
auth: {
enabled: true,
username: 'user',
password: 'pass'
},
bypassProxy: 'localhost'
}
};
// Read (migrate)
const afterRead = await transformBrunoConfigAfterRead(oldConfig, '/test/path');
// Save (cleanup)
const beforeSave = transformBrunoConfigBeforeSave(afterRead);
// Read again (should not change)
const afterSecondRead = await transformBrunoConfigAfterRead(beforeSave, '/test/path');
expect(afterSecondRead.proxy).toEqual(beforeSave.proxy);
expect(afterSecondRead.proxy.inherit).toBe(false);
expect(afterSecondRead.proxy.disabled).toBeUndefined();
expect(afterSecondRead.proxy.config.auth.disabled).toBeUndefined();
});
test('should handle read -> save -> read cycle for new format', async () => {
const newConfig = {
name: 'Test Collection',
proxy: {
inherit: false,
config: {
protocol: 'https',
hostname: 'proxy.example.com',
port: 8443,
auth: {
username: 'user',
password: 'pass'
},
bypassProxy: '*.local'
}
}
};
// Read (no migration)
const afterRead = await transformBrunoConfigAfterRead(newConfig, '/test/path');
// Save (cleanup)
const beforeSave = transformBrunoConfigBeforeSave(afterRead);
// Read again (should not change)
const afterSecondRead = await transformBrunoConfigAfterRead(beforeSave, '/test/path');
expect(afterSecondRead.proxy).toEqual(newConfig.proxy);
});
});
});

View File

@@ -22,6 +22,7 @@
"devDependencies": {
"@babel/preset-env": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
"@opencollection/types": "~0.5.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",

View File

@@ -88,7 +88,8 @@ const buildPkce = (pkce?: boolean | null): OAuth2PKCE | undefined => {
return undefined;
}
return { enabled: Boolean(pkce) };
// If pkce is false, set disabled: true; if true, return empty object (enabled by default)
return pkce ? {} : { disabled: true };
};
const buildTokenConfig = (oauth: BrunoOAuth2): OAuth2TokenConfig | undefined => {
@@ -540,8 +541,9 @@ export const toBrunoOAuth2 = (oauth: AuthOAuth2 | null | undefined): BrunoOAuth2
if (brunoOAuth.grantType === 'authorization_code' && oauth.flow === 'authorization_code') {
const authCodeFlow = oauth as OAuth2AuthorizationCodeFlow;
if (authCodeFlow.pkce?.enabled !== undefined) {
brunoOAuth.pkce = authCodeFlow.pkce.enabled;
if (authCodeFlow.pkce !== undefined) {
// If pkce.disabled is true, set pkce to false; otherwise set to true
brunoOAuth.pkce = !authCodeFlow.pkce.disabled;
}
}

View File

@@ -50,43 +50,53 @@ const parseCollection = (ymlString: string): ParsedCollection => {
};
}
// proxy
// proxy - only support newer format
// Default: inherit from global preferences
brunoConfig.proxy = {
enabled: false,
protocol: '',
hostname: '',
port: '',
auth: {
enabled: false,
username: '',
password: ''
inherit: true,
config: {
protocol: 'http',
hostname: '',
port: '',
auth: {
username: '',
password: ''
},
bypassProxy: ''
}
};
if (oc.config?.proxy) {
if (oc.config.proxy === 'inherit') {
brunoConfig.proxy.enabled = 'global';
} else if (typeof oc.config.proxy === 'object') {
if (oc.config?.proxy && typeof oc.config.proxy === 'object') {
// Validate newer format: must have 'inherit' and 'config' properties
const proxyConfig = oc.config.proxy as any;
if ('inherit' in proxyConfig && typeof proxyConfig.inherit === 'boolean' && proxyConfig.config && typeof proxyConfig.config === 'object') {
// Valid newer format
brunoConfig.proxy = {
enabled: true,
protocol: oc.config.proxy.protocol || '',
hostname: oc.config.proxy.hostname || '',
port: oc.config.proxy.port || '',
auth: {
enabled: false,
username: '',
password: ''
inherit: proxyConfig.inherit,
config: {
protocol: proxyConfig.config.protocol || 'http',
hostname: proxyConfig.config.hostname || '',
port: proxyConfig.config.port || '',
auth: {
username: proxyConfig.config.auth?.username || '',
password: proxyConfig.config.auth?.password || ''
},
bypassProxy: proxyConfig.config.bypassProxy || ''
}
};
if (oc.config.proxy.auth && typeof oc.config.proxy.auth === 'object') {
brunoConfig.proxy.auth = {
enabled: true,
username: oc.config.proxy.auth.username || '',
password: oc.config.proxy.auth.password || ''
};
// Handle optional disabled field
if (proxyConfig.disabled === true) {
brunoConfig.proxy.disabled = true;
}
// Handle optional auth.disabled field
if (proxyConfig.config.auth?.disabled === true) {
brunoConfig.proxy.config.auth.disabled = true;
}
}
// If not in newer format, use default (inherit: true)
}
// client certificates

View File

@@ -18,8 +18,15 @@ const hasCollectionConfig = (brunoConfig: any): boolean => {
|| brunoConfig.protobuf?.importPaths?.length > 0
);
// proxy
const hasProxy = !!brunoConfig.proxy?.enabled;
// proxy - check if proxy is configured in newer format
// Valid newer format: has 'inherit' property and 'config' object
const isValidProxyFormat = brunoConfig.proxy
&& typeof brunoConfig.proxy === 'object'
&& 'inherit' in brunoConfig.proxy
&& brunoConfig.proxy.config
&& typeof brunoConfig.proxy.config === 'object';
const hasProxy = isValidProxyFormat;
// client certificates
const hasClientCertificates = brunoConfig.clientCertificates?.certs?.length > 0;
@@ -52,7 +59,6 @@ const hasPresets = (brunoConfig: any): boolean => {
};
const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
console.log('brunoConfig', brunoConfig);
try {
const oc: OpenCollection = {};
@@ -78,22 +84,38 @@ const stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {
};
}
// proxy
if (brunoConfig.proxy?.enabled) {
if (brunoConfig.proxy.enabled === 'global') {
oc.config.proxy = 'inherit';
} else {
oc.config.proxy = {
protocol: brunoConfig.proxy.protocol,
hostname: brunoConfig.proxy.hostname,
port: brunoConfig.proxy.port
};
// proxy - only write newer format
// Validate that brunoConfig.proxy is in newer format before writing
const isValidProxyFormat = brunoConfig.proxy
&& typeof brunoConfig.proxy === 'object'
&& 'inherit' in brunoConfig.proxy
&& brunoConfig.proxy.config
&& typeof brunoConfig.proxy.config === 'object';
if (brunoConfig.proxy.auth?.enabled) {
oc.config.proxy.auth = {
username: brunoConfig.proxy.auth.username,
password: brunoConfig.proxy.auth.password
};
if (isValidProxyFormat) {
oc.config.proxy = {
inherit: brunoConfig.proxy.inherit,
config: {
protocol: brunoConfig.proxy.config.protocol || 'http',
hostname: brunoConfig.proxy.config.hostname || '',
port: brunoConfig.proxy.config.port || '',
auth: {
username: brunoConfig.proxy.config.auth?.username || '',
password: brunoConfig.proxy.config.auth?.password || ''
},
bypassProxy: brunoConfig.proxy.config.bypassProxy || ''
}
};
// Add optional disabled field if true
if (brunoConfig.proxy.disabled === true) {
oc.config.proxy.disabled = true;
}
// Add optional auth.disabled field if true
if (brunoConfig.proxy.config?.auth?.disabled === true) {
if (oc.config.proxy.config && oc.config.proxy.config.auth) {
oc.config.proxy.config.auth.disabled = true;
}
}
}