Compare commits

...

32 Commits

Author SHA1 Message Date
lohxt1
bad5f0003c Merge remote-tracking branch 'upstream/main' into feat/oauth2-improvements 2025-03-19 18:03:58 +05:30
lohxt1
ee5e260890 postman import useBrowser prop check 2025-03-19 17:44:33 +05:30
lohxt1
c3f8959c54 updates 2025-03-19 17:09:40 +05:30
lohxt1
cabd520ef1 authorize in browser - updates 2025-03-19 17:05:33 +05:30
lohxt1
0e7b0a0ce5 option for oauth2 authroization using default browser 2025-03-19 16:20:52 +05:30
lohxt1
d3fcb42a8f timeline ui updates wip 2025-03-17 14:09:36 +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
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
pooja-bruno
4e88cbf318 feat: add refresh url for oauth2 (#4028) 2025-02-18 17:24:56 +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
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
103 changed files with 6351 additions and 1449 deletions

630
package-lock.json generated
View File

@@ -18,6 +18,10 @@
"packages/bruno-toml",
"packages/bruno-graphql-docs"
],
"dependencies": {
"find-process": "^1.4.10",
"pid-port": "^1.0.2"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
@@ -50,7 +54,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -1471,7 +1474,6 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
@@ -1502,7 +1504,6 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -1520,7 +1521,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/@babel/generator": {
@@ -1800,7 +1800,6 @@
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.25.9",
@@ -6831,6 +6830,12 @@
}
}
},
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"license": "MIT"
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -6851,6 +6856,18 @@
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
@@ -7767,7 +7784,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
@@ -7780,7 +7796,6 @@
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/linkify-it": "*",
@@ -7791,7 +7806,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
@@ -8291,6 +8305,15 @@
"node": ">=0.4.0"
}
},
"node_modules/address": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/address/-/address-2.0.3.tgz",
"integrity": "sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==",
"license": "MIT",
"engines": {
"node": ">= 16.0.0"
}
},
"node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
@@ -11028,7 +11051,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -11266,7 +11288,6 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -11913,6 +11934,22 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/detect-port": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/detect-port/-/detect-port-2.1.0.tgz",
"integrity": "sha512-epZuWb/6Q62L+nDHJc/hQAqf8pylsqgk3BpZXVBx1CDnr3nkrVNn73Uu1rXcFzkNcc+hkP3whuOg7JZYaQB65Q==",
"license": "MIT",
"dependencies": {
"address": "^2.0.1"
},
"bin": {
"detect": "dist/commonjs/bin/detect-port.js",
"detect-port": "dist/commonjs/bin/detect-port.js"
},
"engines": {
"node": ">= 16.0.0"
}
},
"node_modules/dev-null": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz",
@@ -12639,7 +12676,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -13017,6 +13053,52 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express-basic-auth": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz",
@@ -13026,6 +13108,30 @@
"basic-auth": "^2.0.1"
}
},
"node_modules/express/node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -13195,6 +13301,33 @@
"pend": "~1.2.0"
}
},
"node_modules/figures": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
"license": "MIT",
"dependencies": {
"is-unicode-supported": "^2.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/figures/node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/file": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/file/-/file-0.2.2.tgz",
@@ -13296,6 +13429,45 @@
"node": ">= 0.8"
}
},
"node_modules/find-process": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz",
"integrity": "sha512-ncYFnWEIwL7PzmrK1yZtaccN8GhethD37RzBHG6iOZoFYB4vSmLLXfeWJjeN5nMvCJMjOtBvBBF8OgxEcikiZg==",
"license": "MIT",
"dependencies": {
"chalk": "~4.1.2",
"commander": "^12.1.0",
"loglevel": "^1.9.2"
},
"bin": {
"find-process": "bin/find-process.js"
}
},
"node_modules/find-process/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/find-process/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -13607,7 +13779,6 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -15076,6 +15247,18 @@
"node": ">=8"
}
},
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -15199,7 +15382,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/isobject": {
@@ -16943,6 +17125,19 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -17423,7 +17618,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true,
"license": "MIT"
},
"node_modules/merge2": {
@@ -18636,6 +18830,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse-ms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
@@ -18803,7 +19009,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -18849,6 +19054,12 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/path/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
@@ -18971,6 +19182,155 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pid-port": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pid-port/-/pid-port-1.0.2.tgz",
"integrity": "sha512-Khqp07zX8IJpmIg56bHrLxS3M0iSL4cq6wnMq8YE7r/hSw3Kn4QxYS6QJg8Bs22Z7CSVj7eSsxFuigYVIFWmjg==",
"license": "MIT",
"dependencies": {
"execa": "^8.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pid-port/node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^8.0.1",
"human-signals": "^5.0.0",
"is-stream": "^3.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^5.1.0",
"onetime": "^6.0.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^3.0.0"
},
"engines": {
"node": ">=16.17"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/pid-port/node_modules/get-stream": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pid-port/node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/pid-port/node_modules/is-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pid-port/node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pid-port/node_modules/npm-run-path": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
"license": "MIT",
"dependencies": {
"path-key": "^4.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pid-port/node_modules/onetime": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
"license": "MIT",
"dependencies": {
"mimic-fn": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pid-port/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pid-port/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/pid-port/node_modules/strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
@@ -20042,6 +20402,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/pretty-ms": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
"integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
"license": "MIT",
"dependencies": {
"parse-ms": "^4.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pretty-quick": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.3.1.tgz",
@@ -22393,7 +22768,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -22406,7 +22780,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -24167,7 +24540,7 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -24239,6 +24612,18 @@
"node": ">=4"
}
},
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unique-filename": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
@@ -24755,7 +25140,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -25005,6 +25389,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yoctocolors": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
"integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yup": {
"version": "0.32.11",
"resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz",
@@ -26293,11 +26689,15 @@
"chokidar": "^3.5.3",
"content-disposition": "^0.5.4",
"decomment": "^0.9.5",
"detect-port": "^2.1.0",
"dotenv": "^16.0.3",
"electron-is-dev": "^2.0.0",
"electron-notarize": "^1.2.2",
"electron-store": "^8.1.0",
"electron-util": "^0.17.2",
"execa": "^9.5.2",
"express": "^4.21.2",
"find-process": "^1.4.10",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"graphql": "^16.6.0",
@@ -26309,6 +26709,7 @@
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"nanoid": "3.3.8",
"pid-port": "^1.0.2",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^4.1.3",
@@ -27362,6 +27763,32 @@
"proxy-from-env": "^1.1.0"
}
},
"packages/bruno-electron/node_modules/execa": {
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
"integrity": "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==",
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^4.0.0",
"cross-spawn": "^7.0.3",
"figures": "^6.1.0",
"get-stream": "^9.0.0",
"human-signals": "^8.0.0",
"is-plain-obj": "^4.1.0",
"is-stream": "^4.0.1",
"npm-run-path": "^6.0.0",
"pretty-ms": "^9.0.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^4.0.0",
"yoctocolors": "^2.0.0"
},
"engines": {
"node": "^18.19.0 || >=20.5.0"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"packages/bruno-electron/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -27376,6 +27803,43 @@
"node": ">=12"
}
},
"packages/bruno-electron/node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
"license": "MIT",
"dependencies": {
"@sec-ant/readable-stream": "^0.4.1",
"is-stream": "^4.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/bruno-electron/node_modules/human-signals": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz",
"integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"packages/bruno-electron/node_modules/is-stream": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/bruno-electron/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
@@ -27394,6 +27858,58 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"packages/bruno-electron/node_modules/npm-run-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
"license": "MIT",
"dependencies": {
"path-key": "^4.0.0",
"unicorn-magic": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/bruno-electron/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/bruno-electron/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"packages/bruno-electron/node_modules/strip-final-newline": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/bruno-graphql-docs": {
"name": "@usebruno/graphql-docs",
"version": "0.1.0",
@@ -27598,61 +28114,6 @@
"proxy-from-env": "^1.1.0"
}
},
"packages/bruno-tests/node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"packages/bruno-tests/node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"packages/bruno-tests/node_modules/fast-xml-parser": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.0.9.tgz",
@@ -27671,27 +28132,6 @@
"fxparser": "src/cli/cli.js"
}
},
"packages/bruno-tests/node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"packages/bruno-tests/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"packages/bruno-tests/node_modules/strnum": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.0.5.tgz",

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,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)}
</StyledWrapper>
);
};

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}
folder={folder}
/>
{grantTypeComponentMap(collection, folder)}
</>
);
}
case 'none': {
return null;
}
default:
return null;
}
};
return (
<StyledWrapper className="w-full">
<div className="text-xs mb-4 text-muted">
Configures authentication for the entire folder. This applies to all requests using the{' '}
<span className="font-medium">Inherit</span> option in the <span className="font-medium">Auth</span> tab.
</div>
<div className="flex flex-grow justify-start items-center mb-4">
<AuthMode collection={collection} folder={folder} />
</div>
{getAuthView()}
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};
export default Auth;

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

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

@@ -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,112 @@
import React from 'react';
import React, { useRef, forwardRef, useState, useEffect } from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { IconCaretDown, IconLoader2, IconSettings, IconKey, 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 { clearOauth2Cache, fetchOauth2Credentials, refreshOauth2Credentials } 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 { cloneDeep, find } from 'lodash';
import { interpolateStringUsingCollectionAndItem } from 'utils/collections/index';
import { BRUNO_OAUTH2_SERVER_CALLBACK_URL } from 'utils/common/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 [fetchingToken, toggleFetchingToken] = useState(false);
const [refreshingToken, toggleRefreshingToken] = useState(false);
const [showRefreshButton, setShowRefreshButton] = useState(false);
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,
refreshUrl,
autoRefreshToken,
autoFetchToken,
authorizeInDefaultBrowser
} = oAuth;
const handleRun = async () => {
dispatch(sendRequest(item, collection.uid));
};
const refreshUrlAvailable = refreshUrl?.trim() !== '';
const isAutoRefreshDisabled = !refreshUrlAvailable;
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
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 handleFetchOauth2Credentials = async () => {
let requestCopy = cloneDeep(request);
requestCopy.oauth2 = requestCopy?.auth.oauth2;
requestCopy.headers = {};
toggleFetchingToken(true);
try {
await dispatch(fetchOauth2Credentials({
itemUid: item.uid,
request: requestCopy,
collection,
folderUid: folder?.uid || null
}));
toggleFetchingToken(false);
toast.success('token fetched successfully!');
}
catch (error) {
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 {
await dispatch(refreshOauth2Credentials({ request: requestCopy, collection }));
toggleRefreshingToken(false);
toast.success('token refreshed successfully!');
}
catch (error) {
console.error(error);
toggleRefreshingToken(false);
toast.error('An error occured while refreshing token!');
}
}
const handleSave = () => { save(); };
const handleChange = (key, value) => {
dispatch(
@@ -40,7 +124,16 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
state,
scope,
pkce,
[key]: value
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshUrl,
autoRefreshToken,
autoFetchToken,
authorizeInDefaultBrowser,
[key]: value,
}
})
);
@@ -61,6 +154,13 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
clientSecret,
state,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
autoFetchToken,
authorizeInDefaultBrowser,
pkce: !Boolean(oAuth?.['pkce'])
}
})
@@ -68,7 +168,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
};
const handleClearCache = (e) => {
clearOauth2Cache(collection?.uid)
const interpolatedAccessTokenUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
.then(() => {
toast.success('cleared cache successfully');
})
@@ -77,16 +178,58 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
});
};
const { uid: collectionUid } = collection;
const interpolatedUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
const credentialsData = find(collection?.oauth2Credentials, creds => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);
const creds = credentialsData?.credentials || {};
useEffect(() => {
// Update visibility whenever credentials change
setShowRefreshButton(Boolean(creds?.refresh_token && creds?.access_token));
}, [creds?.refresh_token, creds?.access_token, credentialsData]);
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>
{/* Authorize in default browser */}
<div className="flex items-center gap-4 w-full">
<input
type="checkbox"
checked={Boolean(authorizeInDefaultBrowser)}
onChange={(e) => handleChange('authorizeInDefaultBrowser', e.target.checked)}
className={`cursor-pointer ml-1`}
/>
<label className={`block min-w-[140px]`}>Authorize In Default Browser</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">
Use the default browser to authorize the user.
</span>
</div>
</div>
</div>
{inputsConfig.map((input) => {
const { key, label, isSecret } = input;
let { key, label, isSecret } = input;
let value = oAuth[key] || '';
let shouldAuthorizeInDefaultBrowser = key == 'callbackUrl' && Boolean(authorizeInDefaultBrowser);
if (shouldAuthorizeInDefaultBrowser) {
value = BRUNO_OAUTH2_SERVER_CALLBACK_URL;
}
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] || ''}
value={value}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange(key, val)}
@@ -94,13 +237,39 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
collection={collection}
item={item}
isSecret={isSecret}
readOnly={shouldAuthorizeInDefaultBrowser}
/>
</div>
</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,10 +277,160 @@ 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
<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.refreshUrl', '')}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange("refreshUrl", 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>
<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>
{showRefreshButton && (
<button onClick={handleRefreshAccessToken} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
Refresh Token{refreshingToken ? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
</button>
)}
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
@@ -120,4 +439,4 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
);
};
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,100 @@
import React from 'react';
import React, { useRef, forwardRef, useState } from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconAdjustmentsHorizontal, 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 { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } 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 toast from 'react-hot-toast';
import { cloneDeep } from 'lodash';
import { interpolateStringUsingCollectionAndItem } from 'utils/collections/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 [fetchingToken, toggleFetchingToken] = useState(false);
const [refreshingToken, toggleRefreshingToken] = useState(false);
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,
refreshUrl,
autoRefreshToken,
autoFetchToken
} = oAuth;
const refreshUrlAvailable = refreshUrl?.trim() !== '';
const isAutoRefreshDisabled = !refreshUrlAvailable;
const handleFetchOauth2Credentials = async () => {
let requestCopy = cloneDeep(request);
requestCopy.oauth2 = requestCopy?.auth.oauth2;
requestCopy.headers = {};
toggleFetchingToken(true);
try {
await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
toggleFetchingToken(false);
toast.success('Token fetched successfully!');
}
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 {
await dispatch(refreshOauth2Credentials({ request: requestCopy, collection }));
toggleRefreshingToken(false);
toast.success('token refreshed successfully!');
}
catch(error) {
console.error(error);
toggleRefreshingToken(false);
toast.error('An error occured while refreshing token!');
}
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleSave = () => { save(); };
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
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,20 +108,48 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshUrl,
autoRefreshToken,
autoFetchToken,
[key]: value
}
})
);
};
const handleClearCache = (e) => {
const interpolatedAccessTokenUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
.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}
@@ -62,9 +164,199 @@ 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.refreshUrl', '')}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange("refreshUrl", 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>
<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>
<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>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
</div>
</StyledWrapper>
);
};

View File

@@ -2,12 +2,13 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
display: block;
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
textarea {
height: fit-content;
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};
}

View File

@@ -0,0 +1,80 @@
import React, { useEffect, useState } from 'react';
import { readOauth2CachedCredentials } from 'utils/network';
import { sendCollectionOauth2Request, sendRequest, clearOauth2Cache } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const CredentialsPreview = ({ item, collection }) => {
const oauth2CredentialsAreaRef = React.createRef();
const [oauth2Credentials, setOauth2Credentials] = useState({});
const dispatch = useDispatch();
useEffect(() => {
oauth2CredentialsAreaRef.current.value = oauth2Credentials;
readOauth2CachedCredentials(collection.uid).then((credentials) => setOauth2Credentials(credentials));
}, [oauth2CredentialsAreaRef]);
const handleRun = async () => {
if (item) {
dispatch(sendRequest(item, collection.uid));
} else {
dispatch(sendCollectionOauth2Request(collection.uid));
}
};
const handleClearCache = (e) => {
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: '' }))
.then(() => {
readOauth2CachedCredentials(collection.uid).then((credentials) => {
setOauth2Credentials(credentials);
toast.success('Cleared cache successfully');
});
})
.catch((err) => {
toast.error(err.message);
});
};
const sortedFields = () => {
const tokens = {};
const extras = {};
Object.entries(oauth2Credentials).forEach(([key, value]) => {
if (key.endsWith('_token')) {
tokens[key] = value;
} else {
extras[key] = value;
}
});
return { ...tokens, ...extras };
};
return (
<StyledWrapper className="flex flex-col w-full gap-1 mt-4">
<div className="credential-item-wrapper" ref={oauth2CredentialsAreaRef}>
{Object.entries(oauth2Credentials).length > 0 ? (
<>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Access Token Cache
</button>
<details className="cursor-pointer flex flex-row w-full mt-2 gap-2">
<summary>Cached OAuth2 Credentials</summary>
{Object.entries(sortedFields()).map(([field, value]) => (
<div key={field}>
<label className="text-xs">{field}</label>
<textarea className="w-full h-24 p-2 text-xs border rounded" value={value} readOnly />
</div>
))}
</details>
</>
) : (
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
Get Access Token
</button>
)}
</div>
</StyledWrapper>
);
};
export default CredentialsPreview;

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,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,162 @@
import { find } from "lodash";
import { interpolateStringUsingCollectionAndItem } from "utils/collections/index";
import StyledWrapper from "./StyledWrapper";
import { useState, useEffect } from "react";
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
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 = interpolateStringUsingCollectionAndItem({ collection, item, string: 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 ? (
<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,101 @@
import React from 'react';
import React, { useRef, forwardRef, useState } from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconAdjustmentsHorizontal, 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 { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } 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 toast from 'react-hot-toast';
import { interpolateStringUsingCollectionAndItem } from 'utils/collections/index';
import { cloneDeep } from 'lodash';
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 [fetchingToken, toggleFetchingToken] = useState(false);
const [refreshingToken, toggleRefreshingToken] = useState(false);
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,
refreshUrl,
autoRefreshToken,
autoFetchToken
} = oAuth;
const refreshUrlAvailable = refreshUrl?.trim() !== '';
const isAutoRefreshDisabled = !refreshUrlAvailable;
const handleFetchOauth2Credentials = async () => {
let requestCopy = cloneDeep(request);
requestCopy.oauth2 = requestCopy?.auth.oauth2;
requestCopy.headers = {};
toggleFetchingToken(true);
try {
await dispatch(fetchOauth2Credentials({ itemUid: item.uid, request: requestCopy, collection }));
toggleFetchingToken(false);
toast.success('Token fetched successfully!');
}
catch (error) {
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 {
await dispatch(refreshOauth2Credentials({ request: requestCopy, collection }));
toggleRefreshingToken(false);
toast.success('token refreshed successfully!');
}
catch(error) {
console.error(error);
toggleRefreshingToken(false);
toast.error('An error occured while refreshing token!');
}
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleSave = () => { save(); }
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
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,20 +111,48 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
tokenPlacement,
tokenHeaderPrefix,
tokenQueryKey,
refreshUrl,
autoRefreshToken,
autoFetchToken,
[key]: value
}
})
);
};
const handleClearCache = (e) => {
const interpolatedAccessTokenUrl = interpolateStringUsingCollectionAndItem({ collection, item, string: accessTokenUrl });
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))
.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}
@@ -64,11 +167,201 @@ 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.refreshUrl', '')}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleChange("refreshUrl", 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>
<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>
<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>
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
Clear Cache
</button>
</div>
</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, 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

