mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
32 Commits
dependabot
...
feat/oauth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bad5f0003c | ||
|
|
ee5e260890 | ||
|
|
c3f8959c54 | ||
|
|
cabd520ef1 | ||
|
|
0e7b0a0ce5 | ||
|
|
d3fcb42a8f | ||
|
|
51be153527 | ||
|
|
5728b7c8a8 | ||
|
|
71b6907c31 | ||
|
|
eead96ca26 | ||
|
|
f99e8770f0 | ||
|
|
8cda05c431 | ||
|
|
7af7ff92bf | ||
|
|
3169e6cdf4 | ||
|
|
4e88cbf318 | ||
|
|
413b121ce1 | ||
|
|
90dff3d1e1 | ||
|
|
3fc0b0a668 | ||
|
|
b5e53ec25c | ||
|
|
01a62d66cc | ||
|
|
f668e93f52 | ||
|
|
c5eeb190d3 | ||
|
|
1d1e701ccb | ||
|
|
f38c7ae03a | ||
|
|
8f754142c7 | ||
|
|
3bd8f09c88 | ||
|
|
dd9cb21f8c | ||
|
|
2064cc88ab | ||
|
|
d982e35a17 | ||
|
|
4afcd44216 | ||
|
|
63252d3ee2 | ||
|
|
22a9502976 |
630
package-lock.json
generated
630
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -95,7 +95,7 @@ const AuthMode = ({ collection }) => {
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
Oauth2
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,4 +13,4 @@ const Wrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateFolderAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
import AuthMode from '../AuthMode';
|
||||
|
||||
const grantTypeComponentMap = (collection, folder) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
let request = get(folder, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
|
||||
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const Auth = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
let request = get(folder, 'root.request', {});
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'oauth2': {
|
||||
return (
|
||||
<>
|
||||
<GrantTypeSelector
|
||||
request={request}
|
||||
updateAuth={updateFolderAuth}
|
||||
collection={collection}
|
||||
folder={folder}
|
||||
/>
|
||||
{grantTypeComponentMap(collection, folder)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'none': {
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Configures authentication for the entire folder. This applies to all requests using the{' '}
|
||||
<span className="font-medium">Inherit</span> option in the <span className="font-medium">Auth</span> tab.
|
||||
</div>
|
||||
<div className="flex flex-grow justify-start items-center mb-4">
|
||||
<AuthMode collection={collection} folder={folder} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.auth-mode-selector {
|
||||
border: 1px solid ${({ theme }) => theme.colors.border};
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateFolderAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AuthMode = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateFolderAuthMode({
|
||||
mode: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthMode;
|
||||
@@ -8,7 +8,9 @@ import Tests from './Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars';
|
||||
import Documentation from './Documentation';
|
||||
import Auth from './Auth';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
@@ -37,6 +39,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
const responseVars = folderRoot?.request?.vars?.res || [];
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
|
||||
const auth = get(folderRoot, 'request.auth.mode');
|
||||
const hasAuth = auth && auth !== 'none';
|
||||
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
updatedFolderSettingsSelectedTab({
|
||||
@@ -61,6 +66,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation collection={collection} folder={folder} />;
|
||||
}
|
||||
@@ -93,6 +101,10 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
Vars
|
||||
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{hasAuth && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.line {
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-family: Inter, sans-serif !important;
|
||||
|
||||
.arrow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.request {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.response {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RunnerTimeline = ({ request, response }) => {
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = typeof response.headers === 'object' ? Object.entries(response.headers) : [];
|
||||
|
||||
request = request || {};
|
||||
response = response || {};
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<div>
|
||||
<pre className="line request font-bold">
|
||||
<span className="arrow">{'>'}</span> {request.method} {request.url}
|
||||
</pre>
|
||||
{requestHeaders.map((h) => {
|
||||
return (
|
||||
<pre className="line request" key={h.name}>
|
||||
<span className="arrow">{'>'}</span> {h.name}: {h.value}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
|
||||
{requestData ? (
|
||||
<pre className="line request">
|
||||
<span className="arrow">{'>'}</span> data{' '}
|
||||
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<pre className="line response font-bold">
|
||||
<span className="arrow">{'<'}</span> {response.status} - {response.statusText}
|
||||
</pre>
|
||||
|
||||
{responseHeaders.map((h) => {
|
||||
return (
|
||||
<pre className="line response" key={h[0]}>
|
||||
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunnerTimeline;
|
||||
@@ -1,11 +1,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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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' ? '>' : '<'} <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' ? '>' : '<'} <span className="opacity-60">{key}:</span>
|
||||
<span>{String(value)}</span>
|
||||
</pre>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default HeadersBlock;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,6 +5,10 @@ const StyledWrapper = styled.div`
|
||||
height: 30px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
background: transparent;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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`;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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')
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
69
packages/bruno-electron/src/utils/oauth2-server.js
Normal file
69
packages/bruno-electron/src/utils/oauth2-server.js
Normal 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 };
|
||||
674
packages/bruno-electron/src/utils/oauth2.js
Normal file
674
packages/bruno-electron/src/utils/oauth2.js
Normal 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
|
||||
};
|
||||
83
packages/bruno-electron/src/utils/pid-port.js
Normal file
83
packages/bruno-electron/src/utils/pid-port.js
Normal 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;
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
: {}
|
||||
}
|
||||
|
||||
@@ -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()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@@ -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()}`)}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -43,6 +43,5 @@ tests {
|
||||
expect(res.getBody()).to.eql({
|
||||
"hello": "bruno"
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
631
packages/bruno-tests/collection/package-lock.json
generated
631
packages/bruno-tests/collection/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "keycloak-authorization_code",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -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:
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "keycloak-client-credentials",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
@@ -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:
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user