Compare commits

...

33 Commits

Author SHA1 Message Date
Anoop M D
0e85b302b8 chore: bumped version to v1.10.0 2024-02-28 00:40:10 +05:30
Anoop M D
3b51621580 chore: updated golden edition org pricing 2024-02-28 00:23:44 +05:30
Gabriel
18e7301550 feat: add middle mouse button click to close tab (#1649)
* feat: add middle click button to close tab
* refactor: remove unused code
* fix: verify if is middle click before trigger close confirmation modal
2024-02-27 23:58:01 +05:30
Anoop M D
96bcc7074a chore: prettify icon styling updates 2024-02-27 23:48:03 +05:30
lohit
f64e13a71f feat(#bru-22): prettify graphql placement and styling (#1675) 2024-02-27 21:14:08 +05:30
lohit
01360d1522 feat/(#1003): oauth2 support - styling fixes, code cleanup (#1674)
* feat/(#1003):  oauth2 support - styling fixes, code cleanup (#1674)
---------
Co-authored-by: lohit-1 <lohit@usebruno.com>
2024-02-27 21:12:34 +05:30
Anoop M D
6729d718cf Merge pull request #526 from j0k3r/feature/prettify-graphql
Add "Prettify GraphQL" button
2024-02-27 02:03:28 +05:30
Anoop M D
17abc19770 chore: fix tests 2024-02-27 02:00:19 +05:30
Anoop M D
13cb71eaef chore: fix tests 2024-02-27 01:43:36 +05:30
Anoop M D
b375620875 chore: fix tests 2024-02-27 01:39:34 +05:30
Anoop M D
1cf8a2f3f1 chore(#1667): graceful handling of none type for backward compatibility 2024-02-27 01:29:10 +05:30
Anoop M D
7c416a99ef Merge pull request #1667 from lohxt1/feature/BRU-18--inherit-auth-mode
feat(#BRU-18) : "inherit" auth mode for inheriting auth to requests from collection
2024-02-27 01:12:15 +05:30
Anoop M D
389a383b99 Merge branch 'main' into feature/BRU-18--inherit-auth-mode 2024-02-27 01:11:55 +05:30
Jeremy Benoist
d1a8f59c79 Add "Prettify GraphQL" button
The goal is to achieve the same behavior from Insomnia which allow to format a GraphQL query using prettier (see 264177b56f/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx (L260-L266)).

I moved the `prettier` deps from `devDependencies` to `dependencies` because it's now used within the application.
I was forced to import `prettier/standalone` & `prettier/parser-graphql` (as it is explained here: https://prettier.io/docs/en/browser.html) otherwise I got that error:

> Couldn't resolve parser "graphql". Parsers must be explicitly added to the standalone bundle.
2024-02-26 17:02:04 +01:00
lohxt1
d477cfc7e1 feat(#BRU-18): reverted local environment file change 2024-02-26 19:34:28 +05:30
lohxt1
e66e26d115 feat(#BRU-18): updated inherit option order in the auth mode select dropdown 2024-02-26 19:31:18 +05:30
lohxt1
7a635810b1 feat(#1655): updated bruno-tests collection with an "inherit auht" request example 2024-02-26 19:11:11 +05:30
lohit
dde6695a43 feat(#1575): make response pane in collection runner screen unaffected by scroll (#1661)
* feat(#1575): make response pane unaffected by scroll
* feat(#1575): styling consistency
2024-02-26 16:46:53 +05:30
lohit
9f81e6dc73 feat(#1003): oauth2 support - resourceOwnerPasswordCredentials, authorization code, client credentials (#1654)
* feat(#1003): oauth2 support
Co-authored-by: lohit-1 <lohit@usebruno.com>
2024-02-26 16:44:38 +05:30
lohxt1
3c2cbe63c4 feat(#1655): inherit auth mode for requests 2024-02-26 14:27:59 +05:30
Anoop M D
a4b13d5c2a fix: fixed awsv4 env var interpolation issue 2024-02-24 00:47:28 +05:30
Anoop M D
064281d438 Merge branch 'main' of github.com:usebruno/bruno 2024-02-23 23:42:45 +05:30
Anoop M D
43c873422f feat: bruno cli awsv4 auth support 2024-02-23 23:42:28 +05:30
Anoop M D
c7c762185e Merge pull request #1600 from mato-meciar/bugfix/empty-pwd-basic-auth
fix(#1545): empty strings encryption
2024-02-21 20:17:12 +05:30
Anoop M D
09c496e516 Merge pull request #1618 from trusta/feat/set-up-ajv-formats-in-scripts
feat(#947): set up ajv-formats in script and test runtimes
2024-02-21 20:08:57 +05:30
lohit
117726a01f feat(#BRU-11) - tailwindcss v3 upgrade (#1597)
* feat(#BRU-11) - tailwindcss v3 upgrade
* feat(#BRU-11) - lock file update to fix PR checks
2024-02-19 17:36:54 +05:30
lohit
e2d754702a feat(#BRU-10) - codeeditor syntax colors for system theme (#1595) 2024-02-19 17:30:49 +05:30
lohit
fee3416c85 feat(#1575) - auto scroll runner output body during collection run (#1588) 2024-02-19 17:29:24 +05:30
Florent Boisselier
a756c49285 feat(#947): set up ajv-formats in script and test runtimes 2024-02-18 14:22:46 +01:00
Martin Meciar
b6abc665a5 fix(#1545): empty strings encryption
enable empty strings to be encrypted
2024-02-15 19:01:07 +01:00
lohit
bd002ca316 feat(#BRU-7) - scrollbar styling for linux and windows (#1589) 2024-02-15 15:00:18 +05:30
Anoop M D
5fece08f4b chore: display bru cli version while running gh workflow 2024-02-14 05:09:25 +05:30
Anoop M D
36b7fbe584 feat: added gh workflow for testing bru cli from npm 2024-02-14 04:58:54 +05:30
109 changed files with 2968 additions and 632 deletions

View File

@@ -1,12 +0,0 @@
name: Bump Homebrew Cask
on:
release:
types: [published]
jobs:
bump:
runs-on: macos-10.15
steps:
- name: Bump Homebrew Cask
run: brew bump-cask-pr bruno --version "${GITHUB_REF_NAME#v}"

48
.github/workflows/npm-bru-cli.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Bru CLI Tests (npm)
on:
workflow_dispatch:
inputs:
build:
description: 'Test Bru CLI (npm)'
required: true
default: 'true'
# Assign permissions for unit tests to be reported.
# See https://github.com/dorny/test-reporter/issues/168
permissions:
statuses: write
checks: write
contents: write
pull-requests: write
actions: write
jobs:
test:
name: CLI Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
- name: Install Bru CLI from NPM
run: npm install -g @usebruno/cli
- name: Display Bru CLI Version
run: bru --version
- name: Run tests
run: |
cd packages/bruno-tests/collection
npm install
bru run --env Prod --output junit.xml --format junit
- name: Publish Test Report
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Test Report
path: packages/bruno-tests/collection/junit.xml
reporter: java-junit

View File

@@ -1,27 +0,0 @@
name: Playwright Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm i --legacy-peer-deps
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

1193
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -53,6 +53,7 @@
"pdfjs-dist": "^3.11.174",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",
"prettier": "^2.7.1",
"qs": "^6.11.0",
"query-string": "^7.0.1",
"react": "18.2.0",
@@ -70,7 +71,6 @@
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"system": "^2.0.1",
"tailwindcss": "^2.2.19",
"url": "^0.11.3",
"xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1",
@@ -82,6 +82,7 @@
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/runtime": "^7.16.3",
"autoprefixer": "^10.4.17",
"babel-loader": "^8.2.3",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
@@ -89,8 +90,9 @@
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.5",
"prettier": "^2.7.1",
"postcss": "^8.4.35",
"style-loader": "^3.3.1",
"tailwindcss": "^3.4.1",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@@ -11,7 +11,7 @@ const BearerAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const bearerToken = get(collection, 'root.request.auth.bearer.token');
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));

View File

@@ -11,7 +11,7 @@ import StyledWrapper from './StyledWrapper';
const Docs = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const docs = get(collection, 'root.docs', '');
const preferences = useSelector((state) => state.app.preferences);
@@ -40,7 +40,7 @@ const Docs = ({ collection }) => {
{isEditing ? (
<CodeEditor
collection={collection}
theme={storedTheme}
theme={displayedTheme}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}

View File

@@ -12,7 +12,7 @@ const Script = ({ collection }) => {
const requestScript = get(collection, 'root.request.script.req', '');
const responseScript = get(collection, 'root.request.script.res', '');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onRequestScriptEdit = (value) => {
@@ -44,7 +44,7 @@ const Script = ({ collection }) => {
<CodeEditor
collection={collection}
value={requestScript || ''}
theme={storedTheme}
theme={displayedTheme}
onEdit={onRequestScriptEdit}
mode="javascript"
onSave={handleSave}
@@ -56,7 +56,7 @@ const Script = ({ collection }) => {
<CodeEditor
collection={collection}
value={responseScript || ''}
theme={storedTheme}
theme={displayedTheme}
onEdit={onResponseScriptEdit}
mode="javascript"
onSave={handleSave}

View File

@@ -11,7 +11,7 @@ const Tests = ({ collection }) => {
const dispatch = useDispatch();
const tests = get(collection, 'root.request.tests', '');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onEdit = (value) => {
@@ -30,7 +30,7 @@ const Tests = ({ collection }) => {
<CodeEditor
collection={collection}
value={tests || ''}
theme={storedTheme}
theme={displayedTheme}
onEdit={onEdit}
mode="javascript"
onSave={handleSave}

View File

@@ -11,7 +11,7 @@ import StyledWrapper from './StyledWrapper';
const Documentation = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const docs = item.draft ? get(item, 'draft.request.docs') : get(item, 'request.docs');
const preferences = useSelector((state) => state.app.preferences);
@@ -45,7 +45,7 @@ const Documentation = ({ item, collection }) => {
{isEditing ? (
<CodeEditor
collection={collection}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
value={docs || ''}
onEdit={onEdit}

View File

@@ -4,7 +4,6 @@ const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
@@ -16,6 +15,7 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
font-weight: 600;
}
td {
padding: 6px 10px;

View File

@@ -38,7 +38,7 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('awsv4');
}}
>
@@ -47,7 +47,7 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('basic');
}}
>
@@ -56,7 +56,7 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('bearer');
}}
>
@@ -65,7 +65,7 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('digest');
}}
>
@@ -74,7 +74,25 @@ const AuthMode = ({ item, collection }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
dropdownTippyRef?.current?.hide();
onModeChange('oauth2');
}}
>
OAuth 2.0
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
onModeChange('inherit');
}}
>
Inherit
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
onModeChange('none');
}}
>

View File

@@ -6,6 +6,7 @@ const Wrapper = styled.div`
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};

View File

@@ -6,6 +6,7 @@ const Wrapper = styled.div`
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};

View File

@@ -6,6 +6,7 @@ const Wrapper = styled.div`
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};

View File

@@ -12,8 +12,8 @@ const BearerAuth = ({ item, collection }) => {
const { storedTheme } = useTheme();
const bearerToken = item.draft
? get(item, 'draft.request.auth.bearer.token')
: get(item, 'request.auth.bearer.token');
? get(item, 'draft.request.auth.bearer.token', '')
: get(item, 'request.auth.bearer.token', '');
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));

View File

@@ -6,6 +6,7 @@ const Wrapper = styled.div`
}
.single-line-editor-wrapper {
max-width: 400px;
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};

View File

@@ -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;

View File

@@ -0,0 +1,65 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
const OAuth2AuthorizationCode = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleChange = (key, value) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'authorization_code',
...oAuth,
[key]: value
}
})
);
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange(key, val)}
onRun={() => {}}
collection={collection}
/>
</div>
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;

View File

@@ -0,0 +1,28 @@
const inputsConfig = [
{
key: 'callbackUrl',
label: 'Callback URL'
},
{
key: 'authorizationUrl',
label: 'Auth URL'
},
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'clientId',
label: 'Client ID'
},
{
key: 'clientSecret',
label: 'Client Secret'
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -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;

View File

@@ -0,0 +1,78 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const OAuth2ClientCredentials = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleClientIdChange = (clientId) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'client_credentials',
clientId: clientId,
clientSecret: oAuth.clientSecret
}
})
);
};
const handleClientSecretChange = (clientSecret) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'client_credentials',
clientId: oAuth.clientId,
clientSecret: clientSecret
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Client Id</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={oAuth.clientId || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleClientIdChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Client Secret</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth.clientSecret || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleClientSecretChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default OAuth2ClientCredentials;

View File

@@ -0,0 +1,54 @@
import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 0.8125rem;
.grant-type-mode-selector {
padding: 0.5rem 0px;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
.dropdown {
width: fit-content;
div[data-tippy-root] {
width: fit-content;
}
.tippy-box {
width: fit-content;
max-width: none !important;
.tippy-content: {
width: fit-content;
max-width: none !important;
}
}
}
.grant-type-label {
width: fit-content;
color: ${(props) => props.theme.colors.text.yellow};
justify-content: space-between;
padding: 0 0.5rem;
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
.label-item {
padding: 0.2rem 0.6rem !important;
}
}
.caret {
color: rgb(140, 140, 140);
fill: rgb(140 140 140);
}
label {
font-size: 0.8125rem;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,92 @@
import React, { useRef, forwardRef } from 'react';
import get from 'lodash/get';
import Dropdown from 'components/Dropdown';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import { IconCaretDown } from '@tabler/icons';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { humanizeGrantType } from 'utils/collections';
import { useEffect } from 'react';
const GrantTypeSelector = ({ item, collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
{humanizeGrantType(oAuth?.grantType)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const onGrantTypeChange = (grantType) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType
}
})
);
};
useEffect(() => {
// initalize redux state with a default oauth2 auth type
// authorization_code - default option
!oAuth?.grantType &&
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'authorization_code'
}
})
);
}, [oAuth]);
return (
<StyledWrapper>
<label className="block font-medium mb-2">Grant Type</label>
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onGrantTypeChange('password');
}}
>
Resource Owner Password Credentials
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onGrantTypeChange('authorization_code');
}}
>
Authorization Code
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onGrantTypeChange('client_credentials');
}}
>
Client Credentials
</div>
</Dropdown>
</div>
</StyledWrapper>
);
};
export default GrantTypeSelector;

View File

@@ -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;

View File

@@ -0,0 +1,78 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const OAuth2Ropc = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'password',
username: username,
password: oAuth.password
}
})
);
};
const handlePasswordChange = (password) => {
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'password',
username: oAuth.username,
password: password
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Username</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={oAuth.username || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleUsernameChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default OAuth2Ropc;

View File

@@ -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;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import GrantTypeSelector from './GrantTypeSelector/index';
import OAuth2Ropc from './Ropc/index';
import OAuth2AuthorizationCode from './AuthorizationCode/index';
import OAuth2ClientCredentials from './ClientCredentials/index';
const grantTypeComponentMap = (grantType, item, collection) => {
switch (grantType) {
case 'password':
return <OAuth2Ropc item={item} collection={collection} />;
break;
case 'authorization_code':
return <OAuth2AuthorizationCode item={item} collection={collection} />;
break;
case 'client_credentials':
return <OAuth2ClientCredentials item={item} collection={collection} />;
break;
default:
return <div>TBD</div>;
break;
}
};
const OAuth2 = ({ item, collection }) => {
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
return (
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector item={item} collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, item, collection)}
</StyledWrapper>
);
};
export default OAuth2;