@@ -139,7 +139,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
return (
<StyledWrapper
className="w-full h-full relative"
className="w-full h-full relative flex"
style={{ maxWidth: width }}
queryFilterEnabled={queryFilterEnabled}
>

View File

@@ -0,0 +1,24 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.line {
white-space: pre-line;
word-wrap: break-word;
word-break: break-all;
font-family: Inter, sans-serif !important;
.arrow {
opacity: 0.5;
}
&.request {
color: ${(props) => props.theme.colors.text.green};
}
&.response {
color: ${(props) => props.theme.colors.text.purple};
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,61 @@
import React from 'react';
import forOwn from 'lodash/forOwn';
import { safeStringifyJSON } from 'utils/common';
import StyledWrapper from './StyledWrapper';
const RunnerTimeline = ({ request, response }) => {
const requestHeaders = [];
const responseHeaders = typeof response.headers === 'object' ? Object.entries(response.headers) : [];
request = request || {};
response = response || {};
forOwn(request.headers, (value, key) => {
requestHeaders.push({
name: key,
value
});
});
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
return (
<StyledWrapper className="pb-4 w-full">
<div>
<pre className="line request font-bold">
<span className="arrow">{'>'}</span> {request.method} {request.url}
</pre>
{requestHeaders.map((h) => {
return (
<pre className="line request" key={h.name}>
<span className="arrow">{'>'}</span> {h.name}: {h.value}
</pre>
);
})}
{requestData ? (
<pre className="line request">
<span className="arrow">{'>'}</span> data{' '}
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
</pre>
) : null}
</div>
<div className="mt-4">
<pre className="line response font-bold">
<span className="arrow">{'<'}</span> {response.status} - {response.statusText}
</pre>
{responseHeaders.map((h) => {
return (
<pre className="line response" key={h[0]}>
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
</pre>
);
})}
</div>
</StyledWrapper>
);
};
export default RunnerTimeline;

View File

@@ -1,11 +1,109 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.timeline-event {
padding: 8px 0 0 0;
cursor: pointer;
}
.timeline-event-content {
border-radius: 4px;
padding: 12px;
margin-top: 0.5rem;
}
.timeline-event-header {
color: ${(props) => props.theme.text};
}
.method-label {
font-weight: 600;
}
.status-code {
font-weight: 600;
}
.url-text {
color: ${(props) => props.theme.colors.text.muted};
font-size: 0.875rem;
margin-top: 0.25rem;
}
.timestamp {
color: ${(props) => props.theme.colors.text.muted};
font-size: 0.875rem;
}
.meta-info {
color: ${(props) => props.theme.colors.text.muted};
font-size: 0.875rem;
}
.oauth-section {
.oauth-header {
display: flex;
align-items: center;
color: ${(props) => props.theme.text};
font-weight: 600;
span {
margin-left: 0.5rem;
}
}
}
.tabs-switcher {
border-bottom: 1px solid ${(props) => props.theme.modal.input.border};
margin-bottom: 16px;
button {
position: relative;
padding: 8px 16px;
color: ${(props) => props.theme.colors.text.muted};
&.active {
color: ${(props) => props.theme.tabs.active.color};
&:after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: ${(props) => props.theme.tabs.active.border};
}
}
}
}
.network-logs {
background: ${(props) => props.theme.codemirror.bg};
color: ${(props) => props.theme.text};
border-radius: 4px;
}
.oauth-request-item-content {
border-radius: 4px;
margin-top: 0.5rem;
}
.collapsible-section {
margin-bottom: 12px;
.section-header {
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
}
.line {
white-space: pre-line;
word-wrap: break-word;
word-break: break-all;
font-family: Inter, sans-serif !important;
font-family: ${(props) => props.theme.font || 'Inter, sans-serif'} !important;
.arrow {
opacity: 0.5;
@@ -19,6 +117,35 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.colors.text.purple};
}
}
.request-label {
font-size: 0.75rem;
padding: 2px 6px;
border-radius: 3px;
margin-left: 8px;
background: ${(props) => props.theme.requestTabs.bg};
}
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
table-layout: fixed;
thead,
td {
border: 1px solid ${(props) => props.theme.table.border};
}
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
user-select: none;
}
td {
padding: 6px 10px;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,37 @@
import QueryResult from "components/ResponsePane/QueryResult/index";
import { useState } from "react";
const BodyBlock = ({ collection, data, dataBuffer, headers, error, item, width }) => {
const [isBodyCollapsed, toggleBody] = useState(true);
return (
<div className="collapsible-section">
<div className="section-header" onClick={() => toggleBody(!isBodyCollapsed)}>
<pre className="flex flex-row items-center text-lg text-indigo-500/80 dark:text-indigo-500/80">
<div className="opacity-70">{isBodyCollapsed ? '▼' : '▶'}</div> Body
</pre>
</div>
{isBodyCollapsed && (
<div className="mt-2">
{data || dataBuffer ? (
<div className="h-96 overflow-auto">
<QueryResult
item={item}
collection={collection}
width={width}
data={data}
dataBuffer={dataBuffer}
headers={headers}
error={error}
key={item?.uid}
/>
</div>
) : (
<div className="text-gray-500">No Body found</div>
)}
</div>
)}
</div>
)
}
export default BodyBlock;

View File

@@ -0,0 +1,54 @@
import { useState } from "react";
const HeadersBlock = ({ headers, type }) => {
const [areHeadersCollapsed, toggleHeaders] = useState(true);
return (
<div className="collapsible-section mt-2">
<div className="section-header" onClick={() => toggleHeaders(!areHeadersCollapsed)}>
<pre className="flex flex-row items-center text-lg text-indigo-500/80 dark:text-indigo-500/80">
<div className="opacity-70">{areHeadersCollapsed ? '▼' : '▶'}</div> Headers
{headers && Object.keys(headers).length > 0 &&
<div className="ml-1">({Object.keys(headers).length})</div>
}
</pre>
</div>
{areHeadersCollapsed && (
<div className="mt-1">
{headers && Object.keys(headers).length > 0
? <Headers headers={headers} type={type} />
: <div className="text-gray-500">No Headers found</div>
}
</div>
)}
</div>
)
};
const Headers = ({ headers, type }) => {
if (Array.isArray(headers)) {
return (
<div className="mt-1 text-sm">
{headers.map((header, index) => (
<pre key={index} className="mb-1 whitespace-pre-wrap">
{type === 'request' ? '>' : '<'}&nbsp;<span className="opacity-60">{header?.name}:</span>
<span className="whitespace-pre-wrap">{String(header?.value)}</span>
</pre>
))}
</div>
);
} else {
return (
<div className="mt-1 text-sm">
{Object.entries(headers).map(([key, value], index) => (
<pre key={index} className="mb-1 whitespace-pre-wrap">
{type === 'request' ? '>' : '<'}&nbsp;<span className="opacity-60">{key}:</span>
<span>{String(value)}</span>
</pre>
))}
</div>
);
}
};
export default HeadersBlock;

View File

@@ -0,0 +1,19 @@
const Method = ({ method }) => {
return (
<span className={`${methodColors[method?.toUpperCase()] || 'text-white'} font-bold`}>
{method?.toUpperCase()}
</span>
)
}
const methodColors = {
GET: 'text-green-500',
POST: 'text-blue-500',
PUT: 'text-yellow-500',
DELETE: 'text-red-500',
PATCH: 'text-purple-500',
OPTIONS: 'text-gray-500',
HEAD: 'text-gray-500',
};
export default Method;

View File

@@ -0,0 +1,26 @@
const Status = ({ statusCode, statusText }) => {
return (
<span
className={`${
statusColor(statusCode) || 'text-white'
} font-bold`}
>
{statusCode}{' '}
{statusText || ''}
</span>
)
}
const statusColor = (statusCode) => {
if (statusCode >= 200 && statusCode < 300) {
return 'text-green-500';
} else if (statusCode >= 300 && statusCode < 400) {
return 'text-yellow-500';
} else if (statusCode >= 400 && statusCode < 600) {
return 'text-red-500';
} else {
return 'text-gray-500';
}
};
export default Status;

View File

@@ -0,0 +1,36 @@
import { useState, useEffect } from "react";
const getRelativeTime = (date) => {
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
const diff = (date - new Date()) / 1000;
const timeUnits = [
{ unit: 'year', seconds: 31536000 },
{ unit: 'month', seconds: 2592000 },
{ unit: 'week', seconds: 604800 },
{ unit: 'day', seconds: 86400 },
{ unit: 'hour', seconds: 3600 },
{ unit: 'minute', seconds: 60 },
{ unit: 'second', seconds: 1 }
];
for (const { unit, seconds } of timeUnits) {
if (Math.abs(diff) >= seconds || unit === 'second') {
return rtf.format(Math.round(diff / seconds), unit);
}
}
};
export const RelativeTime = ({ timestamp }) => {
const [relativeTime, setRelativeTime] = useState(getRelativeTime(new Date(timestamp)));
useEffect(() => {
const interval = setInterval(() => {
setRelativeTime(getRelativeTime(new Date(timestamp)));
}, 1000);
return () => clearInterval(interval);
}, [timestamp]);
return <pre className="text-xs">{relativeTime}</pre>;
};

View File

@@ -0,0 +1,46 @@
const Network = ({ logs }) => {
return (
<div className="bg-black/5 text-white network-logs rounded overflow-auto h-96">
<pre className="whitespace-pre-wrap">
{logs.map((entry, index) => (
<NetworkLogsEntry key={index} entry={entry} />
))}
</pre>
</div>
)
}
const NetworkLogsEntry = ({ entry }) => {
const { type, message } = entry;
let className = '';
switch (type) {
case 'request':
className = 'text-blue-500';
break;
case 'response':
className = 'text-green-500';
break;
case 'error':
className = 'text-red-500';
break;
case 'tls':
className = 'text-purple-500';
break;
case 'info':
className = 'text-yellow-500';
break;
default:
className = 'text-gray-400';
break;
}
return (
<div className={`${className}`}>
<div>{message}</div>
</div>
);
};
export default Network;

View File

@@ -0,0 +1,23 @@
import Headers from "../Common/Headers/index";
import BodyBlock from "../Common/Body/index";
const Request = ({ collection, request, item, width }) => {
const { url, headers, data, dataBuffer, error } = request || {};
return (
<div>
{/* Method and URL */}
<div className="mb-1 flex gap-2">
<pre className="whitespace-pre-wrap">{url}</pre>
</div>
{/* Headers */}
<Headers headers={headers} type={'request'} />
{/* Body */}
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
</div>
)
}
export default Request;

View File

@@ -0,0 +1,26 @@
import BodyBlock from "../Common/Body/index";
import Headers from "../Common/Headers/index";
import Status from "../Common/Status/index";
const Response = ({ collection, response, item, width }) => {
const { status, statusCode, statusText, headers, data, dataBuffer, error } = response || {};
return (
<div>
{/* Status */}
<div className="mb-1">
<Status statusCode={status || statusCode} statusText={statusText} />
{response.duration && <span className="text-sm text-gray-400 ml-2">{response.duration}ms</span>}
{response.size && <span className="text-sm text-gray-400 ml-2">{response.size}B</span>}
</div>
{/* Headers */}
<Headers headers={headers} type={'response'} />
{/* Body */}
<BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} width={width} />
</div>
)
}
export default Response;

View File

@@ -0,0 +1,79 @@
import { useState } from "react";
import Network from "./Network/index";
import Request from "./Request/index";
import Response from "./Response/index";
import Method from "./Common/Method/index";
import Status from "./Common/Status/index";
import { RelativeTime } from "./Common/Time/index";
const TimelineItem = ({ timestamp, request, response, item, collection, width, isOauth2 }) => {
const [isCollapsed, _toggleCollapse] = useState(false);
const [activeTab, setActiveTab] = useState('request');
const toggleCollapse = () => _toggleCollapse(prev => !prev);
const { method, status, statusCode, statusText, url = '' } = request || {};
const showNetworkLogs = response.timeline && response.timeline.length > 0;
return (
<div className={`border-b-2 ${isOauth2 ? 'border-indigo-700/50' : 'border-amber-700/50' } py-2`}>
<div className="oauth-request-item-header cursor-pointer" onClick={toggleCollapse}>
<div className="flex justify-between items-center min-w-0">
<div className="flex items-center space-x-2 min-w-0">
<Method method={method} />
<Status statusCode={status || statusCode} statusText={statusText} />
{isOauth2 ? <pre className="opacity-50">[oauth2.0]</pre> : null}
<pre className="opacity-70">[{new Date(timestamp).toISOString()}]</pre>
</div>
<span className="text-sm text-gray-400 flex-shrink-0 overflow-hidden text-ellipsis whitespace-nowrap">
<RelativeTime timestamp={timestamp} />
</span>
</div>
<div className="truncate text-sm mt-1">{url}</div>
</div>
{isCollapsed && (<div className="text-sm overflow-hidden">
{/* Tabs */}
<div className="tabs-switcher flex mb-4">
<button
className={`mr-4 ${activeTab === 'request' ? 'active' : 'text-gray-400'}`}
onClick={() => setActiveTab('request')}
>
Request
</button>
<button
className={`mr-4 ${activeTab === 'response' ? 'active' : 'text-gray-400'}`}
onClick={() => setActiveTab('response')}
>
Response
</button>
{showNetworkLogs && (
<button
className={`${activeTab === 'networkLogs' ? 'active' : 'text-gray-400'}`}
onClick={() => setActiveTab('networkLogs')}
>
Network Logs
</button>
)}
</div>
{/* Tab Content */}
<div className="tab-content">
{/* Request Tab */}
{activeTab === 'request' && (
<Request request={request} item={item} collection={collection} width={width} />
)}
{/* Response Tab */}
{activeTab === 'response' && (
<Response response={response} item={item} collection={collection} width={width} />
)}
{/* Network Logs Tab */}
{activeTab === 'networkLogs' && showNetworkLogs && (
<Network logs={response?.timeline} />
)}
</div>
</div>)}
</div>
);
};
export default TimelineItem;

View File

@@ -1,61 +1,123 @@
import React from 'react';
import forOwn from 'lodash/forOwn';
import { safeStringifyJSON } from 'utils/common';
import React, { useState } from 'react';
import StyledWrapper from './StyledWrapper';
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
import { get } from 'lodash';
import TimelineItem from './TimelineItem/index';
const Timeline = ({ request, response }) => {
const requestHeaders = [];
const responseHeaders = typeof response.headers === 'object' ? Object.entries(response.headers) : [];
const getEffectiveAuthSource = (collection, item) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
if (authMode !== 'inherit') return null;
request = request || {};
response = response || {};
const collectionAuth = get(collection, 'root.request.auth');
let effectiveSource = {
type: 'collection',
uid: collection.uid,
auth: collectionAuth
};
forOwn(request.headers, (value, key) => {
requestHeaders.push({
name: key,
value
});
});
// Get path from collection to item
let path = [];
let currentItem = findItemInCollection(collection, item?.uid);
while (currentItem) {
path.unshift(currentItem);
currentItem = findParentItemInCollection(collection, currentItem?.uid);
}
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
// Check folders in reverse to find the closest auth configuration
for (let i of [...path].reverse()) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
uid: i.uid,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const Timeline = ({ collection, item, width }) => {
// Get the effective auth source if auth mode is inherit
const authSource = getEffectiveAuthSource(collection, item);
// Filter timeline entries based on new rules
const combinedTimeline = ([...(collection.timeline || [])]).filter(obj => {
// Always show entries for this item
if (obj.itemUid === item.uid) return true;
// For OAuth2 entries, also show if auth is inherited
if (obj.type === 'oauth2' && authSource) {
if (authSource.type === 'folder' && obj.folderUid === authSource.uid) return true;
if (authSource.type === 'collection' && !obj.folderUid) return true;
}
return false;
}).sort((a, b) => b.timestamp - a.timestamp);
return (
<StyledWrapper className="pb-4 w-full">
<div>
<pre className="line request font-bold">
<span className="arrow">{'>'}</span> {request.method} {request.url}
</pre>
{requestHeaders.map((h) => {
<StyledWrapper
className="pb-4 w-full flex flex-grow flex-col"
style={{ maxWidth: width - 60, overflowWrap: 'break-word' }}
>
{combinedTimeline.map((event, index) => {
if (event.type === 'request') {
const { data, timestamp } = event;
const { request, response } = data;
return (
<pre className="line request" key={h.name}>
<span className="arrow">{'>'}</span> {h.name}: {h.value}
</pre>
<div key={index} className="timeline-event mb-2">
<TimelineItem
timestamp={timestamp}
request={request}
response={response}
item={item}
collection={collection}
width={width}
/>
</div>
);
})}
{requestData ? (
<pre className="line request">
<span className="arrow">{'>'}</span> data{' '}
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
</pre>
) : null}
</div>
<div className="mt-4">
<pre className="line response font-bold">
<span className="arrow">{'<'}</span> {response.status} - {response.statusText}
</pre>
{responseHeaders.map((h) => {
} else if (event.type === 'oauth2') {
const { data, timestamp } = event;
const { debugInfo } = data;
return (
<pre className="line response" key={h[0]}>
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
</pre>
<div key={index} className="timeline-event">
<div className="timeline-event-header cursor-pointer flex items-center">
<div className="flex items-center">
<span className="font-bold">OAuth2.0 Calls</span>
</div>
</div>
<div className="mt-2">
{debugInfo && debugInfo.length > 0 ? (
debugInfo.map((data, idx) => (
<div className='ml-4'>
<TimelineItem
key={idx}
timestamp={timestamp}
request={data?.request}
response={data?.response}
item={item}
collection={collection}
width={width - 50}
isOauth2={true}
/>
</div>
))
) : (
<div>No debug information available.</div>
)}
</div>
</div>
);
})}
</div>
}
return null;
})}
</StyledWrapper>
);
};
export default Timeline;
export default Timeline;

View File

@@ -63,7 +63,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
return <ResponseHeaders headers={response.headers} />;
}
case 'timeline': {
return <Timeline request={item.requestSent} response={item.response} />;
return <Timeline collection={collection} item={item} width={rightPaneWidth} />;
}
case 'tests': {
return <TestResults results={item.testResults} assertionResults={item.assertionResults} />;

View File

@@ -7,10 +7,10 @@ import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
import StatusCode from 'components/ResponsePane/StatusCode';
import ResponseTime from 'components/ResponsePane/ResponseTime';
import ResponseSize from 'components/ResponsePane/ResponseSize';
import Timeline from 'components/ResponsePane/Timeline';
import TestResults from 'components/ResponsePane/TestResults';
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
import StyledWrapper from './StyledWrapper';
import RunnerTimeline from 'components/ResponsePane/RunnerTimeline';
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const [selectedTab, setSelectedTab] = useState('response');
@@ -45,7 +45,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
return <ResponseHeaders headers={headers} />;
}
case 'timeline': {
return <Timeline request={requestSent} response={responseReceived} />;
return <RunnerTimeline request={requestSent} response={responseReceived} />;
}
case 'tests': {
return <TestResults results={testResults} assertionResults={assertionResults} />;

View File

@@ -6,11 +6,11 @@ import Modal from 'components/Modal';
import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs';
import { cloneItem } from 'providers/ReduxStore/slices/collections/actions';
import path from "utils/common/path";
import { IconArrowBackUp, IconEdit, IconCaretDown } from "@tabler/icons";
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
import Help from 'components/Help';
import PathDisplay from 'components/PathDisplay/index';
import path from 'utils/common/path';
import Portal from 'components/Portal';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
@@ -172,8 +172,6 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
) : (
<div className='relative flex flex-row gap-1 items-center justify-between'>
<PathDisplay
collection={collection}
dirName={path.relative(collection?.pathname, path.dirname(item?.pathname))}
baseName={formik.values.filename}
/>
</div>

View File

@@ -5,6 +5,10 @@ const StyledWrapper = styled.div`
height: 30px;
overflow-y: hidden;
overflow-x: hidden;
&.disabled {
opacity: 0.5;
}
.CodeMirror {
background: transparent;

View File

@@ -53,6 +53,7 @@ class SingleLineEditor extends Component {
},
scrollbarStyle: null,
tabindex: 0,
readOnly: this.props.readOnly,
extraKeys: {
Enter: runHandler,
'Ctrl-Enter': runHandler,
@@ -127,6 +128,9 @@ class SingleLineEditor extends Component {
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
this.editor.setOption('readOnly', this.props.readOnly);
}
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
this.cachedValue = String(this.props.value);
this.editor.setValue(String(this.props.value) || '');
@@ -175,7 +179,7 @@ class SingleLineEditor extends Component {
render() {
return (
<div className="flex flex-row justify-between w-full overflow-x-auto">
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
<StyledWrapper ref={this.editorRef} className={`single-line-editor grow ${this.props.readOnly? 'disabled' : ''}`} />
{this.secretEye(this.props.isSecret)}
</div>
);

View File

@@ -24,6 +24,7 @@ import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import { isElectron } from 'utils/common/platform';
import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';
import { collectionAddOauth2CredentialsByUrl } from 'providers/ReduxStore/slices/collections/index';
const useIpcEvents = () => {
const dispatch = useDispatch();
@@ -160,7 +161,17 @@ const useIpcEvents = () => {
const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => {
dispatch(hydrateCollectionWithUiStateSnapshot(val));
})
});
const removeCollectionOauth2CredentialsUpdatesListener = ipcRenderer.on('main:credentials-update', (val) => {
const payload = {
...val,
itemUid: val.itemUid || null,
folderUid: val.folderUid || null,
credentialsId: val.credentialsId || 'credentials'
};
dispatch(collectionAddOauth2CredentialsByUrl(payload));
});
return () => {
removeCollectionTreeUpdateListener();
@@ -181,6 +192,7 @@ const useIpcEvents = () => {
removeSystemProxyEnvUpdatesListener();
removeGlobalEnvironmentsUpdatesListener();
removeSnapshotHydrationListener();
removeCollectionOauth2CredentialsUpdatesListener();
};
}, [isElectron]);
};

View File

@@ -37,7 +37,9 @@ import {
resetRunResults,
responseReceived,
updateLastAction,
setCollectionSecurityConfig
setCollectionSecurityConfig,
collectionAddOauth2CredentialsByUrl,
collectionClearOauth2CredentialsByUrl
} from './index';
import { each } from 'lodash';
@@ -237,11 +239,20 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
.then((response) => {
// Ensure any timestamps in the response are converted to numbers
const serializedResponse = {
...response,
timeline: response.timeline?.map(entry => ({
...entry,
timestamp: entry.timestamp instanceof Date ? entry.timestamp.getTime() : entry.timestamp
}))
};
return dispatch(
responseReceived({
itemUid: item.uid,
collectionUid: collectionUid,
response: response
response: serializedResponse
})
);
})
@@ -1230,33 +1241,92 @@ export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (
export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getState) => {
const collectionSnapshotData = payload;
return new Promise((resolve, reject) => {
const state = getState();
try {
if(!collectionSnapshotData) resolve();
const { pathname, selectedEnvironment } = collectionSnapshotData;
const collection = findCollectionByPathname(state.collections.collections, pathname);
const collectionCopy = cloneDeep(collection);
const collectionUid = collectionCopy?.uid;
const collectionSnapshotData = payload;
return new Promise((resolve, reject) => {
const state = getState();
try {
if(!collectionSnapshotData) resolve();
const { pathname, selectedEnvironment } = collectionSnapshotData;
const collection = findCollectionByPathname(state.collections.collections, pathname);
const collectionCopy = cloneDeep(collection);
const collectionUid = collectionCopy?.uid;
// update selected environment
if (selectedEnvironment) {
const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment);
if (environment) {
dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid }));
}
// update selected environment
if (selectedEnvironment) {
const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment);
if (environment) {
dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid }));
}
}
// todo: add any other redux state that you want to save
// todo: add any other redux state that you want to save
resolve();
}
catch(error) {
reject(error);
}
});
};
export const fetchOauth2Credentials = (payload) => async (dispatch, getState) => {
const { request, collection, itemUid, folderUid } = payload;
return new Promise((resolve, reject) => {
window.ipcRenderer
.invoke('renderer:fetch-oauth2-credentials', { itemUid, request, collection })
.then(({ credentials, url, collectionUid, credentialsId, debugInfo }) => {
dispatch(
collectionAddOauth2CredentialsByUrl({
credentials,
url,
collectionUid,
credentialsId,
debugInfo,
folderUid: folderUid || null,
itemUid: !folderUid ? itemUid : null
})
);
resolve(credentials);
})
.catch(reject);
});
};
export const refreshOauth2Credentials = (payload) => async (dispatch, getState) => {
const { request, collection, folderUid, itemId } = payload;
return new Promise((resolve, reject) => {
window.ipcRenderer
.invoke('renderer:refresh-oauth2-credentials', { request, collection })
.then(({ credentials, url, collectionUid, debugInfo }) => {
dispatch(
collectionAddOauth2CredentialsByUrl({
credentials,
url,
collectionUid,
debugInfo,
folderUid: folderUid || null,
itemId: !folderUid ? itemId : null
})
);
resolve(credentials);
})
.catch(reject);
});
};
export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
const { collectionUid, url, credentialsId } = payload;
return new Promise((resolve, reject) => {
window.ipcRenderer
.invoke('clear-oauth2-cache', collectionUid, url, credentialsId)
.then(() => {
// We do not dispatch any action to modify the Redux store,
// since we are only clearing the session on the main process side.
resolve();
}
catch(error) {
reject(error);
}
});
};
})
.catch(reject);
});
};
export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {

View File

@@ -281,13 +281,38 @@ export const collectionsSlice = createSlice({
},
responseReceived: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item) {
item.requestState = 'received';
item.response = action.payload.response;
item.cancelTokenUid = null;
if (!collection.timeline) {
collection.timeline = [];
}
// Ensure timestamp is a number (milliseconds since epoch)
const timestamp = item?.requestSent?.timestamp instanceof Date
? item.requestSent.timestamp.getTime()
: item?.requestSent?.timestamp || Date.now();
console.log("response reieved", JSON.stringify(item), JSON.stringify(item.requestSent));
// Append the new timeline entry with numeric timestamp
collection.timeline.push({
type: "request",
collectionUid: collection.uid,
folderUid: null,
itemUid: item.uid,
timestamp: timestamp,
data: {
request: item.requestSent || item.request,
response: action.payload.response,
timestamp: timestamp,
}
});
}
}
},
@@ -1539,6 +1564,26 @@ export const collectionsSlice = createSlice({
set(folder, 'root.request.tests', action.payload.tests);
}
},
updateFolderAuth: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
console.log('action.payload.content inside bro', action.payload);
if (!collection) return;
const folder = collection ? findItemInCollection(collection, action.payload.itemUid) : null;
console.log('folder inside bro', folder);
if (!folder) return;
if (folder) {
set(folder, 'root.request.auth', {});
set(folder, 'root.request.auth.mode', action.payload.mode);
switch (action.payload.mode) {
case 'oauth2':
set(folder, 'root.request.auth.oauth2', action.payload.content);
break;
}
}
},
addCollectionHeader: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -2019,7 +2064,85 @@ export const collectionsSlice = createSlice({
set(folder, 'root.docs', action.payload.docs);
}
}
}
},
collectionAddOauth2CredentialsByUrl: (state, action) => {
const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (!collection) return;
// Update oauth2Credentials (latest token)
if (!collection.oauth2Credentials) {
collection.oauth2Credentials = [];
}
let collectionOauth2Credentials = cloneDeep(collection.oauth2Credentials);
// Remove existing credentials for the same combination
const filteredOauth2Credentials = filter(
collectionOauth2Credentials,
(creds) =>
!(creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId)
);
// Add the new credential with folderUid and itemUid
filteredOauth2Credentials.push({
collectionUid,
folderUid,
itemUid,
url,
credentials,
credentialsId,
debugInfo
});
collection.oauth2Credentials = filteredOauth2Credentials;
if (!collection.timeline) {
collection.timeline = [];
}
if(debugInfo) {
collection.timeline.push({
type: "oauth2",
collectionUid,
folderUid,
itemUid,
timestamp: Date.now(),
data: {
collectionUid,
folderUid,
itemUid,
url,
credentials,
credentialsId,
debugInfo: debugInfo.data,
}
});
}
},
collectionClearOauth2CredentialsByUrl: (state, action) => {
// Since we don't want to remove tokens from oauth2Credentials or timeline,
},
collectionGetOauth2CredentialsByUrl: (state, action) => {
const { collectionUid, url, credentialsId } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
const oauth2Credential = find(
collection?.oauth2Credentials || [],
(creds) =>
creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId
);
return oauth2Credential;
},
updateFolderAuthMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
set(folder, 'root.request.auth', {});
set(folder, 'root.request.auth.mode', action.payload.mode);
}
},
}
});
@@ -2124,6 +2247,11 @@ export const {
resetCollectionRunner,
updateRequestDocs,
updateFolderDocs,
collectionAddOauth2CredentialsByUrl,
collectionClearOauth2CredentialsByUrl,
collectionGetOauth2CredentialsByUrl,
updateFolderAuth,
updateFolderAuthMode,
moveCollection
} = collectionsSlice.actions;

View File

@@ -1,6 +1,8 @@
import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
import { uuid } from 'utils/common';
import path from 'utils/common/path';
import brunoCommon from '@usebruno/common';
const { interpolate } = brunoCommon;
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
if (!str || !str.length || !isString(str)) {
@@ -381,11 +383,19 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
di.request.auth.oauth2 = {
grantType: grantType,
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
refreshUrl: get(si.request, 'auth.oauth2.refreshUrl', ''),
username: get(si.request, 'auth.oauth2.username', ''),
password: get(si.request, 'auth.oauth2.password', ''),
clientId: get(si.request, 'auth.oauth2.clientId', ''),
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
scope: get(si.request, 'auth.oauth2.scope', '')
scope: get(si.request, 'auth.oauth2.scope', ''),
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
};
break;
case 'authorization_code':
@@ -394,19 +404,36 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
callbackUrl: get(si.request, 'auth.oauth2.callbackUrl', ''),
authorizationUrl: get(si.request, 'auth.oauth2.authorizationUrl', ''),
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
refreshUrl: get(si.request, 'auth.oauth2.refreshUrl', ''),
clientId: get(si.request, 'auth.oauth2.clientId', ''),
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
scope: get(si.request, 'auth.oauth2.scope', ''),
pkce: get(si.request, 'auth.oauth2.pkce', false)
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
pkce: get(si.request, 'auth.oauth2.pkce', false),
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
authorizeInDefaultBrowser: get(si.request, 'auth.oauth2.authorizeInDefaultBrowser', ''),
};
break;
case 'client_credentials':
di.request.auth.oauth2 = {
grantType: grantType,
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
refreshUrl: get(si.request, 'auth.oauth2.refreshUrl', ''),
clientId: get(si.request, 'auth.oauth2.clientId', ''),
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
scope: get(si.request, 'auth.oauth2.scope', '')
scope: get(si.request, 'auth.oauth2.scope', ''),
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
};
break;
}
@@ -953,12 +980,15 @@ export const getAllVariables = (collection, item) => {
const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])];
const oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials })
return {
...globalEnvironmentVariables,
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...oauth2CredentialVariables,
...runtimeVariables,
pathParams: {
...pathParams
@@ -1025,4 +1055,44 @@ const mergeVars = (collection, requestTreePath = []) => {
folderVariables,
requestVariables
};
};
};
export const interpolateStringUsingCollectionAndItem = ({ collection, item, string }) => {
const variables = getAllVariables(collection, item);
const value = interpolate(string, variables);
return value;
}
export const getEnvVars = (environment = {}) => {
const variables = environment.variables;
if (!variables || !variables.length) {
return {
__name__: environment.name
};
}
const envVars = {};
each(variables, (variable) => {
if (variable.enabled) {
envVars[variable.name] = variable.value;
}
});
return {
...envVars,
__name__: environment.name
};
};
export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {
let credentialsVariables = {};
oauth2Credentials.forEach(({ credentialsId, credentials }) => {
if (credentials) {
Object.entries(credentials).forEach(([key, value]) => {
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
});
}
});
return credentialsVariables;
};

View File

@@ -190,3 +190,5 @@ export const getEncoding = (headers) => {
const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(headers?.['content-type'] || '');
return charsetMatch?.[1];
}
export const BRUNO_OAUTH2_SERVER_CALLBACK_URL = `http://localhost:9876/callback`;

View File

@@ -5,6 +5,7 @@ import { BrunoError } from 'utils/common/error';
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
import { postmanTranslation } from 'utils/importers/translators/postman_translation';
import each from 'lodash/each';
import { BRUNO_OAUTH2_SERVER_CALLBACK_URL } from 'utils/common/index';
const readFile = (files) => {
return new Promise((resolve, reject) => {
@@ -431,6 +432,67 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
value: authValues.value?.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it,
placement: "header" //By default we are placing the apikey values in headers!
}
} else if (auth.type === 'oauth2'){
const findValueUsingKey = (key) => {
return auth?.oauth2?.find(v => v?.key == key)?.value ?? ''
}
const oauth2GrantTypeMaps = {
'authorization_code_with_pkce': 'authorization_code',
'authorization_code': 'authorization_code',
'client_credentials': 'client_credentials',
'password_credentials': 'password_credentials'
}
const grantType = oauth2GrantTypeMaps[findValueUsingKey('grant_type')] || 'authorization_code';
if (grantType) {
brunoRequestItem.request.auth.mode = 'oauth2';
switch(grantType) {
case 'authorization_code':
brunoRequestItem.request.auth.oauth2 = {
grantType: 'authorization_code',
authorizationUrl: findValueUsingKey('authUrl'),
callbackUrl: Boolean(findValueUsingKey('useBrowser')) ? BRUNO_OAUTH2_SERVER_CALLBACK_URL : findValueUsingKey('redirect_uri'),
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
refreshUrl: findValueUsingKey('refreshTokenUrl'),
clientId: findValueUsingKey('clientId'),
clientSecret: findValueUsingKey('clientSecret'),
scope: findValueUsingKey('scope'),
state: findValueUsingKey('state'),
pkce: Boolean(findValueUsingKey('grant_type') == 'authorization_code_with_pkce'),
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header',
authorizeInDefaultBrowser: Boolean(findValueUsingKey('useBrowser')),
};
break;
case 'password_credentials':
brunoRequestItem.request.auth.oauth2 = {
grantType: 'password',
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
refreshUrl: findValueUsingKey('refreshTokenUrl'),
username: findValueUsingKey('username'),
password: findValueUsingKey('password'),
clientId: findValueUsingKey('clientId'),
clientSecret: findValueUsingKey('clientSecret'),
scope: findValueUsingKey('scope'),
state: findValueUsingKey('state'),
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
};
break;
case 'client_credentials':
brunoRequestItem.request.auth.oauth2 = {
grantType: 'client_credentials',
accessTokenUrl: findValueUsingKey('accessTokenUrl'),
refreshUrl: findValueUsingKey('refreshTokenUrl'),
clientId: findValueUsingKey('clientId'),
clientSecret: findValueUsingKey('clientSecret'),
scope: findValueUsingKey('scope'),
state: findValueUsingKey('state'),
tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url',
credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header'
};
break;
}
}
}
}

View File

@@ -14,7 +14,8 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
size: response.size,
status: response.status,
statusText: response.statusText,
duration: response.duration
duration: response.duration,
timeline: response.timeline
});
})
.catch((err) => reject(err));
@@ -36,17 +37,14 @@ const sendHttpRequest = async (item, collection, environment, runtimeVariables)
export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer
.invoke('send-collection-oauth2-request', collection, environment, runtimeVariables)
.then(resolve)
.catch(reject);
resolve({});
});
};
export const clearOauth2Cache = async (uid) => {
export const readOauth2CachedCredentials = async (uid) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('clear-oauth2-cache', uid).then(resolve).catch(reject);
ipcRenderer.invoke('read-oauth2-cached-credentials', uid).then(resolve).catch(reject);
});
};

