Compare commits

..

157 Commits

Author SHA1 Message Date
ramki-bruno
5af8034c74 Added Playwright-codegen setup 2025-04-01 17:58:35 +05:30
Anoop M D
bd25097e44 Rename SECURITY.md to security.md 2025-03-31 14:03:06 +05:30
ganesh-bruno
3f140e818f replace example.com to usebruno website 2025-03-31 14:01:13 +05:30
lohxt1
dbba22131c fix: proxy and certs not being used for oauth2 calls 2025-03-27 22:56:00 +05:30
lohit
8908828af0 Merge pull request #4353 from naman-bruno/bugfix/system-proxy-agent
Fix: system proxy agent
2025-03-27 19:31:21 +05:30
lohit
20c9e1d406 removed the domain protocol check 2025-03-27 19:28:00 +05:30
naman-bruno
741576526d Fix: logic and design 2025-03-27 18:24:45 +05:30
naman-bruno
b928ec112e add: protocol verification for certificate domain 2025-03-27 16:36:42 +05:30
naman-bruno
559026b65c Fix: right Agent for system proxy 2025-03-27 16:36:22 +05:30
naman-bruno
eb6fef63b3 Removed log 2025-03-26 20:30:39 +05:30
naman-bruno
1b767f8d26 fix: params 2025-03-26 20:30:39 +05:30
naman-bruno
3b061cda89 Fix: agentOptions not passing properly to baseAgent 2025-03-26 20:30:39 +05:30
ganesh
e0d7bb50f3 2025-03-26 16:27:33 +05:30
ramki-bruno
20e8e9167f Perf improvements in Response-preview with useMemo 2025-03-25 23:07:47 +05:30
lohit
268ede869d handle oauth2 timeline response, remove dataBuffer from debugInfo obj (#4330) 2025-03-25 22:28:29 +05:30
ramki-bruno
8b8ddaf31b Revert "Fix: Prettify JSON for Res-preview without parsing to avoid JS specific roundings"
This reverts commit 56581b3641.
2025-03-25 21:58:18 +05:30
lohit
f16fbeade7 Merge pull request #4328 from lohxt1/main
revert serialization of debugInfo data
2025-03-25 19:52:35 +05:30
lohxt1
d27677030d revert serialization of debugInfo data 2025-03-25 19:50:06 +05:30
pooja-bruno
578e912faf fix: codemirror lint errors (#4321)
* fix: codemirror lint errors
* chore: added comments for await lint fix and removed the indent config prop that was not needed

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2025-03-25 19:44:22 +05:30
lohit
bd8c6caa39 Merge pull request #4325 from lohxt1/main
oauth2 fixes
2025-03-25 18:33:10 +05:30
lohxt1
7635230c88 oauth2 fixes 2025-03-25 18:29:44 +05:30
lohit
eb0d746082 Merge pull request #4324 from naman-bruno/bugfix/redirect-proxy
Fix: Redirect not working on proxy
2025-03-25 18:05:17 +05:30
naman-bruno
61b3853390 overwrite maxredirect on request obj 2025-03-25 18:01:40 +05:30
naman-bruno
76485cdd56 reseted max redirect counts 2025-03-25 17:56:31 +05:30
lohit
3d5401a8db Merge pull request #4323 from lohxt1/timeline_fixes
oauth2 fixes
2025-03-25 17:53:00 +05:30
lohxt1
6faecc2874 setting the default value for auto_refresh_token to false 2025-03-25 17:46:06 +05:30
lohit
4650ca40c1 Merge pull request #4322 from lohxt1/timeline_fixes
oauth2 fixes and improvements
2025-03-25 17:19:27 +05:30
lohxt1
14ee26557a query stringify request payload data before the oauth2 token url request 2025-03-25 17:09:36 +05:30
lohxt1
8e873013a9 oauth2 fixes 2025-03-25 15:42:58 +05:30
Pooja Belaramani
b0caf46406 fix: request run crash 2025-03-25 15:31:28 +05:30
lohit
a0926c4064 Merge pull request #4309 from lohxt1/timeline_fixes
oauth2 related fixes
2025-03-25 12:22:30 +05:30
lohxt1
b795b1c5ce fixes 2025-03-25 12:03:38 +05:30
lohxt1
61ba5f5c39 check if token is expired only if expires_in prop is present, clear response and clear timeline are 2 different things, clear redux state after clearing oauth2 credentials cache 2025-03-24 22:49:21 +05:30
lohxt1
f177287fb6 fix folder settings auth selector comp prop 2025-03-24 19:43:56 +05:30
anusree-bruno
ef929e78f3 Create SECURITY.md (#4266) 2025-03-24 17:33:51 +05:30
lohxt1
274a55e257 folder level oauth2 save fn fix 2025-03-24 15:46:31 +05:30
Ryan
9e24223085 Delete .github/repository-metadata.yml 2025-03-21 19:47:23 +05:30
rreyn-bruno
3cd3d8bdcc Remove sponsorship content from repository 2025-03-21 19:47:23 +05:30
lohit
eed3f2ff4c Merge pull request #4300 from lohxt1/oauth2_fixes
timeline ui fixes, oauth2 validation fixes
2025-03-21 15:58:32 +05:30
lohxt1
b09b4b1d17 timeline ui fixes, oauth2 validation fixes 2025-03-21 15:56:02 +05:30
lohit
cd9c667e8a Merge pull request #4297 from lohxt1/oauth2_fixes
oauth2 fixes, ui validations, timeline ui updates
2025-03-21 00:44:54 +05:30
lohxt1
2675e79dbd oauth2 fixes, ui validations, timeline updates (wip) 2025-03-21 00:41:07 +05:30
lohit
ef94f55c82 Merge pull request #4296 from lohxt1/version_upgrade_2.0.0
chore: version upgrade to 2.0.0
2025-03-20 20:33:43 +05:30
lohxt1
926919524b chore: version upgrade to 2.0.0 2025-03-20 20:31:53 +05:30
lohit
087f691544 Merge pull request #3867 from usebruno/feat/oauth2-improvements
OAuth2 Revamp
2025-03-20 20:22:28 +05:30
lohxt1
3a81ebf0e2 oauth2 fixes 2025-03-20 20:11:12 +05:30
lohxt1
3ffaaab8f3 Merge remote-tracking branch 'upstream/main' into feat/oauth2-improvements 2025-03-20 19:38:54 +05:30
lohxt1
5a98da2031 oauth2 fixes 2025-03-20 19:27:14 +05:30
lohxt1
2e4014863f fix the validations for oauth2 json_to_bru 2025-03-20 17:06:47 +05:30
Anoop M D
ccd4a14da6 feat: refactored about menu + added static path updates for win build 2025-03-18 21:49:35 +05:30
lohxt1
98bd997665 chore: fix font not loading issue, fix about menu item, fix padding for preferences modal 2025-03-18 21:49:35 +05:30
Anoop M D
a7cf24278e style: update font-family for improved typography consistency 2025-03-18 21:18:10 +05:30
Anoop M D
039c157f33 feat: improved item info ux 2025-03-18 19:51:36 +05:30
Anoop M D
1009d42f92 chore: fix caret color 2025-03-18 00:50:59 +05:30
Anoop M D
1be0e8d31c feat: custom filename can be shown by clicking advanced options 2025-03-18 00:50:59 +05:30
Ed Brannin
ab9befd773 UI: Change the default request auth mode from "none" to "inherit"
Fix #2315
2025-03-17 17:02:51 +05:30
lohit
7506f83800 Merge pull request #4250 from lohxt1/browse_files_fn_fix
fix incorrect vars in browse_files function
2025-03-17 16:58:22 +05:30
lohxt1
74d9b0aafe fix browse_files function 2025-03-17 16:55:56 +05:30
lohxt1
d3fcb42a8f timeline ui updates wip 2025-03-17 14:09:36 +05:30
Anoop M D
3808089e60 feat: added helptips for file and folder custom names 2025-03-17 02:54:36 +05:30
lohit
cd2f5d5233 filename support ui updates, regex update for generating validation error for the last char, getEncoding function headers props optional chaining validation (#4243) 2025-03-17 01:36:39 +05:30
lohxt1
51be153527 fix bruno-electron unit tests 2025-03-16 14:28:21 +05:30
lohxt1
5728b7c8a8 fix bruno-lang unit tests 2025-03-16 14:24:04 +05:30
lohxt1
71b6907c31 fix bruno-cli unit tests 2025-03-16 14:11:19 +05:30
lohxt1
eead96ca26 Merge remote-tracking branch 'upstream/main' into feat/oauth2-improvements 2025-03-16 14:02:12 +05:30
lohxt1
f99e8770f0 ~ reverting the bruno-electron ipc-network files refactoring work to keep the diff minimal 2025-03-16 13:37:59 +05:30
lohit
7ae33d05c9 Merge pull request #4241 from usebruno/feat/file-location-ux-improvements
Feat/file location ux improvements
2025-03-15 14:15:49 +05:30
Anoop M D
df196f5d25 feat: add Help tooltip component and help info for collection location 2025-03-14 23:16:00 +05:30
Anoop M D
1f8a10d1df feat: improved ux for filepath display 2025-03-14 23:11:33 +05:30
Pieter Oliver
ccb951dadd [DX]: Document running test suite for all workspaces (#4095) 2025-03-14 21:19:33 +05:30
lohit
9bde3c44f7 filename support for requests and folders (#4111) 2025-03-14 20:07:33 +05:30
Adam Armistead
5ac52a531f feat: Natural sort collection names with numbers
Sorts collections by name in alphabetical order
Collections with numbers in the names are sorted in numerical order.

Results in `['Test 10', 'Test 2', 'Test 1']`
being sorted to: `['Test 1', 'Test 2', 'Test 10']`
instead of: `['Test 1', 'Test 10', 'Test 2']`

Accurately sorts numbers with decimals as well.
2025-03-14 03:31:58 +05:30
therealrinku
51e60d5083 fix: update delay cli example 2025-03-14 03:14:51 +05:30
therealrinku
01b982a0e7 fix: add missing example description for delay 2025-03-14 03:14:51 +05:30
therealrinku
d0b16841c9 fix: check for invalid delay properly 2025-03-14 03:14:51 +05:30
therealrinku
59d7141f70 feat: add runner delay for bruno cli 2025-03-14 03:14:51 +05:30
Hans Knöchel
11c14530eb chore: rephrase placeholder for clarity 2025-03-14 03:07:51 +05:30
naman-bruno
0fb926648b Fix: empty url export 2025-03-14 02:48:22 +05:30
Joshua Weber
d37cf28e10 Ignores caching for AWS credentials provider (#4000) 2025-03-13 15:32:39 +05:30
russssl
f8a14e35fa update ukrainian readme
Update readme_ua.md
2025-03-13 05:24:11 +05:30
Anoop M D
eefb0f836b fix: revert pr #4225 that broke tests 2025-03-13 03:22:09 +05:30
naman-bruno
989e553648 Fixed issue where Global Environment dropdown was not hiding after click (#3828) 2025-03-13 02:45:31 +05:30
Anoop M D
cd1d4f09d2 fix: fixed failing tests by downgrading bruno-js in electron and cli 2025-03-13 02:43:15 +05:30
Nathan Baulch
122e0a1d02 fix: typos 2025-03-13 02:22:10 +05:30
Anoop M D
7e16304426 chore: updated bruno lib versions 2025-03-13 02:16:44 +05:30
dependabot[bot]
0888219e95 chore(deps-dev): bump ts-jest in the jest-dependencies group
Bumps the jest-dependencies group with 1 update: [ts-jest](https://github.com/kulshekhar/ts-jest).


Updates `ts-jest` from 29.2.5 to 29.2.6
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.2.5...v29.2.6)

---
updated-dependencies:
- dependency-name: ts-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: jest-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 01:43:13 +05:30
dependabot[bot]
d89fd455ff chore(deps): bump fast-xml-parser from 4.5.1 to 5.0.8
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 4.5.1 to 5.0.8.
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v4.5.1...v5.0.8)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 01:42:38 +05:30
Pragadesh-45
df4a682f97 chore: add res.getStatusText() to code editor autocomplete suggestions 2025-03-13 01:37:10 +05:30
Pragadesh-45
2385c4d5c1 feat: add statusText support to BrunoResponse on safe mode
Enhance BrunoResponse with statusText functionality:
- Added `getStatusText()` method to BrunoResponse class
- Updated QuickJS shim to support statusText
2025-03-13 01:37:10 +05:30
dependabot[bot]
243398bcd0 chore(deps): bump dorny/test-reporter from 1 to 2
Bumps [dorny/test-reporter](https://github.com/dorny/test-reporter) from 1 to 2.
- [Release notes](https://github.com/dorny/test-reporter/releases)
- [Changelog](https://github.com/dorny/test-reporter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dorny/test-reporter/compare/v1...v2)

---
updated-dependencies:
- dependency-name: dorny/test-reporter
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 01:34:59 +05:30
dependabot[bot]
2a2f2dfa15 chore(deps): bump actions/setup-node from 3 to 4
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 01:34:29 +05:30
Jair Henrique
d57634e6ea ci: configure dependabot 2025-03-13 01:32:23 +05:30
Anoop M D
1be0b97895 chore: deps update - axios, jsonpath-plus, @aws-sdk/credential-providers, express 2025-03-13 01:27:37 +05:30
Pragadesh-45
6a85635c49 Fix: Inconsistent JSON parsing and formatting in res.body and Res-preview (#4103)
* Fix: Revert selective JSON parsing where string response is not parsed

- Revert "Merge pull request #3706 from Pragadesh-45/fix/response-format-updates"
  - e897dc1eb0
- Revert "Merge pull request #3676 from pooja-bruno/fix/string-json-response"
  - 1f2bee1f90

* Fix: Revert interpreting Assert RHS-value wrapped in quotes literally

- Revert "Merge pull request #3806 from Pragadesh-45/fix/handle-assert-results"
  - 63d3cb380d
- Revert "Merge pull request #3805 from Pragadesh-45/fix/handle-assert-results"
  - 6abd063749

* Fix: Inconsistent JSON formatting in preview when encoded value is a string

* Fix: Prettify JSON for Res-preview without parsing to avoid JS specific roundings

* Fix(testbench): req.body is always Buffer after the binary req body related changes

* Added `/api/echo/custom` where response can be configured using request itself

* Added tests for validating Assert and Response-preview

Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>

* Handle char-encoding in Response-preview and added more tests

* Updated API endpoint in tests to use httpfaker api

* QuickJS (Safe Mode) exec logic to handle template literals similar to Developer Mode

* Safe Mode bru.runRequest to return statusText similar to Developer Mode

---------

Co-authored-by: ramki-bruno <ramki@usebruno.com>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2025-03-13 00:49:57 +05:30
pooja-bruno
0fbbe8a996 feat: show response errors while keeping response preview intact (#4082)
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2025-03-10 19:51:48 +05:30
naman-bruno
4ff4e3b732 Added new postman v2 schema urls 2025-03-10 17:08:29 +05:30
Anoop M D
7c65317b07 chore: improved cookie styling ux 2025-03-08 23:09:46 +05:30
ramki-bruno
b57c996564 Added appropriate auto-focus on inputs for add-new/add/updating cookie 2025-03-08 23:09:46 +05:30
ramki-bruno
5de75892a2 Fix: Show validation errors in raw-edit mode in manage-cookie UI 2025-03-08 23:09:46 +05:30
ramki-bruno
d5e828aef2 Refactoring and styling fixes in Manage-Cookie UI 2025-03-08 23:09:46 +05:30
sanish-bruno
51c86bc0e9 Added UI to manage cookies 2025-03-08 23:09:46 +05:30
Anoop M D
253cb8b315 chore: updated share collection color theme 2025-03-08 20:11:24 +05:30
lohxt1
0876ad0dab Revert "revert changes from another pr"
This reverts commit 94dfaf45cd.
2025-03-08 17:05:31 +05:30
lohxt1
a1c133b303 revert changes from another pr 2025-03-08 17:05:31 +05:30
lohxt1
38cf206075 revert import colleciton modal ui changes 2025-03-08 17:05:31 +05:30
lohxt1
9d598db55e share collection and import collection ui updates
~ added share collection option in the collection overview tab
2025-03-08 17:05:31 +05:30
naman-bruno
8cda05c431 updated timeline to show body in oauth (#4168) 2025-03-06 17:04:07 +05:30
naman-bruno
7af7ff92bf Fix: redirect to relative path (#4167) 2025-03-06 14:26:02 +05:30
naman-bruno
3169e6cdf4 Oauth2 folder (#4105) 2025-03-06 11:03:34 +05:30
therealrinku
233c57e625 feat: auto select body tab if it exists when params isnt active 2025-03-04 15:27:33 +05:30
Jens
b399576dab Update readme.md
Updated link to Roadmap.
2025-03-04 01:46:15 +05:30
tlaloc911
655eec09c1 fix windows build (#4043) 2025-02-26 15:31:22 +05:30
Tim Nikischin
51eda3f08c Implement correct Runner title (#3854)
Implement correct Runner title fixes: #3763
Use title instead of filepath in runner.
2025-02-26 12:57:33 +05:30
sanish-bruno
a438c06b97 fix: remove duplicate search components 2025-02-26 12:50:52 +05:30
Ryan
4977dbeb11 Update BugReport.yaml
Added context around cause of bug, better placeholders for OS versions
2025-02-26 12:12:00 +05:30
tlaloc911
7cacc255b4 fix h1 and h2 style 2025-02-20 21:39:49 +05:30
Sanjai Kumar
b28b60d4a7 chore: update BugReport template for clarity and additional information 2025-02-18 21:59:23 +05:30
Sanjai Kumar
2fc45de430 chore: update BugReport template to include version and OS information 2025-02-18 21:59:23 +05:30
Ryan
c58604716e Update FeatureRequest.yaml (#3974)
updated submittal questions
2025-02-18 21:56:41 +05:30
lohit
31409c6206 feat: reuse worker threads for bru file parsing (#4054) 2025-02-18 19:58:37 +05:30
pooja-bruno
4e88cbf318 feat: add refresh url for oauth2 (#4028) 2025-02-18 17:24:56 +05:30
Anoop M D
dfb0b1b966 fix: allow popus in notification iframes
This is to allow is to allow users to open pages (like bruno downloads) from the app
2025-02-14 21:25:28 +05:30
Anoop M D
f8b4a0b85b feat: notification visibility rules based on semver 2025-02-14 18:16:34 +05:30
naman-bruno
200732bac5 fix: space on collection docs 2025-02-14 17:54:20 +05:30
ramki-bruno
c997924c42 Strengthen CSP 2025-02-14 16:01:25 +05:30
ramki-bruno
711eb24c01 Refactoring InfoTip to support just text and children instead of HTML 2025-02-14 16:00:59 +05:30
ramki-bruno
3b8a613914 Refactor ToolHint to use text prop as text instead of HTML
Currently there is no usage of ToolHint where we are passing HTML
content, so this should not break anything.
2025-02-14 16:00:59 +05:30
naman-bruno
81930f6fe6 Fix: Import collection failed for postman custom method 2025-02-14 02:43:31 +05:30
sreelakshmi-bruno
e0750148e6 Styling improvements in failed-load-request summary (#3956)
Co-authored-by: Sreelakshmi Jayarajan <sreelakshmi@Sreelakshmis-MacBook-Air.local>
2025-02-14 02:42:14 +05:30
Anoop M D
528f822294 feat: notifications are displayed using iframe 2025-02-14 02:37:07 +05:30
lohit
31c11830a6 fix: should be able to save the request after reverting the changes (#3999) 2025-02-11 20:38:04 +05:30
lohxt1
eff3b29bfb fix: should be able to save request after reverting all the changes 2025-02-11 20:27:05 +05:30
lohxt1
4656958f2f fix: should be able to save request after reverting all the changes 2025-02-11 20:27:05 +05:30
ramki-bruno
aa4575b0ea Fix: CLI-Tests Workflow lacks contents:read permission 2025-02-11 18:08:01 +05:30
Sanjai Kumar
14d798eea7 Revert "fix: improve handling of Buffer responses and set default charset to utf-8"
This reverts commit d8a2e6f405.
2025-02-11 18:06:30 +05:30
Sanjai Kumar
8af285d41e Revert "refactor: enhance JSON parsing logic for Buffer responses"
This reverts commit 14e9227e07.
2025-02-11 18:06:30 +05:30
naman-bruno
6a0eb1ccde fix: nonReplaceableTabTypes getting duplicate 2025-02-11 15:35:52 +05:30
naman-bruno
4a613ed1b7 Fix: duplicate collection tabs 2025-02-11 15:35:52 +05:30
sanjai0py
7566d658d4 Fix: Prevent interpolation of buffer values in JSON requests 2025-02-11 15:32:44 +05:30
lohit
413b121ce1 Merge pull request #3989 from lohxt1/feat/oauth2__improvements
oauth2 improvements
2025-02-11 12:33:24 +05:30
lohit
90dff3d1e1 Merge branch 'feat/oauth2-improvements' into feat/oauth2__improvements 2025-02-11 12:32:04 +05:30
lohxt1
3fc0b0a668 oauth2 improvements - collection import default type 2025-02-11 12:27:58 +05:30
ramki-bruno
70ee819bae Fix: Postman-import-translation not adding comments for unsupported APIs
There was an error in one of the WIP feature which is breaking the
translation, for now commenting it out.
2025-02-10 21:47:43 +05:30
lohxt1
b5e53ec25c include oauth2 request data along with headers in the access token url call 2025-02-10 20:20:40 +05:30
lohxt1
01a62d66cc oauth2 postman import fix and include client certs and proxy config while fetching access token 2025-02-05 19:06:23 +05:30
lohxt1
f668e93f52 oauth2 postman import fix and include client certs and proxy config while fetching access token 2025-02-05 16:06:41 +05:30
lohit
c5eeb190d3 oauth2 updates (#3876)
~ changed tokenPrefix to tokenHeaderPrefix
~ updated the logic for token timer component
2025-01-24 19:39:29 +05:30
lohit
1d1e701ccb oauth2 workflow improvements (#3874)
~ basic auth credentials should be assigned to `request.basicAuth` instead `request.auth` object
~ added credentials_placement option, fixed headers issue client credentials flow
~ cache input field values when grant type select box value changes
~ updated logic for - cache input field values when grant type select box value changes
~ updated token expiry timer component logic
2025-01-24 18:44:02 +05:30
lohit
f38c7ae03a oauth2 ui/ux improvements (#3868) 2025-01-23 22:06:50 +05:30
Anoop M D
8f754142c7 Merge branch 'pr-2077' into feat/oauth2-improvements 2025-01-23 16:45:51 +05:30
Mateusz Pietryga
3bd8f09c88 feat: OAuth2 - Supported at the collection level (#1704) 2024-09-23 21:59:16 +02:00
Mateusz Pietryga
dd9cb21f8c feat: OAuth2 - UI for OAuth2 Credentials independent of the Request Output pane
fix: typo - rename OAuth2PasswordCredentials component
fix: typo - Use the same name for AuthMode - OAuth 2.0 in collection and request level
2024-09-23 21:59:16 +02:00
Mateusz Pietryga
2064cc88ab feat: OAuth2 - automatically handle Bearer token type only
According to RFC6749 Section 7.1, The client MUST NOT use an access token
if it does not understand the token type.
At this point bruno only understands 'bearer' token_type.
2024-09-23 21:59:16 +02:00
Mateusz Pietryga
d982e35a17 feat: OAuth2 - Do not make axios request when executing collection level Get Access Token action
The actual the authorization request is now part of request preparation, and its response is returned for post-request script processing.
2024-09-23 21:59:16 +02:00
Mateusz Pietryga
4afcd44216 feat: OAuth2 - Include resolved authorization details in req object to be usable by scripts
The new variable 'credentials' is now available in 'req' object. It is added automatically during request preparation if oauth2 method is used and is value is either evaluated or retrieved from collection oauth2 cache.
2024-09-23 21:59:16 +02:00
Mateusz Pietryga
63252d3ee2 feat: OAuth2 - Store authorization information
Results of oauth2 authorization flow (i.e. access_token but also refresh_token, id_token, scope or any other information returned from token request) are stored in a collection specific cache. It is persisted in the file system, and will be automatically reused when executing requests until the cache is purged (using Clear Cache button available in all related views).
2024-09-23 20:50:41 +02:00
Mateusz Pietryga
22a9502976 fix: OAuth2 - auth is successful but token endpoint is returned instead of api endpoint (#1999)
Setting oauth2 authorization no longer equals overwriting user-specified data in a request. The pre-requests made to obtain oauth2 access_token are now separated from actual API request.
2024-09-23 20:50:37 +02:00
278 changed files with 14153 additions and 2780 deletions

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
github: helloanoop

View File

@@ -6,26 +6,58 @@ body:
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Before submitting, please make sure you've searched existing issues:
👉 [Search existing issues](https://github.com/usebruno/bruno/issues?q=is%3Aissue)
- type: checkboxes
attributes:
label: 'I have checked the following:'
options:
- label: I use the newest version of bruno.
required: true
- label: I've searched existing issues and found nothing related to my issue.
- label: "I have searched existing issues and found nothing related to my issue."
required: true
- type: checkboxes
attributes:
label: 'This bug is:'
options:
- label: making Bruno unusable for me
required: false
- label: slowing me down but I'm able to continue working
required: false
- label: annoying
required: false
- type: input
attributes:
label: Bruno version
description: Please specify the version of Bruno you are using in which the issue occurs.
placeholder: 1.38.1
validations:
required: true
- type: input
attributes:
label: Operating System
description: Information about the operating system the issue occurs on.
placeholder: Windows 11 26100.3037 / macOS 15.1 (24B83) / Linux 6.13.1
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of the bug.
description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce.
validations:
required: true
- type: textarea
attributes:
label: .bru file to reproduce the bug
description: Attach your .bru file here that can reqroduce the problem.
description: Attach your .bru file here that can reproduce the problem.
validations:
required: false
- type: textarea
attributes:
label: Screenshots/Live demo link

View File

@@ -8,13 +8,23 @@ body:
options:
- label: I've searched existing issues and found nothing related to my issue.
required: true
- type: checkboxes
attributes:
label: 'This feature'
options:
- label: blocks me from using Bruno
required: false
- label: would improve my quality of life in Bruno
required: false
- label: is something I've never seen an API client do before
required: false
- type: markdown
attributes:
value: |
Suggest an idea for this project.
- type: textarea
attributes:
label: Describe the feature you want to add
label: Describe the feature you want to add, and how it would change your usage of Bruno
description: A clear and concise description of the feature you want to be added.
validations:
required: true
@@ -23,4 +33,4 @@ body:
label: Mockups or Images of the feature
description: Add some images to support your feature.
validations:
required: true
required: false

31
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
groups:
bruno-dependencies:
patterns:
- "*usebruno*"
babel-dependencies:
patterns:
- "*babel*"
fortawesome-dependencies:
patterns:
- "*fortawesome*"
electron-dependencies:
patterns:
- "*electron*"
rollup-dependencies:
patterns:
- "*rollup*"
jest-dependencies:
patterns:
- "*jest*"

View File

@@ -26,7 +26,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -43,7 +43,7 @@ jobs:
bru run --env Prod --output junit.xml --format junit
- name: Publish Test Report
uses: dorny/test-reporter@v1
uses: dorny/test-reporter@v2
if: success() || failure()
with:
name: Test Report

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -86,11 +86,11 @@ find . -type f -name "package-lock.json" -delete
### Testing
```bash
# bruno-schema
# run bruno-schema tests
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# run tests over all workspaces
npm test --workspaces --if-present
```
### Raising Pull Requests

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Testing (পরীক্ষা)
```bash
# bruno-schema
# ব্রুনো-স্কিমা পরীক্ষা চালান
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# সমস্ত কর্মক্ষেত্রে পরীক্ষা চালান
npm test --workspaces --if-present
```
### Raising Pull Request (পুল অনুরোধ উত্থাপন)

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### 测试
```bash
# bruno-schema
# 运行 bruno-schema 测试
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# 在所有工作区上运行测试
npm test --workspaces --if-present
```
### 提交 Pull Request

View File

@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Testen
```bash
# bruno-schema
# Führen Sie Bruno-Schema-Tests aus
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# Führen Sie Tests für alle Arbeitsbereiche durch
npm test --workspaces --if-present
```

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Pruebas
```bash
# bruno-schema
# ejecutar pruebas de esquema bruno
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# ejecutar pruebas en todos los espacios de trabajo
npm test --workspaces --if-present
```
### Crea un Pull Request

View File

@@ -73,11 +73,11 @@ find . -type f -name "package-lock.json" -delete
### Tests
```bash
# bruno-schema
# exécuter des tests de schéma bruno
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# exécuter des tests sur tous les espaces de travail
npm test --workspaces --if-present
```
### Ouvrir une Pull Request

View File

@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
### परिक्षण
```bash
# bruno-schema
# ब्रूनो-स्कीमा परीक्षण चलाएँ
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# सभी कार्यस्थानों पर परीक्षण चलाएँ
npm test --workspaces --if-present
```
### पुल अनुरोध प्रक्रिया

View File

@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Tests
```bash
# bruno-schema
# esegui i test dello schema bruno
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# esegui test su tutti gli spazi di lavoro
npm test --workspaces --if-present
```

View File

@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
### テストを動かすには
```bash
# bruno-schema
# ブルーノスキーマのテストを実行します
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# すべてのワークスペースでテストを実行します
npm test --workspaces --if-present
```
### プルリクエストの手順

View File

@@ -66,11 +66,11 @@ find . -type f -name "package-lock.json" -delete
### 테스팅
```bash
# bruno-schema
# bruno-schema 테스트 실행
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# 모든 작업 공간에서 테스트 실행
npm test --workspaces --if-present
```
### Pull Requests 요청

View File

@@ -65,11 +65,11 @@ find . -type f -name "package-lock.json" -delete
### Testen
```bash
# bruno-schema
# voer bruno-schema tests uit
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# voer tests uit over alle werkruimten
npm test --workspaces --if-present
```
### Pull Requests indienen

View File

@@ -71,11 +71,11 @@ find . -type f -name "package-lock.json" -delete
### Testowanie
```bash
# bruno-schema
# uruchom testy bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# uruchom testy we wszystkich przestrzeniach roboczych
npm test --workspaces --if-present
```
### Tworzenie Pull Request

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Testando
```bash
# bruno-schema
# executar testes do bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# executar testes em todos os ambientes de trabalho
npm test --workspaces --if-present
```
### Envio de Pull Request

View File

@@ -64,11 +64,11 @@ find . -type f -name "package-lock.json" -delete
### Testarea
```shell
# bruno-schema
# executați teste bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# executați teste peste toate spațiile de lucru
npm test --workspaces --if-present
```
### Crearea unui Pull Request

View File

@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Тестирование
```bash
# bruno-schema
# запустите тесты bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# запустите тесты во всех рабочих пространствах
npm test --workspaces --if-present
```

View File

@@ -67,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
### Testovanie
````bash
# bruno-schema
# spustiť bruno-schema testy
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# spustiť testy vo všetkých pracovných priestoroch
npm test --workspaces --if-present
```
### Vyrobenie Pull Request

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### Test
```bash
# bruno-schema
# bruno-schema testlerini çalıştır
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# tüm çalışma alanlarında testleri çalıştır
npm test --workspaces --if-present
```
### Pull Request Oluşturma

View File

@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
### Тестування
```bash
# bruno-schema
# запустити тести bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# запустити тести у всіх робочих просторах
npm test --workspaces --if-present
```

View File

@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
### 測試
```bash
# bruno-schema
# 執行布魯諾架構測試
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
# 對所有工作區執行測試
npm test --workspaces --if-present
```
### 發送 Pull Request

View File

@@ -29,13 +29,13 @@
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статусy кво, запровадженого інструментами на кшталт Postman.
Bruno зберігає ваші колекції напряму у теці на вашому диску. Він використовує текстову мову розмітки Bru для збереження інформації про ваші API запити.
Ви можете використовувати git або будь-яку іншу систему контролю версій щоб спільно працювати над вашими колекціями API запитів.
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Взнати більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Дізнатись більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
![bruno](/assets/images/landing-2.png) <br /><br />
@@ -69,13 +69,13 @@ Bruno є повністю автономним. Немає жодних план
### Поділитись відгуками 📣
Якщо Bruno допоміг вам у вашій роботі і вашим командам, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
Якщо Bruno допоміг у роботі вам або вашій команді, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
### Зробити свій внесок 👩‍💻🧑‍💻
Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](../contributing/contributing_ua.md)
Навіть якщо ви не можете зробити свій внесок пишучи програмний код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
Навіть якщо ви не можете зробити свій внесок пишучи код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
### Автори

3245
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
"rimraf": "^6.0.1",
"ts-jest": "^29.0.5"
"ts-jest": "^29.2.6"
},
"scripts": {
"setup": "node ./scripts/setup.js",
@@ -47,6 +47,7 @@
"build:electron:deb": "./scripts/build-electron.sh deb",
"build:electron:rpm": "./scripts/build-electron.sh rpm",
"build:electron:snap": "./scripts/build-electron.sh snap",
"test:codegen": "npm run dev:web & node ./scripts/playwright-codegen.js",
"test:e2e": "npx playwright test",
"test:report": "npx playwright show-report",
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",

View File

@@ -1,6 +1,6 @@
{
"name": "@usebruno/app",
"version": "0.3.0",
"version": "2.0.0",
"private": true,
"scripts": {
"dev": "rsbuild dev",
@@ -24,6 +24,7 @@
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
"cookie": "0.7.1",
"dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
@@ -35,17 +36,20 @@
"graphql-request": "^3.7.0",
"httpsnippet": "^3.0.9",
"i18next": "24.1.2",
"iconv-lite": "^0.6.3",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
"json5": "^2.2.3",
"jsonc-parser": "^3.2.1",
"jsonpath-plus": "10.2.0",
"jsonpath-plus": "^10.3.0",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
"moment": "^2.30.1",
"moment-timezone": "^0.5.47",
"mousetrap": "^1.6.5",
"nanoid": "3.3.8",
"path": "^0.12.7",
@@ -68,6 +72,7 @@
"react-redux": "^7.2.9",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"semver": "^7.7.1",
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"system": "^2.0.1",

View File

@@ -0,0 +1,62 @@
import React, { createContext, useContext, useState } from 'react';
import { IconChevronDown } from '@tabler/icons';
import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrapper';
const AccordionContext = createContext();
const Accordion = ({ children, defaultIndex }) => {
const [openIndex, setOpenIndex] = useState(defaultIndex);
const toggleItem = (index) => {
setOpenIndex(openIndex === index ? null : index);
};
return (
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
<div>{children}</div>
</AccordionContext.Provider>
);
};
const Item = ({ index, children, ...props }) => {
return (
<AccordionItem {...props}>
{React.Children.map(children, (child) => React.cloneElement(child, { index }))}
</AccordionItem>
);
};
export const Header = ({ index, children, ...props }) => {
const { openIndex, toggleItem } = useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<AccordionHeader onClick={() => toggleItem(index)} {...props} className={isOpen ? 'open' : ''}>
<div className="w-full">{children}</div>
<IconChevronDown
className="w-5 h-5 ml-auto"
style={{
transform: `rotate(${isOpen ? '180deg' : '0deg'})`,
transition: 'transform 0.3s ease-in-out'
}}
/>
</AccordionHeader>
);
};
const Content = ({ index, children, ...props }) => {
const { openIndex } = useContext(AccordionContext);
const isOpen = openIndex === index;
return (
<AccordionContent isOpen={isOpen} {...props}>
{children}
</AccordionContent>
);
};
Accordion.Item = Item;
Accordion.Header = Header;
Accordion.Content = Content;
export default Accordion;

View File

@@ -0,0 +1,28 @@
import styled from 'styled-components';
const AccordionItem = styled.div`
border: 1px solid ${(props) => props.theme.input.border};
border-radius: 4px;
overflow: hidden;
margin-bottom: 1rem;
`;
const AccordionHeader = styled.button`
width: 100%;
display: flex;
padding: 0.75rem 1rem;
background: transparent;
cursor: pointer;
font-weight: 500;
&.open, &:hover {
background-color: ${(props) => props.theme.plainGrid.hoverBg};
}
`;
const AccordionContent = styled.div`
padding: ${(props) => (props.isOpen ? '1rem' : '0')};
max-height: ${(props) => (props.isOpen ? 'auto' : '0')};
`;
export { AccordionItem, AccordionHeader, AccordionContent };

View File

@@ -31,6 +31,7 @@ if (!SERVER_RENDERED) {
'res.body',
'res.responseTime',
'res.getStatus()',
'res.getStatusText()',
'res.getHeader(name)',
'res.getHeaders()',
'res.getBody()',
@@ -83,7 +84,7 @@ if (!SERVER_RENDERED) {
'bru.runner',
'bru.runner.setNextRequest(requestName)',
'bru.runner.skipRequest()',
'bru.runner.stopExecution()',
'bru.runner.stopExecution()'
];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
const cursor = editor.getCursor();
@@ -174,11 +175,21 @@ export default class CodeEditor extends React.Component {
}
},
'Cmd-F': (cm) => {
if (this._isSearchOpen()) {
// replace the older search component with the new one
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
search && search.remove();
}
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Ctrl-F': (cm) => {
if (this._isSearchOpen()) {
// replace the older search component with the new one
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
search && search.remove();
}
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
@@ -365,6 +376,10 @@ export default class CodeEditor extends React.Component {
}
};
_isSearchOpen = () => {
return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
};
/**
* Bind handler to search input to count number of search results
*/

View File

@@ -95,7 +95,7 @@ const AuthMode = ({ collection }) => {
onModeChange('oauth2');
}}
>
Oauth2
OAuth 2.0
</div>
<div
className="dropdown-item"

View File

@@ -1,120 +0,0 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { clearOauth2Cache } from 'utils/network/index';
import toast from 'react-hot-toast';
const OAuth2AuthorizationCode = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const oAuth = get(collection, 'root.request.auth.oauth2', {});
const handleRun = async () => {
dispatch(sendCollectionOauth2Request(collection.uid));
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
const handleChange = (key, value) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'authorization_code',
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
state,
pkce,
[key]: value
}
})
);
};
const handlePKCEToggle = (e) => {
dispatch(
updateCollectionAuth({
mode: 'oauth2',
collectionUid: collection.uid,
content: {
grantType: 'authorization_code',
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
state,
pkce: !Boolean(oAuth?.['pkce'])
}
})
);
};
const handleClearCache = (e) => {
clearOauth2Cache(collection?.uid)
.then(() => {
toast.success('cleared cache successfully');
})
.catch((err) => {
toast.error(err.message);
});
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
isSecret={isSecret}
/>
</div>
</div>
);
})}
<div className="flex flex-row w-full gap-4" key="pkce">
<label className="block font-medium">Use PKCE</label>
<input
className="cursor-pointer"
type="checkbox"
checked={Boolean(oAuth?.['pkce'])}
onChange={handlePKCEToggle}
/>
</div>
<div className="flex flex-row gap-4">
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
</div>
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;

View File

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

View File

@@ -1,16 +0,0 @@
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

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

View File

@@ -1,21 +0,0 @@
const inputsConfig = [
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'clientId',
label: 'Client ID'
},
{
key: 'clientSecret',
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -1,54 +0,0 @@
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

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

View File

@@ -1,16 +0,0 @@
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

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

View File

@@ -1,29 +0,0 @@
const inputsConfig = [
{
key: 'accessTokenUrl',
label: 'Access Token URL'
},
{
key: 'username',
label: 'Username'
},
{
key: 'password',
label: 'Password'
},
{
key: 'clientId',
label: 'Client ID'
},
{
key: 'clientSecret',
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',
label: 'Scope'
}
];
export { inputsConfig };

View File

@@ -1,21 +1,33 @@
import React from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import GrantTypeSelector from './GrantTypeSelector/index';
import OAuth2PasswordCredentials from './PasswordCredentials/index';
import OAuth2AuthorizationCode from './AuthorizationCode/index';
import OAuth2ClientCredentials from './ClientCredentials/index';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { useDispatch } from 'react-redux';
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
const GrantTypeComponentMap = ({collection }) => {
const dispatch = useDispatch();
const save = () => {
dispatch(saveCollectionRoot(collection.uid));
};
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
const grantType = get(request, 'auth.oauth2.grantType', {});
const grantTypeComponentMap = (grantType, collection) => {
switch (grantType) {
case 'password':
return <OAuth2PasswordCredentials collection={collection} />;
return <OAuth2PasswordCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
break;
case 'authorization_code':
return <OAuth2AuthorizationCode collection={collection} />;
return <OAuth2AuthorizationCode save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
break;
case 'client_credentials':
return <OAuth2ClientCredentials collection={collection} />;
return <OAuth2ClientCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
break;
default:
return <div>TBD</div>;
@@ -24,12 +36,12 @@ const grantTypeComponentMap = (grantType, collection) => {
};
const OAuth2 = ({ collection }) => {
const oAuth = get(collection, 'root.request.auth.oauth2', {});
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
return (
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, collection)}
<GrantTypeSelector request={request} updateAuth={updateCollectionAuth} collection={collection} />
<GrantTypeComponentMap collection={collection} />
</StyledWrapper>
);
};

View File

@@ -8,9 +8,7 @@ import { useState } from 'react';
import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
import path from 'path';
import slash from 'utils/common/slash';
import { isWindowsOS } from 'utils/common/platform';
import path from 'utils/common/path';
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const certFilePathInputRef = useRef();
@@ -27,7 +25,10 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
passphrase: ''
},
validationSchema: Yup.object({
domain: Yup.string().required(),
domain: Yup.string()
.required()
.trim()
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
type: Yup.string().required().oneOf(['cert', 'pfx']),
certFilePath: Yup.string().when('type', {
is: (type) => type == 'cert',
@@ -47,7 +48,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
let relevantValues = {};
if (values.type === 'cert') {
relevantValues = {
domain: values.domain,
domain: values.domain?.trim(),
type: values.type,
certFilePath: values.certFilePath,
keyFilePath: values.keyFilePath,
@@ -55,7 +56,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
};
} else {
relevantValues = {
domain: values.domain,
domain: values.domain?.trim(),
type: values.type,
pfxFilePath: values.pfxFilePath,
passphrase: values.passphrase
@@ -70,12 +71,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const getFile = (e) => {
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
if (filePath) {
let relativePath;
if (isWindowsOS()) {
relativePath = slash(path.win32.relative(root, filePath));
} else {
relativePath = path.posix.relative(root, filePath);
}
let relativePath = path.relative(root, filePath);
formik.setFieldValue(e.name, relativePath);
}
};
@@ -109,23 +105,23 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<ul className="mt-4">
{!clientCertConfig.length
? 'No client certificates added'
: clientCertConfig.map((clientCert) => (
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>
: clientCertConfig.map((clientCert, index) => (
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
</li>
))}
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>
</div>
</li>
))}
</ul>
<h1 className="font-semibold mt-8 mb-2">Add Client Certificate</h1>
@@ -134,15 +130,20 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<label className="settings-label" htmlFor="domain">
Domain
</label>
<input
id="domain"
type="text"
name="domain"
placeholder="*.example.org"
className="block textbox non-passphrase-input"
onChange={formik.handleChange}
value={formik.values.domain || ''}
/>
<div className="relative flex items-center">
<div className="absolute left-0 pl-2 text-gray-400 pointer-events-none flex items-center h-full">
https://
</div>
<input
id="domain"
type="text"
name="domain"
placeholder="example.org"
className="block textbox non-passphrase-input !pl-[60px]"
onChange={formik.handleChange}
value={formik.values.domain || ''}
/>
</div>
{formik.touched.domain && formik.errors.domain ? (
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
) : null}
@@ -198,9 +199,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(slash(formik.values.certFilePath))}
title={path.basename(formik.values.certFilePath)}
>
{path.basename(slash(formik.values.certFilePath))}
{path.basename(formik.values.certFilePath)}
</div>
<IconTrash
size={18}
@@ -238,9 +239,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(slash(formik.values.keyFilePath))}
title={path.basename(formik.values.keyFilePath)}
>
{path.basename(slash(formik.values.keyFilePath))}
{path.basename(formik.values.keyFilePath)}
</div>
<IconTrash
size={18}
@@ -281,9 +282,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<div className="flex flex-row gap-2 items-center">
<div
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
title={path.basename(slash(formik.values.pfxFilePath))}
title={path.basename(formik.values.pfxFilePath)}
>
{path.basename(slash(formik.values.pfxFilePath))}
{path.basename(formik.values.pfxFilePath)}
</div>
<IconTrash
size={18}

View File

@@ -1,11 +1,7 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
.CodeMirror-scroll {
padding-bottom: 0px;
}
}
.editing-mode {
cursor: pointer;
}

View File

@@ -119,6 +119,6 @@ This documentation supports Markdown formatting! You can use:
- **Bold** and *italic* text
- \`code blocks\` and syntax highlighting
- Tables and lists
- [Links](https://example.com)
- [Links](https://usebruno.com)
- And more!
`;

View File

@@ -1,10 +1,21 @@
import React from 'react';
import React from "react";
import { getTotalRequestCountInCollection } from 'utils/collections/';
import { IconFolder, IconFileOff, IconWorld, IconApi } from '@tabler/icons';
import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
import { areItemsLoading, getItemsLoadStats } from "utils/collections/index";
import { useState } from "react";
import ShareCollection from "components/ShareCollection/index";
const Info = ({ collection }) => {
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
const isCollectionLoading = areItemsLoading(collection);
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
const handleToggleShowShareCollectionModal = (value) => (e) => {
toggleShowShareCollectionModal(value);
}
return (
<div className="w-full flex flex-col h-fit">
<div className="rounded-lg py-6">
@@ -42,15 +53,30 @@ const Info = ({ collection }) => {
</div>
<div className="ml-4">
<div className="font-semibold text-sm">Requests</div>
<div className="mt-1 text-sm text-muted">
{totalRequestsInCollection} request{totalRequestsInCollection !== 1 ? 's' : ''} in collection
<div className="mt-1 text-sm text-muted font-mono">
{
isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
}
</div>
</div>
</div>
<div className="flex items-start group cursor-pointer" onClick={handleToggleShowShareCollectionModal(true)}>
<div className="flex-shrink-0 p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
<IconShare className="w-5 h-5 text-indigo-500" stroke={1.5} />
</div>
<div className="ml-4 h-full flex flex-col justify-start">
<div className="font-semibold text-sm h-fit my-auto">Share</div>
<div className="mt-1 text-sm group-hover:underline text-link">
Share Collection
</div>
</div>
</div>
{showShareCollectionModal && <ShareCollection collection={collection} onClose={handleToggleShowShareCollectionModal(false)} />}
</div>
</div>
</div>
);
};
export default Info;
export default Info;

View File

@@ -2,8 +2,15 @@ import React from 'react';
import { flattenItems } from "utils/collections";
import { IconAlertTriangle } from '@tabler/icons';
import StyledWrapper from "./StyledWrapper";
import { useDispatch, useSelector } from 'react-redux';
import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
import { getDefaultRequestPaneTab } from 'utils/collections/index';
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
import { hideHomePage } from 'providers/ReduxStore/slices/app';
const RequestsNotLoaded = ({ collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const flattenedItems = flattenItems(collection.items);
const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
@@ -11,6 +18,29 @@ const RequestsNotLoaded = ({ collection }) => {
return null;
}
const handleRequestClick = (item) => e => {
e.preventDefault();
if (isItemARequest(item)) {
dispatch(hideHomePage());
if (itemIsOpenedInTabs(item, tabs)) {
dispatch(
focusTab({
uid: item.uid
})
);
return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item)
})
);
return;
}
}
return (
<StyledWrapper className="w-full card my-2">
<div className="flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20">
@@ -31,7 +61,7 @@ const RequestsNotLoaded = ({ collection }) => {
<tbody>
{flattenedItems?.map((item, index) => (
item?.partial && !item?.loading ? (
<tr key={index}>
<tr key={index} className='cursor-pointer' onClick={handleRequestClick(item)}>
<td className="py-1.5 px-3">
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
</td>

View File

@@ -104,18 +104,15 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Config
<InfoTip
text={`
<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"}}>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>
</ul>
</div>
`}
infotipId="request-var"
/>
</InfoTip>
</label>
<div className="flex items-center">
<label className="flex items-center">

View File

@@ -89,7 +89,7 @@ const VarsTable = ({ collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Expr</span>
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
<InfoTip content="You can write any valid JS Template Literal here" infotipId="request-var" />
</div>
</td>
)}

View File

@@ -0,0 +1,5 @@
import styled from 'styled-components';
const StyledWrapper = styled.div``;
export default StyledWrapper;

View File

@@ -0,0 +1,371 @@
import React, { useState, useRef, useEffect } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import Modal from 'components/Modal/index';
import { modifyCookie, addCookie, getParsedCookie, createCookieString } from 'providers/ReduxStore/slices/app';
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import ToggleSwitch from 'components/ToggleSwitch/index';
import { IconInfoCircle } from '@tabler/icons';
import moment from 'moment';
import 'moment-timezone';
import { Tooltip } from 'react-tooltip';
import { isEmpty } from 'lodash';
const removeEmptyValues = (obj) => {
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined));
};
const ModifyCookieModal = ({ onClose, domain, cookie }) => {
const dispatch = useDispatch();
const [isRawMode, setIsRawMode] = useState(false);
const [cookieString, setCookieString] = useState('');
const initialParseRef = useRef(false);
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...(cookie ? cookie : {}),
key: cookie?.key || '',
value: cookie?.value || '',
path: cookie?.path || '/',
domain: cookie?.domain || domain || '',
expires: cookie?.expires ? moment(cookie.expires).format(moment.HTML5_FMT.DATETIME_LOCAL) : '',
secure: cookie?.secure || false,
httpOnly: cookie?.httpOnly || false
},
validationSchema: Yup.object({
key: Yup.string().required('Key is required'),
value: Yup.string().required('Value is required'),
domain: Yup.string().required('Domain is required'),
secure: Yup.boolean(),
httpOnly: Yup.boolean(),
expires: Yup.mixed()
.nullable()
.transform((value) => {
if (!value || value === '') return null;
return moment(value).isValid() ? moment(value).toDate() : null;
})
.test('future-date', 'Expiration date must be in the future', (value) => {
if (!value) return true;
return moment(value).isAfter(moment());
})
}),
onSubmit: (values) => {
const modValues = removeEmptyValues({
...(cookie ? cookie : {}),
...values,
expires: values.expires
? moment(values.expires).isValid()
? moment(values.expires).toDate()
: Infinity
: Infinity
});
handleCookieDispatch(cookie, domain, modValues, onClose);
}
});
const title = cookie ? 'Modify Cookie' : 'Add Cookie';
const handleCookieDispatch = (cookie, domain, modValues, onClose) => {
if (cookie) {
dispatch(modifyCookie(domain, cookie, modValues))
.then(() => {
toast.success('Cookie modified successfully');
onClose();
})
.catch((err) => {
toast.error('An error occurred while modifying cookie');
console.error(err);
});
} else {
dispatch(addCookie(domain, modValues))
.then(() => {
toast.success('Cookie added successfully');
onClose();
})
.catch((err) => {
toast.error('An error occurred while adding cookie');
console.error(err);
});
}
};
const onSubmit = async () => {
try {
if (isRawMode) {
const cookieObj = await dispatch(getParsedCookie(cookieString));
const modifiedCookie = removeEmptyValues({
...formik.values,
...cookieObj,
expires: cookieObj?.expires
? moment(cookieObj.expires).isValid()
? moment(cookieObj.expires).toDate()
: Infinity
: Infinity
});
if (!cookieObj) {
toast.error('Please enter a valid cookie string');
return;
}
const validationErrors = await formik.setValues(
(values) => ({
...values,
...modifiedCookie,
expires:
modifiedCookie?.expires && moment(modifiedCookie.expires).isValid()
? moment(new Date(modifiedCookie.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
: ''
}),
true
);
if (!isEmpty(validationErrors)) {
toast.error(Object.values(validationErrors).join("\n"));
return;
}
handleCookieDispatch(cookie, domain, modifiedCookie, onClose);
} else {
formik.handleSubmit();
}
} catch (error) {
const errMsg = error.message || 'An error occurred while parsing cookie string';
toast.error(errMsg);
}
};
useEffect(() => {
if (!isRawMode) return;
const loadCookieString = async () => {
if (cookie) {
const str = await dispatch(createCookieString(cookie));
setCookieString(str);
}
return '';
};
loadCookieString();
}, [cookie, isRawMode]);
// create the cookieString when raw mode is enabled
useEffect(() => {
if (isRawMode) {
const createCookieStr = async () => {
const str = await dispatch(createCookieString(formik.values));
setCookieString(str);
};
createCookieStr();
}
}, [isRawMode, formik.values]);
useEffect(() => {
// Reset the ref when raw mode changes
if (isRawMode) {
initialParseRef.current = false;
return;
}
const setParsedCookie = async () => {
if (!isRawMode && cookieString && !initialParseRef.current) {
initialParseRef.current = true;
try {
const cookieObj = await dispatch(getParsedCookie(cookieString));
if (!cookieObj) return;
formik.setValues(
(values) => ({
...values,
...removeEmptyValues(cookieObj),
expires:
cookieObj?.expires && moment(cookieObj.expires).isValid()
? moment(new Date(cookieObj.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
: ''
}),
true
);
} catch (error) {
const errMsg = error.message || 'An error occurred while parsing cookie string';
toast.error(errMsg);
}
}
};
setParsedCookie();
}, [isRawMode, cookieString, dispatch, formik]);
return (
<Modal
size="lg"
title={title}
onClose={onClose}
handleCancel={onClose}
handleConfirm={onSubmit}
customHeader={
<div className="flex items-center justify-between w-full">
<h2 className="text-sm font-bold">{title}</h2>
<div className="ml-auto flex items-center ">
<ToggleSwitch
className="mr-2"
isOn={isRawMode}
size="2xs"
handleToggle={(e) => {
setIsRawMode(e.target.checked);
}}
/>
<label className="text-sm font-normal mr-4 normal-case">Edit Raw</label>
</div>
</div>
}
>
<form onSubmit={(e) => e.preventDefault()} className="px-2">
{isRawMode ? (
<div>
<div className="flex items-center gap-2 mb-1">
<label className="block text-sm">Set-Cookie String</label>
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="text-gray-400" />
<Tooltip
anchorId="cookie-raw-info"
className="tooltip-mod"
html="Key, Path, and Domain are immutable properties and cannot be modified for existing cookies"
/>
</div>
<textarea
value={cookieString}
onChange={(e) => setCookieString(e.target.value)}
className="block textbox w-full h-24"
placeholder="key=value; key2=value2"
/>
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm mb-1">
Domain<span className="text-red-600">*</span>{' '}
</label>
<input
type="text"
name="domain"
// Auto-focus if its add-new i.e. when domain prop is empty
autoFocus={!domain && !formik.values.domain}
value={formik.values.domain}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full disabled:opacity-50"
disabled={!!cookie}
/>
{formik.touched.domain && formik.errors.domain && (
<div className="text-red-500 text-sm mt-1">{formik.errors.domain}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">Path</label>
<input
type="text"
name="path"
value={formik.values.path}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full disabled:opacity-50"
disabled={!!cookie}
/>
{formik.touched.path && formik.errors.path && (
<div className="text-red-500 text-sm mt-1">{formik.errors.path}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">
Key<span className="text-red-600">*</span>{' '}
</label>
<input
type="text"
name="key"
// Auto focus when add-for-domain i.e. if domain is already prefilled
autoFocus={!!domain && !formik.values.key}
value={formik.values.key}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full disabled:opacity-50"
disabled={!!cookie}
/>
{formik.touched.key && formik.errors.key && (
<div className="text-red-500 text-sm mt-1">{formik.errors.key}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">
Value<span className="text-red-600">*</span>{' '}
</label>
<input
type="text"
name="value"
// Auto-focus when its in edit mode i.e. cookie prop is present
autoFocus={!!cookie}
value={formik.values.value}
onChange={formik.handleChange}
className="block textbox non-passphrase-input w-full"
/>
{formik.touched.value && formik.errors.value && (
<div className="text-red-500 text-sm mt-1">{formik.errors.value}</div>
)}
</div>
</div>
{/* Date Picker */}
<div className="w-full flex items-end">
<div>
<label className="block text-sm mb-1">Expiration ({moment.tz.guess()})</label>
<input
type="datetime-local"
name="expires"
value={formik.values.expires}
onChange={(e) => {
formik.handleChange(e);
}}
className="block textbox non-passphrase-input w-full"
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
/>
{formik.touched.expires && formik.errors.expires && (
<div className="text-red-500 text-sm mt-1">{formik.errors.expires}</div>
)}
</div>
{/* Checkboxes */}
<div className="flex space-x-4 ml-auto">
<label className="flex items-center">
<input
type="checkbox"
name="secure"
checked={formik.values.secure}
onChange={formik.handleChange}
className="mr-2"
/>
<span className="text-sm">Secure</span>
</label>
<label className="flex items-center">
<input
type="checkbox"
name="httpOnly"
checked={formik.values.httpOnly}
onChange={formik.handleChange}
className="mr-2"
/>
<span className="text-sm">HTTP Only</span>
</label>
</div>
</div>
</div>
)}
</form>
</Modal>
);
};
export default ModifyCookieModal;

View File

@@ -11,6 +11,65 @@ const Wrapper = styled.div`
user-select: none;
}
}
&.header {
input {
padding: 0.3rem 0.5rem;
}
}
.textbox {
line-height: 1.42857143;
border: 1px solid #ccc;
padding: 0.45rem;
box-shadow: none;
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out 0.1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
.scroll-box {
max-height: 500px;
overflow-y: auto;
background:
/* Shadow Cover TOP */
linear-gradient(
${(props) => props.theme.modal.body.bg} 20%,
rgba(255, 255, 255, 0)
) center top,
/* Shadow Cover BOTTOM */
linear-gradient(
rgba(255, 255, 255, 0),
${(props) => props.theme.modal.body.bg} 80%
) center bottom,
/* Shadow TOP */
linear-gradient(
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0) 100%
) center top,
/* Shadow BOTTOM */
linear-gradient(
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.1) 100%
) center bottom;
background-repeat: no-repeat;
background-size: 100% 30px, 100% 30px, 100% 10px, 100% 10px;
background-attachment: local, local, scroll, scroll;
}
`;
export default Wrapper;

View File

@@ -1,53 +1,331 @@
import React from 'react';
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Modal from 'components/Modal';
import { IconTrash } from '@tabler/icons';
import { deleteCookiesForDomain } from 'providers/ReduxStore/slices/app';
import Accordion from 'components/Accordion/index';
import { IconTrash, IconEdit, IconCirclePlus, IconCookieOff, IconAlertTriangle, IconSearch } from '@tabler/icons';
import { deleteCookiesForDomain, deleteCookie } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';
import StyledWrapper from './StyledWrapper';
import moment from 'moment';
import { Tooltip } from 'react-tooltip';
const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
<Modal onClose={onClose} handleCancel={onClose} title="Clear Domain Cookies" hideFooter={true}>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
</div>
<div className="font-normal mt-4">
Are you sure you want to clear all cookies for the domain {domain}?
</div>
<div className="flex justify-between mt-6">
<div>
<button className="btn btn-sm btn-close" onClick={onClose}>
Close
</button>
</div>
<div>
<button className="btn btn-sm btn-danger" onClick={onClear}>
Clear All
</button>
</div>
</div>
</Modal>
);
const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
<Modal onClose={onClose} handleCancel={onClose} title="Delete Cookie" hideFooter={true}>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
</div>
<div className="font-normal mt-4">
Are you sure you want to delete the cookie {cookieName}?
</div>
<div className="flex justify-between mt-6">
<div>
<button className="btn btn-sm btn-close" onClick={onClose}>
Close
</button>
</div>
<div>
<button className="btn btn-sm btn-danger" onClick={onDelete}>
Delete
</button>
</div>
</div>
</Modal>
);
const CollectionProperties = ({ onClose }) => {
const dispatch = useDispatch();
const cookies = useSelector((state) => state.app.cookies) || [];
const [isModifyCookieModalOpen, setIsModifyCookieModalOpen] = useState(false);
const [currentDomain, setCurrentDomain] = useState(null);
const [cookieToEdit, setCookieToEdit] = useState(null);
const handleDeleteDomain = (domain) => {
dispatch(deleteCookiesForDomain(domain))
.then(() => {
toast.success('Domain deleted successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to delete domain'));
const [domainToClear, setDomainToClear] = useState(null);
const [cookieToDelete, setCookieToDelete] = useState(null);
const [searchText, setSearchText] = useState(null);
const handleAddCookie = (domain) => {
if(domain) setCurrentDomain(domain);
setIsModifyCookieModalOpen(true);
};
const handleEditCookie = (domain, cookie) => {
setCurrentDomain(domain);
setCookieToEdit(cookie);
setIsModifyCookieModalOpen(true);
};
const handleClearDomainCookies = (domain) => {
setDomainToClear(domain);
};
const clearDomainCookiesAction = () => {
dispatch(deleteCookiesForDomain(domainToClear))
.then(() => {
toast.success('Domain cookies cleared successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to clear domain cookies'));
setDomainToClear(null);
};
const handleDeleteCookie = (domain, path, key) => {
setCookieToDelete({ key, domain, path });
};
const deleteCookieAction = () => {
if (cookieToDelete) {
const { domain, path, key } = cookieToDelete;
dispatch(deleteCookie(domain, path, key))
.then(() => {
toast.success('Cookie deleted successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to delete cookie'));
}
setCookieToDelete(null);
};
const filteredCookies = useMemo(() => {
if (!searchText) return cookies;
return cookies.filter((cookie) =>
cookie.domain.toLowerCase().includes(searchText.toLowerCase())
);
}, [cookies, searchText]);
const shouldShowHeader = cookies && cookies.length > 0;
return (
<Modal size="md" title="Cookies" hideFooter={true} handleCancel={onClose}>
<StyledWrapper>
<table className="w-full border-collapse" style={{ marginTop: '-1rem' }}>
<thead>
<tr>
<th className="py-2 px-2 text-left">Domain</th>
<th className="py-2 px-2 text-left">Cookie</th>
<th className="py-2 px-2 text-center" style={{ width: 80 }}>
Actions
</th>
</tr>
</thead>
<tbody>
{cookies.map((cookie) => (
<tr key={cookie.domain}>
<td className="py-2 px-2">{cookie.domain}</td>
<td className="py-2 px-2 break-all">{cookie.cookieString}</td>
<td className="text-center">
<button tabIndex="-1" onClick={() => handleDeleteDomain(cookie.domain)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</StyledWrapper>
</Modal>
<>
<Modal
size="xl"
title="Cookies"
hideFooter={true}
handleCancel={onClose}
customHeader={shouldShowHeader ? (
<StyledWrapper className="header flex items-center justify-between w-full">
<h2 className="text-xs font-medium">Cookies</h2>
<input
type="search"
placeholder="Search by domain"
value={searchText || ''}
onChange={(e) => setSearchText(e.target.value)}
className="block textbox non-passphrase-input ml-auto font-normal"
/>
<button
type="submit"
className="submit btn btn-sm btn-secondary flex items-center gap-1 mx-4 font-medium"
onClick={(e) => {
e.stopPropagation();
handleAddCookie();
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
<span>Add Cookie</span>
</button>
</StyledWrapper>
) : null}
>
<StyledWrapper>
{!cookies || !cookies.length ? (
// No cookies found
<div className="flex items-center justify-center flex-col">
<IconCookieOff size={48} strokeWidth={1.5} className="text-gray-500" />
<h2 className="text-lg font-semibold mt-4">No cookies found</h2>
<p className="text-gray-500 mt-2">Add cookies to get started</p>
<button
type="submit"
className="submit btn btn-sm btn-secondary flex items-center gap-1 mt-8"
onClick={(e) => {
e.stopPropagation();
handleAddCookie();
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
<span>Add Cookie</span>
</button>
</div>
) : cookies.length && !filteredCookies.length ? (
// No search results
<div className="flex items-center justify-center flex-col">
<IconSearch size={48} />
<h2 className="text-lg font-semibold mt-4">No search results</h2>
<p className="text-gray-500 mt-2">Try a different search term</p>
</div>
) : (
// Show cookies list
<div className="scroll-box">
<Accordion defaultIndex={0}>
{filteredCookies.map((domainWithCookies, i) => (
<Accordion.Item key={i} index={i}>
<Accordion.Header index={i} className="flex items-center">
<div className="flex items-center">
<span>{domainWithCookies.domain}</span>
<span className="ml-2 text-xs dark:text-gray-300 text-gray-500">
({domainWithCookies.cookies.length}{' '}
{domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})
</span>
<div className="ml-auto flex items-center gap-2">
<button
type="submit"
className="flex items-center gap-1 text-gray-500 hover:text-gray-950 dark:text-white dark:hover:text-gray-300"
onClick={(e) => {
e.stopPropagation();
handleAddCookie(domainWithCookies.domain);
}}
>
<IconCirclePlus strokeWidth={1.5} size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleClearDomainCookies(domainWithCookies.domain);
}}
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600 mr-2"
>
<IconTrash strokeWidth={1.5} size={16} />
</button>
</div>
</div>
</Accordion.Header>
<Accordion.Content index={i}>
<div className="flex items-center justify-between">
<table className="w-full">
<thead>
<tr className="text-left border-b border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-gray-300">
<th className="py-2 px-4 font-semibold w-32">Name</th>
<th className="py-2 px-4 font-semibold w-52">Value</th>
<th className="py-2 px-4 font-semibold">Path</th>
<th className="py-2 px-4 font-semibold">Expires</th>
<th className="py-2 px-4 font-semibold text-center">Secure</th>
<th className="py-2 px-4 font-semibold text-center">HTTP Only</th>
<th className="py-2 px-4 font-semibold text-right w-24">Actions</th>
</tr>
</thead>
<tbody>
{domainWithCookies.cookies.map((cookie) => (
<tr key={cookie.key} className="border-b border-gray-200 dark:border-neutral-600 last:border-none">
<td className="py-2 px-4 truncate">
<span id={`cookie-key-${cookie.key}`}>{cookie.key}</span>
<Tooltip
anchorId={`cookie-key-${cookie.key}`}
className="tooltip-mod"
html={cookie.key}
/>
</td>
<td className="py-2 px-4 truncate">
<span id={`cookie-value-${cookie.key}`}>{cookie.value}</span>
<Tooltip
anchorId={`cookie-value-${cookie.key}`}
className="tooltip-mod"
html={cookie.value}
/>
</td>
<td className="py-2 px-4 truncate">{cookie.path || '/'}</td>
<td className="py-2 px-4 truncate">
<span id={`cookie-expires-${cookie.key}`}>
{cookie.expires && moment(cookie.expires).isValid()
? new Date(cookie.expires).toLocaleString()
: 'Session'}
</span>
{cookie.expires && moment(cookie.expires).isValid() && (
<Tooltip
anchorId={`cookie-expires-${cookie.key}`}
className="tooltip-mod"
html={new Date(cookie.expires).toLocaleString()}
/>
)}
</td>
<td className="py-2 px-4 text-center">{cookie.secure ? '✓' : ''}</td>
<td className="py-2 px-4 text-center">{cookie.httpOnly ? '✓' : ''}</td>
<td className="py-2 px-4">
<div className="flex items-center justify-end gap-2">
<button
onClick={(e) => {
e.stopPropagation();
handleEditCookie(domainWithCookies.domain, cookie);
}}
className="text-gray-700 hover:text-gray-950
dark:text-white dark:hover:text-gray-300"
>
<IconEdit strokeWidth={1.5} size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key);
}}
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600"
>
<IconTrash strokeWidth={1.5} size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion>
</div>
)}
</StyledWrapper>
</Modal>
{isModifyCookieModalOpen && (
<ModifyCookieModal
onClose={() => {
setCookieToEdit(null);
setCurrentDomain(null);
setIsModifyCookieModalOpen(false);
}}
domain={currentDomain}
cookie={cookieToEdit}
/>
)}
{domainToClear ? (
<ClearDomainCookiesModal
onClose={() => setDomainToClear(null)}
domain={domainToClear}
onClear={clearDomainCookiesAction}
/>
) : null}
{cookieToDelete ? (
<DeleteCookieModal
onClose={() => setCookieToDelete(null)}
cookieName={cookieToDelete.key}
onDelete={deleteCookieAction}
/>
) : null}
</>
);
};

View File

@@ -6,6 +6,7 @@ import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { validateName, validateNameError } from 'utils/common/regex';
const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
@@ -23,7 +24,11 @@ const CreateEnvironment = ({ collection, onClose }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'Must be at least 1 character')
.max(50, 'Must be 50 characters or less')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.required('Name is required')
.test('duplicate-name', 'Environment already exists', validateEnvironmentName)
}),

View File

@@ -6,6 +6,7 @@ import { useFormik } from 'formik';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import { validateName, validateNameError } from 'utils/common/regex';
const RenameEnvironment = ({ onClose, environment, collection }) => {
const dispatch = useDispatch();
@@ -18,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.required('name is required')
}),
onSubmit: (values) => {

View File

@@ -1,10 +1,9 @@
import React from 'react';
import path from 'path';
import path from 'utils/common/path';
import { useDispatch } from 'react-redux';
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
import { IconX } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
import slash from 'utils/common/slash';
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
const dispatch = useDispatch();
@@ -27,7 +26,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
const collectionDir = collection.pathname;
if (filePath.startsWith(collectionDir)) {
return path.relative(slash(collectionDir), slash(filePath));
return path.relative(collectionDir, filePath);
}
return filePath;

View File

@@ -0,0 +1,86 @@
import React from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
import { updateFolderAuth } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
import AuthMode from '../AuthMode';
const GrantTypeComponentMap = ({ collection, folder }) => {
const dispatch = useDispatch();
const save = () => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
let request = get(folder, 'root.request', {});
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
switch (grantType) {
case 'password':
return <OAuth2PasswordCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
case 'authorization_code':
return <OAuth2AuthorizationCode save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
case 'client_credentials':
return <OAuth2ClientCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
default:
return <div>TBD</div>;
}
};
const Auth = ({ collection, folder }) => {
const dispatch = useDispatch();
let request = get(folder, 'root.request', {});
const authMode = get(folder, 'root.request.auth.mode');
const handleSave = () => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
const getAuthView = () => {
switch (authMode) {
case 'oauth2': {
return (
<>
<GrantTypeSelector
request={request}
updateAuth={updateFolderAuth}
collection={collection}
item={folder}
/>
<GrantTypeComponentMap collection={collection} folder={folder} />
</>
);
}
case 'none': {
return null;
}
default:
return null;
}
};
return (
<StyledWrapper className="w-full">
<div className="text-xs mb-4 text-muted">
Configures authentication for the entire folder. This applies to all requests using the{' '}
<span className="font-medium">Inherit</span> option in the <span className="font-medium">Auth</span> tab.
</div>
<div className="flex flex-grow justify-start items-center mb-4">
<AuthMode collection={collection} folder={folder} />
</div>
{getAuthView()}
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Auth;

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.auth-mode-selector {
border: 1px solid ${({ theme }) => theme.colors.border};
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8125rem;
}
.auth-mode-label {
color: ${({ theme }) => theme.colors.text};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,62 @@
import React, { useRef, forwardRef } from 'react';
import get from 'lodash/get';
import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { useDispatch } from 'react-redux';
import { updateFolderAuthMode } from 'providers/ReduxStore/slices/collections';
import { humanizeRequestAuthMode } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
const AuthMode = ({ collection, folder }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = get(folder, 'root.request.auth.mode');
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const onModeChange = (value) => {
dispatch(
updateFolderAuthMode({
mode: value,
collectionUid: collection.uid,
folderUid: folder.uid
})
);
};
return (
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('oauth2');
}}
>
OAuth 2.0
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('none');
}}
>
No Auth
</div>
</Dropdown>
</div>
</StyledWrapper>
);
};
export default AuthMode;

View File

@@ -88,7 +88,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
<td>
<div className="flex items-center">
<span>Expr</span>
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
<InfoTip content="You can write any valid JS expression here" infotipId="response-var" />
</div>
</td>
)}

View File

@@ -8,7 +8,9 @@ import Tests from './Tests';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars';
import Documentation from './Documentation';
import Auth from './Auth';
import DotIcon from 'components/Icons/Dot';
import get from 'lodash/get';
const ContentIndicator = () => {
return (
@@ -37,6 +39,9 @@ const FolderSettings = ({ collection, folder }) => {
const responseVars = folderRoot?.request?.vars?.res || [];
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const auth = get(folderRoot, 'request.auth.mode');
const hasAuth = auth && auth !== 'none';
const setTab = (tab) => {
dispatch(
updatedFolderSettingsSelectedTab({
@@ -61,6 +66,9 @@ const FolderSettings = ({ collection, folder }) => {
case 'vars': {
return <Vars collection={collection} folder={folder} />;
}
case 'auth': {
return <Auth collection={collection} folder={folder} />;
}
case 'docs': {
return <Documentation collection={collection} folder={folder} />;
}
@@ -93,6 +101,10 @@ const FolderSettings = ({ collection, folder }) => {
Vars
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
Auth
{hasAuth && <ContentIndicator />}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
Docs
</div>

View File

@@ -81,7 +81,10 @@ const EnvironmentSelector = () => {
<IconDatabaseOff size={18} strokeWidth={1.5} />
<span className="ml-2">No Environment</span>
</div>
<div className="dropdown-item border-top" onClick={handleSettingsIconClick}>
<div className="dropdown-item border-top" onClick={() => {
handleSettingsIconClick();
dropdownTippyRef.current.hide();
}}>
<div className="pr-2 text-gray-600">
<IconSettings size={18} strokeWidth={1.5} />
</div>

View File

@@ -6,6 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { validateName, validateNameError } from 'utils/common/regex';
const CreateEnvironment = ({ onClose }) => {
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
@@ -25,7 +26,11 @@ const CreateEnvironment = ({ onClose }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'Must be at least 1 character')
.max(50, 'Must be 50 characters or less')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.required('Name is required')
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
}),

View File

@@ -3,10 +3,10 @@ import Portal from 'components/Portal/index';
import Modal from 'components/Modal/index';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { validateName, validateNameError } from 'utils/common/regex';
const RenameEnvironment = ({ onClose, environment }) => {
const dispatch = useDispatch();
@@ -19,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment }) => {
validationSchema: Yup.object({
name: Yup.string()
.min(1, 'must be at least 1 character')
.max(50, 'must be 50 characters or less')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
.required('name is required')
}),
onSubmit: (values) => {

View File

@@ -0,0 +1,11 @@
import styled from 'styled-components';
const Wrapper = styled.div`
font-weight: 400;
font-size: 0.75rem;
background-color: ${props => props.theme.infoTip.bg};
border: 1px solid ${props => props.theme.infoTip.border};
box-shadow: ${props => props.theme.infoTip.boxShadow};
`;
export default Wrapper;

View File

@@ -0,0 +1,40 @@
/**
* The InfoTip components needs to be nuked
* This component will be the future replacement
* We should allow icon and placement props to be passed in
*/
import React, { useState } from 'react';
import HelpIcon from 'components/Icons/Help';
import StyledWrapper from './StyledWrapper';
const Help = ({ children, width = 200 }) => {
const [showTooltip, setShowTooltip] = useState(false);
return (
<div className="flex items-center relative">
<span
className="flex items-center"
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<HelpIcon size={14}/>
</span>
{showTooltip && (
<StyledWrapper
className="absolute z-50 rounded-md p-3"
style={{
top: '50%',
left: 'calc(100% + 8px)',
transform: 'translateY(-50%)',
width: `${width}px`
}}
>
{children}
</StyledWrapper>
)}
</div>
);
};
export default Help;

View File

@@ -0,0 +1,20 @@
import React from 'react';
const HelpIcon = ({ size = 14 }) => {
return (
<svg
tabIndex="-1"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
fill="currentColor"
className="inline-block ml-2 cursor-pointer"
viewBox="0 0 16 16"
>
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
</svg>
)
}
export default HelpIcon;

View File

@@ -0,0 +1,104 @@
const OpenApiLogo = () => {
return (
<svg width="28" height="28" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<path
style={{
fill: "#91d400",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M43.125 51.148H20.781l.012.325c.012.21.027.418.039.625.004.09.008.18.016.27a41.442 41.442 0 0 0 .164 1.687c0 .027.004.05.008.078.035.285.07.574.113.86 0 .003 0 .007.004.01a36.98 36.98 0 0 0 1.152 5.255c.004.008.008.012.008.02a27.978 27.978 0 0 0 .265.859c.004.015.012.031.016.047.078.242.164.484.246.73.024.059.043.121.067.184.074.207.148.418.226.629.04.093.074.187.11.285.07.172.136.343.203.52.054.128.11.257.164.39.054.133.113.27.168.406.074.164.148.328.218.492.047.102.09.2.133.297.09.195.184.395.278.59l.093.191c.11.227.223.45.336.672.02.035.035.07.051.102a36.344 36.344 0 0 0 .41.773c.028.051.059.102.086.149L44.45 56.156l.07-.043a15.031 15.031 0 0 1-1.394-4.965Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#91d400",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m22.563 61.137-.727.207.008.02zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m48.613 61.355-.05.055-15.739 15.664c.082.074.16.149.242.223.149.133.297.266.446.394.078.067.152.137.23.204.18.152.36.3.54.449.046.043.097.082.144.12a21.669 21.669 0 0 0 .691.556c.227.175.45.343.676.515.012.008.02.012.027.02.95.707 1.93 1.367 2.946 1.98.035.024.07.043.105.067l.578.34c.117.066.239.132.356.203.113.062.222.125.336.187.207.11.41.223.617.328a35.567 35.567 0 0 0 1.824.887l.559-1.348 7.918-19.133.027-.07a15.337 15.337 0 0 1-2.473-1.64Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#68a338",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M46.977 59.797a16.778 16.778 0 0 1-.899-1.102c-.152-.203-.3-.406-.437-.617a15.93 15.93 0 0 1-.41-.633L26.124 68.902c.297.485.602.957.914 1.422.012.016.02.035.031.051l.012.016c.008.015.02.03.027.046.004.004.004.004.004.008.028.035.051.07.078.11 0 .004 0 .004.004.007v.004a35.121 35.121 0 0 0 1.051 1.465c.008.012.016.02.02.031.156.2.308.403.464.602.024.027.043.05.063.078.164.203.328.406.496.61.04.046.078.093.117.144.153.18.305.36.457.535.067.078.137.153.203.227.13.148.262.297.395.445.074.082.152.16.226.242.032.035.067.075.102.11l.293.316.121.121c.176.184.352.363.531.54l15.758-15.684a22.18 22.18 0 0 1-.515-.551Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M67.867 61.348c-.176.136-.347.273-.527.406l.039.066L78.87 80.805a38.54 38.54 0 0 0 1.57-1.078 38.099 38.099 0 0 0 3.227-2.653L67.93 61.41Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#91d400",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m77.418 81.707.023-.012-.023.012zm.023-.012c.051-.027.102-.054.153-.086l-.004-.004c-.05.032-.098.06-.149.09zm-.023.012-.008.004zm-.039-.039.027.047zm.039.039.023-.012-.023.012zm-.016.012.004-.004zm.008-.009-.004.005c.004-.004.008-.004.012-.008-.004.004-.004.004-.008.004zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#91d400",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M77.441 81.695c.051-.03.102-.054.153-.086-.051.032-.102.059-.153.086zm.149-.09.004.004zm-.2.118.005-.004zm-.19-.763-.391-.644-10.727-17.722c-.215.133-.437.25-.66.367-.223.121-.45.23-.676.34a15.303 15.303 0 0 1-6.523 1.469c-1.465 0-2.93-.211-4.344-.63-.238-.074-.473-.167-.711-.25-.238-.085-.48-.156-.715-.253l-7.91 19.117-.309.75-.265.644h-.004l.062.024c.024.012.043.016.067.027h.004c.004 0 .007.004.011.004.188.078.375.145.563.215.238.094.473.184.707.27.121.042.238.097.36.136a38.13 38.13 0 0 0 7.648 1.824c.105.012.207.028.308.04l.32.035c.2.023.4.047.602.066l.149.012c.25.023.496.043.742.062.082.004.168.008.25.016.219.016.433.027.648.039.133.004.266.008.399.016.172.004.343.011.515.015.246.008.496.008.746.012h.176c2.082 0 4.164-.172 6.223-.516l.105-.015c.215-.04.434-.078.653-.117.125-.024.246-.047.37-.075.126-.023.255-.05.384-.078.21-.043.421-.09.632-.14l.118-.024a37.8 37.8 0 0 0 8.992-3.34c.187-.097.367-.207.554-.308.22-.121.438-.246.657-.371.152-.086.308-.164.457-.254l.004-.004h.004l.003-.004c.004 0 .004 0 .004-.004l-.027-.047.027.047h.004c.004-.004.008-.004.008-.004.008-.008.016-.012.023-.016.051-.03.102-.058.149-.09zM48.625 37.938c.172-.141.348-.274.523-.407l-.039-.066L37.621 18.48c-.535.348-1.062.704-1.578 1.082a37.213 37.213 0 0 0-3.219 2.649l15.739 15.664Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M31.73 23.254c-.18.18-.347.363-.523.543-.172.18-.352.36-.523.543a38.163 38.163 0 0 0-3.32 4.125c-.106.156-.216.312-.321.469-.11.164-.219.328-.324.496-.04.058-.078.12-.117.18a37.099 37.099 0 0 0-5.82 18.527c-.009.25-.016.504-.02.754s-.012.5-.012.75h22.29c0-.25.023-.5.034-.75.012-.254.016-.504.043-.754a15.002 15.002 0 0 1 3.36-8.078c.16-.192.34-.375.507-.559.172-.188.329-.379.508-.559zm45.993-5.508a46.43 46.43 0 0 0-.684-.402c-.117-.067-.23-.133-.348-.196-.117-.066-.23-.128-.347-.195-.2-.11-.403-.219-.606-.324-.031-.016-.062-.031-.093-.05a38.014 38.014 0 0 0-4.02-1.798c-.035-.015-.074-.027-.11-.039a37.527 37.527 0 0 0-8.41-2.102c-.101-.015-.207-.027-.312-.042l-.313-.036a31.754 31.754 0 0 0-.777-.078c-.238-.023-.48-.043-.719-.062-.093-.004-.187-.012-.28-.016-.204-.015-.415-.027-.618-.039l-.328-.012v22.239c1.144.117 2.281.363 3.383.734l16.445-16.367a41.117 41.117 0 0 0-1.863-1.215zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#68a338",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m38.898 17.68.391.644zm18.59-5.34c-.25.004-.504.004-.754.015a38.117 38.117 0 0 0-4.71.48c-.036.009-.07.013-.106.02-.219.036-.434.079-.652.118l-.371.07c-.13.027-.258.05-.383.082a36.99 36.99 0 0 0-.75.164 37.925 37.925 0 0 0-8.996 3.336c-.184.098-.368.21-.551.312-.219.118-.438.243-.66.368-.16.093-.325.18-.489.277h-.003a.162.162 0 0 1-.036.02c-.043.027-.086.046-.129.074l.004.004.387.644 11.117 18.367c.215-.132.438-.25.66-.367a15.15 15.15 0 0 1 5.668-1.73c.25-.028.5-.047.754-.063.25-.011.504-.023.758-.023V12.324c-.254 0-.504.008-.758.016zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#4c5930",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="m95.695 47.809-.035-.598c-.008-.102-.012-.2-.02-.3l-.058-.704c-.008-.059-.012-.121-.016-.18a53.501 53.501 0 0 0-.086-.785c-.003-.023-.003-.043-.007-.062 0-.012 0-.02-.004-.032-.035-.28-.07-.562-.114-.843 0-.012 0-.02-.003-.028a36.772 36.772 0 0 0-1.153-5.246 47.929 47.929 0 0 0-.258-.832c-.011-.035-.023-.07-.03-.105-.083-.242-.161-.48-.247-.719-.023-.063-.043-.129-.066-.195a39.302 39.302 0 0 0-.34-.91c-.067-.172-.133-.344-.2-.512-.054-.137-.109-.27-.163-.403-.055-.132-.114-.261-.168-.394l-.223-.504-.129-.285c-.09-.2-.188-.402-.281-.602-.032-.058-.059-.12-.086-.18-.113-.23-.227-.456-.34-.683-.016-.031-.031-.062-.05-.094-.13-.25-.259-.5-.395-.746l-.012-.023a36.958 36.958 0 0 0-2.133-3.446L72.628 44.77c.376 1.097.618 2.23.735 3.37h22.344l-.012-.331zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#68a338",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M73.45 49.64c0 .255-.024.505-.036.755-.012.25-.016.503-.043.753a15.002 15.002 0 0 1-3.36 8.079c-.16.191-.335.375-.507.558-.168.188-.328.38-.508.559L84.758 76.03c.18-.18.347-.363.523-.543.172-.183.352-.363.52-.547a36.965 36.965 0 0 0 3.195-3.937c.04-.055.074-.11.11-.16.117-.168.234-.34.347-.508.102-.152.203-.3.3-.453.048-.074.095-.153.142-.223a37.055 37.055 0 0 0 5.808-18.515c.012-.25.016-.5.024-.75.003-.25.011-.5.011-.754zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
<path
style={{
fill: "#3f3f42",
fillOpacity: 1,
fillRule: "nonzero",
stroke: "none"
}} d="M102.36 5.734c-4.079-4.058-10.688-4.058-14.766 0-3.254 3.235-3.903 8.075-1.965 11.961l-22.746 22.64c-3.906-1.925-8.77-1.28-12.024 1.958-4.078 4.059-4.074 10.64 0 14.7 4.082 4.058 10.692 4.054 14.77 0a10.356 10.356 0 0 0 1.965-11.966l22.746-22.64c3.906 1.93 8.765 1.281 12.02-1.957a10.355 10.355 0 0 0 0-14.696zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
/>
</svg>
);
};
export default OpenApiLogo;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Tooltip as ReactInfoTip } from 'react-tooltip';
const InfoTip = ({ text, infotipId }) => {
const InfoTip = ({ html: _ignored, infotipId, ...props }) => {
return (
<>
<svg
@@ -17,7 +17,7 @@ const InfoTip = ({ text, infotipId }) => {
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
</svg>
<ReactInfoTip anchorId={infotipId} html={text} />
<ReactInfoTip anchorId={infotipId} {...props} />
</>
);
};

View File

@@ -15,14 +15,14 @@ const StyledMarkdownBodyWrapper = styled.div`
margin: 0.67em 0;
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
font-size: 1.3;
font-size: 1.3em;
border-bottom: 1px solid var(--color-border-muted);
}
h2 {
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: 0.3em;
font-size: 1.2;
font-size: 1.2em;
border-bottom: 1px solid var(--color-border-muted);
}

View File

@@ -16,7 +16,7 @@ const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
</div>
);
const ModalContent = ({ children }) => <div className="bruno-modal-content px-4 py-6">{children}</div>;
const ModalContent = ({ children }) => <div className="bruno-modal-content px-4 py-4">{children}</div>;
const ModalFooter = ({
confirmText,

View File

@@ -3,6 +3,7 @@ import { useState } from 'react';
import StyledWrapper from './StyleWrapper';
import Modal from 'components/Modal/index';
import { useEffect } from 'react';
import { useApp } from 'providers/App';
import {
fetchNotifications,
markAllNotificationsAsRead,
@@ -11,18 +12,18 @@ import {
import { useDispatch, useSelector } from 'react-redux';
import { humanizeDate, relativeDate } from 'utils/common';
import ToolHint from 'components/ToolHint';
import { useTheme } from 'providers/Theme';
import DOMPurify from 'dompurify';
const PAGE_SIZE = 5;
const Notifications = () => {
const dispatch = useDispatch();
const { version } = useApp();
const notifications = useSelector((state) => state.notifications.notifications);
const [showNotificationsModal, setShowNotificationsModal] = useState(false);
const [selectedNotification, setSelectedNotification] = useState(null);
const [pageNumber, setPageNumber] = useState(1);
const { storedTheme } = useTheme();
const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;
const notificationsEndIndex = pageNumber * PAGE_SIZE;
@@ -30,7 +31,9 @@ const Notifications = () => {
const unreadNotifications = notifications.filter((notification) => !notification.read);
useEffect(() => {
dispatch(fetchNotifications());
dispatch(fetchNotifications({
currentVersion: version
}));
}, []);
useEffect(() => {
@@ -66,6 +69,13 @@ const Notifications = () => {
dispatch(markNotificationAsRead({ notificationId: notification?.id }));
};
const getSanitizedDescription = (description) => {
return DOMPurify.sanitize(encodeURIComponent(description), {
ALLOWED_TAGS: ['a', 'ul', 'img', 'li', 'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
ALLOWED_ATTR: ['href', 'style', 'target', 'src', 'alt']
});
};
const modalCustomHeader = (
<div className="flex flex-row gap-8">
<div>NOTIFICATIONS</div>
@@ -90,7 +100,9 @@ const Notifications = () => {
<a
className="relative cursor-pointer"
onClick={() => {
dispatch(fetchNotifications());
dispatch(fetchNotifications({
currentVersion: version
}));
setShowNotificationsModal(true);
}}
aria-label="Check all Notifications"
@@ -179,10 +191,11 @@ const Notifications = () => {
<div className="w-full notification-date text-xs mb-4">
{humanizeDate(selectedNotification?.date)}
</div>
<div
className="flex w-full flex-col flex-wrap h-fit"
dangerouslySetInnerHTML={{ __html: selectedNotification?.description }}
></div>
<iframe
src={`data:text/html,${getSanitizedDescription(selectedNotification?.description)}`}
sandbox="allow-popups"
style={{ width: '100%', height: '100%' }}
></iframe>
</div>
</div>
) : (

View File

@@ -0,0 +1,39 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
width: 100%;
.path-display {
background: ${(props) => props.theme.requestTabPanel.url.bg};
border-radius: 4px;
padding: 8px 12px;
font-size: 0.8125rem;
border: 1px solid rgba(0, 0, 0, 0.08);
.icon-column {
padding-right: 8px;
align-items: flex-start;
padding-top: 2px;
}
.path-container {
flex-wrap: wrap;
}
.path-segment {
white-space: nowrap;
}
.name-container, .file-extension {
color: ${(props) => props.theme.colors.text.yellow};
}
.separator {
color: ${(props) => props.theme.text};
opacity: 0.6;
margin: 0 2px;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { IconFolder, IconFile } from '@tabler/icons';
import path from 'utils/common/path';
import StyledWrapper from './StyledWrapper';
const PathDisplay = ({
baseName = '',
iconType = 'file'
}) => {
return (
<StyledWrapper>
<div className="path-display mt-2">
<div className="path-layout flex font-mono">
<div className="icon-column flex">
{iconType === 'file' ? <IconFile size={16} /> : <IconFolder size={16} />}
</div>
<span className="name-container">
{baseName}
</span>
</div>
</div>
</StyledWrapper>
);
};
export default PathDisplay;

View File

@@ -6,8 +6,7 @@ import { savePreferences } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
import path from 'path';
import slash from 'utils/common/slash';
import path from 'utils/common/path';
import { IconTrash } from '@tabler/icons';
const General = ({ close }) => {
@@ -134,7 +133,7 @@ const General = ({ close }) => {
className={`flex items-center mt-2 pl-6 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
>
<span className="flex items-center border px-2 rounded-md">
{path.basename(slash(formik.values.customCaCertificate.filePath))}
{path.basename(formik.values.customCaCertificate.filePath)}
<button
type="button"
tabIndex="-1"

View File

@@ -46,7 +46,7 @@ const Preferences = ({ onClose }) => {
return (
<StyledWrapper>
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem]'>
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem] py-2'>
<div className="flex flex-col items-center tabs" role="tablist">
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
General

View File

@@ -11,6 +11,47 @@ const Wrapper = styled.div`
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
.token-placement-selector {
padding: 0.5rem 0px;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
min-width: 100px;
.dropdown {
width: fit-content;
min-width: 100px;
div[data-tippy-root] {
width: fit-content;
min-width: 100px;
}
.tippy-box {
width: fit-content;
max-width: none !important;
min-width: 100px;
.tippy-content: {
width: fit-content;
max-width: none !important;
min-width: 100px;
}
}
}
.token-placement-label {
width: fit-content;
// color: ${(props) => props.theme.colors.text.yellow};
justify-content: space-between;
padding: 0 0.5rem;
min-width: 100px;
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
}
`;
export default Wrapper;

View File

@@ -1,28 +1,63 @@
import React from 'react';
import React, { useRef, forwardRef } from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { IconCaretDown, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import { clearOauth2Cache } from 'utils/network/index';
import toast from 'react-hot-toast';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
const OAuth2AuthorizationCode = ({ item, collection }) => {
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
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 oAuth = get(request, 'auth.oauth2', {});
const {
callbackUrl,
authorizationUrl,
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsPlacement,
state,
pkce,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken
} = oAuth;
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const TokenPlacementIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
const CredentialsPlacementIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const handleSave = () => { save(); };
const handleChange = (key, value) => {
dispatch(
@@ -40,7 +75,15 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
state,
scope,
pkce,
[key]: value
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken,
[key]: value,
}
})
);
@@ -61,30 +104,35 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
clientSecret,
state,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
autoFetchToken,
pkce: !Boolean(oAuth?.['pkce'])
}
})
);
};
const handleClearCache = (e) => {
clearOauth2Cache(collection?.uid)
.then(() => {
toast.success('cleared cache successfully');
})
.catch((err) => {
toast.error(err.message);
});
};
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />
<div className="flex items-center gap-2.5 mt-2">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium">
Configuration
</span>
</div>
{inputsConfig.map((input) => {
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
<label className="block min-w-[140px]">{label}</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
@@ -99,8 +147,33 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
</div>
);
})}
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
<label className="block min-w-[140px]">Add Credentials to</label>
<div className="inline-flex items-center cursor-pointer token-placement-selector">
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'body');
}}
>
Request Body
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'basic_auth_header');
}}
>
Basic Auth Header
</div>
</Dropdown>
</div>
</div>
<div className="flex flex-row w-full gap-4" key="pkce">
<label className="block font-medium">Use PKCE</label>
<label className="block">Use PKCE</label>
<input
className="cursor-pointer"
type="checkbox"
@@ -108,16 +181,154 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={handlePKCEToggle}
/>
</div>
<div className="flex flex-row gap-4">
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
<div className="flex items-center gap-2.5 mt-2">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
Token
</span>
</div>
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
<label className="block min-w-[140px]">Token ID</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['credentialsId'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('credentialsId', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
</div>
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
<label className="block min-w-[140px]">Add token to</label>
<div className="inline-flex items-center cursor-pointer token-placement-selector">
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('tokenPlacement', 'header');
}}
>
Header
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('tokenPlacement', 'url');
}}
>
URL
</div>
</Dropdown>
</div>
</div>
{
tokenPlacement === 'header' ?
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
<label className="block min-w-[140px]">Header Prefix</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenHeaderPrefix'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
:
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
<label className="block font-medium min-w-[140px]">Query Param Key</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenQueryKey'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenQueryKey', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
}
<div className="flex items-center gap-2.5 mt-4 mb-2">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
Advanced Settings
</span>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Refresh Token URL</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange("refreshTokenUrl", val)}
collection={collection}
item={item}
/>
</div>
</div>
<div className="flex items-center gap-2.5 mt-4">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium">Settings</span>
</div>
{/* Automatically Fetch Token */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoFetchToken)}
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
className="cursor-pointer ml-1"
/>
<label className="block min-w-[140px]">Automatically fetch token if not found</label>
<div className="flex items-center gap-2">
<div className="relative group cursor-pointer">
<IconHelp size={16} className="text-gray-500" />
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
Automatically fetch a new token when you try to access a resource and don't have one.
</span>
</div>
</div>
</div>
{/* Auto Refresh Token (With Refresh URL) */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoRefreshToken)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={isAutoRefreshDisabled}
/>
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>
<div className="flex items-center gap-2">
<div className="relative group cursor-pointer">
<IconHelp size={16} className="text-gray-500" />
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
Automatically refresh your token using the refresh URL when it expires.
</span>
</div>
</div>
</div>
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;
export default OAuth2AuthorizationCode;

View File

@@ -11,6 +11,47 @@ const Wrapper = styled.div`
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
.token-placement-selector {
padding: 0.5rem 0px;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
min-width: 100px;
.dropdown {
width: fit-content;
min-width: 100px;
div[data-tippy-root] {
width: fit-content;
min-width: 100px;
}
.tippy-box {
width: fit-content;
max-width: none !important;
min-width: 100px;
.tippy-content: {
width: fit-content;
max-width: none !important;
min-width: 100px;
}
}
}
.token-placement-label {
width: fit-content;
// color: ${(props) => props.theme.colors.text.yellow};
justify-content: space-between;
padding: 0 0.5rem;
min-width: 100px;
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
}
`;
export default Wrapper;

View File

@@ -1,26 +1,61 @@
import React from 'react';
import React, { useRef, forwardRef } from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import Dropdown from 'components/Dropdown';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
const OAuth2ClientCredentials = ({ item, collection }) => {
const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
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 oAuth = get(request, 'auth.oauth2', {});
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const {
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken
} = oAuth;
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
const handleSave = () => { save(); };
const TokenPlacementIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const CredentialsPlacementIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const handleChange = (key, value) => {
dispatch(
@@ -34,6 +69,14 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken,
[key]: value
}
})
@@ -42,12 +85,21 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />
<div className="flex items-center gap-2.5 mt-2">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium">
Configuration
</span>
</div>
{inputsConfig.map((input) => {
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
<label className="block min-w-[140px]">{label}</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
@@ -62,9 +114,190 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
<label className="block min-w-[140px]">Add Credentials to</label>
<div className="inline-flex items-center cursor-pointer token-placement-selector">
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'body');
}}
>
Request Body
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'basic_auth_header');
}}
>
Basic Auth Header
</div>
</Dropdown>
</div>
</div>
<div className="flex items-center gap-2.5 mt-2">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
Token
</span>
</div>
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
<label className="block min-w-[140px]">Token ID</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['credentialsId'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('credentialsId', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
</div>
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
<label className="block min-w-[140px]">Add token to</label>
<div className="inline-flex items-center cursor-pointer token-placement-selector w-fit">
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('tokenPlacement', 'header');
}}
>
Header
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('tokenPlacement', 'url');
}}
>
URL
</div>
</Dropdown>
</div>
</div>
{
tokenPlacement === 'header' ?
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
<label className="block min-w-[140px]">Header Prefix</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenHeaderPrefix'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
:
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
<label className="block font-medium min-w-[140px]">Query Param Key</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenQueryKey'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenQueryKey', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
}
<div className="flex items-center gap-2.5 mt-4 mb-2">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
Advanced Settings
</span>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Refresh Token URL</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange("refreshTokenUrl", val)}
collection={collection}
item={item}
/>
</div>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Auto-refresh token</label>
<input
type="checkbox"
className="cursor-pointer w-4 h-4 accent-indigo-600"
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
/>
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
</div>
<div className="flex items-center gap-2.5 mt-4">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium">Settings</span>
</div>
{/* Automatically Fetch Token */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoFetchToken)}
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
className="cursor-pointer ml-1"
/>
<label className="block min-w-[140px]">Automatically fetch token if not found</label>
<div className="flex items-center gap-2">
<div className="relative group cursor-pointer">
<IconHelp size={16} className="text-gray-500" />
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
Automatically fetch a new token when you try to access a resource and don't have one.
</span>
</div>
</div>
</div>
{/* Auto Refresh Token (With Refresh URL) */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoRefreshToken)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={isAutoRefreshDisabled}
/>
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>
<div className="flex items-center gap-2">
<div className="relative group cursor-pointer">
<IconHelp size={16} className="text-gray-500" />
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
Automatically refresh your token using the refresh URL when it expires.
</span>
</div>
</div>
</div>
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
</StyledWrapper>
);
};

View File

@@ -3,18 +3,20 @@ 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 { IconCaretDown, IconKey } from '@tabler/icons';
import { humanizeGrantType } from 'utils/collections';
import { useEffect } from 'react';
import { useState } from 'react';
const GrantTypeSelector = ({ item, collection }) => {
const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const oAuth = get(request, 'auth.oauth2', {});
const [valuesCache, setValuesCache] = useState({
...oAuth
});
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
@@ -24,13 +26,19 @@ const GrantTypeSelector = ({ item, collection }) => {
});
const onGrantTypeChange = (grantType) => {
let updatedValues = {
...valuesCache,
...oAuth,
grantType
};
setValuesCache(updatedValues);
dispatch(
updateAuth({
mode: 'oauth2',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType
...updatedValues
}
})
);
@@ -46,7 +54,18 @@ const GrantTypeSelector = ({ item, collection }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
grantType: 'authorization_code'
grantType: 'authorization_code',
accessTokenUrl: '',
username: '',
password: '',
clientId: '',
clientSecret: '',
scope: '',
credentialsPlacement: 'body',
credentialsId: 'credentials',
tokenPlacement: 'header',
tokenHeaderPrefix: 'Bearer',
tokenQueryKey: 'access_token',
}
})
);
@@ -54,7 +73,14 @@ const GrantTypeSelector = ({ item, collection }) => {
return (
<StyledWrapper>
<label className="block font-medium mb-2">Grant Type</label>
<div className="flex items-center gap-2.5 my-4">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium">
Grant Type
</span>
</div>
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
@@ -89,4 +115,4 @@ const GrantTypeSelector = ({ item, collection }) => {
</StyledWrapper>
);
};
export default GrantTypeSelector;
export default GrantTypeSelector;

View File

@@ -0,0 +1,96 @@
import { useMemo, useState } from "react";
import { useDispatch } from "react-redux";
import toast from 'react-hot-toast';
import { cloneDeep, find } from 'lodash';
import { IconLoader2 } from '@tabler/icons';
import brunoCommon from '@usebruno/common';
const { interpolate } = brunoCommon;
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
import { getAllVariables } from "utils/collections/index";
const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, credentialsId }) => {
const { uid: collectionUid } = collection;
const dispatch = useDispatch();
const [fetchingToken, toggleFetchingToken] = useState(false);
const [refreshingToken, toggleRefreshingToken] = useState(false);
const interpolatedAccessTokenUrl = useMemo(() => {
const variables = getAllVariables(collection, item);
return interpolate(accessTokenUrl, variables);
}, [collection, item, accessTokenUrl]);
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedAccessTokenUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
const creds = credentialsData?.credentials || {};
const handleFetchOauth2Credentials = async () => {
let requestCopy = cloneDeep(request);
requestCopy.oauth2 = requestCopy?.auth.oauth2;
requestCopy.headers = {};
toggleFetchingToken(true);
try {
const credentials = await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
toggleFetchingToken(false);
if (credentials?.access_token) {
toast.success('token fetched successfully!');
}
else {
toast.error('An error occured while fetching token!');
}
}
catch (error) {
console.error('could not fetch the token!');
console.error(error);
toggleFetchingToken(false);
toast.error('An error occured while fetching token!');
}
}
const handleRefreshAccessToken = async () => {
let requestCopy = cloneDeep(request);
requestCopy.oauth2 = requestCopy?.auth.oauth2;
requestCopy.headers = {};
toggleRefreshingToken(true);
try {
const credentials = await dispatch(refreshOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
toggleRefreshingToken(false);
if (credentials?.access_token) {
toast.success('token refreshed successfully!');
}
else {
toast.error('An error occured while refreshing token!');
}
}
catch(error) {
console.error(error);
toggleRefreshingToken(false);
toast.error('An error occured while refreshing token!');
}
};
const handleClearCache = (e) => {
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
.then(() => {
toast.success('cleared cache successfully');
})
.catch((err) => {
toast.error(err.message);
});
};
return (
<div className="flex flex-row gap-4 mt-4">
<button onClick={handleFetchOauth2Credentials} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
Get Access Token{fetchingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
</button>
{creds?.refresh_token ? <button onClick={handleRefreshAccessToken} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
Refresh Token{refreshingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
</button> : null}
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
</div>
)
}
export default Oauth2ActionButtons;

View File

@@ -0,0 +1,12 @@
import styled from 'styled-components';
const Wrapper = styled.div`
ol[role="tree"] {
overflow: hidden;
}
ol[role="group"] span {
line-break: anywhere;
}
`;
export default Wrapper;

View File

@@ -0,0 +1,173 @@
import { find } from "lodash";
import StyledWrapper from "./StyledWrapper";
import { useState, useEffect } from "react";
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
import { getAllVariables } from 'utils/collections/index';
import brunoCommon from '@usebruno/common';
const { interpolate } = brunoCommon;
const TokenSection = ({ title, token }) => {
if (!token) return null;
const [isExpanded, setIsExpanded] = useState(false);
const [decodedToken, setDecodedToken] = useState(null);
const [copied, setCopied] = useState(false);
useEffect(() => {
if (token) {
try {
const parts = token.split('.');
if (parts.length === 3) {
const payload = JSON.parse(atob(parts[1]));
setDecodedToken(payload);
}
} catch (err) {
console.error('Error decoding token:', err);
}
}
}, [token]);
const handleCopy = async (text) => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="mb-2 border dark:border-gray-700 rounded-lg overflow-hidden">
<div
className="flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center space-x-2 w-full">
{isExpanded ?
<IconChevronDown size={18} className="text-gray-500" /> :
<IconChevronRight size={18} className="text-gray-500" />
}
<div className="flex flex-row justify-between w-full">
<h3 className="text-sm font-medium">{title}</h3>
{decodedToken?.exp && <ExpiryTimer expiresIn={decodedToken?.exp} />}
</div>
</div>
</div>
{isExpanded && (
<div className="p-3 text-sm">
<div className="relative group">
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleCopy(token)}
className="p-1 bg-indigo-100 dark:hover:bg-indigo-200 rounded"
title="Copy token"
>
{copied ?
<IconCheck size={16} className="text-green-700" /> :
<IconCopy size={16} className="text-gray-500" />
}
</button>
</div>
<div className="font-mono text-xs bg-gray-50 dark:bg-gray-800 p-2 rounded break-all">
{token}
</div>
</div>
{decodedToken && (
<div className="mt-3">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Decoded Payload</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
{Object.entries(decodedToken).map(([key, value]) => (
<div key={key} className="overflow-hidden text-ellipsis">
<span className="font-medium text-xs">{key}: </span>
<span className="text-xs text-gray-600 dark:text-gray-300">
{typeof value === 'object' ? JSON.stringify(value) : value.toString()}
</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
};
const formatExpiryTime = (seconds) => {
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
};
const ExpiryTimer = ({ expiresIn }) => {
if (!expiresIn) return null;
const calculateTimeLeft = () => Math.max(0, Math.floor(expiresIn - Date.now() / 1000));
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft);
useEffect(() => {
setTimeLeft(calculateTimeLeft());
const timer = setInterval(() => {
setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0));
}, 1000);
return () => clearInterval(timer);
}, [expiresIn]);
return (
<div
className={`text-xs px-2 py-1 rounded-full min-w-[120px] text-center ${timeLeft <= 30
? "bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400"
: "bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"
}`}
>
{timeLeft > 0 ? `Expires in ${formatExpiryTime(timeLeft)}` : `Expired`}
</div>
);
};
const Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun }) => {
const { uid: collectionUid } = collection;
const interpolatedUrl = useMemo(() => {
const variables = getAllVariables(collection, item);
return interpolate(url, variables);
}, [collection, item, url]);
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
const creds = credentialsData?.credentials || {};
return (
<StyledWrapper className="relative w-auto h-fit mt-2">
{Object.keys(creds)?.length ? (
creds?.error ? (
<pre className="text-red-600 dark:text-red-400">Error fetching token. Check network logs for more details.</pre>
) : (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-sm">
<TokenSection title="Access Token" token={creds.access_token} />
<TokenSection title="Refresh Token" token={creds.refresh_token} />
<TokenSection title="ID Token" token={creds.id_token} />
{(creds.token_type || creds.scope) ? <div className="mt-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg text-xs">
<div className="grid grid-cols-2 gap-2">
{creds.token_type ? <div className="flex items-center space-x-1">
<span className="font-medium">Token Type:</span>
<span className="text-gray-600 dark:text-gray-300">{creds.token_type}</span>
</div> : null}
{creds?.scope ? <div className="flex items-center space-x-1 min-w-0">
<span className="font-medium flex-shrink-0">Scope:</span>
<span className="text-gray-600 dark:text-gray-300 truncate" title={creds.scope}>
{creds.scope}
</span>
</div> : null}
</div>
</div> : null}
</div>
)
) : (
<div className="text-sm text-gray-500 dark:text-gray-400">No token found</div>
)}
</StyledWrapper>
);
};
export default Oauth2TokenViewer;

View File

@@ -11,6 +11,47 @@ const Wrapper = styled.div`
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
.token-placement-selector {
padding: 0.5rem 0px;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
min-width: 100px;
.dropdown {
width: fit-content;
min-width: 100px;
div[data-tippy-root] {
width: fit-content;
min-width: 100px;
}
.tippy-box {
width: fit-content;
max-width: none !important;
min-width: 100px;
.tippy-content: {
width: fit-content;
max-width: none !important;
min-width: 100px;
}
}
}
.token-placement-label {
width: fit-content;
// color: ${(props) => props.theme.colors.text.yellow};
justify-content: space-between;
padding: 0 0.5rem;
min-width: 100px;
}
.dropdown-item {
padding: 0.2rem 0.6rem !important;
}
}
`;
export default Wrapper;

View File

@@ -1,26 +1,62 @@
import React from 'react';
import React, { useRef, forwardRef } from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { inputsConfig } from './inputsConfig';
import Dropdown from 'components/Dropdown';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
const OAuth2AuthorizationCode = ({ item, collection }) => {
const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
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 oAuth = get(request, 'auth.oauth2', {});
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const {
accessTokenUrl,
username,
password,
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken
} = oAuth;
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
const isAutoRefreshDisabled = !refreshTokenUrlAvailable;
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
const handleSave = () => { save(); }
const TokenPlacementIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
{tokenPlacement == 'url' ? 'URL' : 'Headers'}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const CredentialsPlacementIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-end token-placement-label select-none">
{credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}
<IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
</div>
);
});
const handleChange = (key, value) => {
dispatch(
@@ -36,6 +72,14 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshTokenUrl,
autoRefreshToken,
autoFetchToken,
[key]: value
}
})
@@ -44,12 +88,21 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />
<div className="flex items-center gap-2.5 mt-2">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium">
Configuration
</span>
</div>
{inputsConfig.map((input) => {
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
<div className="single-line-editor-wrapper">
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
<label className="block min-w-[140px]">{label}</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth[key] || ''}
theme={storedTheme}
@@ -64,11 +117,190 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
</div>
);
})}
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
<div className="flex items-center gap-4 w-full" key={`input-credentials-placement`}>
<label className="block min-w-[140px]">Add Credentials to</label>
<div className="inline-flex items-center cursor-pointer token-placement-selector">
<Dropdown onCreate={onDropdownCreate} icon={<CredentialsPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'body');
}}
>
Request Body
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('credentialsPlacement', 'basic_auth_header');
}}
>
Basic Auth Header
</div>
</Dropdown>
</div>
</div>
<div className="flex items-center gap-2.5 mt-2">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconKey size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
Token
</span>
</div>
<div className="flex items-center gap-4 w-full" key={`input-token-name`}>
<label className="block min-w-[140px]">Token ID</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['credentialsId'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('credentialsId', val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
</div>
<div className="flex items-center gap-4 w-full" key={`input-token-placement`}>
<label className="block min-w-[140px]">Add token to</label>
<div className="inline-flex items-center cursor-pointer token-placement-selector">
<Dropdown onCreate={onDropdownCreate} icon={<TokenPlacementIcon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('tokenPlacement', 'header');
}}
>
Header
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
handleChange('tokenPlacement', 'url');
}}
>
URL
</div>
</Dropdown>
</div>
</div>
{
tokenPlacement === 'header' ?
<div className="flex items-center gap-4 w-full" key={`input-token-prefix`}>
<label className="block min-w-[140px]">Header Prefix</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenHeaderPrefix'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenHeaderPrefix', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
:
<div className="flex items-center gap-4 w-full" key={`input-token-query-param-key`}>
<label className="block font-medium min-w-[140px]">Query Param Key</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={oAuth['tokenQueryKey'] || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange('tokenQueryKey', val)}
onRun={handleRun}
collection={collection}
/>
</div>
</div>
}
<div className="flex items-center gap-2.5 mt-4 mb-2">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
Advanced Settings
</span>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Refresh Token URL</label>
<div className="single-line-editor-wrapper flex-1">
<SingleLineEditor
value={get(request, 'auth.oauth2.refreshTokenUrl', '')}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange("refreshTokenUrl", val)}
collection={collection}
item={item}
/>
</div>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Auto-refresh token</label>
<input
type="checkbox"
className="cursor-pointer w-4 h-4 accent-indigo-600"
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
/>
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
</div>
<div className="flex items-center gap-2.5 mt-4">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />
</div>
<span className="text-sm font-medium">Settings</span>
</div>
{/* Automatically Fetch Token */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoFetchToken)}
onChange={(e) => handleChange('autoFetchToken', e.target.checked)}
className="cursor-pointer ml-1"
/>
<label className="block min-w-[140px]">Automatically fetch token if not found</label>
<div className="flex items-center gap-2">
<div className="relative group cursor-pointer">
<IconHelp size={16} className="text-gray-500" />
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
Automatically fetch a new token when you try to access a resource and don't have one.
</span>
</div>
</div>
</div>
{/* Auto Refresh Token (With Refresh URL) */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(autoRefreshToken)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={isAutoRefreshDisabled}
/>
<label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>
<div className="flex items-center gap-2">
<div className="relative group cursor-pointer">
<IconHelp size={16} className="text-gray-500" />
<span className="group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200">
Automatically refresh your token using the refresh URL when it expires.
</span>
</div>
</div>
</div>
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
</StyledWrapper>
);
};
export default OAuth2AuthorizationCode;
export default OAuth2PasswordCredentials;

View File

@@ -5,17 +5,34 @@ import GrantTypeSelector from './GrantTypeSelector/index';
import OAuth2PasswordCredentials from './PasswordCredentials/index';
import OAuth2AuthorizationCode from './AuthorizationCode/index';
import OAuth2ClientCredentials from './ClientCredentials/index';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
const GrantTypeComponentMap = ({ item, collection }) => {
const dispatch = useDispatch();
const save = () => {
dispatch(saveRequest(item.uid, collection.uid));
};
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
const grantType = get(request, 'auth.oauth2.grantType', {});
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const grantTypeComponentMap = (grantType, item, collection) => {
switch (grantType) {
case 'password':
return <OAuth2PasswordCredentials item={item} collection={collection} />;
return <OAuth2PasswordCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
break;
case 'authorization_code':
return <OAuth2AuthorizationCode item={item} collection={collection} />;
return <OAuth2AuthorizationCode item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
break;
case 'client_credentials':
return <OAuth2ClientCredentials item={item} collection={collection} />;
return <OAuth2ClientCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;
break;
default:
return <div>TBD</div>;
@@ -24,12 +41,12 @@ const grantTypeComponentMap = (grantType, item, collection) => {
};
const OAuth2 = ({ item, collection }) => {
const oAuth = item.draft ? get(item, 'draft.request.auth.oauth2', {}) : get(item, 'request.auth.oauth2', {});
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
return (
<StyledWrapper className="mt-2 w-full">
<GrantTypeSelector item={item} collection={collection} />
{grantTypeComponentMap(oAuth?.grantType, item, collection)}
<GrantTypeSelector item={item} request={request} updateAuth={updateAuth} collection={collection} />
<GrantTypeComponentMap item={item} collection={collection} />
</StyledWrapper>
);
};

View File

@@ -10,14 +10,51 @@ import NTLMAuth from './NTLMAuth';
import ApiKeyAuth from './ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAuthMode } from 'utils/collections/index';
import { humanizeRequestAuthMode } from 'utils/collections';
import OAuth2 from './OAuth2/index';
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item?.uid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
const Auth = ({ item, collection }) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
const collectionRoot = get(collection, 'root', {});
const collectionAuth = get(collectionRoot, 'request.auth');
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Check folders in reverse to find the closest auth configuration
for (let i of [...requestTreePath].reverse()) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: i.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const getAuthView = () => {
switch (authMode) {
@@ -46,32 +83,21 @@ const Auth = ({ item, collection }) => {
return <ApiKeyAuth collection={collection} item={item} />;
}
case 'inherit': {
const source = getEffectiveAuthSource();
return (
<div className="flex flex-row w-full mt-2 gap-2">
{collectionAuth?.mode === 'oauth2' ? (
<div className="flex flex-col gap-2">
<div className="flex flex-row gap-1">
<div>Collection level auth is: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
</div>
<div className="text-sm opacity-50">
Note: You need to use scripting to set the access token in the request headers.
</div>
</div>
) : (
<>
<div>Auth inherited from the Collection: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(collectionAuth?.mode)}</div>
</>
)}
</div>
<>
<div className="flex flex-row w-full mt-2 gap-2">
<div>Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
</div>
</>
);
}
}
};
return (
<StyledWrapper className="w-full mt-1">
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
<div className="flex flex-grow justify-start items-center">
<AuthMode item={item} collection={collection} />
</div>

View File

@@ -15,6 +15,7 @@ import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
import { useEffect } from 'react';
const ContentIndicator = () => {
return (
@@ -24,6 +25,14 @@ const ContentIndicator = () => {
);
};
const ErrorIndicator = () => {
return (
<sup className="ml-[.125rem] opacity-80 font-medium text-red-500">
<DotIcon width="10" ></DotIcon>
</sup>
);
};
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
@@ -111,6 +120,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
requestVars.filter((request) => request.enabled).length +
responseVars.filter((response) => response.enabled).length;
useEffect(() => {
if (activeParamsLength === 0 && body.mode !== 'none') {
selectTab('body');
}
}, []);
return (
<StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center tabs" role="tablist">
@@ -136,7 +151,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
{(script.req || script.res) && <ContentIndicator />}
{(script.req || script.res) && (
item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage ?
<ErrorIndicator /> :
<ContentIndicator />
)}
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert

View File

@@ -109,7 +109,7 @@ export default class QueryEditor extends React.Component {
this.props.onPrettifyQuery();
}
},
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Pretiffy */
/* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Prettify */
'Shift-Ctrl-F': () => {
if (this.props.onPrettifyQuery) {
this.props.onPrettifyQuery();

View File

@@ -176,8 +176,7 @@ const QueryParams = ({ item, collection }) => {
</button>
<div className="mb-2 title text-xs flex items-stretch">
<span>Path</span>
<InfoTip
text={`
<InfoTip infotipId="path-param-InfoTip">
<div>
Path variables are automatically added whenever the
<code className="font-mono mx-2">:name</code>
@@ -186,9 +185,7 @@ const QueryParams = ({ item, collection }) => {
https://example.com/v1/users/<span>:id</span>
</code>
</div>
`}
infotipId="path-param-InfoTip"
/>
</InfoTip>
</div>
<table>
<thead>

View File

@@ -98,7 +98,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
) : (
<div className="flex items-center">
<span>Expr</span>
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
<InfoTip content="You can write any valid JS expression here" infotipId="response-var" />
</div>
), accessor: 'value', width: '46%' },
{ name: '', accessor: '', width: '14%' }

View File

@@ -1,4 +1,4 @@
import { IconLoader2, IconFile } from '@tabler/icons';
import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons';
import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
@@ -15,65 +15,59 @@ const RequestNotLoaded = ({ collection, item }) => {
return <StyledWrapper>
<div className='flex flex-col p-4'>
<div className='card shadow-sm rounded-md p-4 w-[600px]'>
<div className='card shadow-sm rounded-md p-4 w-[685px]'>
<div>
<div className='font-medium flex items-center gap-2 pb-4'>
<IconFile size={16} strokeWidth={1.5} className="text-gray-400" />
File Info
</div>
<div className='hr'/>
<div className='hr' />
<div className='flex items-center mt-2'>
<span className='w-12 mr-2 text-muted'>Name:</span>
<div>{item?.name}</div>
</div>
<div className='flex items-center mt-1'>
<span className='w-12 mr-2 text-muted'>Path:</span>
<div className='break-all'>{item?.pathname}</div>
</div>
<div className='flex items-center mt-1 pb-4'>
<span className='w-12 mr-2 text-muted'>Size:</span>
<div>{item?.size?.toFixed?.(2)} MB</div>
</div>
{!item?.error && (
<>
<div className='hr'/>
<div className='text-muted text-xs mt-4 mb-2'>
Due to its large size, this request wasn't loaded automatically.
<div className='flex flex-col'>
<div className='flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20'>
<IconAlertTriangle size={16} className="text-yellow-500" />
<span>The request wasn't loaded due to its large size. Please try again with the following options:</span>
</div>
<div className='flex flex-col gap-6 mt-4'>
<div className='flex flex-col'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequest}
>
Load Request
</button>
<small className='text-muted mt-1'>
May cause the app to freeze temporarily while it runs.
</small>
</div>
<div className='flex flex-col'>
<div className='flex flex-row mt-6 gap-2 items-center w-full'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequestViaWorker}
>
Load Request in Background
Load in background
</button>
<small className='text-muted mt-1'>
Runs in background.
</small>
</div>
<p>(Runs in background)</p>
</div>
</>
<div className='flex flex-row mt-6 items-center gap-2 w-full'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequest}
>
Force load
</button>
<p>(May cause the app to freeze temporarily while it runs)</p>
</div>
</div>
)}
{item?.loading && (
<>
<div className='hr mt-4'/>
<div className='hr mt-4' />
<div className='flex items-center gap-2 mt-4'>
<IconLoader2 className="animate-spin" size={16} strokeWidth={2} />
<span>Loading...</span>

View File

@@ -35,7 +35,7 @@ const CollectionToolBar = ({ collection }) => {
const viewCollectionSettings = () => {
dispatch(
addTab({
uid: uuid(),
uid: collection.uid,
collectionUid: collection.uid,
type: 'collection-settings'
})

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