View File

@@ -1,5 +1,11 @@
import styled from 'styled-components';
const Wrapper = styled.div``;
const Wrapper = styled.div`
.inherit-mode-text {
color: ${(props) => props.theme.colors.text.yellow};
}
.inherit-mode-label {
}
`;
export default Wrapper;

View File

@@ -6,10 +6,15 @@ import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections/index';
import OAuth2 from './OAuth2/index';
const Auth = ({ item, collection }) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const collectionRoot = get(collection, 'root', {});
const collectionAuth = get(collectionRoot, 'request.auth');
const getAuthView = () => {
switch (authMode) {
case 'awsv4': {
@@ -24,6 +29,17 @@ const Auth = ({ item, collection }) => {
case 'digest': {
return <DigestAuth collection={collection} item={item} />;
}
case 'oauth2': {
return <OAuth2 collection={collection} item={item} />;
}
case 'inherit': {
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>
);
}
}
};
@@ -36,4 +52,5 @@ const Auth = ({ item, collection }) => {
</StyledWrapper>
);
};
export default Auth;

View File

@@ -10,7 +10,7 @@ import StyledWrapper from './StyledWrapper';
const GraphQLVariables = ({ variables, item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onEdit = (value) => {
@@ -31,7 +31,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
<CodeEditor
collection={collection}
value={variables || ''}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
onEdit={onEdit}
mode="javascript"

View File

@@ -8,9 +8,13 @@
import React from 'react';
import isEqual from 'lodash/isEqual';
import MD from 'markdown-it';
import { format } from 'prettier/standalone';
import prettierPluginGraphql from 'prettier/parser-graphql';
import { getAllVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import { IconWand } from '@tabler/icons';
import onHasCompletion from './onHasCompletion';
@@ -178,6 +182,19 @@ export default class QueryEditor extends React.Component {
}
}
beautifyRequestBody = () => {
try {
const prettyQuery = format(this.props.value, {
parser: 'graphql',
plugins: [prettierPluginGraphql]
});
this.editor.setValue(prettyQuery);
} catch (e) {
toast.error('Error occurred while prettifying GraphQL query');
}
};
// Todo: Overlay is messing up with schema hint
// Fix this
addOverlay = () => {
@@ -189,13 +206,23 @@ export default class QueryEditor extends React.Component {
render() {
return (
<StyledWrapper
className="h-full w-full"
aria-label="Query Editor"
ref={(node) => {
this._node = node;
}}
/>
<>
<StyledWrapper
className="h-full w-full relative"
aria-label="Query Editor"
ref={(node) => {
this._node = node;
}}
>
<button
className="btn-add-param text-link px-4 py-4 select-none absolute top-0 right-0 z-10"
onClick={this.beautifyRequestBody}
title="prettify"
>
<IconWand size={20} strokeWidth={1.5} />
</button>
</StyledWrapper>
</>
);
}

View File

@@ -13,7 +13,7 @@ const RequestBody = ({ item, collection }) => {
const dispatch = useDispatch();
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onEdit = (value) => {
@@ -48,7 +48,7 @@ const RequestBody = ({ item, collection }) => {
<StyledWrapper className="w-full">
<CodeEditor
collection={collection}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
value={bodyContent[bodyMode] || ''}
onEdit={onEdit}

View File

@@ -12,7 +12,7 @@ const Script = ({ item, collection }) => {
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onRequestScriptEdit = (value) => {
@@ -45,7 +45,7 @@ const Script = ({ item, collection }) => {
<CodeEditor
collection={collection}
value={requestScript || ''}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
onEdit={onRequestScriptEdit}
mode="javascript"
@@ -58,7 +58,7 @@ const Script = ({ item, collection }) => {
<CodeEditor
collection={collection}
value={responseScript || ''}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
onEdit={onResponseScriptEdit}
mode="javascript"

View File

@@ -11,7 +11,7 @@ const Tests = ({ item, collection }) => {
const dispatch = useDispatch();
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const onEdit = (value) => {
@@ -32,7 +32,7 @@ const Tests = ({ item, collection }) => {
<CodeEditor
collection={collection}
value={tests || ''}
theme={storedTheme}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
onEdit={onEdit}
mode="javascript"

View File

@@ -28,6 +28,19 @@ const RequestTab = ({ tab, collection }) => {
);
};
const handleMouseUp = (e) => {
if (e.button === 1) {
e.stopPropagation();
e.preventDefault();
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
}
};
const getMethodColor = (method = '') => {
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
@@ -124,7 +137,18 @@ const RequestTab = ({ tab, collection }) => {
}}
/>
)}
<div className="flex items-baseline tab-label pl-2">
<div
className="flex items-baseline tab-label pl-2"
onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e);
if (e.button === 1) {
e.stopPropagation();
e.preventDefault();
setShowConfirmClose(true);
}
}}
>
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
{method}
</span>

View File

@@ -19,7 +19,7 @@ const QueryResultPreview = ({
collection,
mode,
disableRunEventListener,
storedTheme
displayedTheme
}) => {
const preferences = useSelector((state) => state.app.preferences);
const dispatch = useDispatch();
@@ -71,7 +71,7 @@ const QueryResultPreview = ({
<CodeEditor
collection={collection}
font={get(preferences, 'font.codeFont', 'default')}
theme={storedTheme}
theme={displayedTheme}
onRun={onRun}
value={formattedData}
mode={mode}

View File

@@ -51,7 +51,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
const [filter, setFilter] = useState(null);
const formattedData = formatResponse(data, mode, filter);
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const debouncedResultFilterOnChange = debounce((e) => {
setFilter(e.target.value);
@@ -132,7 +132,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
collection={collection}
allowedPreviewModes={allowedPreviewModes}
disableRunEventListener={disableRunEventListener}
storedTheme={storedTheme}
displayedTheme={displayedTheme}
/>
{queryFilterEnabled && <QueryResultFilter onChange={debouncedResultFilterOnChange} mode={mode} />}
</>

View File

@@ -22,13 +22,23 @@ const getRelativePath = (fullPath, pathname) => {
export default function RunnerResults({ collection }) {
const dispatch = useDispatch();
const listWrapperRef = useRef();
const [selectedItem, setSelectedItem] = useState(null);
// ref for the runner output body
const runnerBodyRef = useRef();
const autoScrollRunnerBody = () => {
if (runnerBodyRef?.current) {
// mimicks the native terminal scroll style
runnerBodyRef.current.scrollTo(0, 100000);
}
};
useEffect(() => {
if (!collection.runnerResult) {
setSelectedItem(null);
}
autoScrollRunnerBody();
}, [collection, setSelectedItem]);
const collectionCopy = cloneDeep(collection);
@@ -67,11 +77,6 @@ export default function RunnerResults({ collection }) {
})
.filter(Boolean);
useEffect(() => {
if (listWrapperRef.current) {
listWrapperRef.current.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
}
}, [items]);
const runCollection = () => {
dispatch(runCollectionFolder(collection.uid, null, true));
};
@@ -102,12 +107,11 @@ export default function RunnerResults({ collection }) {
if (!items || !items.length) {
return (
<StyledWrapper className="px-4">
<StyledWrapper className="px-4 pb-4">
<div className="font-medium mt-6 title flex items-center">
Runner
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
</div>
<div className="mt-6">
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
</div>
@@ -124,21 +128,24 @@ export default function RunnerResults({ collection }) {
}
return (
<StyledWrapper ref={listWrapperRef} className="px-4 pb-4 flex flex-grow flex-col relative">
<div className="flex flex-row mt-6 mb-4">
<div className="font-medium title flex items-center">
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative">
<div className="flex flex-row">
<div className="font-medium my-6 title flex items-center">
Runner
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
</div>
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && (
<button className="btn ml-6 btn-sm btn-danger" onClick={cancelExecution}>
<button className="btn ml-6 my-4 btn-sm btn-danger" onClick={cancelExecution}>
Cancel Execution
</button>
)}
</div>
<div className="flex flex-1">
<div className="flex flex-col flex-1">
<div className="py-2 font-medium test-summary">
<div className="flex flex-row gap-4">
<div
className="flex flex-col flex-1 overflow-y-auto h-[calc(100vh_-_12rem)] max-h-[calc(100vh_-_12rem)] w-full"
ref={runnerBodyRef}
>
<div className="pb-2 font-medium test-summary">
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
</div>
{items.map((item) => {
@@ -227,8 +234,8 @@ export default function RunnerResults({ collection }) {
</div>
) : null}
</div>
<div className="flex flex-1" style={{ width: '50%' }}>
{selectedItem ? (
{selectedItem ? (
<div className="flex flex-1 w-[50%]">
<div className="flex flex-col w-full overflow-auto">
<div className="flex items-center px-3 mb-4 font-medium">
<span className="mr-2">{selectedItem.relativePath}</span>
@@ -243,8 +250,8 @@ export default function RunnerResults({ collection }) {
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
<ResponsePane item={selectedItem} collection={collection} />
</div>
) : null}
</div>
</div>
) : null}
</div>
</StyledWrapper>
);

View File

@@ -11,7 +11,7 @@ import { IconCopy } from '@tabler/icons';
import { findCollectionByItemUid } from '../../../../../../../utils/collections/index';
const CodeView = ({ language, item }) => {
const { storedTheme } = useTheme();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const { target, client, language: lang } = language;
const requestHeaders = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
@@ -45,7 +45,7 @@ const CodeView = ({ language, item }) => {
readOnly
value={snippet}
font={get(preferences, 'font.codeFont', 'default')}
theme={storedTheme}
theme={displayedTheme}
mode={lang}
/>
</StyledWrapper>

View File

@@ -110,7 +110,6 @@ const GoldenEdition = ({ onClose }) => {
const handlePricingOptionChange = (option) => {
setPricingOption(option);
};
console.log(displayedTheme);
const themeBasedContainerClassNames = displayedTheme === 'light' ? 'text-gray-900' : 'text-white';
const themeBasedTabContainerClassNames = displayedTheme === 'light' ? 'bg-gray-200' : 'bg-gray-800';
@@ -131,8 +130,7 @@ const GoldenEdition = ({ onClose }) => {
target="_blank"
className="flex text-white bg-yellow-600 hover:bg-yellow-700 font-medium rounded-lg text-sm px-4 py-2 text-center cursor-pointer"
>
<IconHeart size={18} strokeWidth={1.5} />{' '}
<span className="ml-2">{pricingOption === 'individuals' ? 'Buy' : 'Subscribe'}</span>
<IconHeart size={18} strokeWidth={1.5} /> <span className="ml-2">Buy</span>
</a>
</div>
{pricingOption === 'individuals' ? (
@@ -146,9 +144,11 @@ const GoldenEdition = ({ onClose }) => {
) : (
<div>
<div className="my-4">
<span className="text-3xl font-extrabold">$5</span>
<span className="text-3xl font-extrabold">$49</span>
<span className="ml-2">/&nbsp;user</span>
</div>
<p>/user/month</p>
<p className="bg-yellow-200 text-black rounded-md px-2 py-1 mb-2 inline-flex text-sm">One Time Payment</p>
<p className="text-sm">perpetual license with 2 years of updates</p>
</div>
)}
<div

View File

@@ -124,7 +124,7 @@ const Sidebar = () => {
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.9.0</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.10.0</div>
</div>
</div>
</div>

View File

@@ -159,6 +159,33 @@ const GlobalStyle = createGlobalStyle`
}
}
// scrollbar styling
// the below media query target non-macos devices
// (macos scrollbar styling is the ideal style reference)
@media not all and (pointer: coarse) {
* {
scrollbar-width: thin;
scrollbar-color: ${(props) => props.theme.scrollbar.color};
}
*::-webkit-scrollbar {
width: 5px;
}
*::-webkit-scrollbar-track {
background: transparent;
border-radius: 5px;
}
*::-webkit-scrollbar-thumb {
background-color: ${(props) => props.theme.scrollbar.color};
border-radius: 14px;
border: 3px solid ${(props) => props.theme.scrollbar.color};
}
}
// codemirror
.CodeMirror {
.cm-variable-valid {

View File

@@ -10,7 +10,6 @@ import ErrorBoundary from './ErrorBoundary';
import '../styles/app.scss';
import '../styles/globals.css';
import 'tailwindcss/dist/tailwind.min.css';
import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css';
import 'react-tooltip/dist/react-tooltip.css';

View File

@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
version: '1.9.0'
version: '1.10.0'
}
});
};

View File

@@ -402,6 +402,10 @@ export const collectionsSlice = createSlice({
item.draft.request.auth.mode = 'digest';
item.draft.request.auth.digest = action.payload.content;
break;
case 'oauth2':
item.draft.request.auth.mode = 'oauth2';
item.draft.request.auth.oauth2 = action.payload.content;
break;
}
}
}

View File

@@ -1,3 +1,7 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
:root {
--color-brand: #546de5;
--color-text: rgb(52 52 52);

View File

@@ -238,6 +238,10 @@ const darkTheme = {
plainGrid: {
hoverBg: '#3D3D3D'
},
scrollbar: {
color: 'rgb(52 51 49)'
}
};

View File

@@ -242,6 +242,10 @@ const lightTheme = {
plainGrid: {
hoverBg: '#f4f4f4'
},
scrollbar: {
color: 'rgb(152 151 149)'
}
};

View File

@@ -489,6 +489,10 @@ export const humanizeRequestBodyMode = (mode) => {
export const humanizeRequestAuthMode = (mode) => {
let label = 'No Auth';
switch (mode) {
case 'inherit': {
label = 'Inherit';
break;
}
case 'awsv4': {
label = 'AWS Sig V4';
break;
@@ -505,6 +509,30 @@ export const humanizeRequestAuthMode = (mode) => {
label = 'Digest Auth';
break;
}
case 'oauth2': {
label = 'OAuth 2.0';
break;
}
}
return label;
};
export const humanizeGrantType = (mode) => {
let label = 'No Auth';
switch (mode) {
case 'password': {
label = 'Resource Owner Password Credentials';
break;
}
case 'authorization_code': {
label = 'Authorization Code';
break;
}
case 'client_credentials': {
label = 'Client Credentials';
break;
}
}
return label;

View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {}
},
plugins: []
};

View File

@@ -1,6 +1,6 @@
{
"name": "@usebruno/cli",
"version": "1.9.0",
"version": "1.9.2",
"license": "MIT",
"main": "src/index.js",
"bin": {
@@ -24,9 +24,11 @@
"package.json"
],
"dependencies": {
"@aws-sdk/credential-providers": "^3.425.0",
"@usebruno/common": "0.1.0",
"@usebruno/js": "0.10.1",
"@usebruno/lang": "0.10.0",
"aws4-axios": "^3.3.0",
"axios": "^1.5.1",
"chai": "^4.3.7",
"chalk": "^3.0.0",

View File

@@ -0,0 +1,56 @@
const { fromIni } = require('@aws-sdk/credential-providers');
const { aws4Interceptor } = require('aws4-axios');
function isStrPresent(str) {
return str && str !== '' && str !== 'undefined';
}
async function resolveAwsV4Credentials(request) {
const awsv4 = request.awsv4config;
if (isStrPresent(awsv4.profileName)) {
try {
credentialsProvider = fromIni({
profile: awsv4.profileName
});
credentials = await credentialsProvider();
awsv4.accessKeyId = credentials.accessKeyId;
awsv4.secretAccessKey = credentials.secretAccessKey;
awsv4.sessionToken = credentials.sessionToken;
} catch {
console.error('Failed to fetch credentials from AWS profile.');
}
}
return awsv4;
}
function addAwsV4Interceptor(axiosInstance, request) {
if (!request.awsv4config) {
console.warn('No Auth Config found!');
return;
}
const awsv4 = request.awsv4config;
if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) {
console.warn('Required Auth Fields are not present');
return;
}
const interceptor = aws4Interceptor({
options: {
region: awsv4.region,
service: awsv4.service
},
credentials: {
accessKeyId: awsv4.accessKeyId,
secretAccessKey: awsv4.secretAccessKey,
sessionToken: awsv4.sessionToken
}
});
axiosInstance.interceptors.request.use(interceptor);
}
module.exports = {
addAwsV4Interceptor,
resolveAwsV4Credentials
};

View File

@@ -31,12 +31,19 @@ const prepareRequest = (request, collectionRoot) => {
headers: headers
};
// Authentication
// A request can override the collection auth with another auth
// But it cannot override the collection auth with no auth
// We will provide support for disabling the auth via scripting in the future
/**
* 27 Feb 2024:
* ['inherit', 'none'].includes(request.auth.mode)
* We are mainitaining the old behavior where 'none' used to inherit the collection auth.
*
* Very soon, 'none' will be treated as no auth and 'inherit' will be the only way to inherit collection auth.
* We will request users to update their collection files to use 'inherit' instead of 'none'.
* Don't want to break ongoing CI pipelines.
*
* Hoping to remove this by 1 April 2024.
*/
const collectionAuth = get(collectionRoot, 'request.auth');
if (collectionAuth) {
if (collectionAuth && ['inherit', 'none'].includes(request.auth.mode)) {
if (collectionAuth.mode === 'basic') {
axiosRequest.auth = {
username: get(collectionAuth, 'basic.username'),
@@ -57,6 +64,17 @@ const prepareRequest = (request, collectionRoot) => {
};
}
if (request.auth.mode === 'awsv4') {
axiosRequest.awsv4config = {
accessKeyId: get(request, 'auth.awsv4.accessKeyId'),
secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'),
sessionToken: get(request, 'auth.awsv4.sessionToken'),
service: get(request, 'auth.awsv4.service'),
region: get(request, 'auth.awsv4.region'),
profileName: get(request, 'auth.awsv4.profileName')
};
}
if (request.auth.mode === 'bearer') {
axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`;
}

View File

@@ -15,6 +15,7 @@ const https = require('https');
const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('../utils/axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
@@ -190,6 +191,24 @@ const runSingleRequest = async function (
// run request
const axiosInstance = makeAxiosInstance();
if (request.awsv4config) {
// todo: make this happen in prepare-request.js
// interpolate the aws v4 config
request.awsv4config.accessKeyId = interpolateString(request.awsv4config.accessKeyId, interpolationOptions);
request.awsv4config.secretAccessKey = interpolateString(
request.awsv4config.secretAccessKey,
interpolationOptions
);
request.awsv4config.sessionToken = interpolateString(request.awsv4config.sessionToken, interpolationOptions);
request.awsv4config.service = interpolateString(request.awsv4config.service, interpolationOptions);
request.awsv4config.region = interpolateString(request.awsv4config.region, interpolationOptions);
request.awsv4config.profileName = interpolateString(request.awsv4config.profileName, interpolationOptions);
request.awsv4config = await resolveAwsV4Credentials(request);
addAwsV4Interceptor(axiosInstance, request);
delete request.awsv4config;
}
/** @type {import('axios').AxiosResponse} */
response = await axiosInstance(request);

View File

@@ -1,5 +1,5 @@
{
"version": "v1.9.0",
"version": "v1.10.0",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",

View File

@@ -21,7 +21,8 @@ const contentSecurityPolicy = [
"script-src * 'unsafe-inline' 'unsafe-eval'",
"connect-src * 'unsafe-inline'",
"font-src 'self' https:",
"form-action 'none'",
// this has been commented out to make oauth2 work
// "form-action 'none'",
"img-src 'self' blob: data: https:",
"style-src 'self' 'unsafe-inline' https:"
];

View File

@@ -0,0 +1,61 @@
const { BrowserWindow } = require('electron');
const authorizeUserInWindow = ({ authorizeUrl, callbackUrl }) => {
return new Promise(async (resolve, reject) => {
let finalUrl = null;
const window = new BrowserWindow({
webPreferences: {
nodeIntegration: false
},
show: false
});
window.on('ready-to-show', window.show.bind(window));
function onWindowRedirect(url) {
// check if the url contains an authorization code
if (url.match(/(code=).*/)) {
finalUrl = url;
if (url && finalUrl.includes(callbackUrl)) {
window.close();
} else {
reject(new Error('Invalid Callback Url'));
}
}
}
window.on('close', () => {
if (finalUrl) {
try {
const callbackUrlWithCode = new URL(finalUrl);
const authorizationCode = callbackUrlWithCode.searchParams.get('code');
return resolve(authorizationCode);
} catch (error) {
return reject(error);
}
} else {
return reject(new Error('Authorization window closed'));
}
});
// 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) {
reject(error);
window.close();
}
});
};
module.exports = { authorizeUserInWindow };

View File

@@ -9,7 +9,7 @@ const Mustache = require('mustache');
const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
const { ipcMain } = require('electron');
const { isUndefined, isNull, each, get, compact } = require('lodash');
const { isUndefined, isNull, each, get, compact, cloneDeep } = require('lodash');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request');
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
@@ -29,6 +29,7 @@ const { addDigestInterceptor } = require('./digestauth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem');
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
const { resolveOAuth2AuthorizationCodecessToken } = require('./oauth2-authorization-code-helper');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
@@ -189,6 +190,16 @@ const configureRequest = async (
const axiosInstance = makeAxiosInstance();
if (request.oauth2) {
if (request?.oauth2?.grantType == 'authorization_code') {
let requestCopy = cloneDeep(request);
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
const { data, url } = await resolveOAuth2AuthorizationCodecessToken(requestCopy);
request.data = data;
request.url = url;
}
}
if (request.awsv4config) {
request.awsv4config = await resolveAwsV4Credentials(request);
addAwsV4Interceptor(axiosInstance, request);
@@ -484,7 +495,6 @@ const registerNetworkIpc = (mainWindow) => {
setCookieHeaders = Array.isArray(response.headers['set-cookie'])
? response.headers['set-cookie']
: [response.headers['set-cookie']];
for (let setCookieHeader of setCookieHeaders) {
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
addCookieToJar(setCookieHeader, request.url);
@@ -495,6 +505,7 @@ const registerNetworkIpc = (mainWindow) => {
// send domain cookies to renderer
const domainsWithCookies = await getDomainsWithCookies();
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
await runPostResponse(

View File

@@ -98,17 +98,53 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
}
// todo: we have things happening in two places w.r.t basic auth
// need to refactor this in the future
// need to refactor this in the future
// the request.auth (basic auth) object gets set inside the prepare-request.js file
if (request.auth) {
const username = _interpolate(request.auth.username) || '';
const password = _interpolate(request.auth.password) || '';
// use auth header based approach and delete the request.auth object
request.headers['authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
delete request.auth;
}
if (request?.oauth2?.grantType) {
switch (request.oauth2.grantType) {
case 'password':
let username = _interpolate(request.oauth2.username) || '';
let password = _interpolate(request.oauth2.password) || '';
request.oauth2.username = username;
request.oauth2.password = password;
request.data = {
grant_type: 'password',
username,
password
};
break;
case 'authorization_code':
request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || '';
request.oauth2.authorizationUrl = _interpolate(request.oauth2.authorizationUrl) || '';
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
break;
case 'client_credentials':
let clientId = _interpolate(request.oauth2.clientId) || '';
let clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.clientId = clientId;
request.oauth2.clientSecret = clientSecret;
request.data = {
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret
};
break;
default:
break;
}
}
// interpolate vars for aws sigv4 auth
if (request.awsv4config) {
request.awsv4config.accessKeyId = _interpolate(request.awsv4config.accessKeyId) || '';

View File

@@ -0,0 +1,41 @@
const { get, cloneDeep } = require('lodash');
const { authorizeUserInWindow } = require('./authorize-user-in-window');
const resolveOAuth2AuthorizationCodecessToken = async (request) => {
let requestCopy = cloneDeep(request);
const authorization_code = await getOAuth2AuthorizationCode(requestCopy);
const oAuth = get(requestCopy, 'oauth2', {});
const { clientId, clientSecret, callbackUrl, scope } = oAuth;
const data = {
grant_type: 'authorization_code',
code: authorization_code,
redirect_uri: callbackUrl,
client_id: clientId,
client_secret: clientSecret,
scope: scope
};
const url = requestCopy?.oauth2?.accessTokenUrl;
return {
data,
url
};
};
const getOAuth2AuthorizationCode = (request) => {
return new Promise(async (resolve, reject) => {
const { oauth2 } = request;
const { callbackUrl, clientId, authorizationUrl, scope } = oauth2;
const authorizationUrlWithQueryParams = `${authorizationUrl}?client_id=${clientId}&redirect_uri=${callbackUrl}&response_type=code&scope=${scope}`;
try {
const code = await authorizeUserInWindow({ authorizeUrl: authorizationUrlWithQueryParams, callbackUrl });
resolve(code);
} catch (err) {
reject(err);
}
});
};
module.exports = {
resolveOAuth2AuthorizationCodecessToken,
getOAuth2AuthorizationCode
};

View File

@@ -29,13 +29,20 @@ const parseFormData = (datas, collectionPath) => {
return form;
};
// Authentication
// A request can override the collection auth with another auth
// But it cannot override the collection auth with no auth
// We will provide support for disabling the auth via scripting in the future
/**
* 27 Feb 2024:
* ['inherit', 'none'].includes(request.auth.mode)
* We are mainitaining the old behavior where 'none' used to inherit the collection auth.
*
* Very soon, 'none' will be treated as no auth and 'inherit' will be the only way to inherit collection auth.
* We will request users to update their collection files to use 'inherit' instead of 'none'.
* Don't want to break ongoing CI pipelines.
*
* Hoping to remove this by 1 April 2024.
*/
const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const collectionAuth = get(collectionRoot, 'request.auth');
if (collectionAuth) {
if (collectionAuth && ['inherit', 'none'].includes(request.auth.mode)) {
switch (collectionAuth.mode) {
case 'awsv4':
axiosRequest.awsv4config = {
@@ -62,6 +69,36 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
password: get(collectionAuth, 'digest.password')
};
break;
case 'oauth2':
const grantType = get(collectionAuth, 'auth.oauth2.grantType');
switch (grantType) {
case 'password':
axiosRequest.oauth2 = {
grantType: grantType,
username: get(collectionAuth, 'auth.oauth2.username'),
password: get(collectionAuth, 'auth.oauth2.password')
};
break;
case 'authorization_code':
axiosRequest.oauth2 = {
grantType: grantType,
callbackUrl: get(collectionAuth, 'auth.oauth2.callbackUrl'),
authorizationUrl: get(collectionAuth, 'auth.oauth2.authorizationUrl'),
accessTokenUrl: get(collectionAuth, 'auth.oauth2.accessTokenUrl'),
clientId: get(collectionAuth, 'auth.oauth2.clientId'),
clientSecret: get(collectionAuth, 'auth.oauth2.clientSecret'),
scope: get(collectionAuth, 'auth.oauth2.scope')
};
break;
case 'client_credentials':
axiosRequest.oauth2 = {
grantType: grantType,
clientId: get(collectionAuth, 'auth.oauth2.clientId'),
clientSecret: get(collectionAuth, 'auth.oauth2.clientSecret')
};
break;
}
break;
}
}
@@ -91,6 +128,37 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
username: get(request, 'auth.digest.username'),
password: get(request, 'auth.digest.password')
};
break;
case 'oauth2':
const grantType = get(request, 'auth.oauth2.grantType');
switch (grantType) {
case 'password':
axiosRequest.oauth2 = {
grantType: grantType,
username: get(request, 'auth.oauth2.username'),
password: get(request, 'auth.oauth2.password')
};
break;
case 'authorization_code':
axiosRequest.oauth2 = {
grantType: grantType,
callbackUrl: get(request, 'auth.oauth2.callbackUrl'),
authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'),
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope')
};
break;
case 'client_credentials':
axiosRequest.oauth2 = {
grantType: grantType,
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret')
};
break;
}
break;
}
}

View File

@@ -28,7 +28,7 @@ class EnvironmentSecretsStore {
}
isValidValue(val) {
return val && typeof val === 'string' && val.length > 0;
return typeof val === 'string' && val.length >= 0;
}
storeEnvSecrets(collectionPathname, environment) {

View File

@@ -48,7 +48,7 @@ function safeStorageDecrypt(str) {
}
function encryptString(str) {
if (!str || typeof str !== 'string' || str.length === 0) {
if (typeof str !== 'string') {
throw new Error('Encrypt failed: invalid string');
}

View File

@@ -14,7 +14,6 @@ describe('Encryption and Decryption Tests', () => {
it('encrypt should throw an error for invalid string', () => {
expect(() => encryptString(null)).toThrow('Encrypt failed: invalid string');
expect(() => encryptString('')).toThrow('Encrypt failed: invalid string');
});
it('decrypt should throw an error for invalid string', () => {

View File

@@ -16,6 +16,7 @@
"dependencies": {
"@usebruno/query": "0.1.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"atob": "^2.1.2",
"axios": "^1.5.1",
"btoa": "^1.2.1",
@@ -28,7 +29,7 @@
"moment": "^2.29.4",
"nanoid": "3.3.4",
"node-fetch": "2.*",
"uuid": "^9.0.0",
"node-vault": "^0.10.2"
"node-vault": "^0.10.2",
"uuid": "^9.0.0"
}
}

View File

@@ -16,6 +16,7 @@ const { cleanJson } = require('../utils');
// Inbuilt Library Support
const ajv = require('ajv');
const addFormats = require('ajv-formats');
const atob = require('atob');
const btoa = require('btoa');
const lodash = require('lodash');
@@ -102,6 +103,7 @@ class ScriptRuntime {
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
atob,
btoa,
lodash,
@@ -194,6 +196,7 @@ class ScriptRuntime {
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
atob,
btoa,
lodash,

View File

@@ -19,6 +19,7 @@ const { cleanJson } = require('../utils');
// Inbuilt Library Support
const ajv = require('ajv');
const addFormats = require('ajv-formats');
const atob = require('atob');
const btoa = require('btoa');
const lodash = require('lodash');
@@ -120,6 +121,7 @@ class TestRuntime {
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
btoa,
atob,
lodash,

View File

@@ -1,5 +1,6 @@
const { describe, it, expect } = require('@jest/globals');
const TestRuntime = require('../src/runtime/test-runtime');
const ScriptRuntime = require('../src/runtime/script-runtime');
describe('runtime', () => {
describe('test-runtime', () => {
@@ -49,5 +50,129 @@ describe('runtime', () => {
{ description: 'async test', status: 'pass' }
]);
});
it('should have ajv and ajv-formats dependencies available', async () => {
const testFile = `
const Ajv = require('ajv');
const addFormats = require("ajv-formats");
const ajv = new Ajv();
addFormats(ajv);
const schema = {
type: 'string',
format: 'date-time'
};
const validate = ajv.compile(schema)
test('format valid', () => {
const valid = validate(new Date().toISOString())
expect(valid).to.be.true;
})
`;
const runtime = new TestRuntime();
const result = await runtime.runTests(
testFile,
{ ...baseRequest },
{ ...baseResponse },
{},
{},
'.',
null,
process.env
);
expect(result.results.map((el) => ({ description: el.description, status: el.status }))).toEqual([
{ description: 'format valid', status: 'pass' }
]);
});
});
describe('script-runtime', () => {
describe('run-request-script', () => {
const baseRequest = {
method: 'GET',
url: 'http://localhost:3000/',
headers: {},
data: undefined
};
it('should have ajv and ajv-formats dependencies available', async () => {
const script = `
const Ajv = require('ajv');
const addFormats = require("ajv-formats");
const ajv = new Ajv();
addFormats(ajv);
const schema = {
type: 'string',
format: 'date-time'
};
const validate = ajv.compile(schema)
bru.setVar('validation', validate(new Date().toISOString()))
`;
const runtime = new ScriptRuntime();
const result = await runtime.runRequestScript(script, { ...baseRequest }, {}, {}, '.', null, process.env);
expect(result.collectionVariables.validation).toBeTruthy();
});
});
describe('run-response-script', () => {
const baseRequest = {
method: 'GET',
url: 'http://localhost:3000/',
headers: {},
data: undefined
};
const baseResponse = {
status: 200,
statusText: 'OK',
data: [
{
id: 1
},
{
id: 2
},
{
id: 3
}
]
};
it('should have ajv and ajv-formats dependencies available', async () => {
const script = `
const Ajv = require('ajv');
const addFormats = require("ajv-formats");
const ajv = new Ajv();
addFormats(ajv);
const schema = {
type: 'string',
format: 'date-time'
};
const validate = ajv.compile(schema)
bru.setVar('validation', validate(new Date().toISOString()))
`;
const runtime = new ScriptRuntime();
const result = await runtime.runResponseScript(
script,
{ ...baseRequest },
{ ...baseResponse },
{},
{},
'.',
null,
process.env
);
expect(result.collectionVariables.validation).toBeTruthy();
});
});
});
});

View File

@@ -9,7 +9,7 @@ const safeParseJson = (json) => {
const indentString = (str) => {
if (!str || !str.length) {
return str;
return str || '';
}
return str
@@ -20,7 +20,7 @@ const indentString = (str) => {
const outdentString = (str) => {
if (!str || !str.length) {
return str;
return str || '';
}
return str

View File

@@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils');
*/
const grammar = ohm.grammar(`Bru {
BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)*
auths = authawsv4 | authbasic | authbearer | authdigest
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
bodyforms = bodyformurlencoded | bodymultipart
@@ -80,6 +80,7 @@ const grammar = ohm.grammar(`Bru {
authbasic = "auth:basic" dictionary
authbearer = "auth:bearer" dictionary
authdigest = "auth:digest" dictionary
authOAuth2 = "auth:oauth2" dictionary
body = "body" st* "{" nl* textblock tagend
bodyjson = "body:json" st* "{" nl* textblock tagend
@@ -380,6 +381,46 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
authOAuth2(_1, dictionary) {
const auth = mapPairListToKeyValPairs(dictionary.ast, false);
const grantTypeKey = _.find(auth, { name: 'grant_type' });
const usernameKey = _.find(auth, { name: 'username' });
const passwordKey = _.find(auth, { name: 'password' });
const callbackUrlKey = _.find(auth, { name: 'callback_url' });
const authorizationUrlKey = _.find(auth, { name: 'authorization_url' });
const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' });
const clientIdKey = _.find(auth, { name: 'client_id' });
const clientSecretKey = _.find(auth, { name: 'client_secret' });
const scopeKey = _.find(auth, { name: 'scope' });
return {
auth: {
oauth2:
grantTypeKey?.value && grantTypeKey?.value == 'password'
? {
grantType: grantTypeKey ? grantTypeKey.value : '',
username: usernameKey ? usernameKey.value : '',
password: passwordKey ? passwordKey.value : ''
}
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
? {
grantType: grantTypeKey ? grantTypeKey.value : '',
callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',
authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
clientId: clientIdKey ? clientIdKey.value : '',
clientSecret: clientSecretKey ? clientSecretKey.value : '',
scope: scopeKey ? scopeKey.value : ''
}
: grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
? {
grantType: grantTypeKey ? grantTypeKey.value : '',
clientId: clientIdKey ? clientIdKey.value : '',
clientSecret: clientSecretKey ? clientSecretKey.value : ''
}
: {}
}
};
},
bodyformurlencoded(_1, dictionary) {
return {
body: {

View File

@@ -89,12 +89,12 @@ const jsonToBru = (json) => {
if (auth && auth.awsv4) {
bru += `auth:awsv4 {
${indentString(`accessKeyId: ${auth.awsv4.accessKeyId}`)}
${indentString(`secretAccessKey: ${auth.awsv4.secretAccessKey}`)}
${indentString(`sessionToken: ${auth.awsv4.sessionToken}`)}
${indentString(`service: ${auth.awsv4.service}`)}
${indentString(`region: ${auth.awsv4.region}`)}
${indentString(`profileName: ${auth.awsv4.profileName}`)}
${indentString(`accessKeyId: ${auth?.awsv4?.accessKeyId || ''}`)}
${indentString(`secretAccessKey: ${auth?.awsv4?.secretAccessKey || ''}`)}
${indentString(`sessionToken: ${auth?.awsv4?.sessionToken || ''}`)}
${indentString(`service: ${auth?.awsv4?.service || ''}`)}
${indentString(`region: ${auth?.awsv4?.region || ''}`)}
${indentString(`profileName: ${auth?.awsv4?.profileName || ''}`)}
}
`;
@@ -102,8 +102,8 @@ ${indentString(`profileName: ${auth.awsv4.profileName}`)}
if (auth && auth.basic) {
bru += `auth:basic {
${indentString(`username: ${auth.basic.username}`)}
${indentString(`password: ${auth.basic.password}`)}
${indentString(`username: ${auth?.basic?.username || ''}`)}
${indentString(`password: ${auth?.basic?.password || ''}`)}
}
`;
@@ -111,7 +111,7 @@ ${indentString(`password: ${auth.basic.password}`)}
if (auth && auth.bearer) {
bru += `auth:bearer {
${indentString(`token: ${auth.bearer.token}`)}
${indentString(`token: ${auth?.bearer?.token || ''}`)}
}
`;
@@ -119,13 +119,49 @@ ${indentString(`token: ${auth.bearer.token}`)}
if (auth && auth.digest) {
bru += `auth:digest {
${indentString(`username: ${auth.digest.username}`)}
${indentString(`password: ${auth.digest.password}`)}
${indentString(`username: ${auth?.digest?.username || ''}`)}
${indentString(`password: ${auth?.digest?.password || ''}`)}
}
`;
}
if (auth && auth.oauth2) {
switch (auth?.oauth2?.grantType) {
case 'password':
bru += `auth:oauth2 {
${indentString(`grant_type: password`)}
${indentString(`username: ${auth?.oauth2?.username || ''}`)}
${indentString(`password: ${auth?.oauth2?.password || ''}`)}
}
`;
break;
case 'authorization_code':
bru += `auth:oauth2 {
${indentString(`grant_type: authorization_code`)}
${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}
${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
}
`;
break;
case 'client_credentials':
bru += `auth:oauth2 {
${indentString(`grant_type: client_credentials`)}
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
}
`;
break;
}
}
if (body && body.json && body.json.length) {
bru += `body:json {
${indentString(body.json)}
@@ -332,3 +368,5 @@ ${indentString(docs)}
};
module.exports = jsonToBru;
// alternative to writing the below code to avoif undefined

View File

@@ -45,6 +45,16 @@ auth:digest {
password: secret
}
auth:oauth2 {
grant_type: authorization_code
callback_url: http://localhost:8080/api/auth/oauth2/ac/callback
authorization_url: http://localhost:8080/api/auth/oauth2/ac/authorize
access_token_url: http://localhost:8080/api/auth/oauth2/ac/token
client_id: client_id_1
client_secret: client_secret_1
scope: read write
}
body:json {
{
"hello": "world"

View File

@@ -63,6 +63,15 @@
"digest": {
"username": "john",
"password": "secret"
},
"oauth2": {
"grantType": "authorization_code",
"clientId": "client_id_1",
"clientSecret": "client_secret_1",
"authorizationUrl": "http://localhost:8080/api/auth/oauth2/ac/authorize",
"callbackUrl": "http://localhost:8080/api/auth/oauth2/ac/callback",
"accessTokenUrl": "http://localhost:8080/api/auth/oauth2/ac/token",
"scope": "read write"
}
},
"body": {

View File

@@ -119,12 +119,63 @@ const authDigestSchema = Yup.object({
.noUnknown(true)
.strict();
const oauth2Schema = Yup.object({
grantType: Yup.string()
.oneOf(['client_credentials', 'password', 'authorization_code'])
.required('grantType is required'),
username: Yup.string().when('grantType', {
is: (val) => ['client_credentials', 'password'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
password: Yup.string().when('grantType', {
is: (val) => ['client_credentials', 'password'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
callbackUrl: Yup.string().when('grantType', {
is: (val) => ['authorization_code'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
authorizationUrl: Yup.string().when('grantType', {
is: (val) => ['authorization_code'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
accessTokenUrl: Yup.string().when('grantType', {
is: (val) => ['authorization_code'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
clientId: Yup.string().when('grantType', {
is: (val) => ['authorization_code', 'client_credentials'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
clientSecret: Yup.string().when('grantType', {
is: (val) => ['authorization_code', 'client_credentials'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
scope: Yup.string().when('grantType', {
is: (val) => ['authorization_code'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
})
})
.noUnknown(true)
.strict();
const authSchema = Yup.object({
mode: Yup.string().oneOf(['none', 'awsv4', 'basic', 'bearer', 'digest']).required('mode is required'),
mode: Yup.string()
.oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'oauth2'])
.required('mode is required'),
awsv4: authAwsV4Schema.nullable(),
basic: authBasicSchema.nullable(),
bearer: authBearerSchema.nullable(),
digest: authDigestSchema.nullable()
digest: authDigestSchema.nullable(),
oauth2: oauth2Schema.nullable()
})
.noUnknown(true)
.strict();

View File

@@ -1,24 +0,0 @@
meta {
name: Bearer Auth 401
type: http
seq: 2
}
get {
url: {{host}}/api/auth/bearer/protected
body: none
auth: none
}
auth:bearer {
token: {{bearerAuthToken}}zz
}
assert {
res.status: 401
res.body.message: Unauthorized
}
script:post-response {
bru.setEnvVar("foo", "bar");
}

View File

@@ -1,18 +1,18 @@
meta {
name: Bearer Auth 401
name: inherit Bearer Auth 200
type: http
seq: 2
}
get {
url: {{host}}/api/auth/bearer/protected
body: json
auth: none
body: none
auth: inherit
}
assert {
res.status: 401
res.body.message: Unauthorized
res.status: 200
res.body.message: Authentication successful
}
script:post-response {

View File

@@ -15,7 +15,7 @@
"bypassProxy": ""
},
"scripts": {
"moduleWhitelist": ["crypto"],
"moduleWhitelist": ["crypto", "buffer"],
"filesystemAccess": {
"allow": true
}

View File

@@ -3,7 +3,7 @@ headers {
}
auth {
mode: none
mode: bearer
}
auth:basic {
@@ -12,7 +12,7 @@ auth:basic {
}
auth:bearer {
token: {{bearerAuthToken}}
token: {{bearer_auth_token}}
}
docs {

View File

@@ -1,8 +1,28 @@
vars {
host: http://localhost:80
host: http://localhost:8080
bearer_auth_token: your_secret_token
basic_auth_password: della
env.var1: envVar1
env-var2: envVar2
bark: {{process.env.PROC_ENV_VAR}}
client_id: client_id_1
client_secret: client_secret_1
auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize
callback_url: http://localhost:8080/api/auth/oauth2/ac/callback
access_token_url: http://localhost:8080/api/auth/oauth2/ac/token
ropc_username: foo
ropc_password: bar
github_authorize_url: https://github.com/login/oauth/authorize
github_access_token_url: https://github.com/login/oauth/access_token
google_auth_url: https://accounts.google.com/o/oauth2/auth
google_access_token_url: https://accounts.google.com/o/oauth2/token
google_scope: https://www.googleapis.com/auth/userinfo.email
}
vars:secret [
github_client_secret,
github_client_id,
google_client_id,
google_client_secret,
github_authorization_code,
ropc_access_token,
cc_access_token,
ac_access_token,
github_access_token
]

View File

@@ -0,0 +1,24 @@
meta {
name: spacex
type: graphql
seq: 1
}
post {
url: https://spacex-production.up.railway.app/
body: graphql
auth: none
}
body:graphql {
{
company {
ceo
}
}
}
assert {
res.status: eq 200
}

View File

@@ -8,7 +8,9 @@
"name": "@usebruno/test-collection",
"version": "0.0.1",
"dependencies": {
"@faker-js/faker": "^8.4.0"
"@faker-js/faker": "^8.4.0",
"jsonwebtoken": "^9.0.2",
"lru-map-cache": "^0.1.0"
}
},
"node_modules/@faker-js/faker": {
@@ -25,6 +27,153 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
"npm": ">=6.14.13"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/lru-map-cache": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/lru-map-cache/-/lru-map-cache-0.1.0.tgz",
"integrity": "sha512-r1lasvJbg3lrTS37W5h4Ugy9miaWluYqviZGbfH9A6AbjxSDJCtPNqtGr5MRl/RG/EfYrwe07DC4zQEBnY2q4w=="
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
}

View File

@@ -2,6 +2,8 @@
"name": "@usebruno/test-collection",
"version": "0.0.1",
"dependencies": {
"@faker-js/faker": "^8.4.0"
"@faker-js/faker": "^8.4.0",
"jsonwebtoken": "^9.0.2",
"lru-map-cache": "^0.1.0"
}
}

View File

@@ -0,0 +1 @@
PROC_ENV_VAR=woof

View File

@@ -0,0 +1 @@
!.env

View File

@@ -0,0 +1 @@
v18

View File

@@ -0,0 +1,25 @@
meta {
name: github token with authorize
type: http
seq: 1
}
post {
url: github.com
body: none
auth: oauth2
}
auth:oauth2 {
grant_type: authorization_code
callback_url: {{callback_url}}
authorization_url: {{github_authorize_url}}
access_token_url: {{github_access_token_url}}
client_id: {{github_client_id}}
client_secret: {{github_client_secret}}
scope: repo,gist
}
script:post-response {
bru.setEnvVar('github_access_token',res.body.split('access_token=')[1]?.split('&scope')[0]);
}

View File

@@ -0,0 +1,25 @@
meta {
name: google token with authorize
type: http
seq: 4
}
post {
url:
body: none
auth: oauth2
}
auth:oauth2 {
grant_type: authorization_code
callback_url: {{callback_url}}
authorization_url: {{google_auth_url}}
access_token_url: {{google_access_token_url}}
client_id: {{google_client_id}}
client_secret: {{google_client_secret}}
scope: {{google_scope}}
}
script:post-response {
bru.setEnvVar('ac_access_token', res.body.access_token);
}

View File

@@ -0,0 +1,27 @@
meta {
name: resource
type: http
seq: 3
}
post {
url: {{host}}/api/auth/oauth2/ac/resource?token={{ac_access_token}}
body: json
auth: none
}
query {
token: {{ac_access_token}}
}
auth:bearer {
token:
}
body:json {
{
"code": "eb30dbf783b65bec4539ee1dcb068606",
"client_id": "{{client_id}}",
"client_secret": "{{client_secret}}"
}
}

View File

@@ -0,0 +1,25 @@
meta {
name: token with authorize
type: http
seq: 4
}
post {
url:
body: none
auth: oauth2
}
auth:oauth2 {
grant_type: authorization_code
callback_url: {{callback_url}}
authorization_url: {{auth_url}}
access_token_url: {{access_token_url}}
client_id: {{client_id}}
client_secret: {{client_secret}}
scope:
}
script:post-response {
bru.setEnvVar('ac_access_token', res.body.access_token);
}

View File

@@ -0,0 +1,15 @@
meta {
name: resource
type: http
seq: 2
}
get {
url: {{host}}/api/auth/oauth2/cc/resource?token={{cc_access_token}}
body: none
auth: none
}
query {
token: {{cc_access_token}}
}

View File

@@ -0,0 +1,21 @@
meta {
name: token
type: http
seq: 1
}
post {
url: {{host}}/api/auth/oauth2/cc/token
body: none
auth: oauth2
}
auth:oauth2 {
grant_type: client_credentials
client_id: {{client_id}}
client_secret: {{client_secret}}
}
script:post-response {
bru.setEnvVar('cc_access_token', res.body.access_token);
}

View File

@@ -0,0 +1,15 @@
meta {
name: resource
type: http
seq: 2
}
post {
url: {{host}}/api/auth/oauth2/ropc/resource
body: none
auth: bearer
}
auth:bearer {
token: {{ropc_access_token}}
}

View File

@@ -0,0 +1,21 @@
meta {
name: token
type: http
seq: 1
}
post {
url: {{host}}/api/auth/oauth2/ropc/token
body: none
auth: oauth2
}
auth:oauth2 {
grant_type: password
username: {{ropc_username}}
password: {{ropc_password}}
}
script:post-response {
bru.setEnvVar('ropc_access_token', res.body.access_token);
}

View File

@@ -0,0 +1,31 @@
{
"version": "1",
"name": "bruno-testbench",
"type": "collection",
"proxy": {
"enabled": false,
"protocol": "http",
"hostname": "{{proxyHostname}}",
"port": 4000,
"auth": {
"enabled": false,
"username": "anoop",
"password": "password"
},
"bypassProxy": ""
},
"scripts": {
"moduleWhitelist": ["crypto"],
"filesystemAccess": {
"allow": true
}
},
"clientCertificates": {
"enabled": true,
"certs": []
},
"presets": {
"requestType": "http",
"requestUrl": "http://localhost:6000"
}
}

View File

@@ -0,0 +1,22 @@
headers {
check: again
}
auth {
mode: none
}
auth:basic {
username: bruno
password: {{basicAuthPassword}}
}
auth:bearer {
token: {{bearerAuthToken}}
}
docs {
# bruno-testbench 🐶
This is a test collection that I am using to test various functionalities around bruno
}

View File

@@ -0,0 +1,26 @@
vars {
host: http://localhost:8080
bearer_auth_token: your_secret_token
basic_auth_password: della
client_id: client_id_1
client_secret: client_secret_1
auth_url: http://localhost:8080/api/auth/oauth2/ac/authorize
callback_url: http://localhost:8080/api/auth/oauth2/ac/callback
access_token_url: http://localhost:8080/api/auth/oauth2/ac/token
ropc_username: foo
ropc_password: bar
github_authorize_url: https://github.com/login/oauth/authorize
github_access_token_url: https://github.com/login/oauth/access_token
google_auth_url: https://accounts.google.com/o/oauth2/auth
google_access_token_url: https://accounts.google.com/o/oauth2/token
google_scope: https://www.googleapis.com/auth/userinfo.email
}
vars:secret [
github_client_secret,
github_client_id,
google_client_id,
google_client_secret,
github_authorization_code,
github_access_token,
ac_access_token
]

View File

@@ -0,0 +1,8 @@
vars {
host: https://testbench-sanity.usebruno.com
bearer_auth_token: your_secret_token
basic_auth_password: della
env.var1: envVar1
env-var2: envVar2
bark: {{process.env.PROC_ENV_VAR}}
}

View File

@@ -0,0 +1,3 @@
{
"hello": "bruno"
}

Some files were not shown because too many files have changed in this diff Show More