View File

@@ -147,13 +147,13 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
// todo: we have things happening in two places w.r.t basic auth
// need to refactor this in the future
// the request.auth (basic auth) object gets set inside the prepare-request.js file
if (request.auth) {
const username = _interpolate(request.auth.username) || '';
const password = _interpolate(request.auth.password) || '';
if (request.basicAuth) {
const username = _interpolate(request.basicAuth.username) || '';
const password = _interpolate(request.basicAuth.password) || '';
// use auth header based approach and delete the request.auth object
request.headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
delete request.auth;
delete request.basicAuth;
}
if (request.awsv4config) {
@@ -165,12 +165,14 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
request.awsv4config.profileName = _interpolate(request.awsv4config.profileName) || '';
}
// interpolate vars for ntlmConfig auth
if (request.ntlmConfig) {
request.ntlmConfig.username = _interpolate(request.ntlmConfig.username) || '';
request.ntlmConfig.password = _interpolate(request.ntlmConfig.password) || '';
request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || '';
}
// interpolate vars for ntlmConfig auth
if (request.ntlmConfig) {
request.ntlmConfig.username = _interpolate(request.ntlmConfig.username) || '';
request.ntlmConfig.password = _interpolate(request.ntlmConfig.password) || '';
request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || '';
}
if(request?.auth) delete request.auth;
if (request) return request;
};

View File

@@ -38,7 +38,7 @@ const prepareRequest = (item = {}, collection = {}) => {
const collectionAuth = get(collection, 'root.request.auth');
if (collectionAuth && request.auth?.mode === 'inherit') {
if (collectionAuth.mode === 'basic') {
axiosRequest.auth = {
axiosRequest.basicAuth = {
username: get(collectionAuth, 'basic.username'),
password: get(collectionAuth, 'basic.password')
};
@@ -69,7 +69,7 @@ const prepareRequest = (item = {}, collection = {}) => {
if (request.auth && request.auth.mode !== 'inherit') {
if (request.auth.mode === 'basic') {
axiosRequest.auth = {
axiosRequest.basicAuth = {
username: get(request, 'auth.basic.username'),
password: get(request, 'auth.basic.password')
};

View File

@@ -117,7 +117,7 @@ describe('prepare-request: prepareRequest', () => {
const result = prepareRequest(item, collection);
const expected = { username: 'testUser', password: 'testPass123' };
expect(result.auth).toEqual(expected);
expect(result.basicAuth).toEqual(expected);
});
});

View File

@@ -45,6 +45,7 @@
"electron-notarize": "^1.2.2",
"electron-store": "^8.1.0",
"electron-util": "^0.17.2",
"express": "^4.21.2",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"graphql": "^16.6.0",

View File

@@ -54,6 +54,7 @@ const jsonToCollectionBru = async (json, isFolder) => {
res: _.get(json, 'request.vars.res', [])
},
tests: _.get(json, 'request.tests', ''),
auth: _.get(json, 'request.auth', {}),
docs: _.get(json, 'docs', '')
};
@@ -66,10 +67,6 @@ const jsonToCollectionBru = async (json, isFolder) => {
};
}
if (!isFolder) {
collectionBruJson.auth = _.get(json, 'request.auth', {});
}
return _jsonToCollectionBru(collectionBruJson);
} catch (error) {
return Promise.reject(error);

View File

@@ -31,6 +31,12 @@ const { deleteCookiesForDomain, getDomainsWithCookies, addCookieForDomain, modif
const EnvironmentSecretsStore = require('../store/env-secrets');
const CollectionSecurityStore = require('../store/collection-security');
const UiStateSnapshotStore = require('../store/ui-state-snapshot');
const Oauth2Store = require('../store/oauth2');
const interpolateVars = require('./network/interpolate-vars');
const { getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../utils/collection');
const { getProcessEnvVars } = require('../store/process-env');
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2');
const { configureRequestWithCertsAndProxy } = require('./network');
const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
const environmentSecretsStore = new EnvironmentSecretsStore();
@@ -896,6 +902,63 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
ipcMain.handle('renderer:get-stored-oauth2-credentials', async (event, collectionUid, url, credentialsId) => {
try {
const oauth2Store = new Oauth2Store();
const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId });
return { credentials, collectionUid, url };
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:fetch-oauth2-credentials', async (event, { itemUid, request, collection }) => {
try {
if (request.oauth2) {
let requestCopy = _.cloneDeep(request);
const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;
const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const partialItem = { uid: itemUid };
const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem);
if (requestTreePath && requestTreePath.length > 0) {
mergeVars(collection, requestCopy, requestTreePath);
}
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const {newRequest} = await configureRequestWithCertsAndProxy({
collectionUid,
request: requestCopy,
envVars,
runtimeVariables,
processEnvVars,
collectionPath
});
requestCopy = newRequest
const { oauth2: { grantType }} = requestCopy || {};
let credentials, url, credentialsId;
switch (grantType) {
case 'authorization_code':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, forceFetch: true }));
break;
case 'client_credentials':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, forceFetch: true }));
break;
case 'password':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, forceFetch: true }));
break;
}
return { credentials, url, collectionUid, credentialsId, debugInfo };
}
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:load-request-via-worker', async (event, { collectionUid, pathname }) => {
let fileStats;
try {
@@ -945,6 +1008,32 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
ipcMain.handle('renderer:refresh-oauth2-credentials', async (event, { request, collection }) => {
try {
if (request.oauth2) {
let requestCopy = _.cloneDeep(request);
const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;
const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const {newRequest} = await configureRequestWithCertsAndProxy({
collectionUid,
request: requestCopy,
envVars,
runtimeVariables,
processEnvVars,
collectionPath
});
requestCopy = newRequest
let { credentials, url, credentialsId } = await refreshOauth2Token(requestCopy, collectionUid);
return { credentials, url, collectionUid, credentialsId };
}
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => {
let fileStats;
try {

View File

@@ -8,12 +8,15 @@ const matchesCallbackUrl = (url, callbackUrl) => {
const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
return new Promise(async (resolve, reject) => {
let finalUrl = null;
let debugInfo = {
data: []
};
let currentMainRequest = null;
let allOpenWindows = BrowserWindow.getAllWindows();
// main window id is '1'
// get all other windows
let windowsExcludingMain = allOpenWindows.filter((w) => w.id != 1);
// Close all windows except the main window (assumed to have id 1)
let windowsExcludingMain = allOpenWindows.filter((w) => w.id !== 1);
windowsExcludingMain.forEach((w) => {
w.close();
});
@@ -27,26 +30,104 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
});
window.on('ready-to-show', window.show.bind(window));
// We want browser window to comply with "SSL/TLS Certificate Verification" toggle in Preferences
// Ensure the browser window complies with "SSL/TLS Certificate Verification" preference
window.webContents.on('certificate-error', (event, url, error, certificate, callback) => {
event.preventDefault();
callback(!preferencesUtil.shouldVerifyTls());
});
const { session: webSession } = window.webContents;
// Intercept request events and gather data
webSession.webRequest.onBeforeRequest((details, callback) => {
const { id: requestId, url, method, resourceType, frameId } = details;
if (resourceType === 'mainFrame') {
// This is a main frame request
currentMainRequest = {
requestId,
resourceType,
frameId,
request: {
url,
method,
headers: {},
error: null
},
response: {
headers: {},
status: null,
statusText: null,
error: null
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
};
// Add to mainRequests
// pushing the currentMainRequest to debugInfo
// the currentMainRequest will be further updated by object reference
debugInfo.data.push(currentMainRequest);
}
callback({ cancel: false });
});
webSession.webRequest.onBeforeSendHeaders((details, callback) => {
const { id: requestId, requestHeaders, method, url } = details;
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.request = {
url,
headers: requestHeaders,
method
};
}
callback({ cancel: false, requestHeaders });
});
webSession.webRequest.onHeadersReceived((details, callback) => {
const { id: requestId, url, statusCode, responseHeaders, method } = details;
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.response = {
url,
method,
status: statusCode,
headers: responseHeaders
};
}
callback({ cancel: false, responseHeaders });
});
webSession.webRequest.onCompleted((details) => {
const { id: requestId, fromCache } = details;
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.completed = true;
currentMainRequest.fromCache = fromCache;
}
});
webSession.webRequest.onErrorOccurred((details) => {
const { id: requestId, error } = details;
if (currentMainRequest?.requestId === requestId) {
currentMainRequest.response.error = error;
}
});
function onWindowRedirect(url) {
// check if the redirect is to the callback URL and if it contains an authorization code
// Handle redirects as needed
// Check if redirect is to the callback URL and contains an authorization code
if (matchesCallbackUrl(new URL(url), new URL(callbackUrl))) {
if (!new URL(url).searchParams.has('code')) {
reject(new Error('Invalid Callback URL: Does not contain an authorization code'));
}
finalUrl = url;
window.close();
}
if (url.match(/(error=).*/) || url.match(/(error_description=).*/) || url.match(/(error_uri=).*/)) {
const _url = new URL(url);
const error = _url.searchParams.get('error');
const errorDescription = _url.searchParams.get('error_description');
const errorUri = _url.searchParams.get('error_uri');
// Handle OAuth error responses
const urlObj = new URL(url);
if (urlObj.searchParams.has('error')) {
const error = urlObj.searchParams.get('error');
const errorDescription = urlObj.searchParams.get('error_description');
const errorUri = urlObj.searchParams.get('error_uri');
let errorData = {
message: 'Authorization Failed!',
error,
@@ -58,13 +139,36 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
}
}
// Update currentMainRequest when navigation occurs
window.webContents.on('did-start-navigation', (event, url, isInPlace, isMainFrame) => {
if (isMainFrame) {
// Reset currentMainRequest since a new navigation is starting
currentMainRequest = null;
}
});
window.webContents.on('did-navigate', (event, url) => {
onWindowRedirect(url);
});
window.webContents.on('will-redirect', (event, url) => {
onWindowRedirect(url);
});
window.on('close', () => {
// Clean up listeners to prevent memory leaks
window.webContents.removeAllListeners();
webSession.webRequest.onBeforeRequest(null);
webSession.webRequest.onBeforeSendHeaders(null);
webSession.webRequest.onHeadersReceived(null);
webSession.webRequest.onCompleted(null);
webSession.webRequest.onErrorOccurred(null);
if (finalUrl) {
try {
const callbackUrlWithCode = new URL(finalUrl);
const authorizationCode = callbackUrlWithCode.searchParams.get('code');
return resolve({ authorizationCode });
return resolve({ authorizationCode, debugInfo });
} catch (error) {
return reject(error);
}
@@ -73,20 +177,10 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
}
});
// wait for the window to navigate to the callback url
const didNavigateListener = (_, url) => {
onWindowRedirect(url);
};
window.webContents.on('did-navigate', didNavigateListener);
const willRedirectListener = (_, authorizeUrl) => {
onWindowRedirect(authorizeUrl);
};
window.webContents.on('will-redirect', willRedirectListener);
try {
await window.loadURL(authorizeUrl);
} catch (error) {
// If browser redirects before load finished, loadURL throws an error with code ERR_ABORTED. This should be ignored.
// Ignore ERR_ABORTED errors that occur during redirects
if (error.code === 'ERR_ABORTED') {
console.debug('Ignoring ERR_ABORTED during authorizeUserInWindow');
return;
@@ -97,4 +191,4 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
});
};
module.exports = { authorizeUserInWindow, matchesCallbackUrl };
module.exports = { authorizeUserInWindow, matchesCallbackUrl };

View File

@@ -3,11 +3,31 @@ const Socket = require('net').Socket;
const axios = require('axios');
const connectionCache = new Map(); // Cache to store checkConnection() results
const electronApp = require("electron");
const { setupProxyAgents } = require('../../utils/proxy-util');
const { addCookieToJar, getCookieStringForUrl } = require('../../utils/cookies');
const { preferencesUtil } = require('../../store/preferences');
const LOCAL_IPV6 = '::1';
const LOCAL_IPV4 = '127.0.0.1';
const LOCALHOST = 'localhost';
const version = electronApp?.app?.getVersion()?.substring(1) ?? "";
const redirectResponseCodes = [301, 302, 303, 307, 308];
const saveCookies = (url, headers) => {
if (preferencesUtil.shouldStoreCookies()) {
let setCookieHeaders = [];
if (headers['set-cookie']) {
setCookieHeaders = Array.isArray(headers['set-cookie'])
? headers['set-cookie']
: [headers['set-cookie']];
for (let setCookieHeader of setCookieHeaders) {
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
addCookieToJar(setCookieHeader, url);
}
}
}
}
}
const getTld = (hostname) => {
if (!hostname) {
@@ -49,13 +69,16 @@ const checkConnection = (host, port) =>
* @see https://github.com/axios/axios/issues/695
* @returns {axios.AxiosInstance}
*/
function makeAxiosInstance() {
function makeAxiosInstance({
proxyMode = 'off',
proxyConfig = {},
requestMaxRedirects = 5,
httpsAgentRequestFields = {},
interpolationOptions = {}
} = {}) {
/** @type {axios.AxiosInstance} */
const instance = axios.create({
transformRequest: function transformRequest(data, headers) {
// doesn't apply the default transformRequest if the data is a string, so that axios doesn't add quotes see :
// https://github.com/usebruno/bruno/issues/2043
// https://github.com/axios/axios/issues/4034
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
const hasJSONContentType = contentType.includes('json');
if (typeof data === 'string' && hasJSONContentType) {
@@ -75,6 +98,51 @@ function makeAxiosInstance() {
instance.interceptors.request.use(async (config) => {
const url = URL.parse(config.url);
config.metadata = config.metadata || {};
config.metadata.startTime = new Date().getTime();
config.metadata.timeline = config.metadata.timeline || [];
// Add initial request details to the timeline
config.metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Preparing request to ${config.url}`,
});
config.metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Current time is ${new Date().toISOString()}`,
});
// Add request method and headers
config.metadata.timeline.push({
timestamp: new Date(),
type: 'request',
message: `${config.method.toUpperCase()} ${config.url}`,
});
Object.entries(config.headers).forEach(([key, value]) => {
config.metadata.timeline.push({
timestamp: new Date(),
type: 'requestHeader',
message: `${key}: ${value}`,
});
});
// Add request data if available
if (config.data) {
let requestData;
try {
requestData = typeof config.data === 'string' ? config.data : JSON.stringify(config.data, null, 2);
} catch (err) {
requestData = config.data.toString();
}
config.metadata.timeline.push({
timestamp: new Date(),
type: 'requestData',
message: requestData,
});
}
// Resolve all *.localhost to localhost and check if it should use IPv6 or IPv4
// RFC: 6761 section 6.3 (https://tools.ietf.org/html/rfc6761#section-6.3)
@@ -91,14 +159,73 @@ function makeAxiosInstance() {
}
config.headers['request-start-time'] = Date.now();
const agentOptions = {
...httpsAgentRequestFields,
keepAlive: true,
};
// Now call setupProxyAgents and pass the timeline
setupProxyAgents({
requestConfig: config,
proxyMode: proxyMode, // 'on', 'off', or 'system', depending on your settings
proxyConfig: proxyConfig,
httpsAgentRequestFields: agentOptions,
interpolationOptions: interpolationOptions, // Provide your interpolation options
timeline: config.metadata.timeline,
});
return config;
});
let redirectCount = 0
instance.interceptors.response.use(
(response) => {
const end = Date.now();
const start = response.config.headers['request-start-time'];
response.headers['request-duration'] = end - start;
redirectCount = 0;
const config = response.config;
const metadata = config.metadata;
const duration = end - metadata.startTime;
const httpVersion = response.request?.res?.httpVersion || '1.1';
metadata.timeline.push({
timestamp: new Date(),
type: 'response',
message: `HTTP/${httpVersion} ${response.status} ${response.statusText}`,
});
if (httpVersion.startsWith('2')) {
metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Using HTTP/2, server supports multiplexing`,
});
}
metadata.timeline.push({
timestamp: new Date(),
type: 'response',
message: `HTTP/${response.httpVersion || '1.1'} ${response.status} ${response.statusText}`,
});
Object.entries(response.headers).forEach(([key, value]) => {
metadata.timeline.push({
timestamp: new Date(),
type: 'responseHeader',
message: `${key}: ${value}`,
});
});
metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Request completed in ${duration} ms`,
});
// Attach the timeline to the response
response.timeline = metadata.timeline;
return response;
},
(error) => {
@@ -106,6 +233,99 @@ function makeAxiosInstance() {
const end = Date.now();
const start = error.config.headers['request-start-time'];
error.response.headers['request-duration'] = end - start;
const config = error.config;
const metadata = config.metadata;
const duration = end - metadata.startTime;
if (error.response && redirectResponseCodes.includes(error.response.status)) {
metadata.timeline.push({
timestamp: new Date(),
type: 'response',
message: `HTTP/${error.response.httpVersion || '1.1'} ${error.response.status} ${error.response.statusText}`,
});
Object.entries(error.response.headers).forEach(([key, value]) => {
metadata.timeline.push({
timestamp: new Date(),
type: 'responseHeader',
message: `${key}: ${value}`,
});
});
metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Request completed in ${duration} ms`,
});
// Attach the timeline to the response
error.response.timeline = metadata.timeline;
if (redirectCount >= requestMaxRedirects) {
const dataBuffer = Buffer.from(error.response.data);
return {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data,
dataBuffer: dataBuffer.toString('base64'),
size: Buffer.byteLength(dataBuffer),
duration: error.response.headers.get('request-duration') ?? 0
};
}
// Increase redirect count
redirectCount++;
const locationHeader = error.response.headers.location;
let redirectUrl = locationHeader;
// Handle relative URLs by resolving them against the original request URL
if (locationHeader && !locationHeader.match(/^https?:\/\//i)) {
// It's a relative URL, resolve it against the original URL
redirectUrl = URL.resolve(error.config.url, locationHeader);
metadata.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Resolving relative redirect URL: ${locationHeader}${redirectUrl}`,
});
}
if (preferencesUtil.shouldStoreCookies()) {
saveCookies(redirectUrl, error.response.headers);
}
// Create a new request config for the redirect
const requestConfig = {
...error.config,
url: redirectUrl,
headers: {
...error.config.headers,
},
};
if (preferencesUtil.shouldSendCookies()) {
const cookieString = getCookieStringForUrl(redirectUrl);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
requestConfig.headers['cookie'] = cookieString;
}
}
setupProxyAgents({
requestConfig,
proxyMode,
proxyConfig,
httpsAgentRequestFields,
interpolationOptions,
timeline: metadata.timeline
});
// Make the redirected request
return instance(requestConfig);
}
}
return Promise.reject(error);
}

View File

@@ -1,76 +0,0 @@
const { each, filter } = require('lodash');
const sortCollection = (collection) => {
const items = collection.items || [];
let folderItems = filter(items, (item) => item.type === 'folder');
let requestItems = filter(items, (item) => item.type !== 'folder');
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
collection.items = folderItems.concat(requestItems);
each(folderItems, (item) => {
sortCollection(item);
});
};
const sortFolder = (folder = {}) => {
const items = folder.items || [];
let folderItems = filter(items, (item) => item.type === 'folder');
let requestItems = filter(items, (item) => item.type !== 'folder');
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
folder.items = folderItems.concat(requestItems);
each(folderItems, (item) => {
sortFolder(item);
});
return folder;
};
const findItemInCollection = (collection, itemId) => {
let item = null;
if (collection.uid === itemId) {
return collection;
}
if (collection.items && collection.items.length) {
collection.items.forEach((item) => {
if (item.uid === itemId) {
item = item;
} else if (item.type === 'folder') {
item = findItemInCollection(item, itemId);
}
});
}
return item;
};
const getAllRequestsInFolderRecursively = (folder = {}) => {
let requests = [];
if (folder.items && folder.items.length) {
folder.items.forEach((item) => {
if (item.type !== 'folder') {
requests.push(item);
} else {
requests = requests.concat(getAllRequestsInFolderRecursively(item));
}
});
}
return requests;
};
module.exports = {
sortCollection,
sortFolder,
findItemInCollection,
getAllRequestsInFolderRecursively
};

View File

@@ -1,90 +1,37 @@
const os = require('os');
const fs = require('fs');
const qs = require('qs');
const https = require('https');
const tls = require('tls');
const axios = require('axios');
const path = require('path');
const decomment = require('decomment');
const iconv = require('iconv-lite');
const fs = require('fs');
const tls = require('tls');
const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
const FormData = require('form-data');
const { ipcMain } = require('electron');
const { isUndefined, isNull, each, get, compact, cloneDeep, forOwn, extend } = require('lodash');
const { each, get, extend, cloneDeep } = require('lodash');
const { NtlmClient } = require('axios-ntlm');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request');
const prepareCollectionRequest = require('./prepare-collection-request');
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
const { uuid } = require('../../utils/common');
const interpolateVars = require('./interpolate-vars');
const { interpolateString } = require('./interpolate-string');
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
const { resolveAwsV4Credentials, addAwsV4Interceptor } = require('./awsv4auth-helper');
const { addDigestInterceptor } = require('./digestauth-helper');
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
const { prepareRequest } = require('./prepare-request');
const interpolateVars = require('./interpolate-vars');
const { makeAxiosInstance } = require('./axios-instance');
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
const { uuid, safeStringifyJSON, safeParseJSON } = require('../../utils/common');
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
const { createFormData } = require('../../utils/form-data');
const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars } = require('../../utils/collection');
const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials } = require('../../utils/oauth2');
const { setupProxyAgents } = require('../../utils/proxy-util');
const { preferencesUtil } = require('../../store/preferences');
const { getProcessEnvVars } = require('../../store/process-env');
const { getBrunoConfig } = require('../../store/bruno-config');
const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('./axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { addDigestInterceptor } = require('./digestauth-helper');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
const { chooseFileToSave, writeFile } = require('../../utils/filesystem');
const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies');
const {
resolveOAuth2AuthorizationCodeAccessToken,
transformClientCredentialsRequest,
transformPasswordCredentialsRequest
} = require('./oauth2-helper');
const Oauth2Store = require('../../store/oauth2');
const iconv = require('iconv-lite');
const FormData = require('form-data');
const { createFormData } = require('../../utils/form-data');
const { findItemInCollectionByPathname } = require('../../utils/collection');
const { NtlmClient } = require('axios-ntlm');
const safeStringifyJSON = (data) => {
try {
return JSON.stringify(data);
} catch (e) {
return data;
}
};
const safeParseJSON = (data) => {
try {
return JSON.parse(data);
} catch (e) {
return data;
}
};
const getEnvVars = (environment = {}) => {
const variables = environment.variables;
if (!variables || !variables.length) {
return {
__name__: environment.name
};
}
const envVars = {};
each(variables, (variable) => {
if (variable.enabled) {
envVars[variable.name] = variable.value;
}
});
return {
...envVars,
__name__: environment.name
};
};
const getJsSandboxRuntime = (collection) => {
const securityConfig = get(collection, 'securityConfig', {});
return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
};
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const saveCookies = (url, headers) => {
if (preferencesUtil.shouldStoreCookies()) {
@@ -102,18 +49,19 @@ const saveCookies = (url, headers) => {
}
}
const configureRequest = async (
const getJsSandboxRuntime = (collection) => {
const securityConfig = get(collection, 'securityConfig', {});
return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
};
const configureRequestWithCertsAndProxy = async ({
collectionUid,
request,
envVars,
runtimeVariables,
processEnvVars,
collectionPath
) => {
if (!protocolRegex.test(request.url)) {
request.url = `http://${request.url}`;
}
}) => {
/**
* @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors
* @see https://github.com/nodejs/node/pull/43522 keepAlive was changed to true globally on Node v19+
@@ -204,114 +152,114 @@ const configureRequest = async (
proxyMode = get(proxyConfig, 'mode', 'off');
}
if (proxyMode === 'on') {
const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));
if (shouldProxy) {
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
setupProxyAgents({
requestConfig: request,
proxyMode,
proxyConfig,
httpsAgentRequestFields,
interpolationOptions
});
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
}
if (socksEnabled) {
request.httpsAgent = new SocksProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new SocksProxyAgent(proxyUri);
} else {
request.httpsAgent = new PatchedHttpsProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
request.httpAgent = new HttpProxyAgent(proxyUri);
}
} else {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
}
} else if (proxyMode === 'system') {
const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables();
const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || '');
if (shouldUseSystemProxy) {
try {
if (http_proxy?.length) {
new URL(http_proxy);
request.httpAgent = new HttpProxyAgent(http_proxy);
}
} catch (error) {
throw new Error('Invalid system http_proxy');
}
try {
if (https_proxy?.length) {
new URL(https_proxy);
request.httpsAgent = new PatchedHttpsProxyAgent(
https_proxy,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
}
} catch (error) {
throw new Error('Invalid system https_proxy');
}
} else {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
}
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
});
return {proxyMode, newRequest: request, proxyConfig, httpsAgentRequestFields, interpolationOptions};
}
const configureRequest = async (
collectionUid,
request,
envVars,
runtimeVariables,
processEnvVars,
collectionPath
) => {
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
if (!protocolRegex.test(request.url)) {
request.url = `http://${request.url}`;
}
let axiosInstance = makeAxiosInstance();
const {proxyMode, newRequest, proxyConfig, httpsAgentRequestFields, interpolationOptions} = await configureRequestWithCertsAndProxy({
collectionUid,
request,
envVars,
runtimeVariables,
processEnvVars,
collectionPath
});
request = newRequest
let requestMaxRedirects = request.maxRedirects
// Don't override maxRedirects here, let it be controlled by the request object
// request.maxRedirects = 0
// Set default value for requestMaxRedirects if not explicitly set
if (requestMaxRedirects === undefined) {
requestMaxRedirects = 5; // Default to 5 redirects
}
let axiosInstance = makeAxiosInstance({
proxyMode,
proxyConfig,
requestMaxRedirects,
httpsAgentRequestFields,
interpolationOptions
});
if (request.ntlmConfig) {
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
delete request.ntlmConfig;
}
if (request.oauth2) {
let requestCopy = cloneDeep(request);
switch (request?.oauth2?.grantType) {
const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey } = {} } = requestCopy || {};
let credentials, credentialsId;
switch (grantType) {
case 'authorization_code':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } =
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy, collectionUid);
request.method = 'POST';
request.headers['content-type'] = 'application/x-www-form-urlencoded';
request.data = authorizationCodeData;
request.url = authorizationCodeAccessTokenUrl;
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid }));
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
if (tokenPlacement == 'header') {
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
}
else {
try {
const url = new URL(request.url);
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
request.url = url?.toString();
}
catch(error) {}
}
break;
case 'client_credentials':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const { data: clientCredentialsData, url: clientCredentialsAccessTokenUrl } =
await transformClientCredentialsRequest(requestCopy);
request.method = 'POST';
request.headers['content-type'] = 'application/x-www-form-urlencoded';
request.data = clientCredentialsData;
request.url = clientCredentialsAccessTokenUrl;
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid }));
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
if (tokenPlacement == 'header') {
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
}
else {
try {
const url = new URL(request.url);
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
request.url = url?.toString();
}
catch(error) {}
}
break;
case 'password':
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
const { data: passwordData, url: passwordAccessTokenUrl } = await transformPasswordCredentialsRequest(
requestCopy
);
request.method = 'POST';
request.headers['content-type'] = 'application/x-www-form-urlencoded';
request.data = passwordData;
request.url = passwordAccessTokenUrl;
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid }));
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
if (tokenPlacement == 'header') {
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
}
else {
try {
const url = new URL(request.url);
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
request.url = url?.toString();
}
catch(error) {}
}
break;
}
}
@@ -663,6 +611,17 @@ const registerNetworkIpc = (mainWindow) => {
cancelTokenUid
});
if (request?.oauth2Credentials) {
mainWindow.webContents.send('main:credentials-update', {
credentials: request?.oauth2Credentials?.credentials,
url: request?.oauth2Credentials?.url,
collectionUid,
credentialsId: request?.oauth2Credentials?.credentialsId,
...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),
debugInfo: request?.oauth2Credentials?.debugInfo,
});
}
let response, responseTime;
try {
/** @type {import('axios').AxiosResponse} */
@@ -811,7 +770,8 @@ const registerNetworkIpc = (mainWindow) => {
data: response.data,
dataBuffer: dataBuffer.toString('base64'),
size: Buffer.byteLength(dataBuffer),
duration: responseTime ?? 0
duration: responseTime ?? 0,
timeline: response.timeline
};
} catch (error) {
deleteCancelToken(cancelTokenUid);
@@ -828,86 +788,11 @@ const registerNetworkIpc = (mainWindow) => {
return await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false });
});
ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, runtimeVariables) => {
try {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
const requestUid = uuid();
const collectionRoot = get(collection, 'root', {});
const _request = collectionRoot?.request;
const request = prepareCollectionRequest(_request, collection, collectionPath);
request.__bruno__executionMode = 'standalone';
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
await runPreRequest(
request,
requestUid,
envVars,
collectionPath,
collection,
collectionUid,
runtimeVariables,
processEnvVars,
scriptingConfig
);
interpolateVars(request, envVars, collection.runtimeVariables, processEnvVars);
const axiosInstance = await configureRequest(
collection.uid,
request,
envVars,
collection.runtimeVariables,
processEnvVars,
collectionPath
);
try {
response = await axiosInstance(request);
} catch (error) {
if (error?.response) {
response = error.response;
} else {
return Promise.reject(error);
}
}
const { data } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
response.data = data;
await runPostResponse(
request,
response,
requestUid,
envVars,
collectionPath,
collection,
collectionUid,
runtimeVariables,
processEnvVars,
scriptingConfig
);
return {
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data
};
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('clear-oauth2-cache', async (event, uid) => {
ipcMain.handle('clear-oauth2-cache', async (event, uid, url, credentialsId) => {
return new Promise((resolve, reject) => {
try {
const oauth2Store = new Oauth2Store();
oauth2Store.clearSessionIdOfCollection(uid);
oauth2Store.clearSessionIdOfCollection({ collectionUid: uid, url, credentialsId });
resolve();
} catch (err) {
reject(new Error('Could not clear oauth2 cache'));
@@ -915,6 +800,17 @@ const registerNetworkIpc = (mainWindow) => {
});
});
ipcMain.handle('read-oauth2-cached-credentials', async (event, uid) => {
return new Promise((resolve, reject) => {
try {
const oauth2Store = new Oauth2Store();
return resolve(oauth2Store.getOauth2DataOfCollection(uid).credentials ?? {});
} catch (err) {
reject(new Error('Could not read cached oauth2 credentials'));
}
});
});
ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => {
return new Promise((resolve, reject) => {
if (cancelTokenUid && cancelTokens[cancelTokenUid]) {
@@ -1164,6 +1060,15 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath
);
if (request?.oauth2Credentials) {
mainWindow.webContents.send('main:credentials-update', {
credentials: request?.oauth2Credentials?.credentials,
url: request?.oauth2Credentials?.url,
collectionUid,
credentialsId: request?.oauth2Credentials?.credentialsId
});
}
timeStart = Date.now();
let response, responseTime;
try {
@@ -1444,3 +1349,4 @@ const registerNetworkIpc = (mainWindow) => {
module.exports = registerNetworkIpc;
module.exports.configureRequest = configureRequest;
module.exports.configureRequestWithCertsAndProxy = configureRequestWithCertsAndProxy;

View File

@@ -15,6 +15,7 @@ const getContentType = (headers = {}) => {
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
@@ -45,6 +46,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
...envVariables,
...folderVariables,
...requestVariables,
...oauth2CredentialVariables,
...runtimeVariables,
process: {
env: {
@@ -151,62 +153,65 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
// todo: we have things happening in two places w.r.t basic auth
// need to refactor this in the future
// the request.auth (basic auth) object gets set inside the prepare-request.js file
if (request.auth) {
const username = _interpolate(request.auth.username) || '';
const password = _interpolate(request.auth.password) || '';
if (request.basicAuth) {
const username = _interpolate(request.basicAuth.username) || '';
const password = _interpolate(request.basicAuth.password) || '';
// use auth header based approach and delete the request.auth object
request.headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
delete request.auth;
delete request.basicAuth;
}
if (request?.oauth2?.grantType) {
let username, password, scope, clientId, clientSecret;
switch (request.oauth2.grantType) {
case 'password':
username = _interpolate(request.oauth2.username) || '';
password = _interpolate(request.oauth2.password) || '';
clientId = _interpolate(request.oauth2.clientId) || '';
clientSecret = _interpolate(request.oauth2.clientSecret) || '';
scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.username = username;
request.oauth2.password = password;
request.oauth2.clientId = clientId;
request.oauth2.clientSecret = clientSecret;
request.oauth2.scope = scope;
request.data = {
grant_type: 'password',
username,
password,
client_id: clientId,
client_secret: clientSecret,
scope
};
request.oauth2.refreshUrl = _interpolate(request.oauth2.refreshUrl) || '';
request.oauth2.username = _interpolate(request.oauth2.username) || '';
request.oauth2.password = _interpolate(request.oauth2.password) || '';
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
break;
case 'authorization_code':
request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || '';
request.oauth2.authorizationUrl = _interpolate(request.oauth2.authorizationUrl) || '';
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.refreshUrl = _interpolate(request.oauth2.refreshUrl) || '';
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.state = _interpolate(request.oauth2.state) || '';
request.oauth2.pkce = _interpolate(request.oauth2.pkce) || false;
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
request.oauth2.authorizeInDefaultBrowser = _interpolate(request.oauth2.authorizeInDefaultBrowser) || false;
break;
case 'client_credentials':
clientId = _interpolate(request.oauth2.clientId) || '';
clientSecret = _interpolate(request.oauth2.clientSecret) || '';
scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';
request.oauth2.clientId = clientId;
request.oauth2.clientSecret = clientSecret;
request.oauth2.scope = scope;
request.data = {
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope
};
request.oauth2.refreshUrl = _interpolate(request.oauth2.refreshUrl) || '';
request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';
request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';
request.oauth2.scope = _interpolate(request.oauth2.scope) || '';
request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';
request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';
request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';
request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';
request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';
request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);
request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);
break;
default:
break;
@@ -243,6 +248,8 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || '';
}
if(request?.auth) delete request.auth;
return request;
};

View File

@@ -1,129 +0,0 @@
const { get, cloneDeep } = require('lodash');
const crypto = require('crypto');
const { authorizeUserInWindow } = require('./authorize-user-in-window');
const Oauth2Store = require('../../store/oauth2');
const generateCodeVerifier = () => {
return crypto.randomBytes(22).toString('hex');
};
const generateCodeChallenge = (codeVerifier) => {
const hash = crypto.createHash('sha256');
hash.update(codeVerifier);
const base64Hash = hash.digest('base64');
return base64Hash.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
// AUTHORIZATION CODE
const resolveOAuth2AuthorizationCodeAccessToken = async (request, collectionUid) => {
let codeVerifier = generateCodeVerifier();
let codeChallenge = generateCodeChallenge(codeVerifier);
let requestCopy = cloneDeep(request);
const { authorizationCode } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
const oAuth = get(requestCopy, 'oauth2', {});
const { clientId, clientSecret, callbackUrl, scope, pkce } = oAuth;
const data = {
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: callbackUrl,
client_id: clientId,
client_secret: clientSecret
};
if (pkce) {
data['code_verifier'] = codeVerifier;
}
const url = requestCopy?.oauth2?.accessTokenUrl;
return {
data,
url
};
};
const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
return new Promise(async (resolve, reject) => {
const { oauth2 } = request;
const { callbackUrl, clientId, authorizationUrl, scope, state, pkce } = oauth2;
const authorizationUrlWithQueryParams = new URL(authorizationUrl);
authorizationUrlWithQueryParams.searchParams.append('response_type', 'code');
authorizationUrlWithQueryParams.searchParams.append('client_id', clientId);
if (callbackUrl) {
authorizationUrlWithQueryParams.searchParams.append('redirect_uri', callbackUrl);
}
if (scope) {
authorizationUrlWithQueryParams.searchParams.append('scope', scope);
}
if (pkce) {
authorizationUrlWithQueryParams.searchParams.append('code_challenge', codeChallenge);
authorizationUrlWithQueryParams.searchParams.append('code_challenge_method', 'S256');
}
if (state) {
authorizationUrlWithQueryParams.searchParams.append('state', state);
}
try {
const oauth2Store = new Oauth2Store();
const { authorizationCode } = await authorizeUserInWindow({
authorizeUrl: authorizationUrlWithQueryParams.toString(),
callbackUrl,
session: oauth2Store.getSessionIdOfCollection(collectionUid)
});
resolve({ authorizationCode });
} catch (err) {
reject(err);
}
});
};
// CLIENT CREDENTIALS
const transformClientCredentialsRequest = async (request) => {
let requestCopy = cloneDeep(request);
const oAuth = get(requestCopy, 'oauth2', {});
const { clientId, clientSecret, scope } = oAuth;
const data = {
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret
};
if (scope) {
data.scope = scope;
}
const url = requestCopy?.oauth2?.accessTokenUrl;
return {
data,
url
};
};
// PASSWORD CREDENTIALS
const transformPasswordCredentialsRequest = async (request) => {
let requestCopy = cloneDeep(request);
const oAuth = get(requestCopy, 'oauth2', {});
const { username, password, clientId, clientSecret, scope } = oAuth;
const data = {
grant_type: 'password',
username,
password,
client_id: clientId,
client_secret: clientSecret
};
if (scope) {
data.scope = scope;
}
const url = requestCopy?.oauth2?.accessTokenUrl;
return {
data,
url
};
};
module.exports = {
resolveOAuth2AuthorizationCodeAccessToken,
getOAuth2AuthorizationCode,
transformClientCredentialsRequest,
transformPasswordCredentialsRequest
};

View File

@@ -1,52 +0,0 @@
const { get, each } = require('lodash');
const { setAuthHeaders } = require('./prepare-request');
const prepareCollectionRequest = (request, collection) => {
const collectionRoot = get(collection, 'root', {});
const headers = {};
let contentTypeDefined = false;
let url = request.url;
// collection headers
each(get(collectionRoot, 'request.headers', []), (h) => {
if (h.enabled) {
headers[h.name] = h.value;
if (h.name.toLowerCase() === 'content-type') {
contentTypeDefined = true;
}
}
});
each(request.headers, (h) => {
if (h.enabled) {
headers[h.name] = h.value;
if (h.name.toLowerCase() === 'content-type') {
contentTypeDefined = true;
}
}
});
let axiosRequest = {
mode: request?.body?.mode,
method: request.method,
url,
headers,
responseType: 'arraybuffer'
};
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
axiosRequest.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
if (request.script) {
axiosRequest.script = request.script;
}
axiosRequest.vars = request.vars;
axiosRequest.method = 'POST';
return axiosRequest;
};
module.exports = prepareCollectionRequest;

View File

@@ -2,12 +2,11 @@ const { get, each, filter, find } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
const fs = require('node:fs/promises');
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection');
const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials, mergeAuth } = require('../../utils/collection');
const { buildFormUrlEncodedPayload } = require('../../utils/form-data');
const path = require('node:path');
const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const collectionAuth = get(collectionRoot, 'request.auth');
if (collectionAuth && request.auth.mode === 'inherit') {
switch (collectionAuth.mode) {
@@ -22,7 +21,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
};
break;
case 'basic':
axiosRequest.auth = {
axiosRequest.basicAuth = {
username: get(collectionAuth, 'basic.username'),
password: get(collectionAuth, 'basic.password')
};
@@ -42,7 +41,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
password: get(collectionAuth, 'ntlm.password'),
domain: get(collectionAuth, 'ntlm.domain')
};
break;
break;
case 'wsse':
const username = get(request, 'auth.wsse.username', '');
const password = get(request, 'auth.wsse.password', '');
@@ -69,6 +68,69 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;
}
break;
case 'oauth2':
const grantType = get(collectionAuth, 'oauth2.grantType');
switch (grantType) {
case 'password':
axiosRequest.oauth2 = {
grantType: grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
username: get(collectionAuth, 'oauth2.username'),
password: get(collectionAuth, 'oauth2.password'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
};
break;
case 'authorization_code':
axiosRequest.oauth2 = {
grantType: grantType,
callbackUrl: get(collectionAuth, 'oauth2.callbackUrl'),
authorizationUrl: get(collectionAuth, 'oauth2.authorizationUrl'),
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
state: get(collectionAuth, 'oauth2.state'),
pkce: get(collectionAuth, 'oauth2.pkce'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),
authorizeInDefaultBrowser: get(collectionAuth, 'oauth2.authorizeInDefaultBrowser')
};
break;
case 'client_credentials':
axiosRequest.oauth2 = {
grantType: grantType,
accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
clientId: get(collectionAuth, 'oauth2.clientId'),
clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
scope: get(collectionAuth, 'oauth2.scope'),
credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken')
};
break;
}
break;
}
}
@@ -85,7 +147,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
};
break;
case 'basic':
axiosRequest.auth = {
axiosRequest.basicAuth = {
username: get(request, 'auth.basic.username'),
password: get(request, 'auth.basic.password')
};
@@ -105,7 +167,6 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
password: get(request, 'auth.ntlm.password'),
domain: get(request, 'auth.ntlm.domain')
};
break;
case 'oauth2':
const grantType = get(request, 'auth.oauth2.grantType');
switch (grantType) {
@@ -113,11 +174,19 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
axiosRequest.oauth2 = {
grantType: grantType,
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
username: get(request, 'auth.oauth2.username'),
password: get(request, 'auth.oauth2.password'),
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope')
scope: get(request, 'auth.oauth2.scope'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
credentialsId: get(request, 'auth.oauth2.credentialsId'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
};
break;
case 'authorization_code':
@@ -126,20 +195,37 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
callbackUrl: get(request, 'auth.oauth2.callbackUrl'),
authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'),
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope'),
state: get(request, 'auth.oauth2.state'),
pkce: get(request, 'auth.oauth2.pkce')
pkce: get(request, 'auth.oauth2.pkce'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
credentialsId: get(request, 'auth.oauth2.credentialsId'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),
authorizeInDefaultBrowser: get(request, 'auth.oauth2.authorizeInDefaultBrowser')
};
break;
case 'client_credentials':
axiosRequest.oauth2 = {
grantType: grantType,
accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
refreshUrl: get(collectionAuth, 'oauth2.refreshUrl'),
clientId: get(request, 'auth.oauth2.clientId'),
clientSecret: get(request, 'auth.oauth2.clientSecret'),
scope: get(request, 'auth.oauth2.scope')
scope: get(request, 'auth.oauth2.scope'),
credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
credentialsId: get(request, 'auth.oauth2.credentialsId'),
tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken')
};
break;
}
@@ -178,7 +264,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const prepareRequest = async (item, collection = {}, abortController) => {
const request = item.draft ? item.draft.request : item.request;
const collectionRoot = get(collection, 'root', {});
const collectionRoot = collection?.draft ? get(collection, 'draft', {}) : get(collection, 'root', {});
const collectionPath = collection?.pathname;
const headers = {};
let contentTypeDefined = false;
@@ -197,7 +283,9 @@ const prepareRequest = async (item, collection = {}, abortController) => {
mergeHeaders(collection, request, requestTreePath);
mergeScripts(collection, request, requestTreePath, scriptFlow);
mergeVars(collection, request, requestTreePath);
mergeAuth(collection, request, requestTreePath);
request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });
}
@@ -319,10 +407,14 @@ const prepareRequest = async (item, collection = {}, abortController) => {
axiosRequest.folderVariables = request.folderVariables;
axiosRequest.requestVariables = request.requestVariables;
axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables;
axiosRequest.oauth2CredentialVariables = request.oauth2CredentialVariables;
axiosRequest.assertions = request.assertions;
axiosRequest.oauth2Credentials = request.oauth2Credentials;
return axiosRequest;
};
module.exports = prepareRequest;
module.exports.setAuthHeaders = setAuthHeaders;
module.exports = {
prepareRequest,
setAuthHeaders
}

View File

@@ -2,23 +2,40 @@ const _ = require('lodash');
const Store = require('electron-store');
const { uuid } = require('../utils/common');
/**
* Sample secrets store file
*
* {
* "collections": [{
* "path": "/Users/anoop/Code/acme-acpi-collection",
* "environments" : [{
* "name": "Local",
* "secrets": [{
* "name": "token",
* "value": "abracadabra"
* }]
* }]
* }]
* }
*/
class Oauth2Store {
constructor() {
this.store = new Store({
name: 'preferences',
name: 'oauth2',
clearInvalidConfig: true
});
}
// Get oauth2 data for all collections
getAllOauth2Data() {
let oauth2Data = this.store.get('oauth2');
let oauth2Data = this.store.get('credentials');
if (!Array.isArray(oauth2Data)) oauth2Data = [];
return oauth2Data;
}
// Get oauth2 data for a collection
getOauth2DataOfCollection(collectionUid) {
getOauth2DataOfCollection({ collectionUid, url }) {
let oauth2Data = this.getAllOauth2Data();
let oauth2DataForCollection = oauth2Data.find((d) => d?.collectionUid == collectionUid);
@@ -28,7 +45,7 @@ class Oauth2Store {
collectionUid
};
let updatedOauth2Data = [...oauth2Data, newOauth2DataForCollection];
this.store.set('oauth2', updatedOauth2Data);
this.store.set('credentials', updatedOauth2Data);
return newOauth2DataForCollection;
}
@@ -37,18 +54,18 @@ class Oauth2Store {
}
// Update oauth2 data of a collection
updateOauth2DataOfCollection(collectionUid, data) {
updateOauth2DataOfCollection({ collectionUid, url, data }) {
let oauth2Data = this.getAllOauth2Data();
let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);
updatedOauth2Data.push({ ...data });
this.store.set('oauth2', updatedOauth2Data);
this.store.set('credentials', updatedOauth2Data);
}
// Create a new oauth2 Session ID for a collection
createNewOauth2SessionIdForCollection(collectionUid) {
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
// Create a new oauth2 Session Id for a collection
createNewOauth2SessionIdForCollection({ collectionUid, url }) {
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
let newSessionId = uuid();
@@ -57,21 +74,21 @@ class Oauth2Store {
sessionId: newSessionId
};
this.updateOauth2DataOfCollection(collectionUid, newOauth2DataForCollection);
this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });
return newOauth2DataForCollection;
}
// Get session id of a collection
getSessionIdOfCollection(collectionUid) {
getSessionIdOfCollection({ collectionUid, url }) {
try {
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
if (oauth2DataForCollection?.sessionId && typeof oauth2DataForCollection.sessionId === 'string') {
return oauth2DataForCollection.sessionId;
}
let newOauth2DataForCollection = this.createNewOauth2SessionIdForCollection(collectionUid);
let newOauth2DataForCollection = this.createNewOauth2SessionIdForCollection({ collectionUid, url });
return newOauth2DataForCollection?.sessionId;
} catch (err) {
console.log('error retrieving session id from cache', err);
@@ -79,21 +96,68 @@ class Oauth2Store {
}
// clear session id of a collection
clearSessionIdOfCollection(collectionUid) {
clearSessionIdOfCollection({ collectionUid, url }) {
try {
let oauth2Data = this.getAllOauth2Data();
let oauth2DataForCollection = this.getOauth2DataOfCollection(collectionUid);
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
delete oauth2DataForCollection.sessionId;
delete oauth2DataForCollection.credentials;
let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);
updatedOauth2Data.push({ ...oauth2DataForCollection });
this.store.set('oauth2', updatedOauth2Data);
this.store.set('credentials', updatedOauth2Data);
} catch (err) {
console.log('error while clearing the oauth2 session cache', err);
}
}
getCredentialsForCollection({ collectionUid, url, credentialsId }) {
try {
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
let credentials = oauth2DataForCollection?.credentials?.find(c => (c?.url == url) && (c?.credentialsId == credentialsId));
return credentials?.data;
} catch (err) {
console.log('error retrieving oauth2 credentials from cache', err);
}
}
updateCredentialsForCollection({ collectionUid, url, credentialsId, credentials = {} }) {
try {
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
let filteredCredentials = oauth2DataForCollection?.credentials?.filter(c => (c?.url !== url) || (c?.credentialsId !== credentialsId));
if (!filteredCredentials) filteredCredentials = [];
filteredCredentials.push({
url,
data: credentials,
credentialsId
});
let newOauth2DataForCollection = {
...oauth2DataForCollection,
credentials: filteredCredentials
};
this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });
return newOauth2DataForCollection;
} catch (err) {
console.log('error updating oauth2 credentials from cache', err);
}
}
clearCredentialsForCollection({ collectionUid, url, credentialsId }) {
try {
let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });
let filteredCredentials = oauth2DataForCollection?.credentials?.filter(c => (c?.url !== url) || (c?.credentialsId !== credentialsId));
let newOauth2DataForCollection = {
...oauth2DataForCollection,
credentials: filteredCredentials
};
this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });
return newOauth2DataForCollection;
} catch (err) {
console.log('error clearing oauth2 credentials from cache', err);
}
}
}
module.exports = Oauth2Store;

View File

@@ -1,8 +1,7 @@
const { get, each, find, compact, filter } = require('lodash');
const fs = require('fs');
const { getRequestUid } = require('../cache/requestUids');
const { uuid } = require('./common');
const { get, each, find, compact } = require('lodash');
const os = require('os');
const mergeHeaders = (collection, request, requestTreePath) => {
@@ -264,10 +263,137 @@ const findItemInCollectionByPathname = (collection, pathname) => {
return findItemByPathname(flattenedItems, pathname);
};
const sortCollection = (collection) => {
const items = collection.items || [];
let folderItems = filter(items, (item) => item.type === 'folder');
let requestItems = filter(items, (item) => item.type !== 'folder');
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
collection.items = folderItems.concat(requestItems);
each(folderItems, (item) => {
sortCollection(item);
});
};
const sortFolder = (folder = {}) => {
const items = folder.items || [];
let folderItems = filter(items, (item) => item.type === 'folder');
let requestItems = filter(items, (item) => item.type !== 'folder');
folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
requestItems = requestItems.sort((a, b) => a.seq - b.seq);
folder.items = folderItems.concat(requestItems);
each(folderItems, (item) => {
sortFolder(item);
});
return folder;
};
const getAllRequestsInFolderRecursively = (folder = {}) => {
let requests = [];
if (folder.items && folder.items.length) {
folder.items.forEach((item) => {
if (item.type !== 'folder') {
requests.push(item);
} else {
requests = requests.concat(getAllRequestsInFolderRecursively(item));
}
});
}
return requests;
};
const getEnvVars = (environment = {}) => {
const variables = environment.variables;
if (!variables || !variables.length) {
return {
__name__: environment.name
};
}
const envVars = {};
each(variables, (variable) => {
if (variable.enabled) {
envVars[variable.name] = variable.value;
}
});
return {
...envVars,
__name__: environment.name
};
};
const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {
let credentialsVariables = {};
oauth2Credentials.forEach(({ credentialsId, credentials }) => {
if (credentials) {
Object.entries(credentials).forEach(([key, value]) => {
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
});
}
});
return credentialsVariables;
};
const mergeAuth = (collection, request, requestTreePath) => {
// Start with collection level auth (always consider collection auth as base)
let collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
let effectiveAuth = collectionAuth;
let lastFolderWithAuth = null;
// Traverse through the path to find the closest auth configuration
for (let i of requestTreePath) {
if (i.type === 'folder') {
const folderAuth = get(i, 'root.request.auth');
// Only consider folders that have a valid auth mode
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveAuth = folderAuth;
lastFolderWithAuth = i;
}
}
}
// If request is set to inherit, use the effective auth from collection/folders
if (request.auth.mode === 'inherit') {
request.auth = effectiveAuth;
// For OAuth2, we need to handle credentials properly
if (effectiveAuth.mode === 'oauth2') {
if (lastFolderWithAuth) {
// If auth is from folder, add folderUid and clear itemUid
request.oauth2Credentials = {
...request.oauth2Credentials,
folderUid: lastFolderWithAuth.uid,
itemUid: null,
mode: request.auth.mode
};
} else {
// If auth is from collection, ensure no folderUid and no itemUid
request.oauth2Credentials = {
...request.oauth2Credentials,
folderUid: null,
itemUid: null,
mode: request.auth.mode
};
}
}
}
};
module.exports = {
mergeHeaders,
mergeVars,
mergeScripts,
mergeAuth,
getTreePathFromCollectionToItem,
flattenItems,
findItem,
@@ -276,5 +402,10 @@ module.exports = {
findItemInCollectionByPathname,
findParentItemInCollection,
parseBruFileMeta,
sortCollection,
sortFolder,
getAllRequestsInFolderRecursively,
getEnvVars,
getFormattedCollectionOauth2Credentials,
hydrateRequestWithUuid
};

View File

@@ -0,0 +1,69 @@
const express = require("express");
const { shell } = require("electron");
let { exec } = require('child_process');
const portToPid = require("./pid-port");
const BRUNO_OAUTH2_SERVER_PORT = 9876;
const BRUNO_OAUTH2_SERVER_CALLBACK_URL = `http://localhost:9876/callback`;
let server;
const freePort = async () => {
return new Promise(async (resolve, reject) => {
try {
const pid = await portToPid(BRUNO_OAUTH2_SERVER_PORT);
if(pid) {
if(process.platform === "win32") {
exec(`taskkill /PID ${pid} /F`)
}
else {
exec(`kill -9 ${pid}`);
}
}
else {
console.log("port is free");
}
resolve();
}
catch(err) {
reject(err);
}
});
}
async function getOauth2AuthorizationCodeUsingDefaultBrowser({ authorizeUrl, port = BRUNO_OAUTH2_SERVER_PORT }) {
await server?.close?.();
// test and refactor before uncommenting
// await freePort();
const redirectUri = BRUNO_OAUTH2_SERVER_CALLBACK_URL;
const parsedAuthorizeUrl = new URL(authorizeUrl);
parsedAuthorizeUrl?.searchParams.set('redirect_uri', redirectUri);
const finalAuthorizeUrl = parsedAuthorizeUrl.href;
return new Promise((resolve, reject) => {
const app = express();
app.get("/callback", (req, res) => {
const { code } = req.query;
if (!code) {
res.status(400).send("Authorization failed. No code received.");
reject(new Error("No authorization code received"));
return;
}
res.send("Authorization successful. You can close this tab.");
resolve({ authorizationCode: code });
server.close();
});
server = app.listen(port, () => {
shell.openExternal(finalAuthorizeUrl);
});
// Ensure the server is cleaned up in case of error
server.on("error", (err) => {
reject(err);
});
});
}
module.exports = { getOauth2AuthorizationCodeUsingDefaultBrowser, BRUNO_OAUTH2_SERVER_CALLBACK_URL };

View File

@@ -0,0 +1,674 @@
const { get, cloneDeep } = require('lodash');
const crypto = require('crypto');
const { authorizeUserInWindow } = require('../ipc/network/authorize-user-in-window');
const Oauth2Store = require('../store/oauth2');
const { makeAxiosInstance } = require('../ipc/network/axios-instance');
const { safeParseJSON, safeStringifyJSON, uuid } = require('./common');
const { getOauth2AuthorizationCodeUsingDefaultBrowser, BRUNO_OAUTH2_SERVER_CALLBACK_URL } = require('./oauth2-server');
const oauth2Store = new Oauth2Store();
const persistOauth2Credentials = ({ collectionUid, url, credentials, credentialsId }) => {
const enhancedCredentials = {
...credentials,
created_at: Date.now(),
};
oauth2Store.updateCredentialsForCollection({ collectionUid, url, credentials: enhancedCredentials, credentialsId });
};
const clearOauth2Credentials = ({ collectionUid, url, credentialsId }) => {
oauth2Store.clearCredentialsForCollection({ collectionUid, url, credentialsId });
};
const getStoredOauth2Credentials = ({ collectionUid, url, credentialsId }) => {
const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId });
return credentials;
};
const isTokenExpired = (credentials) => {
if (!credentials || !credentials.expires_in || !credentials.created_at) {
return true; // Assume expired if missing data
}
const expiryTime = credentials.created_at + credentials.expires_in * 1000;
return Date.now() > expiryTime;
};
// AUTHORIZATION CODE
const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, forceFetch = false }) => {
let codeVerifier = generateCodeVerifier();
let codeChallenge = generateCodeChallenge(codeVerifier);
let requestCopy = cloneDeep(request);
const oAuth = get(requestCopy, 'oauth2', {});
const {
clientId,
clientSecret,
callbackUrl,
scope,
pkce,
credentialsPlacement,
credentialsId,
autoRefreshToken,
autoFetchToken,
authorizeInDefaultBrowser
} = oAuth;
const url = requestCopy?.oauth2?.accessTokenUrl;
if (!forceFetch) {
const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
if (storedCredentials) {
// Token exists
if (!isTokenExpired(storedCredentials)) {
// Token is valid, use it
return { collectionUid, url, credentials: storedCredentials, credentialsId };
} else {
// Token is expired
if (autoRefreshToken && storedCredentials.refresh_token) {
// Try to refresh token
try {
const refreshedCredentialsData = await refreshOauth2Token(requestCopy, collectionUid);
return { collectionUid, url, credentials: refreshedCredentialsData.credentials, credentialsId };
} catch (error) {
// Refresh failed
clearOauth2Credentials({ collectionUid, url, credentialsId });
if (autoFetchToken) {
// Proceed to fetch new token
} else {
// Proceed with expired token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
}
} else if (autoRefreshToken && !storedCredentials.refresh_token) {
// Cannot refresh; try autoFetchToken
if (autoFetchToken) {
// Proceed to fetch new token
clearOauth2Credentials({ collectionUid, url, credentialsId });
} else {
// Proceed with expired token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
} else if (!autoRefreshToken && autoFetchToken) {
// Proceed to fetch new token
clearOauth2Credentials({ collectionUid, url, credentialsId });
} else {
// Proceed with expired token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
}
} else {
// No stored credentials
if (autoFetchToken && !storedCredentials) {
// Proceed to fetch new token
} else {
// Proceed without token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
}
}
// Fetch new token process
let { authorizationCode, debugInfo } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);
requestCopy.method = 'POST';
requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded';
requestCopy.headers['Accept'] = 'application/json';
if (credentialsPlacement === "basic_auth_header") {
requestCopy.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
}
const data = {
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: authorizeInDefaultBrowser ? BRUNO_OAUTH2_SERVER_CALLBACK_URL : callbackUrl,
client_id: clientId,
};
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
data.client_secret = clientSecret;
}
if (pkce) {
data['code_verifier'] = codeVerifier;
}
if (scope) {
data.scope = scope;
}
requestCopy.data = data;
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
// Initialize variables to hold request and response data for debugging
let axiosRequestInfo = null;
let axiosResponseInfo = null;
try {
const axiosInstance = makeAxiosInstance();
// Interceptor to capture request data
axiosInstance.interceptors.request.use((config) => {
axiosRequestInfo = {
method: config.method.toUpperCase(),
url: config.url,
headers: config.headers,
data: config.data,
timestamp: Date.now(),
};
return config;
});
// Interceptor to capture response data
axiosInstance.interceptors.response.use((response) => {
axiosResponseInfo = {
url: response?.url,
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data,
timestamp: Date.now(),
};
return response;
}, (error) => {
if (error.response) {
axiosResponseInfo = {
url: error?.response?.url,
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data,
timestamp: Date.now(),
};
}
return Promise.reject(error);
});
const response = await axiosInstance(requestCopy);
const parsedResponseData = safeParseJSON(
Buffer.isBuffer(response.data) ? response.data.toString() : response.data
);
// Ensure debugInfo.data is initialized
if (!debugInfo) {
debugInfo = { data: [] };
} else if (!debugInfo.data) {
debugInfo.data = [];
}
// Add the axios request and response info as a main request in debugInfo
const axiosMainRequest = {
requestId: uuid(),
request: {
url: axiosRequestInfo?.url,
method: axiosRequestInfo?.method,
headers: axiosRequestInfo?.headers || {},
body: axiosRequestInfo?.data,
error: null
},
response: {
url: axiosResponseInfo?.url,
headers: axiosResponseInfo?.headers,
data: parsedResponseData,
dataBuffer: axiosResponseInfo?.data,
status: axiosResponseInfo?.status,
statusText: axiosResponseInfo?.statusText,
error: null
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
};
debugInfo.data.push(axiosMainRequest);
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
} catch (error) {
return Promise.reject(safeStringifyJSON(error?.response?.data));
}
};
const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
return new Promise(async (resolve, reject) => {
const { oauth2 } = request;
const { callbackUrl, clientId, authorizationUrl, scope, state, pkce, accessTokenUrl, authorizeInDefaultBrowser = true } = oauth2;
const authorizationUrlWithQueryParams = new URL(authorizationUrl);
authorizationUrlWithQueryParams.searchParams.append('response_type', 'code');
authorizationUrlWithQueryParams.searchParams.append('client_id', clientId);
if (callbackUrl) {
authorizationUrlWithQueryParams.searchParams.append('redirect_uri', callbackUrl);
}
if (scope) {
authorizationUrlWithQueryParams.searchParams.append('scope', scope);
}
if (pkce) {
authorizationUrlWithQueryParams.searchParams.append('code_challenge', codeChallenge);
authorizationUrlWithQueryParams.searchParams.append('code_challenge_method', 'S256');
}
if (state) {
authorizationUrlWithQueryParams.searchParams.append('state', state);
}
try {
const authorizeUrl = authorizationUrlWithQueryParams.toString();
let authorizationCode, debugInfo;
if (authorizeInDefaultBrowser) {
({ authorizationCode, debugInfo } = await getOauth2AuthorizationCodeUsingDefaultBrowser({
authorizeUrl
}));
} else {
({ authorizationCode, debugInfo } = await authorizeUserInWindow({
authorizeUrl,
callbackUrl,
session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: accessTokenUrl })
}));
}
resolve({ authorizationCode, debugInfo });
} catch (err) {
reject(err);
}
});
};
// CLIENT CREDENTIALS
const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, forceFetch = false }) => {
let requestCopy = cloneDeep(request);
const oAuth = get(requestCopy, 'oauth2', {});
const {
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
autoRefreshToken,
autoFetchToken,
} = oAuth;
const url = requestCopy?.oauth2?.accessTokenUrl;
if (!forceFetch) {
const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
if (storedCredentials) {
// Token exists
if (!isTokenExpired(storedCredentials)) {
// Token is valid, use it
return { collectionUid, url, credentials: storedCredentials, credentialsId };
} else {
// Token is expired
if (autoRefreshToken && storedCredentials.refresh_token) {
// Try to refresh token
try {
const refreshedCredentialsData = await refreshOauth2Token(requestCopy, collectionUid);
return { collectionUid, url, credentials: refreshedCredentialsData.credentials, credentialsId };
} catch (error) {
clearOauth2Credentials({ collectionUid, url, credentialsId });
if (autoFetchToken) {
// Proceed to fetch new token
} else {
// Proceed with expired token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
}
} else if (autoRefreshToken && !storedCredentials.refresh_token) {
if (autoFetchToken) {
// Proceed to fetch new token
clearOauth2Credentials({ collectionUid, url, credentialsId });
} else {
// Proceed with expired token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
} else if (!autoRefreshToken && autoFetchToken) {
// Proceed to fetch new token
clearOauth2Credentials({ collectionUid, url, credentialsId });
} else {
// Proceed with expired token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
}
} else {
// No stored credentials
if (autoFetchToken && !storedCredentials) {
// Proceed to fetch new token
} else {
// Proceed without token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
}
}
// Fetch new token process
requestCopy.method = 'POST';
requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded';
requestCopy.headers['Accept'] = 'application/json';
if (credentialsPlacement === "basic_auth_header") {
requestCopy.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
}
const data = {
grant_type: 'client_credentials',
client_id: clientId,
};
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
data.client_secret = clientSecret;
}
if (scope) {
data.scope = scope;
}
requestCopy.data = data;
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
// Initialize variables to hold request and response data for debugging
let axiosRequestInfo = null;
let axiosResponseInfo = null;
let debugInfo = { data: [] };
try {
const axiosInstance = makeAxiosInstance();
// Interceptor to capture request data
axiosInstance.interceptors.request.use((config) => {
axiosRequestInfo = {
method: config.method.toUpperCase(),
url: config.url,
headers: config.headers,
data: config.data,
timestamp: Date.now(),
};
return config;
});
// Interceptor to capture response data
axiosInstance.interceptors.response.use((response) => {
axiosResponseInfo = {
url: response.url,
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data,
timestamp: Date.now(),
};
return response;
}, (error) => {
if (error.response) {
axiosResponseInfo = {
url: error?.response?.url,
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data,
timestamp: Date.now(),
};
}
return Promise.reject(error);
});
const response = await axiosInstance(requestCopy);
const responseData = Buffer.isBuffer(response.data) ? response.data.toString() : response.data;
const parsedResponseData = safeParseJSON(responseData);
// Add the axios request and response info as a main request in debugInfo
const axiosMainRequest = {
requestId: Date.now().toString(),
request: {
url: axiosRequestInfo?.url,
method: axiosRequestInfo?.method,
headers: axiosRequestInfo?.headers || {},
body: axiosRequestInfo?.data,
error: null
},
response: {
url: axiosResponseInfo.url,
headers: axiosResponseInfo?.headers,
data: parsedResponseData,
dataBuffer: axiosResponseInfo?.data?.toString('base64'),
status: axiosResponseInfo?.status,
statusText: axiosResponseInfo?.statusText,
error: null
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
};
debugInfo.data.push(axiosMainRequest);
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
} catch (error) {
return Promise.reject(safeStringifyJSON(error?.response?.data));
}
};
// PASSWORD CREDENTIALS
const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, forceFetch = false }) => {
let requestCopy = cloneDeep(request);
const oAuth = get(requestCopy, 'oauth2', {});
const {
username,
password,
clientId,
clientSecret,
scope,
credentialsPlacement,
credentialsId,
autoRefreshToken,
autoFetchToken,
} = oAuth;
const url = requestCopy?.oauth2?.accessTokenUrl;
if (!forceFetch) {
const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
if (storedCredentials) {
// Token exists
if (!isTokenExpired(storedCredentials)) {
// Token is valid, use it
return { collectionUid, url, credentials: storedCredentials, credentialsId };
} else {
// Token is expired
if (autoRefreshToken && storedCredentials.refresh_token) {
// Try to refresh token
try {
const refreshedCredentialsData = await refreshOauth2Token(requestCopy, collectionUid);
return { collectionUid, url, credentials: refreshedCredentialsData.credentials, credentialsId };
} catch (error) {
clearOauth2Credentials({ collectionUid, url, credentialsId });
if (autoFetchToken) {
// Proceed to fetch new token
} else {
// Proceed with expired token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
}
} else if (autoRefreshToken && !storedCredentials.refresh_token) {
// Cannot refresh; try autoFetchToken
if (autoFetchToken) {
// Proceed to fetch new token
clearOauth2Credentials({ collectionUid, url, credentialsId });
} else {
// Proceed with expired token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
} else if (!autoRefreshToken && autoFetchToken) {
// Proceed to fetch new token
clearOauth2Credentials({ collectionUid, url, credentialsId });
} else {
// Proceed with expired token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
}
} else {
// No stored credentials
if (autoFetchToken && !storedCredentials) {
// Proceed to fetch new token
} else {
// Proceed without token
return { collectionUid, url, credentials: storedCredentials, credentialsId };
}
}
}
// Fetch new token process
requestCopy.method = 'POST';
requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded';
requestCopy.headers['Accept'] = 'application/json';
if (credentialsPlacement === "basic_auth_header") {
requestCopy.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
}
const data = {
grant_type: 'password',
username,
password,
client_id: clientId,
};
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
data.client_secret = clientSecret;
}
if (scope) {
data.scope = scope;
}
requestCopy.data = data;
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
// Initialize variables to hold request and response data for debugging
let axiosRequestInfo = null;
let axiosResponseInfo = null;
let debugInfo = { data: [] };
try {
const axiosInstance = makeAxiosInstance();
// Interceptor to capture request data
axiosInstance.interceptors.request.use((config) => {
axiosRequestInfo = {
method: config.method.toUpperCase(),
url: config.url,
headers: config.headers,
data: config.data,
timestamp: Date.now(),
};
return config;
});
// Interceptor to capture response data
axiosInstance.interceptors.response.use((response) => {
axiosResponseInfo = {
url: response.url,
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data,
timestamp: Date.now(),
};
return response;
}, (error) => {
if (error.response) {
axiosResponseInfo = {
url: error?.response?.url,
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data,
timestamp: Date.now(),
};
}
return Promise.reject(error);
});
const response = await axiosInstance(requestCopy);
const responseData = Buffer.isBuffer(response.data) ? response.data.toString() : response.data;
const parsedResponseData = safeParseJSON(responseData);
// Add the axios request and response info as a main request in debugInfo
const axiosMainRequest = {
requestId: Date.now().toString(),
request: {
url: axiosRequestInfo?.url,
method: axiosRequestInfo?.method,
headers: axiosRequestInfo?.headers || {},
body: axiosRequestInfo?.data,
error: null
},
response: {
url: axiosResponseInfo?.url,
headers: axiosResponseInfo?.headers,
data: parsedResponseData,
dataBuffer: axiosResponseInfo?.data?.toString('base64'),
status: axiosResponseInfo?.status,
statusText: axiosResponseInfo?.statusText,
error: null
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
};
debugInfo.data.push(axiosMainRequest);
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
} catch (error) {
return Promise.reject(safeStringifyJSON(error?.response?.data));
}
};
const refreshOauth2Token = async (requestCopy, collectionUid) => {
const oAuth = get(requestCopy, 'oauth2', {});
const { clientId, clientSecret, credentialsId } = oAuth;
const url = oAuth.refreshUrl ? oAuth.refreshUrl : oAuth.accessTokenUrl;
const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
if (!credentials?.refresh_token) {
clearOauth2Credentials({ collectionUid, url, credentialsId });
// Proceed without token
return { collectionUid, url, credentials: null, credentialsId };
} else {
const data = {
grant_type: 'refresh_token',
client_id: clientId,
refresh_token: credentials.refresh_token,
};
if (clientSecret) {
data.client_secret = clientSecret;
}
requestCopy.method = 'POST';
requestCopy.headers['content-type'] = 'application/x-www-form-urlencoded';
requestCopy.headers['Accept'] = 'application/json';
requestCopy.data = data;
requestCopy.url = url;
const axiosInstance = makeAxiosInstance();
try {
const response = await axiosInstance(requestCopy);
const responseData = Buffer.isBuffer(response.data) ? response.data.toString() : response.data;
const parsedResponseData = safeParseJSON(responseData);
persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
return { collectionUid, url, credentials: parsedResponseData, credentialsId };
} catch (error) {
clearOauth2Credentials({ collectionUid, url, credentialsId });
// Proceed without token
return { collectionUid, url, credentials: null, credentialsId };
}
}
};
// HELPER FUNCTIONS
const generateCodeVerifier = () => {
return crypto.randomBytes(22).toString('hex');
};
const generateCodeChallenge = (codeVerifier) => {
const hash = crypto.createHash('sha256');
hash.update(codeVerifier);
const base64Hash = hash.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return base64Hash;
};
module.exports = {
getOAuth2TokenUsingAuthorizationCode,
getOAuth2AuthorizationCode,
getOAuth2TokenUsingClientCredentials,
getOAuth2TokenUsingPasswordCredentials,
refreshOauth2Token
};

View File

@@ -0,0 +1,83 @@
// https://github.com/sindresorhus/pid-port
// common js util function for the above library
const { spawnSync } = require('node:child_process');
const process = require('node:process');
const runCommand = (command, args) => {
const result = spawnSync(command, args, { encoding: 'utf-8' });
if (result.error) {
throw result.error;
}
return result.stdout || ''; // Handle undefined stdout
};
const netstat = type => runCommand('netstat', ['-anv', '-p', type]);
const macos = async () => {
const result = [netstat('tcp'), netstat('udp')];
const tcp = result[0] || '';
const headerStart = tcp?.indexOf('\n') + 1;
const header = tcp.slice(headerStart, tcp.indexOf('\n', headerStart)) || '';
return {
stdout: result.join('\n'),
addressColumn: 3,
pidColumn: header.includes('rxbytes') ? 10 : 8,
};
};
const linux = async () => {
const stdout = runCommand('ss', ['-tunlp']) || '';
return { stdout, addressColumn: 4, pidColumn: 6 };
};
const windows = async () => {
const stdout = runCommand('netstat', ['-ano']) || '';
return { stdout, addressColumn: 1, pidColumn: 4 };
};
const isProtocol = value => /^\s*(tcp|udp)/i.test(value);
const parsePid = pid => {
if (typeof pid !== 'string') {
return;
}
const { groups } = /(?:^|",|",pid=)(?<pid>\d+)/.exec(pid) || {};
return groups ? Number.parseInt(groups.pid, 10) : undefined;
};
const getPort = (port, { lines, addressColumn, pidColumn }) => {
const regex = new RegExp(`[.:]${port}$`);
const foundPort = lines.find(line => regex.test(line[addressColumn]));
if (!foundPort) {
return null;
}
return parsePid(foundPort[pidColumn]);
};
const implementation = process.platform === 'darwin' ? macos : (process.platform === 'linux' ? linux : windows);
const getList = async () => {
const { stdout, addressColumn, pidColumn } = await implementation();
const lines = (stdout || '').split('\n')
.filter(line => isProtocol(line))
.map(line => line.match(/\S+/g) || []);
return { lines, addressColumn, pidColumn };
};
const portToPid = async port => {
if (Array.isArray(port)) {
const list = await getList();
const tuples = await Promise.all(port.map(port_ => [port_, getPort(port_, list)]));
return new Map(tuples);
}
if (!Number.isInteger(port)) {
throw new TypeError(`Expected an integer, got ${typeof port}`);
}
return getPort(port, await getList());
};
module.exports = portToPid;

View File

@@ -1,6 +1,11 @@
const parseUrl = require('url').parse;
const { isEmpty } = require('lodash');
const https = require('https');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { interpolateString } = require('../ipc/network/interpolate-string');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { HttpProxyAgent } = require('http-proxy-agent');
const { preferencesUtil } = require('../store/preferences');
const { isEmpty, get, isUndefined, isNull } = require('lodash');
const DEFAULT_PORTS = {
ftp: 21,
@@ -79,7 +84,254 @@ class PatchedHttpsProxyAgent extends HttpsProxyAgent {
}
}
function createTimelineAgentClass(BaseAgentClass) {
return class extends BaseAgentClass {
constructor(options, timeline) {
super(options);
this.timeline = timeline;
this.alpnProtocols = options.ALPNProtocols || ['h2', 'http/1.1'];
this.caProvided = !!options.ca;
}
createConnection(options, callback) {
const { host, port } = options;
// Log SSL validation
this.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Enable SSL validation`,
});
// Log ALPN protocols offered
if (this.alpnProtocols && this.alpnProtocols.length > 0) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `ALPN: offers ${this.alpnProtocols.join(', ')}`,
});
}
// Log CAfile and CApath (if possible)
if (this.caProvided) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `CA certificates provided`,
});
} else {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `Using system default CA certificates`,
});
}
// Log "Trying host:port..."
this.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Trying ${host}:${port}...`,
});
const socket = super.createConnection(options, callback);
// Attach event listeners to the socket
socket.on('lookup', (err, address, family, host) => {
if (err) {
this.timeline.push({
timestamp: new Date(),
type: 'error',
message: `DNS lookup error for ${host}: ${err.message}`,
});
} else {
this.timeline.push({
timestamp: new Date(),
type: 'info',
message: `DNS lookup: ${host} -> ${address}`,
});
}
});
socket.on('connect', () => {
const address = socket.remoteAddress || host;
const remotePort = socket.remotePort || port;
this.timeline.push({
timestamp: new Date(),
type: 'info',
message: `Connected to ${host} (${address}) port ${remotePort}`,
});
});
socket.on('secureConnect', () => {
const protocol = socket.getProtocol() || 'SSL/TLS';
const cipher = socket.getCipher();
const cipherSuite = cipher ? `${cipher.name} (${cipher.version})` : 'Unknown cipher';
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `SSL connection using ${protocol} / ${cipherSuite}`,
});
// ALPN protocol
const alpnProtocol = socket.alpnProtocol || 'None';
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `ALPN: server accepted ${alpnProtocol}`,
});
// Server certificate
const cert = socket.getPeerCertificate(true);
if (cert) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `Server certificate:`,
});
if (cert.subject) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: ` subject: ${Object.entries(cert.subject).map(([k, v]) => `${k}=${v}`).join(', ')}`,
});
}
if (cert.valid_from) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: ` start date: ${cert.valid_from}`,
});
}
if (cert.valid_to) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: ` expire date: ${cert.valid_to}`,
});
}
if (cert.subjectaltname) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: ` subjectAltName: ${cert.subjectaltname}`,
});
}
if (cert.issuer) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: ` issuer: ${Object.entries(cert.issuer).map(([k, v]) => `${k}=${v}`).join(', ')}`,
});
}
// SSL certificate verify ok
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `SSL certificate verify ok.`,
});
}
});
socket.on('error', (err) => {
this.timeline.push({
timestamp: new Date(),
type: 'error',
message: `Socket error: ${err.message}`,
});
});
return socket;
}
};
}
function setupProxyAgents({
requestConfig,
proxyMode = 'off',
proxyConfig,
httpsAgentRequestFields,
interpolationOptions,
timeline,
}) {
if (proxyMode === 'on') {
const shouldProxy = shouldUseProxy(requestConfig.url, get(proxyConfig, 'bypassProxy', ''));
if (shouldProxy) {
const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);
const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);
const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);
const proxyAuthEnabled = get(proxyConfig, 'auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions);
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
}
if (socksEnabled) {
const TimelineSocksProxyAgent = createTimelineAgentClass(SocksProxyAgent);
requestConfig.httpAgent = new TimelineSocksProxyAgent({ proxy: proxyUri }, timeline);
requestConfig.httpsAgent = new TimelineSocksProxyAgent({ proxy: proxyUri, ...httpsAgentRequestFields }, timeline);
} else {
const TimelineHttpsProxyAgent = createTimelineAgentClass(HttpsProxyAgent);
requestConfig.httpAgent = new HttpProxyAgent(proxyUri); // For http, no need for timeline
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
proxyUri,
{ ...httpsAgentRequestFields },
timeline
);
}
} else {
// If proxy should not be used, set default HTTPS agent
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
requestConfig.httpsAgent = new TimelineHttpsAgent(httpsAgentRequestFields, timeline);
}
} else if (proxyMode === 'system') {
const { http_proxy, https_proxy, no_proxy } = preferencesUtil.getSystemProxyEnvVariables();
const shouldUseSystemProxy = shouldUseProxy(requestConfig.url, no_proxy || '');
if (shouldUseSystemProxy) {
try {
if (http_proxy?.length) {
new URL(http_proxy);
requestConfig.httpAgent = new HttpProxyAgent(http_proxy);
}
} catch (error) {
throw new Error('Invalid system http_proxy');
}
try {
if (https_proxy?.length) {
new URL(https_proxy);
const TimelineHttpsProxyAgent = createTimelineAgentClass(HttpsProxyAgent);
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
https_proxy,
{ ...httpsAgentRequestFields },
timeline
);
}
} catch (error) {
throw new Error('Invalid system https_proxy');
}
} else {
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
requestConfig.httpsAgent = new TimelineHttpsAgent(httpsAgentRequestFields, timeline);
}
} else {
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
requestConfig.httpsAgent = new TimelineHttpsAgent(httpsAgentRequestFields, timeline);
}
}
module.exports = {
shouldUseProxy,
PatchedHttpsProxyAgent
PatchedHttpsProxyAgent,
setupProxyAgents
};

View File

@@ -1,6 +1,6 @@
const { describe, it, expect } = require('@jest/globals');
const prepareRequest = require('../../src/ipc/network/prepare-request');
const { prepareRequest } = require('../../src/ipc/network/prepare-request');
const { buildFormUrlEncodedPayload } = require('../../src/utils/form-data');
describe('prepare-request: prepareRequest', () => {

View File

@@ -4,7 +4,7 @@ const { interpolate } = require('@usebruno/common');
const variableNameRegex = /^[\w-.]*$/;
class Bru {
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables) {
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables) {
this.envVariables = envVariables || {};
this.runtimeVariables = runtimeVariables || {};
this.processEnvVars = cloneDeep(processEnvVars || {});
@@ -12,6 +12,7 @@ class Bru {
this.folderVariables = folderVariables || {};
this.requestVariables = requestVariables || {};
this.globalEnvironmentVariables = globalEnvironmentVariables || {};
this.oauth2CredentialVariables = oauth2CredentialVariables || {};
this.collectionPath = collectionPath;
this.runner = {
skipRequest: () => {
@@ -37,6 +38,7 @@ class Bru {
...this.envVariables,
...this.folderVariables,
...this.requestVariables,
...this.oauth2CredentialVariables,
...this.runtimeVariables,
process: {
env: {
@@ -92,6 +94,10 @@ class Bru {
this.globalEnvironmentVariables[key] = value;
}
getOauth2CredentialVar(key) {
return this._interpolate(this.oauth2CredentialVariables[key]);
}
hasVar(key) {
return Object.hasOwn(this.runtimeVariables, key);
}

View File

@@ -246,6 +246,7 @@ class AssertRuntime {
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
@@ -279,6 +280,7 @@ class AssertRuntime {
...envVariables,
...folderVariables,
...requestVariables,
...oauth2CredentialVariables,
...runtimeVariables,
...processEnvVars,
...bruContext

View File

@@ -51,10 +51,11 @@ class ScriptRuntime {
runRequestByItemPathname
) {
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables);
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables);
const req = new BrunoRequest(request);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
@@ -122,6 +123,7 @@ class ScriptRuntime {
sandbox: context,
require: {
context: 'sandbox',
builtin: [ "*" ],
external: true,
root: [collectionPath, ...additionalContextRootsAbsolute],
mock: {
@@ -182,10 +184,11 @@ class ScriptRuntime {
runRequestByItemPathname
) {
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
const collectionVariables = request?.collectionVariables || {};
const folderVariables = request?.folderVariables || {};
const requestVariables = request?.requestVariables || {};
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables);
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
@@ -250,6 +253,7 @@ class ScriptRuntime {
sandbox: context,
require: {
context: 'sandbox',
builtin: [ "*" ],
external: true,
root: [collectionPath],
mock: {

View File

@@ -53,6 +53,12 @@ const addBruShimToContext = (vm, bru) => {
vm.setProp(bruObject, 'getGlobalEnvVar', getGlobalEnvVar);
getGlobalEnvVar.dispose();
let getOauth2CredentialVar = vm.newFunction('getOauth2CredentialVar', function (key) {
return marshallToVm(bru.getOauth2CredentialVar(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getOauth2CredentialVar', getOauth2CredentialVar);
getOauth2CredentialVar.dispose();
let setGlobalEnvVar = vm.newFunction('setGlobalEnvVar', function (key, value) {
bru.setGlobalEnvVar(vm.dump(key), vm.dump(value));
});

View File

@@ -514,11 +514,20 @@ const sem = grammar.createSemantics().addAttribute('ast', {
const callbackUrlKey = _.find(auth, { name: 'callback_url' });
const authorizationUrlKey = _.find(auth, { name: 'authorization_url' });
const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' });
const refreshUrlKey = _.find(auth, { name: 'refresh_url' });
const clientIdKey = _.find(auth, { name: 'client_id' });
const clientSecretKey = _.find(auth, { name: 'client_secret' });
const scopeKey = _.find(auth, { name: 'scope' });
const stateKey = _.find(auth, { name: 'state' });
const pkceKey = _.find(auth, { name: 'pkce' });
const credentialsPlacementKey = _.find(auth, { name: 'credentials_placement' });
const credentialsIdKey = _.find(auth, { name: 'credentials_id' });
const tokenPlacementKey = _.find(auth, { name: 'token_placement' });
const tokenHeaderPrefixKey = _.find(auth, { name: 'token_header_prefix' });
const tokenQueryKeyKey = _.find(auth, { name: 'token_query_key' });
const autoFetchTokenKey = _.find(auth, { name: 'auto_fetch_token' });
const autoRefreshTokenKey = _.find(auth, { name: 'auto_refresh_token' });
const authorizeInDefaultBrowserKey = _.find(auth, { name: 'authorize_in_default_browser' });
return {
auth: {
oauth2:
@@ -526,11 +535,19 @@ const sem = grammar.createSemantics().addAttribute('ast', {
? {
grantType: grantTypeKey ? grantTypeKey.value : '',
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
refreshUrl: refreshUrlKey ? refreshUrlKey.value : '',
username: usernameKey ? usernameKey.value : '',
password: passwordKey ? passwordKey.value : '',
clientId: clientIdKey ? clientIdKey.value : '',
clientSecret: clientSecretKey ? clientSecretKey.value : '',
scope: scopeKey ? scopeKey.value : ''
scope: scopeKey ? scopeKey.value : '',
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
}
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
? {
@@ -538,19 +555,36 @@ const sem = grammar.createSemantics().addAttribute('ast', {
callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',
authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
refreshUrl: refreshUrlKey ? refreshUrlKey.value : '',
clientId: clientIdKey ? clientIdKey.value : '',
clientSecret: clientSecretKey ? clientSecretKey.value : '',
scope: scopeKey ? scopeKey.value : '',
state: stateKey ? stateKey.value : '',
pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false
pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false,
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true,
authorizeInDefaultBrowser: authorizeInDefaultBrowserKey ? JSON.parse(authorizeInDefaultBrowserKey?.value) : true
}
: grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
? {
grantType: grantTypeKey ? grantTypeKey.value : '',
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
refreshUrl: refreshUrlKey ? refreshUrlKey.value : '',
clientId: clientIdKey ? clientIdKey.value : '',
clientSecret: clientSecretKey ? clientSecretKey.value : '',
scope: scopeKey ? scopeKey.value : ''
scope: scopeKey ? scopeKey.value : '',
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
}
: {}
}

View File

@@ -274,11 +274,20 @@ const sem = grammar.createSemantics().addAttribute('ast', {
const callbackUrlKey = _.find(auth, { name: 'callback_url' });
const authorizationUrlKey = _.find(auth, { name: 'authorization_url' });
const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' });
const refreshUrlKey = _.find(auth, { name: 'refresh_url' });
const clientIdKey = _.find(auth, { name: 'client_id' });
const clientSecretKey = _.find(auth, { name: 'client_secret' });
const scopeKey = _.find(auth, { name: 'scope' });
const stateKey = _.find(auth, { name: 'state' });
const pkceKey = _.find(auth, { name: 'pkce' });
const credentialsPlacementKey = _.find(auth, { name: 'credentials_placement' });
const credentialsIdKey = _.find(auth, { name: 'credentials_id' });
const tokenPlacementKey = _.find(auth, { name: 'token_placement' });
const tokenHeaderPrefixKey = _.find(auth, { name: 'token_header_prefix' });
const tokenQueryKeyKey = _.find(auth, { name: 'token_query_key' });
const autoFetchTokenKey = _.find(auth, { name: 'auto_fetch_token' });
const autoRefreshTokenKey = _.find(auth, { name: 'auto_refresh_token' });
const authorizeInDefaultBrowserKey = _.find(auth, { name: 'authorize_in_default_browser' });
return {
auth: {
oauth2:
@@ -286,11 +295,19 @@ const sem = grammar.createSemantics().addAttribute('ast', {
? {
grantType: grantTypeKey ? grantTypeKey.value : '',
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
refreshUrl: refreshUrlKey ? refreshUrlKey.value : '',
username: usernameKey ? usernameKey.value : '',
password: passwordKey ? passwordKey.value : '',
clientId: clientIdKey ? clientIdKey.value : '',
clientSecret: clientSecretKey ? clientSecretKey.value : '',
scope: scopeKey ? scopeKey.value : ''
scope: scopeKey ? scopeKey.value : '',
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
}
: grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'
? {
@@ -298,19 +315,36 @@ const sem = grammar.createSemantics().addAttribute('ast', {
callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',
authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
refreshUrl: refreshUrlKey ? refreshUrlKey.value : '',
clientId: clientIdKey ? clientIdKey.value : '',
clientSecret: clientSecretKey ? clientSecretKey.value : '',
scope: scopeKey ? scopeKey.value : '',
state: stateKey ? stateKey.value : '',
pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false
pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false,
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true,
authorizeInDefaultBrowser: authorizeInDefaultBrowserKey ? JSON.parse(authorizeInDefaultBrowserKey?.value) : true
}
: grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'
? {
grantType: grantTypeKey ? grantTypeKey.value : '',
accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',
refreshUrl: refreshUrlKey ? refreshUrlKey.value : '',
clientId: clientIdKey ? clientIdKey.value : '',
clientSecret: clientSecretKey ? clientSecretKey.value : '',
scope: scopeKey ? scopeKey.value : ''
scope: scopeKey ? scopeKey.value : '',
credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',
credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',
tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',
tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : 'Bearer',
tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',
autoFetchToken: autoFetchTokenKey ? JSON.parse(autoFetchTokenKey?.value) : true,
autoRefreshToken: autoRefreshTokenKey ? JSON.parse(autoRefreshTokenKey?.value) : true
}
: {}
}

View File

@@ -183,11 +183,21 @@ ${indentString(`domain: ${auth?.ntlm?.domain || ''}`)}
bru += `auth:oauth2 {
${indentString(`grant_type: password`)}
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
${indentString(`refresh_url: ${auth?.oauth2?.refreshUrl || ''}`)}
${indentString(`username: ${auth?.oauth2?.username || ''}`)}
${indentString(`password: ${auth?.oauth2?.password || ''}`)}
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
}${
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
}
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken|| false).toString()}`)}
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken|| false).toString()}`)}
}
`;
@@ -198,22 +208,42 @@ ${indentString(`grant_type: authorization_code`)}
${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}
${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
${indentString(`refresh_url: ${auth?.oauth2?.refreshUrl || ''}`)}
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
${indentString(`state: ${auth?.oauth2?.state || ''}`)}
${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
}${
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
}
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken|| false).toString()}`)}
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken|| false).toString()}`)}
${indentString(`authorize_in_default_browser: ${(auth?.oauth2?.authorizeInDefaultBrowser|| true).toString()}`)}
}
`;
break;
case 'client_credentials':
bru += `auth:oauth2 {
${indentString(`grant_type: client_credentials`)}
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
${indentString(`refresh_url: ${auth?.oauth2?.refreshUrl || ''}`)}
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
}${
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
}
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken|| false).toString()}`)}
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken|| false).toString()}`)}
}
`;

View File

@@ -149,11 +149,21 @@ ${indentString(`placement: ${auth?.apikey?.placement || ''}`)}
bru += `auth:oauth2 {
${indentString(`grant_type: password`)}
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
${indentString(`refresh_url: ${auth?.oauth2?.refreshUrl || ''}`)}
${indentString(`username: ${auth?.oauth2?.username || ''}`)}
${indentString(`password: ${auth?.oauth2?.password || ''}`)}
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
}${
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
}
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken).toString()}`)}
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken).toString()}`)}
}
`;
@@ -164,11 +174,22 @@ ${indentString(`grant_type: authorization_code`)}
${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}
${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
${indentString(`refresh_url: ${auth?.oauth2?.refreshUrl || ''}`)}
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
${indentString(`state: ${auth?.oauth2?.state || ''}`)}
${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
}${
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
}
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken).toString()}`)}
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken).toString()}`)}
${indentString(`authorize_in_default_browser: ${(auth?.oauth2?.authorizeInDefaultBrowser|| true).toString()}`)}
}
`;
@@ -177,9 +198,19 @@ ${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}
bru += `auth:oauth2 {
${indentString(`grant_type: client_credentials`)}
${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}
${indentString(`refresh_url: ${auth?.oauth2?.refreshUrl || ''}`)}
${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}
${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}
${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}
${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}
${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}
${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${
auth?.oauth2?.tokenPlacement == 'header' ? '\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''
}${
auth?.oauth2?.tokenPlacement !== 'header' ? '\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''
}
${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken).toString()}`)}
${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken).toString()}`)}
}
`;

View File

@@ -59,11 +59,18 @@ auth:oauth2 {
callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback
authorization_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize
access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token
refresh_url:
client_id: client_id_1
client_secret: client_secret_1
scope: read write
state: 807061d5f0be
pkce: false
credentials_placement: body
credentials_id: credentials
token_placement: header
token_header_prefix: Bearer
auto_fetch_token: true
auto_refresh_token: true
}
body:json {

View File

@@ -74,15 +74,23 @@
"password": "secret"
},
"oauth2": {
"grantType": "authorization_code",
"accessTokenUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/token",
"authorizationUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize",
"autoFetchToken": true,
"autoRefreshToken": true,
"callbackUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/callback",
"clientId": "client_id_1",
"clientSecret": "client_secret_1",
"authorizationUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/authorize",
"callbackUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/callback",
"accessTokenUrl": "http://localhost:8080/api/auth/oauth2/authorization_code/token",
"credentialsId": "credentials",
"credentialsPlacement": "body",
"grantType": "authorization_code",
"pkce": false,
"refreshUrl": "",
"scope": "read write",
"state": "807061d5f0be",
"pkce": false
"tokenHeaderPrefix": "Bearer",
"tokenPlacement": "header",
"tokenQueryKey": "access_token"
},
"wsse": {
"username": "john",

View File

@@ -210,6 +210,53 @@ const oauth2Schema = Yup.object({
is: (val) => ['authorization_code'].includes(val),
then: Yup.boolean().default(false),
otherwise: Yup.boolean()
}),
credentialsPlacement: Yup.string().when('grantType', {
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
credentialsId: Yup.string().when('grantType', {
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
tokenPlacement: Yup.string().when('grantType', {
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
tokenHeaderPrefix: Yup.string().when(['grantType', 'tokenPlacement'], {
is: (grantType, tokenPlacement) =>
['client_credentials', 'password', 'authorization_code'].includes(grantType) && tokenPlacement === 'header',
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
tokenQueryKey: Yup.string().when(['grantType', 'tokenPlacement'], {
is: (grantType, tokenPlacement) =>
['client_credentials', 'password', 'authorization_code'].includes(grantType) && tokenPlacement === 'url',
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
refreshUrl: Yup.string().when('grantType', {
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
then: Yup.string().nullable(),
otherwise: Yup.string().nullable().strip()
}),
autoRefreshToken: Yup.boolean().when('grantType', {
is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),
then: Yup.boolean().default(false),
otherwise: Yup.boolean()
}),
autoFetchToken: Yup.boolean().when('grantType', {
is: (val) => ['authorization_code'].includes(val),
then: Yup.boolean().default(true),
otherwise: Yup.boolean()
}),
authorizeInDefaultBrowser: Yup.boolean().when('grantType', {
is: (val) => ['authorization_code'].includes(val),
then: Yup.boolean().default(true),
otherwise: Yup.boolean()
})
})
.noUnknown(true)

View File

@@ -43,6 +43,5 @@ tests {
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
});
}

View File

@@ -10,7 +10,254 @@
"dependencies": {
"@faker-js/faker": "^8.4.0",
"jsonwebtoken": "^9.0.2",
"lru-map-cache": "^0.1.0"
"lru-map-cache": "^0.1.0",
"mssql": "^11.0.1"
}
},
"node_modules/@azure/abort-controller": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-auth": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
"integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.11.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-client": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz",
"integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.4.0",
"@azure/core-rest-pipeline": "^1.9.1",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.6.1",
"@azure/logger": "^1.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-http-compat": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz",
"integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-client": "^1.3.0",
"@azure/core-rest-pipeline": "^1.3.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-lro": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
"integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.2.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-paging": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz",
"integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.1.tgz",
"integrity": "sha512-/wS73UEDrxroUEVywEm7J0p2c+IIiVxyfigCGfsKvCxxCET4V/Hef2aURqltrXMRjNmdmt5IuOgIpl8f6xdO5A==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.8.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-tracing": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz",
"integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-util": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz",
"integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/identity": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.5.0.tgz",
"integrity": "sha512-EknvVmtBuSIic47xkOqyNabAme0RYTw52BTMz8eBgU1ysTyMrD1uOoM+JdS0J/4Yfp98IBT3osqq3BfwSaNaGQ==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.9.0",
"@azure/core-client": "^1.9.2",
"@azure/core-rest-pipeline": "^1.17.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0",
"@azure/msal-browser": "^3.26.1",
"@azure/msal-node": "^2.15.0",
"events": "^3.0.0",
"jws": "^4.0.0",
"open": "^8.0.0",
"stoppable": "^1.1.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/identity/node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/@azure/identity/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/@azure/keyvault-common": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz",
"integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-client": "^1.5.0",
"@azure/core-rest-pipeline": "^1.8.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.10.0",
"@azure/logger": "^1.1.4",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/keyvault-keys": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.9.0.tgz",
"integrity": "sha512-ZBP07+K4Pj3kS4TF4XdkqFcspWwBHry3vJSOFM5k5ZABvf7JfiMonvaFk2nBF6xjlEbMpz5PE1g45iTMme0raQ==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-client": "^1.5.0",
"@azure/core-http-compat": "^2.0.1",
"@azure/core-lro": "^2.2.0",
"@azure/core-paging": "^1.1.1",
"@azure/core-rest-pipeline": "^1.8.1",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.0.0",
"@azure/keyvault-common": "^2.0.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/logger": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz",
"integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/msal-browser": {
"version": "3.28.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.28.0.tgz",
"integrity": "sha512-1c1qUF6vB52mWlyoMem4xR1gdwiQWYEQB2uhDkbAL4wVJr8WmAcXybc1Qs33y19N4BdPI8/DHI7rPE8L5jMtWw==",
"dependencies": {
"@azure/msal-common": "14.16.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "14.16.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz",
"integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-node": {
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz",
"integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==",
"dependencies": {
"@azure/msal-common": "14.16.0",
"jsonwebtoken": "^9.0.0",
"uuid": "^8.3.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@faker-js/faker": {
@@ -28,11 +275,147 @@
"npm": ">=6.14.13"
}
},
"node_modules/@js-joda/core": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.6.3.tgz",
"integrity": "sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA=="
},
"node_modules/@tediousjs/connection-string": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.5.0.tgz",
"integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="
},
"node_modules/@types/node": {
"version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/readable-stream": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz",
"integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==",
"dependencies": {
"@types/node": "*",
"safe-buffer": "~5.1.1"
}
},
"node_modules/@types/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"engines": {
"node": ">= 14"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/bl": {
"version": "6.0.18",
"resolved": "https://registry.npmjs.org/bl/-/bl-6.0.18.tgz",
"integrity": "sha512-2k76XmWCuvu9HTvu3tFOl5HDdCH0wLZ/jHYva/LBVJmc9oX8yUtNQjxrFmbTdXsCSmIxwVTANZPNDfMQrvHFUw==",
"dependencies": {
"@types/readable-stream": "^4.0.0",
"buffer": "^6.0.3",
"inherits": "^2.0.4",
"readable-stream": "^4.2.0"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"engines": {
"node": ">=16"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"engines": {
"node": ">=8"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -41,6 +424,111 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dependencies": {
"is-docker": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/js-md4": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@@ -137,6 +625,74 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mssql": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/mssql/-/mssql-11.0.1.tgz",
"integrity": "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w==",
"dependencies": {
"@tediousjs/connection-string": "^0.5.0",
"commander": "^11.0.0",
"debug": "^4.3.3",
"rfdc": "^1.3.0",
"tarn": "^3.0.2",
"tedious": "^18.2.1"
},
"bin": {
"mssql": "bin/mssql"
},
"engines": {
"node": ">=18"
}
},
"node_modules/native-duplexpair": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz",
"integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="
},
"node_modules/open": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -156,6 +712,11 @@
}
]
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
@@ -170,6 +731,74 @@
"node": ">=10"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
},
"node_modules/stoppable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
"engines": {
"node": ">=4",
"npm": ">=6"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/tarn": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
"integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/tedious": {
"version": "18.6.1",
"resolved": "https://registry.npmjs.org/tedious/-/tedious-18.6.1.tgz",
"integrity": "sha512-9AvErXXQTd6l7TDd5EmM+nxbOGyhnmdbp/8c3pw+tjaiSXW9usME90ET/CRG1LN1Y9tPMtz/p83z4Q97B4DDpw==",
"dependencies": {
"@azure/core-auth": "^1.7.2",
"@azure/identity": "^4.2.1",
"@azure/keyvault-keys": "^4.4.0",
"@js-joda/core": "^5.6.1",
"@types/node": ">=18",
"bl": "^6.0.11",
"iconv-lite": "^0.6.3",
"js-md4": "^0.3.2",
"native-duplexpair": "^1.0.0",
"sprintf-js": "^1.1.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View File

@@ -0,0 +1,9 @@
{
"version": "1",
"name": "keycloak-authorization_code",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@@ -0,0 +1,20 @@
auth {
mode: oauth2
}
auth:oauth2 {
grant_type: authorization_code
callback_url: {{key-host}}/realms/bruno/account
authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
client_id: account
client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
scope: openid
state:
pkce: true
credentials_placement: body
credentials_id: credentials
token_placement: header
token_header_prefix: Bearer
reuse_token:
}

View File

@@ -0,0 +1,21 @@
vars {
host: http://localhost:8081
bearer_auth_token: your_secret_token
basic_auth_password: della
client_id: client_id_1
client_secret: client_secret_1
password_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/password_credentials/token
password_credentials_username: foo
password_credentials_password: bar
password_credentials_scope:
authorization_code_authorize_url: http://localhost:8081/api/auth/oauth2/authorization_code/authorize
authorization_code_callback_url: http://localhost:8081/api/auth/oauth2/authorization_code/callback
authorization_code_access_token_url: http://localhost:8081/api/auth/oauth2/authorization_code/token
authorization_code_access_token: null
client_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/client_credentials/token
client_credentials_client_id: client_id_1
client_credentials_client_secret: client_secret_1
client_credentials_scope: admin
client_credentials_access_token: 870132a2ed28a3c94d34f868e6514720
key-host: http://localhost:8080
}

View File

@@ -0,0 +1,11 @@
meta {
name: user_info_coll-auth
type: http
seq: 1
}
get {
url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
body: none
auth: inherit
}

View File

@@ -0,0 +1,15 @@
meta {
name: user_info_custom
type: http
seq: 2
}
get {
url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
body: none
auth: bearer
}
auth:bearer {
token: {{$oauth2.credentials.access_token}}
}

View File

@@ -0,0 +1,30 @@
meta {
name: user_info_request-auth
type: http
seq: 3
}
get {
url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
body: json
auth: oauth2
}
auth:oauth2 {
grant_type: authorization_code
callback_url: {{key-host}}/realms/bruno/account
authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
refresh_url:
client_id: account
client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
scope: openid
state:
pkce: true
credentials_placement: body
credentials_id: credentials
token_placement: header
token_header_prefix: Bearer
auto_fetch_token: true
auto_refresh_token: true
}

View File

@@ -0,0 +1,9 @@
{
"version": "1",
"name": "keycloak-client-credentials",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@@ -0,0 +1,20 @@
auth {
mode: oauth2
}
auth:oauth2 {
grant_type: authorization_code
callback_url: {{key-host}}/realms/bruno/account
authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
client_id: account
client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
scope: openid
state:
pkce: true
tokenId: keycloak
tokenPlacement: header
tokenHeaderPrefix: Bearer
tokenQueryKey: access_token
reuseToken:
}

View File

@@ -0,0 +1,22 @@
vars {
host: http://localhost:8080
bearer_auth_token: your_secret_token
basic_auth_password: della
client_id: client_id_1
client_secret: client_secret_1
password_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/password_credentials/token
password_credentials_username: foo
password_credentials_password: bar
password_credentials_scope:
authorization_code_authorize_url: http://localhost:8081/api/auth/oauth2/authorization_code/authorize
authorization_code_callback_url: http://localhost:8081/api/auth/oauth2/authorization_code/callback
authorization_code_access_token_url: http://localhost:8081/api/auth/oauth2/authorization_code/token
authorization_code_access_token: null
client_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/client_credentials/token
client_credentials_client_id: client_id_1
client_credentials_client_secret: client_secret_1
client_credentials_scope: admin
client_credentials_access_token: 870132a2ed28a3c94d34f868e6514720
key-host: http://localhost:8080
key-host-1: http://localhost:8082
}

View File

@@ -0,0 +1,11 @@
meta {
name: user_info_coll-auth
type: http
seq: 1
}
get {
url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
body: none
auth: inherit
}

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