mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abce135da2 | ||
|
|
a6c014946a | ||
| f27ac51fc4 | |||
|
|
cb5be1be21 | ||
|
|
d90fa9e5dd | ||
| d99fdd1ec7 | |||
|
|
399b5edad0 | ||
|
|
1dbc12e96b | ||
| e215958b8b | |||
| 9227cd449d | |||
| c67d3ee2f1 | |||
| 6ef40b954a | |||
|
|
0d913baff1 | ||
|
|
3671736c3d | ||
| 34cd84d8a9 | |||
|
|
f7fcc7741a | ||
|
|
18052fdbf6 | ||
|
|
5966016853 | ||
|
|
87c03c5f8d | ||
| 7a162eda8f | |||
| 754704bca8 | |||
|
|
77f8d30baf | ||
|
|
78bea7c154 | ||
|
|
9c3b155c12 | ||
|
|
98e501334f | ||
|
|
bbfd53e79e | ||
| 254bc07da7 | |||
| f978814ca7 | |||
| 68515f95a6 | |||
| d3a516c36e | |||
| c3e3befc17 | |||
|
|
275de9478e | ||
|
|
1a3ef62cb2 | ||
|
|
9eb5f3ca4d | ||
|
|
916947dffa | ||
| 79b7827b7c | |||
| 37e1aa9b61 | |||
| 7e504008b7 | |||
| 5d5a9d3788 | |||
|
|
7c79d6479c | ||
|
|
3e635f422a | ||
|
|
77db14c690 | ||
| b7dff341f0 | |||
| 8a3054e19a | |||
|
|
a531de2adb | ||
|
|
c458d94493 | ||
| 706e228a8e | |||
| 7681722e5a | |||
| 8de032b543 | |||
|
|
998ef213e9 | ||
|
|
f8b0f98678 | ||
| 9640f65264 | |||
| c574b42235 | |||
| 468d1a929d | |||
| 7cdffe30e3 | |||
| 3b1fcb937d | |||
| 3c987c61dd | |||
| 0a780697da | |||
| 83d819df53 | |||
|
|
95df2b21d6 | ||
|
|
accdf914f1 | ||
| 15bdd2d7f0 | |||
| 2ce947d216 | |||
| ce2114e3f6 | |||
| 6c7550286b | |||
| 2360e104bd | |||
| 420a61a5a6 | |||
| 04e0f9efe3 | |||
| 99172cd9ed | |||
| 57daad0c26 | |||
| cc1e4543fc | |||
| 03cb89d14f | |||
| 72140d73c2 | |||
| efcefd2a42 | |||
| 06d7c91fc3 | |||
| 7010a6a120 | |||
| fdcaba9d56 | |||
| 48688a6547 | |||
| 0ce94a553e | |||
| 941917e508 | |||
|
|
5706371ffd | ||
|
|
ce5218a227 | ||
|
|
8b62755f39 | ||
|
|
cb84c3ebbb | ||
|
|
526402fd73 | ||
| 177675bc89 | |||
| 721165ff12 | |||
| 08e0c61e0f | |||
| 3e53f70928 | |||
| 0f19384999 | |||
| 6c35412d2f | |||
| 27410bc32b | |||
| 849b2dd468 | |||
| 30d7a65358 | |||
| 5e930f14d2 | |||
| 9bc68b1cdc |
135
CHANGELOG.md
135
CHANGELOG.md
@@ -1,3 +1,138 @@
|
||||
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove vulnerabitities ([f27ac51](https://github.com/sasjs/server/commit/f27ac51fc4beb21070d0ab551cfdaec1f6ba39e0))
|
||||
|
||||
## [0.15.1](https://github.com/sasjs/server/compare/v0.15.0...v0.15.1) (2022-08-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** fix UI responsiveness ([d99fdd1](https://github.com/sasjs/server/commit/d99fdd1ec7991b94a0d98338d7a7a6216f46ce45))
|
||||
|
||||
# [0.15.0](https://github.com/sasjs/server/compare/v0.14.1...v0.15.0) (2022-08-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* after selecting file in sidebar collapse sidebar in mobile view ([e215958](https://github.com/sasjs/server/commit/e215958b8b05d7a8ce9d82395e0640b5b37fb40d))
|
||||
* improve mobile view for studio page ([c67d3ee](https://github.com/sasjs/server/commit/c67d3ee2f102155e2e9781e13d5d33c1ab227cb4))
|
||||
* improve responsiveness for mobile view ([6ef40b9](https://github.com/sasjs/server/commit/6ef40b954a87ebb0a2621119064f38d58ea85148))
|
||||
* improve user experience for adding permissions ([7a162ed](https://github.com/sasjs/server/commit/7a162eda8fc60383ff647d93e6611799e2e6af7a))
|
||||
* show logout button only when user is logged in ([9227cd4](https://github.com/sasjs/server/commit/9227cd449dc46fd960a488eb281804a9b9ffc284))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add multiple permission for same combination of type and principal at once ([754704b](https://github.com/sasjs/server/commit/754704bca89ecbdbcc3bd4ef04b94124c4f24167))
|
||||
|
||||
## [0.14.1](https://github.com/sasjs/server/compare/v0.14.0...v0.14.1) (2022-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **apps:** App Stream logo fix ([87c03c5](https://github.com/sasjs/server/commit/87c03c5f8dbdfc151d4ff3722ecbcd3f7e409aea))
|
||||
* **cookie:** XSRF cookie is removed and passed token in head section ([77f8d30](https://github.com/sasjs/server/commit/77f8d30baf9b1077279c29f1c3e5ca02a5436bc0))
|
||||
* **env:** check added for not providing WHITELIST ([5966016](https://github.com/sasjs/server/commit/5966016853369146b27ac5781808cb51d65c887f))
|
||||
* **web:** show login on logged-out state ([f7fcc77](https://github.com/sasjs/server/commit/f7fcc7741aa2af93a4a2b1e651003704c9bbff0c))
|
||||
|
||||
# [0.14.0](https://github.com/sasjs/server/compare/v0.13.3...v0.14.0) (2022-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add restriction on add/remove user to public group ([d3a516c](https://github.com/sasjs/server/commit/d3a516c36e45aa1cc76c30c744e6a0e5bd553165))
|
||||
* call jwt.verify in synchronous way ([254bc07](https://github.com/sasjs/server/commit/254bc07da744a9708109bfb792be70aa3f6284f4))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add public group to DB on seed ([c3e3bef](https://github.com/sasjs/server/commit/c3e3befc17102ee1754e1403193040b4f79fb2a7))
|
||||
* bypass authentication when route is enabled for public group ([68515f9](https://github.com/sasjs/server/commit/68515f95a65d422e29c0ed6028f3ea0ae8d9b1bf))
|
||||
|
||||
## [0.13.3](https://github.com/sasjs/server/compare/v0.13.2...v0.13.3) (2022-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* show non-admin user his own permissions only ([8a3054e](https://github.com/sasjs/server/commit/8a3054e19ade82e2792cfb0f2a8af9e502c5eb52))
|
||||
* update schema of Permission ([5d5a9d3](https://github.com/sasjs/server/commit/5d5a9d3788281d75c56f68f0dff231abc9c9c275))
|
||||
|
||||
## [0.13.2](https://github.com/sasjs/server/compare/v0.13.1...v0.13.2) (2022-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding ls=max to reduce log size and improve readability ([916947d](https://github.com/sasjs/server/commit/916947dffacd902ff23ac3e899d1bf5ab6238b75))
|
||||
|
||||
## [0.13.1](https://github.com/sasjs/server/compare/v0.13.0...v0.13.1) (2022-07-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding options to prevent unwanted windows on windows. Closes [#244](https://github.com/sasjs/server/issues/244) ([77db14c](https://github.com/sasjs/server/commit/77db14c690e18145d733ac2b0d646ab0dbe4d521))
|
||||
|
||||
# [0.13.0](https://github.com/sasjs/server/compare/v0.12.1...v0.13.0) (2022-07-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* autofocus input field and submit on enter ([7681722](https://github.com/sasjs/server/commit/7681722e5afdc2df0c9eed201b05add3beda92a7))
|
||||
* move api button to user menu ([8de032b](https://github.com/sasjs/server/commit/8de032b5431b47daabcf783c47ff078bf817247d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add action and command to editor ([706e228](https://github.com/sasjs/server/commit/706e228a8e1924786fd9dc97de387974eda504b1))
|
||||
|
||||
## [0.12.1](https://github.com/sasjs/server/compare/v0.12.0...v0.12.1) (2022-07-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** disable launch icon button when file content is not saved ([c574b42](https://github.com/sasjs/server/commit/c574b4223591c4a6cd3ef5e146ce99cd8f7c9190))
|
||||
* **web:** saveAs functionality fixed in studio page ([3c987c6](https://github.com/sasjs/server/commit/3c987c61ddc258f991e2bf38c1f16a0c4248d6ae))
|
||||
* **web:** show original name as default name in rename file/folder modal ([9640f65](https://github.com/sasjs/server/commit/9640f6526496f3564664ccb1f834d0f659dcad4e))
|
||||
* **web:** webout tab item fixed in studio page ([7cdffe3](https://github.com/sasjs/server/commit/7cdffe30e36e5cad0284f48ea97925958e12704c))
|
||||
* **web:** when no file is selected save the editor content to local storage ([3b1fcb9](https://github.com/sasjs/server/commit/3b1fcb937d06d02ab99c9e8dbe307012d48a7a3a))
|
||||
|
||||
# [0.12.0](https://github.com/sasjs/server/compare/v0.11.5...v0.12.0) (2022-07-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fileTree api response to include an additional attribute isFolder ([0f19384](https://github.com/sasjs/server/commit/0f193849994f1ac8a071afa8f10af5b46f86663d))
|
||||
* remove drive component ([06d7c91](https://github.com/sasjs/server/commit/06d7c91fc34620a954df1fd1c682eff370f79ca6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api end point for delete folder ([08e0c61](https://github.com/sasjs/server/commit/08e0c61e0fd7041d6cded6f4d71fbb410e5615ce))
|
||||
* add sidebar(drive) to left of studio ([6c35412](https://github.com/sasjs/server/commit/6c35412d2f5180d4e49b12e616576d8b8dacb7d8))
|
||||
* created api endpoint for adding empty folder in drive ([941917e](https://github.com/sasjs/server/commit/941917e508ece5009135f9dddf99775dd4002f78))
|
||||
* implemented api for renaming file/folder ([fdcaba9](https://github.com/sasjs/server/commit/fdcaba9d56cddea5d56d7de5a172f1bb49be3db5))
|
||||
* implemented delete file/folder functionality ([177675b](https://github.com/sasjs/server/commit/177675bc897416f7994dd849dc7bb11ba072efe9))
|
||||
* implemented functionality for adding file/folder from sidebar context menu ([0ce94a5](https://github.com/sasjs/server/commit/0ce94a553e53bfcdbd6273b26b322095a080a341))
|
||||
* implemented the functionality for renaming file/folder from context menu ([7010a6a](https://github.com/sasjs/server/commit/7010a6a1201720d0eb4093267a344fb828b90a2f))
|
||||
* prevent user from leaving studio page when there are unsaved changes ([6c75502](https://github.com/sasjs/server/commit/6c7550286b5f505e9dfe8ca63c62fa1db1b60b2e))
|
||||
* **web:** add difference view editor in studio ([420a61a](https://github.com/sasjs/server/commit/420a61a5a6b11dcb5eb0a652ea9cecea5c3bee5f))
|
||||
|
||||
## [0.11.5](https://github.com/sasjs/server/compare/v0.11.4...v0.11.5) (2022-07-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Revert "fix(security): missing cookie flags are added" ([ce5218a](https://github.com/sasjs/server/commit/ce5218a2278cc750f2b1032024685dc6cd72f796))
|
||||
|
||||
## [0.11.4](https://github.com/sasjs/server/compare/v0.11.3...v0.11.4) (2022-07-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **security:** missing cookie flags are added ([526402f](https://github.com/sasjs/server/commit/526402fd73407ee4fa2d31092111a7e6a1741487))
|
||||
|
||||
## [0.11.3](https://github.com/sasjs/server/compare/v0.11.2...v0.11.3) (2022-07-19)
|
||||
|
||||
|
||||
|
||||
503
api/package-lock.json
generated
503
api/package-lock.json
generated
@@ -23,7 +23,7 @@
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"rotating-file-stream": "^3.0.4",
|
||||
"swagger-ui-express": "4.3.0",
|
||||
"unzipper": "^0.10.11",
|
||||
@@ -2184,9 +2184,9 @@
|
||||
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz",
|
||||
"integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
|
||||
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/webidl-conversions": "*"
|
||||
@@ -2834,9 +2834,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-4.5.4.tgz",
|
||||
"integrity": "sha512-wIt0bPACnx8Ju9r6IsS2wVtGDHBr9Dxb+U29A1YED2pu8XOhS8aKjOnLZ8sxyXkPwanoK7iWWVhS1+coxde6xA==",
|
||||
"version": "4.6.5",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-4.6.5.tgz",
|
||||
"integrity": "sha512-uqrgcjyOaZsHfz7ea8zLRCLe1u+QGUSzMZmvXqO24CDW7DWoW1qiN9folSwa7hSneTSgM2ykDIzF5kcQQ8cwNw==",
|
||||
"dependencies": {
|
||||
"buffer": "^5.6.0"
|
||||
},
|
||||
@@ -2903,38 +2903,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
||||
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"dicer": "0.2.5",
|
||||
"readable-stream": "1.1.x"
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy/node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"node_modules/busboy/node_modules/readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy/node_modules/string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||
@@ -3566,39 +3544,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dicer": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
||||
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
|
||||
"dependencies": {
|
||||
"readable-stream": "1.1.x",
|
||||
"streamsearch": "0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dicer/node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"node_modules/dicer/node_modules/readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/dicer/node_modules/string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@@ -4946,6 +4891,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -6788,9 +6738,9 @@
|
||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||
},
|
||||
"node_modules/kareem": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
|
||||
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ=="
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz",
|
||||
"integrity": "sha512-aJ9opVoXroQUPfovYP5kaj2lM7Jn02Gw13bL0lg9v0V7SaUc0qavPs0Eue7d2DcC3NjqI6QAUElXNsuZSeM+EA=="
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
@@ -7093,13 +7043,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
||||
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.8.1.tgz",
|
||||
"integrity": "sha512-/NyiM3Ox9AwP5zrfT9TXjRKDJbXlLaUDQ9Rg//2lbg8D2A8GXV0VidYYnA/gfdK6uwbnL4FnAflH7FbGw3TS7w==",
|
||||
"dependencies": {
|
||||
"bson": "^4.5.4",
|
||||
"bson": "^4.6.5",
|
||||
"denque": "^2.0.1",
|
||||
"mongodb-connection-string-url": "^2.1.0"
|
||||
"mongodb-connection-string-url": "^2.5.2",
|
||||
"socks": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
@@ -7109,21 +7060,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.1.0.tgz",
|
||||
"integrity": "sha512-Qf9Zw7KGiRljWvMrrUFDdVqo46KIEiDuCzvEN97rh/PcKzk2bd6n9KuzEwBwW9xo5glwx69y1mI6s+jFUD/aIQ==",
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.3.tgz",
|
||||
"integrity": "sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ==",
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^8.2.1",
|
||||
"whatwg-url": "^9.1.0"
|
||||
"whatwg-url": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz",
|
||||
"integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==",
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||
"dependencies": {
|
||||
"tr46": "^2.1.0",
|
||||
"webidl-conversions": "^6.1.0"
|
||||
"tr46": "^3.0.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -7221,19 +7191,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "6.0.12",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.0.12.tgz",
|
||||
"integrity": "sha512-BvsZk7zEEhb1AgQFLtxN9C+7qgy5edRuA3ZDDwHU+kHG/HM44vI6FdKV5m6HVdAUeCHHQTiVv+YQh8BRsToSHw==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.2.tgz",
|
||||
"integrity": "sha512-3CFDrSLtK2qjM1pZeZpLTUyqPRkc11Iuh74ZrwS4IwEJ3K2PqGnmyPLw7ex4Kzu37ujIMp3MAuiBlUjfrcb6hw==",
|
||||
"dependencies": {
|
||||
"bson": "^4.2.2",
|
||||
"kareem": "2.3.2",
|
||||
"mongodb": "4.1.3",
|
||||
"mpath": "0.8.4",
|
||||
"mquery": "4.0.0",
|
||||
"ms": "2.1.2",
|
||||
"regexp-clone": "1.0.0",
|
||||
"sift": "13.5.2",
|
||||
"sliced": "1.0.1"
|
||||
"bson": "^4.6.5",
|
||||
"kareem": "2.4.1",
|
||||
"mongodb": "4.8.1",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "4.0.3",
|
||||
"ms": "2.1.3",
|
||||
"sift": "16.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -7255,26 +7223,10 @@
|
||||
"mongoose": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/mongodb": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.3.tgz",
|
||||
"integrity": "sha512-lHvTqODBiSpuqjpCj48DOyYWS6Iq6ElJNUiH9HWdQtONyOfjgsKzJULipWduMGsSzaNO4nFi/kmlMFCLvjox/Q==",
|
||||
"dependencies": {
|
||||
"bson": "^4.5.2",
|
||||
"denque": "^2.0.1",
|
||||
"mongodb-connection-string-url": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"saslprep": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/morgan": {
|
||||
"version": "1.10.0",
|
||||
@@ -7300,30 +7252,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mpath": {
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz",
|
||||
"integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==",
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
|
||||
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mquery": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.0.tgz",
|
||||
"integrity": "sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz",
|
||||
"integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==",
|
||||
"dependencies": {
|
||||
"debug": "4.x",
|
||||
"regexp-clone": "^1.0.0",
|
||||
"sliced": "1.0.1"
|
||||
"debug": "4.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mquery/node_modules/debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
@@ -7347,22 +7297,20 @@
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
|
||||
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
|
||||
"deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.",
|
||||
"version": "1.4.5-lts.1",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
|
||||
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^0.2.11",
|
||||
"busboy": "^1.0.0",
|
||||
"concat-stream": "^1.5.2",
|
||||
"mkdirp": "^0.5.4",
|
||||
"object-assign": "^4.1.1",
|
||||
"on-finished": "^2.3.0",
|
||||
"type-is": "^1.6.4",
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mkdirp": {
|
||||
@@ -8353,11 +8301,6 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regexp-clone": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
|
||||
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw=="
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -8606,9 +8549,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sift": {
|
||||
"version": "13.5.2",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
|
||||
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz",
|
||||
"integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ=="
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.3",
|
||||
@@ -8706,10 +8649,27 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sliced": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
|
||||
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
|
||||
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==",
|
||||
"dependencies": {
|
||||
"ip": "^2.0.0",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
@@ -8808,11 +8768,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
|
||||
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
@@ -9296,6 +9256,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
|
||||
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
@@ -9719,6 +9680,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
|
||||
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.4"
|
||||
}
|
||||
@@ -11724,9 +11686,9 @@
|
||||
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
|
||||
},
|
||||
"@types/whatwg-url": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz",
|
||||
"integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
|
||||
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/webidl-conversions": "*"
|
||||
@@ -12240,9 +12202,9 @@
|
||||
}
|
||||
},
|
||||
"bson": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-4.5.4.tgz",
|
||||
"integrity": "sha512-wIt0bPACnx8Ju9r6IsS2wVtGDHBr9Dxb+U29A1YED2pu8XOhS8aKjOnLZ8sxyXkPwanoK7iWWVhS1+coxde6xA==",
|
||||
"version": "4.6.5",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-4.6.5.tgz",
|
||||
"integrity": "sha512-uqrgcjyOaZsHfz7ea8zLRCLe1u+QGUSzMZmvXqO24CDW7DWoW1qiN9folSwa7hSneTSgM2ykDIzF5kcQQ8cwNw==",
|
||||
"requires": {
|
||||
"buffer": "^5.6.0"
|
||||
}
|
||||
@@ -12283,35 +12245,11 @@
|
||||
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
|
||||
},
|
||||
"busboy": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
||||
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"requires": {
|
||||
"dicer": "0.2.5",
|
||||
"readable-stream": "1.1.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
}
|
||||
"streamsearch": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
@@ -12813,38 +12751,6 @@
|
||||
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
||||
"dev": true
|
||||
},
|
||||
"dicer": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
||||
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
|
||||
"requires": {
|
||||
"readable-stream": "1.1.x",
|
||||
"streamsearch": "0.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@@ -13868,6 +13774,11 @@
|
||||
"p-is-promise": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -15248,9 +15159,9 @@
|
||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||
},
|
||||
"kareem": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
|
||||
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ=="
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz",
|
||||
"integrity": "sha512-aJ9opVoXroQUPfovYP5kaj2lM7Jn02Gw13bL0lg9v0V7SaUc0qavPs0Eue7d2DcC3NjqI6QAUElXNsuZSeM+EA=="
|
||||
},
|
||||
"kleur": {
|
||||
"version": "3.0.3",
|
||||
@@ -15486,32 +15397,46 @@
|
||||
"dev": true
|
||||
},
|
||||
"mongodb": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
||||
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.8.1.tgz",
|
||||
"integrity": "sha512-/NyiM3Ox9AwP5zrfT9TXjRKDJbXlLaUDQ9Rg//2lbg8D2A8GXV0VidYYnA/gfdK6uwbnL4FnAflH7FbGw3TS7w==",
|
||||
"requires": {
|
||||
"bson": "^4.5.4",
|
||||
"bson": "^4.6.5",
|
||||
"denque": "^2.0.1",
|
||||
"mongodb-connection-string-url": "^2.1.0",
|
||||
"saslprep": "^1.0.3"
|
||||
"mongodb-connection-string-url": "^2.5.2",
|
||||
"saslprep": "^1.0.3",
|
||||
"socks": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"mongodb-connection-string-url": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.1.0.tgz",
|
||||
"integrity": "sha512-Qf9Zw7KGiRljWvMrrUFDdVqo46KIEiDuCzvEN97rh/PcKzk2bd6n9KuzEwBwW9xo5glwx69y1mI6s+jFUD/aIQ==",
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.3.tgz",
|
||||
"integrity": "sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ==",
|
||||
"requires": {
|
||||
"@types/whatwg-url": "^8.2.1",
|
||||
"whatwg-url": "^9.1.0"
|
||||
"whatwg-url": "^11.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"whatwg-url": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz",
|
||||
"integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==",
|
||||
"tr46": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||
"requires": {
|
||||
"tr46": "^2.1.0",
|
||||
"webidl-conversions": "^6.1.0"
|
||||
"punycode": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||
},
|
||||
"whatwg-url": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||
"requires": {
|
||||
"tr46": "^3.0.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15583,36 +15508,23 @@
|
||||
}
|
||||
},
|
||||
"mongoose": {
|
||||
"version": "6.0.12",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.0.12.tgz",
|
||||
"integrity": "sha512-BvsZk7zEEhb1AgQFLtxN9C+7qgy5edRuA3ZDDwHU+kHG/HM44vI6FdKV5m6HVdAUeCHHQTiVv+YQh8BRsToSHw==",
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.2.tgz",
|
||||
"integrity": "sha512-3CFDrSLtK2qjM1pZeZpLTUyqPRkc11Iuh74ZrwS4IwEJ3K2PqGnmyPLw7ex4Kzu37ujIMp3MAuiBlUjfrcb6hw==",
|
||||
"requires": {
|
||||
"bson": "^4.2.2",
|
||||
"kareem": "2.3.2",
|
||||
"mongodb": "4.1.3",
|
||||
"mpath": "0.8.4",
|
||||
"mquery": "4.0.0",
|
||||
"ms": "2.1.2",
|
||||
"regexp-clone": "1.0.0",
|
||||
"sift": "13.5.2",
|
||||
"sliced": "1.0.1"
|
||||
"bson": "^4.6.5",
|
||||
"kareem": "2.4.1",
|
||||
"mongodb": "4.8.1",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "4.0.3",
|
||||
"ms": "2.1.3",
|
||||
"sift": "16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"mongodb": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.3.tgz",
|
||||
"integrity": "sha512-lHvTqODBiSpuqjpCj48DOyYWS6Iq6ElJNUiH9HWdQtONyOfjgsKzJULipWduMGsSzaNO4nFi/kmlMFCLvjox/Q==",
|
||||
"requires": {
|
||||
"bson": "^4.5.2",
|
||||
"denque": "^2.0.1",
|
||||
"mongodb-connection-string-url": "^2.0.0",
|
||||
"saslprep": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15645,24 +15557,22 @@
|
||||
}
|
||||
},
|
||||
"mpath": {
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz",
|
||||
"integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g=="
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
|
||||
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="
|
||||
},
|
||||
"mquery": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.0.tgz",
|
||||
"integrity": "sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz",
|
||||
"integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==",
|
||||
"requires": {
|
||||
"debug": "4.x",
|
||||
"regexp-clone": "^1.0.0",
|
||||
"sliced": "1.0.1"
|
||||
"debug": "4.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
@@ -15680,16 +15590,15 @@
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"multer": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
|
||||
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
|
||||
"version": "1.4.5-lts.1",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
|
||||
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
|
||||
"requires": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^0.2.11",
|
||||
"busboy": "^1.0.0",
|
||||
"concat-stream": "^1.5.2",
|
||||
"mkdirp": "^0.5.4",
|
||||
"object-assign": "^4.1.1",
|
||||
"on-finished": "^2.3.0",
|
||||
"type-is": "^1.6.4",
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
@@ -16416,11 +16325,6 @@
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"regexp-clone": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
|
||||
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw=="
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -16605,9 +16509,9 @@
|
||||
}
|
||||
},
|
||||
"sift": {
|
||||
"version": "13.5.2",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
|
||||
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz",
|
||||
"integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ=="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
@@ -16677,10 +16581,19 @@
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"sliced": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
|
||||
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
|
||||
"smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
|
||||
},
|
||||
"socks": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
|
||||
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==",
|
||||
"requires": {
|
||||
"ip": "^2.0.0",
|
||||
"smart-buffer": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
@@ -16771,9 +16684,9 @@
|
||||
}
|
||||
},
|
||||
"streamsearch": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
|
||||
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.3.0",
|
||||
@@ -17140,6 +17053,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
|
||||
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"punycode": "^2.1.1"
|
||||
}
|
||||
@@ -17456,7 +17370,8 @@
|
||||
"webidl-conversions": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
|
||||
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w=="
|
||||
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
|
||||
"dev": true
|
||||
},
|
||||
"whatwg-encoding": {
|
||||
"version": "1.0.5",
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"rotating-file-stream": "^3.0.4",
|
||||
"swagger-ui-express": "4.3.0",
|
||||
"unzipper": "^0.10.11",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ import { apiRoot, sysInitCompiledPath } from '../src/utils/file'
|
||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||
|
||||
const compiledSystemInit = async (systemInit: string) =>
|
||||
'options ps=max;\n' +
|
||||
'options ls=max ps=max;\n' +
|
||||
(await loadDependenciesFile({
|
||||
fileContent: systemInit,
|
||||
type: SASJsFileType.job,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path'
|
||||
import express, { ErrorRequestHandler } from 'express'
|
||||
import csrf from 'csurf'
|
||||
import csrf, { CookieOptions } from 'csurf'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
@@ -32,9 +32,10 @@ const app = express()
|
||||
|
||||
const { PROTOCOL } = process.env
|
||||
|
||||
export const cookieOptions = {
|
||||
export const cookieOptions: CookieOptions = {
|
||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||
httpOnly: true,
|
||||
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
moveFile,
|
||||
createFolder,
|
||||
deleteFile as deleteFileOnSystem,
|
||||
deleteFolder as deleteFolderOnSystem,
|
||||
folderExists,
|
||||
listFilesInFolder,
|
||||
listSubFoldersInFolder,
|
||||
@@ -58,11 +59,32 @@ interface GetFileTreeResponse {
|
||||
tree: TreeNode
|
||||
}
|
||||
|
||||
interface UpdateFileResponse {
|
||||
interface FileFolderResponse {
|
||||
status: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
interface AddFolderPayload {
|
||||
/**
|
||||
* Location of folder
|
||||
* @example "/Public/someFolder"
|
||||
*/
|
||||
folderPath: string
|
||||
}
|
||||
|
||||
interface RenamePayload {
|
||||
/**
|
||||
* Old path of file/folder
|
||||
* @example "/Public/someFolder"
|
||||
*/
|
||||
oldPath: string
|
||||
/**
|
||||
* New path of file/folder
|
||||
* @example "/Public/newFolder"
|
||||
*/
|
||||
newPath: string
|
||||
}
|
||||
|
||||
const fileTreeExample = getTreeExample()
|
||||
|
||||
const successDeployResponse: DeployResponse = {
|
||||
@@ -143,7 +165,7 @@ export class DriveController {
|
||||
/**
|
||||
*
|
||||
* @summary Delete file from SASjs Drive
|
||||
* @query _filePath Location of SAS program
|
||||
* @query _filePath Location of file
|
||||
* @example _filePath "/Public/somefolder/some.file"
|
||||
*/
|
||||
@Delete('/file')
|
||||
@@ -151,20 +173,31 @@ export class DriveController {
|
||||
return deleteFile(_filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Delete folder from SASjs Drive
|
||||
* @query _folderPath Location of folder
|
||||
* @example _folderPath "/Public/somefolder/"
|
||||
*/
|
||||
@Delete('/folder')
|
||||
public async deleteFolder(@Query() _folderPath: string) {
|
||||
return deleteFolder(_folderPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Create a file in SASjs Drive
|
||||
* @param _filePath Location of SAS program
|
||||
* @param _filePath Location of file
|
||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||
*
|
||||
*/
|
||||
@Example<UpdateFileResponse>({
|
||||
@Example<FileFolderResponse>({
|
||||
status: 'success'
|
||||
})
|
||||
@Response<UpdateFileResponse>(403, 'File already exists', {
|
||||
@Response<FileFolderResponse>(403, 'File already exists', {
|
||||
status: 'failure',
|
||||
message: 'File request failed.'
|
||||
})
|
||||
@@ -173,10 +206,28 @@ export class DriveController {
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
): Promise<UpdateFileResponse> {
|
||||
): Promise<FileFolderResponse> {
|
||||
return saveFile((_filePath ?? filePath)!, file)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create an empty folder in SASjs Drive
|
||||
*
|
||||
*/
|
||||
@Example<FileFolderResponse>({
|
||||
status: 'success'
|
||||
})
|
||||
@Response<FileFolderResponse>(409, 'Folder already exists', {
|
||||
status: 'failure',
|
||||
message: 'Add folder request failed.'
|
||||
})
|
||||
@Post('/folder')
|
||||
public async addFolder(
|
||||
@Body() body: AddFolderPayload
|
||||
): Promise<FileFolderResponse> {
|
||||
return addFolder(body.folderPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
@@ -187,10 +238,10 @@ export class DriveController {
|
||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||
*
|
||||
*/
|
||||
@Example<UpdateFileResponse>({
|
||||
@Example<FileFolderResponse>({
|
||||
status: 'success'
|
||||
})
|
||||
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
||||
@Response<FileFolderResponse>(403, `File doesn't exist`, {
|
||||
status: 'failure',
|
||||
message: 'File request failed.'
|
||||
})
|
||||
@@ -199,10 +250,28 @@ export class DriveController {
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
): Promise<UpdateFileResponse> {
|
||||
): Promise<FileFolderResponse> {
|
||||
return updateFile((_filePath ?? filePath)!, file)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Renames a file/folder in SASjs Drive
|
||||
*
|
||||
*/
|
||||
@Example<FileFolderResponse>({
|
||||
status: 'success'
|
||||
})
|
||||
@Response<FileFolderResponse>(409, 'Folder already exists', {
|
||||
status: 'failure',
|
||||
message: 'rename request failed.'
|
||||
})
|
||||
@Post('/rename')
|
||||
public async rename(
|
||||
@Body() body: RenamePayload
|
||||
): Promise<FileFolderResponse> {
|
||||
return rename(body.oldPath, body.newPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Fetch file tree within SASjs Drive.
|
||||
*
|
||||
@@ -249,20 +318,26 @@ const getFile = async (req: express.Request, filePath: string) => {
|
||||
.join(getFilesFolder(), filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot get file outside drive.')
|
||||
}
|
||||
if (!filePathFull.includes(driveFilesPath))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Can't get file outside drive.`
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error("File doesn't exist.")
|
||||
}
|
||||
if (!(await fileExists(filePathFull)))
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: `File doesn't exist.`
|
||||
}
|
||||
|
||||
const extension = path.extname(filePathFull).toLowerCase()
|
||||
if (extension === '.sas') {
|
||||
req.res?.setHeader('Content-type', 'text/plain')
|
||||
}
|
||||
|
||||
req.res?.sendFile(path.resolve(filePathFull))
|
||||
req.res?.sendFile(path.resolve(filePathFull), { dotfiles: 'allow' })
|
||||
}
|
||||
|
||||
const getFolder = async (folderPath?: string) => {
|
||||
@@ -273,17 +348,26 @@ const getFolder = async (folderPath?: string) => {
|
||||
.join(getFilesFolder(), folderPath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!folderPathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot get folder outside drive.')
|
||||
}
|
||||
if (!folderPathFull.includes(driveFilesPath))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Can't get folder outside drive.`
|
||||
}
|
||||
|
||||
if (!(await folderExists(folderPathFull))) {
|
||||
throw new Error("Folder doesn't exist.")
|
||||
}
|
||||
if (!(await folderExists(folderPathFull)))
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: `Folder doesn't exist.`
|
||||
}
|
||||
|
||||
if (!(await isFolder(folderPathFull))) {
|
||||
throw new Error('Not a Folder.')
|
||||
}
|
||||
if (!(await isFolder(folderPathFull)))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: 'Not a Folder.'
|
||||
}
|
||||
|
||||
const files: string[] = await listFilesInFolder(folderPathFull)
|
||||
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
||||
@@ -302,19 +386,51 @@ const deleteFile = async (filePath: string) => {
|
||||
.join(getFilesFolder(), filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot delete file outside drive.')
|
||||
}
|
||||
if (!filePathFull.includes(driveFilesPath))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Can't delete file outside drive.`
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error('File does not exist.')
|
||||
}
|
||||
if (!(await fileExists(filePathFull)))
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: `File doesn't exist.`
|
||||
}
|
||||
|
||||
await deleteFileOnSystem(filePathFull)
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const deleteFolder = async (folderPath: string) => {
|
||||
const driveFolderPath = getFilesFolder()
|
||||
|
||||
const folderPathFull = path
|
||||
.join(getFilesFolder(), folderPath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!folderPathFull.includes(driveFolderPath))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Can't delete folder outside drive.`
|
||||
}
|
||||
|
||||
if (!(await folderExists(folderPathFull)))
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: `Folder doesn't exist.`
|
||||
}
|
||||
|
||||
await deleteFolderOnSystem(folderPathFull)
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const saveFile = async (
|
||||
filePath: string,
|
||||
multerFile: Express.Multer.File
|
||||
@@ -325,13 +441,19 @@ const saveFile = async (
|
||||
.join(driveFilesPath, filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot put file outside drive.')
|
||||
}
|
||||
if (!filePathFull.includes(driveFilesPath))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Can't put file outside drive.`
|
||||
}
|
||||
|
||||
if (await fileExists(filePathFull)) {
|
||||
throw new Error('File already exists.')
|
||||
}
|
||||
if (await fileExists(filePathFull))
|
||||
throw {
|
||||
code: 409,
|
||||
status: 'Conflict',
|
||||
message: 'File already exists.'
|
||||
}
|
||||
|
||||
const folderPath = path.dirname(filePathFull)
|
||||
await createFolder(folderPath)
|
||||
@@ -340,6 +462,88 @@ const saveFile = async (
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const addFolder = async (folderPath: string): Promise<FileFolderResponse> => {
|
||||
const drivePath = getFilesFolder()
|
||||
|
||||
const folderPathFull = path
|
||||
.join(drivePath, folderPath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!folderPathFull.includes(drivePath))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Can't put folder outside drive.`
|
||||
}
|
||||
|
||||
if (await folderExists(folderPathFull))
|
||||
throw {
|
||||
code: 409,
|
||||
status: 'Conflict',
|
||||
message: 'Folder already exists.'
|
||||
}
|
||||
|
||||
await createFolder(folderPathFull)
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const rename = async (
|
||||
oldPath: string,
|
||||
newPath: string
|
||||
): Promise<FileFolderResponse> => {
|
||||
const drivePath = getFilesFolder()
|
||||
|
||||
const oldPathFull = path
|
||||
.join(drivePath, oldPath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
const newPathFull = path
|
||||
.join(drivePath, newPath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!oldPathFull.includes(drivePath))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Old path can't be outside of drive.`
|
||||
}
|
||||
|
||||
if (!newPathFull.includes(drivePath))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `New path can't be outside of drive.`
|
||||
}
|
||||
|
||||
if (await isFolder(oldPathFull)) {
|
||||
if (await folderExists(newPathFull))
|
||||
throw {
|
||||
code: 409,
|
||||
status: 'Conflict',
|
||||
message: 'Folder with new name already exists.'
|
||||
}
|
||||
else moveFile(oldPathFull, newPathFull)
|
||||
|
||||
return { status: 'success' }
|
||||
} else if (await fileExists(oldPathFull)) {
|
||||
if (await fileExists(newPathFull))
|
||||
throw {
|
||||
code: 409,
|
||||
status: 'Conflict',
|
||||
message: 'File with new name already exists.'
|
||||
}
|
||||
else moveFile(oldPathFull, newPathFull)
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'No file/folder found for provided path.'
|
||||
}
|
||||
}
|
||||
|
||||
const updateFile = async (
|
||||
filePath: string,
|
||||
multerFile: Express.Multer.File
|
||||
@@ -350,13 +554,19 @@ const updateFile = async (
|
||||
.join(driveFilesPath, filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot modify file outside drive.')
|
||||
}
|
||||
if (!filePathFull.includes(driveFilesPath))
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Can't modify file outside drive.`
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error(`File doesn't exist.`)
|
||||
}
|
||||
if (!(await fileExists(filePathFull)))
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: `File doesn't exist.`
|
||||
}
|
||||
|
||||
await moveFile(multerFile.path, filePathFull)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Body
|
||||
} from 'tsoa'
|
||||
|
||||
import Group, { GroupPayload } from '../model/Group'
|
||||
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
|
||||
import User from '../model/User'
|
||||
import { UserResponse } from './user'
|
||||
|
||||
@@ -241,6 +241,13 @@ const updateUsersListInGroup = async (
|
||||
message: 'Group not found.'
|
||||
}
|
||||
|
||||
if (group.name === PUBLIC_GROUP_NAME)
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||
}
|
||||
|
||||
const user = await User.findOne({ id: userId })
|
||||
if (!user)
|
||||
throw {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Route, Tags, Example, Get } from 'tsoa'
|
||||
import { getAuthorizedRoutes } from '../utils'
|
||||
export interface AuthorizedRoutesResponse {
|
||||
URIs: string[]
|
||||
paths: string[]
|
||||
}
|
||||
|
||||
export interface InfoResponse {
|
||||
@@ -42,16 +42,16 @@ export class InfoController {
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get authorized routes.
|
||||
* @summary Get the list of available routes to which permissions can be applied. Used to populate the dialog in the URI Permissions feature.
|
||||
*
|
||||
*/
|
||||
@Example<AuthorizedRoutesResponse>({
|
||||
URIs: ['/AppStream', '/SASjsApi/stp/execute']
|
||||
paths: ['/AppStream', '/SASjsApi/stp/execute']
|
||||
})
|
||||
@Get('/authorizedRoutes')
|
||||
public authorizedRoutes(): AuthorizedRoutesResponse {
|
||||
const response = {
|
||||
URIs: getAuthorizedRoutes()
|
||||
paths: getAuthorizedRoutes()
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -143,6 +143,7 @@ export class ExecutionController {
|
||||
name: 'files',
|
||||
relativePath: '',
|
||||
absolutePath: getFilesFolder(),
|
||||
isFolder: true,
|
||||
children: []
|
||||
}
|
||||
|
||||
@@ -152,15 +153,22 @@ export class ExecutionController {
|
||||
const currentNode = stack.pop()
|
||||
|
||||
if (currentNode) {
|
||||
currentNode.isFolder = fs
|
||||
.statSync(currentNode.absolutePath)
|
||||
.isDirectory()
|
||||
|
||||
const children = fs.readdirSync(currentNode.absolutePath)
|
||||
|
||||
for (let child of children) {
|
||||
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
|
||||
const absoluteChildPath = path.join(currentNode.absolutePath, child)
|
||||
// relative path will only be used in frontend component
|
||||
// so, no need to convert '/' to platform specific separator
|
||||
const relativeChildPath = `${currentNode.relativePath}/${child}`
|
||||
const childNode: TreeNode = {
|
||||
name: child,
|
||||
relativePath: relativeChildPath,
|
||||
absolutePath: absoluteChildPath,
|
||||
isFolder: false,
|
||||
children: []
|
||||
}
|
||||
currentNode.children.push(childNode)
|
||||
|
||||
@@ -103,6 +103,9 @@ ${autoExecContent}`
|
||||
autoExecPath,
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
||||
isWindows() ? '-nologo' : ''
|
||||
])
|
||||
.then(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import express from 'express'
|
||||
import {
|
||||
Security,
|
||||
Route,
|
||||
@@ -8,7 +9,8 @@ import {
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body
|
||||
Body,
|
||||
Request
|
||||
} from 'tsoa'
|
||||
|
||||
import Permission from '../model/Permission'
|
||||
@@ -17,12 +19,16 @@ import Group from '../model/Group'
|
||||
import { UserResponse } from './user'
|
||||
import { GroupDetailsResponse } from './group'
|
||||
|
||||
export enum PermissionType {
|
||||
route = 'Route'
|
||||
}
|
||||
|
||||
export enum PrincipalType {
|
||||
user = 'user',
|
||||
group = 'group'
|
||||
}
|
||||
|
||||
export enum PermissionSetting {
|
||||
export enum PermissionSettingForRoute {
|
||||
grant = 'Grant',
|
||||
deny = 'Deny'
|
||||
}
|
||||
@@ -32,12 +38,17 @@ interface RegisterPermissionPayload {
|
||||
* Name of affected resource
|
||||
* @example "/SASjsApi/code/execute"
|
||||
*/
|
||||
uri: string
|
||||
path: string
|
||||
/**
|
||||
* Type of affected resource
|
||||
* @example "Route"
|
||||
*/
|
||||
type: PermissionType
|
||||
/**
|
||||
* The indication of whether (and to what extent) access is provided
|
||||
* @example "Grant"
|
||||
*/
|
||||
setting: PermissionSetting
|
||||
setting: PermissionSettingForRoute
|
||||
/**
|
||||
* Indicates the type of principal
|
||||
* @example "user"
|
||||
@@ -55,12 +66,13 @@ interface UpdatePermissionPayload {
|
||||
* The indication of whether (and to what extent) access is provided
|
||||
* @example "Grant"
|
||||
*/
|
||||
setting: PermissionSetting
|
||||
setting: PermissionSettingForRoute
|
||||
}
|
||||
|
||||
export interface PermissionDetailsResponse {
|
||||
permissionId: number
|
||||
uri: string
|
||||
path: string
|
||||
type: string
|
||||
setting: string
|
||||
user?: UserResponse
|
||||
group?: GroupDetailsResponse
|
||||
@@ -71,13 +83,17 @@ export interface PermissionDetailsResponse {
|
||||
@Tags('Permission')
|
||||
export class PermissionController {
|
||||
/**
|
||||
* @summary Get list of all permissions (uri, setting and userDetail).
|
||||
* Get the list of permission rules applicable the authenticated user.
|
||||
* If the user is an admin, all rules are returned.
|
||||
*
|
||||
* @summary Get the list of permission rules. If the user is admin, all rules are returned.
|
||||
*
|
||||
*/
|
||||
@Example<PermissionDetailsResponse[]>([
|
||||
{
|
||||
permissionId: 123,
|
||||
uri: '/SASjsApi/code/execute',
|
||||
path: '/SASjsApi/code/execute',
|
||||
type: 'Route',
|
||||
setting: 'Grant',
|
||||
user: {
|
||||
id: 1,
|
||||
@@ -88,7 +104,8 @@ export class PermissionController {
|
||||
},
|
||||
{
|
||||
permissionId: 124,
|
||||
uri: '/SASjsApi/code/execute',
|
||||
path: '/SASjsApi/code/execute',
|
||||
type: 'Route',
|
||||
setting: 'Grant',
|
||||
group: {
|
||||
groupId: 1,
|
||||
@@ -100,8 +117,10 @@ export class PermissionController {
|
||||
}
|
||||
])
|
||||
@Get('/')
|
||||
public async getAllPermissions(): Promise<PermissionDetailsResponse[]> {
|
||||
return getAllPermissions()
|
||||
public async getAllPermissions(
|
||||
@Request() request: express.Request
|
||||
): Promise<PermissionDetailsResponse[]> {
|
||||
return getAllPermissions(request)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +129,8 @@ export class PermissionController {
|
||||
*/
|
||||
@Example<PermissionDetailsResponse>({
|
||||
permissionId: 123,
|
||||
uri: '/SASjsApi/code/execute',
|
||||
path: '/SASjsApi/code/execute',
|
||||
type: 'Route',
|
||||
setting: 'Grant',
|
||||
user: {
|
||||
id: 1,
|
||||
@@ -133,7 +153,8 @@ export class PermissionController {
|
||||
*/
|
||||
@Example<PermissionDetailsResponse>({
|
||||
permissionId: 123,
|
||||
uri: '/SASjsApi/code/execute',
|
||||
path: '/SASjsApi/code/execute',
|
||||
type: 'Route',
|
||||
setting: 'Grant',
|
||||
user: {
|
||||
id: 1,
|
||||
@@ -161,33 +182,43 @@ export class PermissionController {
|
||||
}
|
||||
}
|
||||
|
||||
const getAllPermissions = async (): Promise<PermissionDetailsResponse[]> =>
|
||||
(await Permission.find({})
|
||||
.select({
|
||||
_id: 0,
|
||||
permissionId: 1,
|
||||
uri: 1,
|
||||
setting: 1
|
||||
})
|
||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||
.populate({
|
||||
path: 'group',
|
||||
select: 'groupId name description -_id',
|
||||
populate: {
|
||||
path: 'users',
|
||||
select: 'id username displayName isAdmin -_id',
|
||||
options: { limit: 15 }
|
||||
const getAllPermissions = async (
|
||||
req: express.Request
|
||||
): Promise<PermissionDetailsResponse[]> => {
|
||||
const { user } = req
|
||||
|
||||
if (user?.isAdmin) return await Permission.get({})
|
||||
else {
|
||||
const permissions: PermissionDetailsResponse[] = []
|
||||
|
||||
const dbUser = await User.findOne({ id: user?.userId })
|
||||
if (!dbUser)
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'User not found.'
|
||||
}
|
||||
})) as unknown as PermissionDetailsResponse[]
|
||||
|
||||
permissions.push(...(await Permission.get({ user: dbUser._id })))
|
||||
|
||||
for (const group of dbUser.groups) {
|
||||
permissions.push(...(await Permission.get({ group })))
|
||||
}
|
||||
|
||||
return permissions
|
||||
}
|
||||
}
|
||||
|
||||
const createPermission = async ({
|
||||
uri,
|
||||
path,
|
||||
type,
|
||||
setting,
|
||||
principalType,
|
||||
principalId
|
||||
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
|
||||
const permission = new Permission({
|
||||
uri,
|
||||
path,
|
||||
type,
|
||||
setting
|
||||
})
|
||||
|
||||
@@ -212,7 +243,8 @@ const createPermission = async ({
|
||||
}
|
||||
|
||||
const alreadyExists = await Permission.findOne({
|
||||
uri,
|
||||
path,
|
||||
type,
|
||||
user: userInDB._id
|
||||
})
|
||||
|
||||
@@ -220,7 +252,8 @@ const createPermission = async ({
|
||||
throw {
|
||||
code: 409,
|
||||
status: 'Conflict',
|
||||
message: 'Permission already exists with provided URI and User.'
|
||||
message:
|
||||
'Permission already exists with provided Path, Type and User.'
|
||||
}
|
||||
|
||||
permission.user = userInDB._id
|
||||
@@ -243,14 +276,16 @@ const createPermission = async ({
|
||||
}
|
||||
|
||||
const alreadyExists = await Permission.findOne({
|
||||
uri,
|
||||
path,
|
||||
type,
|
||||
group: groupInDB._id
|
||||
})
|
||||
if (alreadyExists)
|
||||
throw {
|
||||
code: 409,
|
||||
status: 'Conflict',
|
||||
message: 'Permission already exists with provided URI and Group.'
|
||||
message:
|
||||
'Permission already exists with provided Path, Type and Group.'
|
||||
}
|
||||
|
||||
permission.group = groupInDB._id
|
||||
@@ -280,7 +315,8 @@ const createPermission = async ({
|
||||
|
||||
return {
|
||||
permissionId: savedPermission.permissionId,
|
||||
uri: savedPermission.uri,
|
||||
path: savedPermission.path,
|
||||
type: savedPermission.type,
|
||||
setting: savedPermission.setting,
|
||||
user,
|
||||
group
|
||||
@@ -301,7 +337,8 @@ const updatePermission = async (
|
||||
.select({
|
||||
_id: 0,
|
||||
permissionId: 1,
|
||||
uri: 1,
|
||||
path: 1,
|
||||
type: 1,
|
||||
setting: 1
|
||||
})
|
||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
fetchLatestAutoExec,
|
||||
ModeType,
|
||||
verifyTokenInDB,
|
||||
isAuthorizingRoute
|
||||
isAuthorizingRoute,
|
||||
isPublicRoute,
|
||||
publicUser
|
||||
} from '../utils'
|
||||
import { desktopUser } from './desktop'
|
||||
import { authorize } from './authorize'
|
||||
@@ -41,7 +43,7 @@ export const authenticateAccessToken: RequestHandler = async (
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
authenticateToken(
|
||||
await authenticateToken(
|
||||
req,
|
||||
res,
|
||||
nextFunction,
|
||||
@@ -50,8 +52,12 @@ export const authenticateAccessToken: RequestHandler = async (
|
||||
)
|
||||
}
|
||||
|
||||
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
||||
authenticateToken(
|
||||
export const authenticateRefreshToken: RequestHandler = async (
|
||||
req,
|
||||
res,
|
||||
next
|
||||
) => {
|
||||
await authenticateToken(
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
@@ -60,7 +66,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
||||
)
|
||||
}
|
||||
|
||||
const authenticateToken = (
|
||||
const authenticateToken = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
@@ -83,12 +89,12 @@ const authenticateToken = (
|
||||
|
||||
const authHeader = req.headers['authorization']
|
||||
const token = authHeader?.split(' ')[1]
|
||||
if (!token) return res.sendStatus(401)
|
||||
|
||||
jwt.verify(token, key, async (err: any, data: any) => {
|
||||
if (err) return res.sendStatus(401)
|
||||
try {
|
||||
if (!token) throw 'Unauthorized'
|
||||
|
||||
const data: any = jwt.verify(token, key)
|
||||
|
||||
// verify this valid token's entry in DB
|
||||
const user = await verifyTokenInDB(
|
||||
data?.userId,
|
||||
data?.clientId,
|
||||
@@ -101,8 +107,16 @@ const authenticateToken = (
|
||||
req.user = user
|
||||
if (tokenType === 'accessToken') req.accessToken = token
|
||||
return next()
|
||||
} else return res.sendStatus(401)
|
||||
} else throw 'Unauthorized'
|
||||
}
|
||||
return res.sendStatus(401)
|
||||
})
|
||||
|
||||
throw 'Unauthorized'
|
||||
} catch (error) {
|
||||
if (await isPublicRoute(req)) {
|
||||
req.user = publicUser
|
||||
return next()
|
||||
}
|
||||
|
||||
res.sendStatus(401)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { RequestHandler } from 'express'
|
||||
import User from '../model/User'
|
||||
import Permission from '../model/Permission'
|
||||
import { PermissionSetting } from '../controllers/permission'
|
||||
import { getUri } from '../utils'
|
||||
import {
|
||||
PermissionSettingForRoute,
|
||||
PermissionType
|
||||
} from '../controllers/permission'
|
||||
import { getPath, isPublicRoute } from '../utils'
|
||||
|
||||
export const authorize: RequestHandler = async (req, res, next) => {
|
||||
const { user } = req
|
||||
@@ -14,23 +17,35 @@ export const authorize: RequestHandler = async (req, res, next) => {
|
||||
// no need to check for permissions when user is admin
|
||||
if (user.isAdmin) return next()
|
||||
|
||||
// no need to check for permissions when route is Public
|
||||
if (await isPublicRoute(req)) return next()
|
||||
|
||||
const dbUser = await User.findOne({ id: user.userId })
|
||||
if (!dbUser) return res.sendStatus(401)
|
||||
|
||||
const uri = getUri(req)
|
||||
const path = getPath(req)
|
||||
|
||||
// find permission w.r.t user
|
||||
const permission = await Permission.findOne({ uri, user: dbUser._id })
|
||||
const permission = await Permission.findOne({
|
||||
path,
|
||||
type: PermissionType.route,
|
||||
user: dbUser._id
|
||||
})
|
||||
|
||||
if (permission) {
|
||||
if (permission.setting === PermissionSetting.grant) return next()
|
||||
if (permission.setting === PermissionSettingForRoute.grant) return next()
|
||||
else return res.sendStatus(401)
|
||||
}
|
||||
|
||||
// find permission w.r.t user's groups
|
||||
for (const group of dbUser.groups) {
|
||||
const groupPermission = await Permission.findOne({ uri, group })
|
||||
if (groupPermission?.setting === PermissionSetting.grant) return next()
|
||||
const groupPermission = await Permission.findOne({
|
||||
path,
|
||||
type: PermissionType.route,
|
||||
group
|
||||
})
|
||||
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
||||
return next()
|
||||
}
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { GroupDetailsResponse } from '../controllers'
|
||||
import User, { IUser } from './User'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
|
||||
export const PUBLIC_GROUP_NAME = 'Public'
|
||||
|
||||
export interface GroupPayload {
|
||||
/**
|
||||
* Name of the group
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
import { PermissionDetailsResponse } from '../controllers'
|
||||
|
||||
interface GetPermissionBy {
|
||||
user?: Schema.Types.ObjectId
|
||||
group?: Schema.Types.ObjectId
|
||||
}
|
||||
|
||||
interface IPermissionDocument extends Document {
|
||||
uri: string
|
||||
path: string
|
||||
type: string
|
||||
setting: string
|
||||
permissionId: number
|
||||
user: Schema.Types.ObjectId
|
||||
@@ -11,10 +18,16 @@ interface IPermissionDocument extends Document {
|
||||
|
||||
interface IPermission extends IPermissionDocument {}
|
||||
|
||||
interface IPermissionModel extends Model<IPermission> {}
|
||||
interface IPermissionModel extends Model<IPermission> {
|
||||
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
|
||||
}
|
||||
|
||||
const permissionSchema = new Schema<IPermissionDocument>({
|
||||
uri: {
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
@@ -28,6 +41,30 @@ const permissionSchema = new Schema<IPermissionDocument>({
|
||||
|
||||
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
|
||||
|
||||
// Static Methods
|
||||
permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
|
||||
PermissionDetailsResponse[]
|
||||
> {
|
||||
return (await this.find(getBy)
|
||||
.select({
|
||||
_id: 0,
|
||||
permissionId: 1,
|
||||
path: 1,
|
||||
type: 1,
|
||||
setting: 1
|
||||
})
|
||||
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
|
||||
.populate({
|
||||
path: 'group',
|
||||
select: 'groupId name description -_id',
|
||||
populate: {
|
||||
path: 'users',
|
||||
select: 'id username displayName isAdmin -_id',
|
||||
options: { limit: 15 }
|
||||
}
|
||||
})) as unknown as PermissionDetailsResponse[]
|
||||
})
|
||||
|
||||
export const Permission: IPermissionModel = model<
|
||||
IPermission,
|
||||
IPermissionModel
|
||||
|
||||
@@ -11,8 +11,10 @@ import {
|
||||
extractName,
|
||||
fileBodyValidation,
|
||||
fileParamValidation,
|
||||
folderBodyValidation,
|
||||
folderParamValidation,
|
||||
isZipFile
|
||||
isZipFile,
|
||||
renameBodyValidation
|
||||
} from '../../utils'
|
||||
|
||||
const controller = new DriveController()
|
||||
@@ -119,7 +121,11 @@ driveRouter.get('/file', async (req, res) => {
|
||||
try {
|
||||
await controller.getFile(req, query._filePath)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -132,7 +138,11 @@ driveRouter.get('/folder', async (req, res) => {
|
||||
const response = await controller.getFolder(query._folderPath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -145,7 +155,28 @@ driveRouter.delete('/file', async (req, res) => {
|
||||
const response = await controller.deleteFile(query._filePath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.delete('/folder', async (req, res) => {
|
||||
const { error: errQ, value: query } = folderParamValidation(req.query, true)
|
||||
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.deleteFolder(query._folderPath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -172,11 +203,33 @@ driveRouter.post(
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
await deleteFile(req.file.path)
|
||||
res.status(403).send(err.toString())
|
||||
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
driveRouter.post('/folder', async (req, res) => {
|
||||
const { error, value: body } = folderBodyValidation(req.body)
|
||||
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.addFolder(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.patch(
|
||||
'/file',
|
||||
(...arg) => multerSingle('file', arg),
|
||||
@@ -200,11 +253,33 @@ driveRouter.patch(
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
await deleteFile(req.file.path)
|
||||
res.status(403).send(err.toString())
|
||||
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
driveRouter.post('/rename', async (req, res) => {
|
||||
const { error, value: body } = renameBodyValidation(req.body)
|
||||
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.rename(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.get('/fileTree', async (req, res) => {
|
||||
try {
|
||||
const response = await controller.getFileTree()
|
||||
|
||||
@@ -11,7 +11,7 @@ const controller = new PermissionController()
|
||||
|
||||
permissionRouter.get('/', async (req, res) => {
|
||||
try {
|
||||
const response = await controller.getAllPermissions()
|
||||
const response = await controller.getAllPermissions(req)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
@@ -32,7 +32,8 @@ import appPromise from '../../../app'
|
||||
import {
|
||||
UserController,
|
||||
PermissionController,
|
||||
PermissionSetting,
|
||||
PermissionType,
|
||||
PermissionSettingForRoute,
|
||||
PrincipalType
|
||||
} from '../../../controllers/'
|
||||
import { getTreeExample } from '../../../controllers/internal'
|
||||
@@ -48,6 +49,12 @@ const user = {
|
||||
isActive: true
|
||||
}
|
||||
|
||||
const permission = {
|
||||
type: PermissionType.route,
|
||||
principalType: PrincipalType.user,
|
||||
setting: PermissionSettingForRoute.grant
|
||||
}
|
||||
|
||||
describe('drive', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
@@ -66,28 +73,29 @@ describe('drive', () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
accessToken = await generateAndSaveToken(dbUser.id)
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/drive/deploy',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/deploy',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/drive/deploy/upload',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/deploy/upload',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/drive/file',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/file',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/drive/folder',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/folder',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
path: '/SASjsApi/drive/rename',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
})
|
||||
|
||||
@@ -543,29 +551,29 @@ describe('drive', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if folder is not present', async () => {
|
||||
it('should respond with Not Found if folder is not present', async () => {
|
||||
const res = await request(app)
|
||||
.get(getFolderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
|
||||
expect(res.text).toEqual(`Folder doesn't exist.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if folderPath outside Drive', async () => {
|
||||
it('should respond with Bad Request if folderPath outside Drive', async () => {
|
||||
const res = await request(app)
|
||||
.get(getFolderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _folderPath: '/../path/code.sas' })
|
||||
.expect(403)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
|
||||
expect(res.text).toEqual(`Can't get folder outside drive.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if folderPath is of a file', async () => {
|
||||
it('should respond with Bad Request if folderPath is of a file', async () => {
|
||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const filePath = '/my/path/code.sas'
|
||||
|
||||
@@ -576,12 +584,96 @@ describe('drive', () => {
|
||||
.get(getFolderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _folderPath: filePath })
|
||||
.expect(403)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Error: Not a Folder.')
|
||||
expect(res.text).toEqual('Not a Folder.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('post', () => {
|
||||
const folderApi = '/SASjsApi/drive/folder'
|
||||
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteFolder(path.join(pathToDrive, 'post'))
|
||||
})
|
||||
|
||||
it('should create a folder on drive', async () => {
|
||||
const res = await request(app)
|
||||
.post(folderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ folderPath: '/post/folder' })
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with Conflict if the folder already exists', async () => {
|
||||
await createFolder(path.join(pathToDrive, '/post/folder'))
|
||||
|
||||
const res = await request(app)
|
||||
.post(folderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ folderPath: '/post/folder' })
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual(`Folder already exists.`)
|
||||
|
||||
expect(res.statusCode).toEqual(409)
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if the folderPath is outside drive', async () => {
|
||||
const res = await request(app)
|
||||
.post(folderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ folderPath: '../sample' })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`Can't put folder outside drive.`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete', () => {
|
||||
const folderApi = '/SASjsApi/drive/folder'
|
||||
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||
|
||||
it('should delete a folder on drive', async () => {
|
||||
await createFolder(path.join(pathToDrive, 'delete'))
|
||||
|
||||
const res = await request(app)
|
||||
.delete(folderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _folderPath: 'delete' })
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with Not Found if the folder does not exists', async () => {
|
||||
const res = await request(app)
|
||||
.delete(folderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _folderPath: 'notExists' })
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual(`Folder doesn't exist.`)
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if the folderPath is outside drive', async () => {
|
||||
const res = await request(app)
|
||||
.delete(folderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _folderPath: '../outsideDrive' })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`Can't delete folder outside drive.`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('file', () => {
|
||||
@@ -627,7 +719,7 @@ describe('drive', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if file is already present', async () => {
|
||||
it('should respond with Conflict if file is already present', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||
|
||||
@@ -642,13 +734,13 @@ describe('drive', () => {
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(403)
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual('Error: File already exists.')
|
||||
expect(res.text).toEqual('File already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||
it('should respond with Bad Request if filePath outside Drive', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/../path/code.sas'
|
||||
|
||||
@@ -657,9 +749,9 @@ describe('drive', () => {
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(403)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Error: Cannot put file outside drive.')
|
||||
expect(res.text).toEqual(`Can't put file outside drive.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -794,19 +886,19 @@ describe('drive', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if file is not present', async () => {
|
||||
it('should respond with Not Found if file is not present', async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', `/my/path/code-3.sas`)
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||
expect(res.text).toEqual(`File doesn't exist.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||
it('should respond with Bad Request if filePath outside Drive', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/../path/code.sas'
|
||||
|
||||
@@ -815,9 +907,9 @@ describe('drive', () => {
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(403)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
|
||||
expect(res.text).toEqual(`Can't modify file outside drive.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -922,25 +1014,25 @@ describe('drive', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if file is not present', async () => {
|
||||
it('should respond with Not Found if file is not present', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: `/my/path/code-4.sas` })
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||
expect(res.text).toEqual(`File doesn't exist.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||
it('should respond with Bad Request if filePath outside Drive', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: '/../path/code.sas' })
|
||||
.expect(403)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Error: Cannot get file outside drive.')
|
||||
expect(res.text).toEqual(`Can't get file outside drive.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -966,6 +1058,139 @@ describe('drive', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rename', () => {
|
||||
const renameApi = '/SASjsApi/drive/rename'
|
||||
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteFolder(path.join(pathToDrive, 'rename'))
|
||||
})
|
||||
|
||||
it('should rename a folder', async () => {
|
||||
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
|
||||
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ oldPath: '/rename/folder', newPath: '/rename/renamed' })
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should rename a file', async () => {
|
||||
await createFile(
|
||||
path.join(pathToDrive, 'rename', 'file.txt'),
|
||||
'some file content'
|
||||
)
|
||||
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({
|
||||
oldPath: '/rename/file.txt',
|
||||
newPath: '/rename/renamed.txt'
|
||||
})
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if the oldPath is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ newPath: 'newPath' })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`\"oldPath\" is required`)
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if the newPath is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ oldPath: 'oldPath' })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`\"newPath\" is required`)
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if the oldPath is outside drive', async () => {
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ oldPath: '../outside', newPath: 'renamed' })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`Old path can't be outside of drive.`)
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if the newPath is outside drive', async () => {
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ oldPath: 'older', newPath: '../outside' })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`New path can't be outside of drive.`)
|
||||
})
|
||||
|
||||
it('should respond with Not Found if the folder does not exist', async () => {
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ oldPath: '/rename/not exists', newPath: '/rename/renamed' })
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('No file/folder found for provided path.')
|
||||
})
|
||||
|
||||
it('should respond with Conflict if the folder already exists', async () => {
|
||||
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
|
||||
await createFolder(path.join(pathToDrive, 'rename', 'exists'))
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ oldPath: '/rename/folder', newPath: '/rename/exists' })
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual('Folder with new name already exists.')
|
||||
})
|
||||
|
||||
it('should respond with Not Found if the file does not exist', async () => {
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ oldPath: '/rename/file.txt', newPath: '/rename/renamed.txt' })
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('No file/folder found for provided path.')
|
||||
})
|
||||
|
||||
it('should respond with Conflict if the file already exists', async () => {
|
||||
await createFile(
|
||||
path.join(pathToDrive, 'rename', 'file.txt'),
|
||||
'some file content'
|
||||
)
|
||||
await createFile(
|
||||
path.join(pathToDrive, 'rename', 'exists.txt'),
|
||||
'some existing content'
|
||||
)
|
||||
const res = await request(app)
|
||||
.post(renameApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ oldPath: '/rename/file.txt', newPath: '/rename/exists.txt' })
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual('File with new name already exists.')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const getExampleService = (): ServiceMember =>
|
||||
|
||||
@@ -5,6 +5,7 @@ import request from 'supertest'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController, GroupController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
import { PUBLIC_GROUP_NAME } from '../../../model/Group'
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
@@ -27,6 +28,12 @@ const group = {
|
||||
description: 'DC group for testing purposes.'
|
||||
}
|
||||
|
||||
const PUBLIC_GROUP = {
|
||||
name: PUBLIC_GROUP_NAME,
|
||||
description:
|
||||
'A special group that can be used to bypass authentication for particular routes.'
|
||||
}
|
||||
|
||||
const userController = new UserController()
|
||||
const groupController = new GroupController()
|
||||
|
||||
@@ -535,6 +542,24 @@ describe('group', () => {
|
||||
expect(res.text).toEqual('User not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request when adding user to Public group', async () => {
|
||||
const dbGroup = await groupController.createGroup(PUBLIC_GROUP)
|
||||
const dbUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'publicUser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('RemoveUser', () => {
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
DriveController,
|
||||
UserController,
|
||||
GroupController,
|
||||
ClientController,
|
||||
PermissionController,
|
||||
PrincipalType,
|
||||
PermissionSetting
|
||||
PermissionType,
|
||||
PermissionSettingForRoute
|
||||
} from '../../../controllers/'
|
||||
import {
|
||||
UserDetailsResponse,
|
||||
@@ -56,10 +56,10 @@ const user = {
|
||||
}
|
||||
|
||||
const permission = {
|
||||
uri: '/SASjsApi/code/execute',
|
||||
setting: PermissionSetting.grant,
|
||||
principalType: PrincipalType.user,
|
||||
principalId: 123
|
||||
path: '/SASjsApi/code/execute',
|
||||
type: PermissionType.route,
|
||||
setting: PermissionSettingForRoute.grant,
|
||||
principalType: PrincipalType.user
|
||||
}
|
||||
|
||||
const group = {
|
||||
@@ -69,7 +69,6 @@ const group = {
|
||||
|
||||
const userController = new UserController()
|
||||
const groupController = new GroupController()
|
||||
const clientController = new ClientController()
|
||||
const permissionController = new PermissionController()
|
||||
|
||||
describe('permission', () => {
|
||||
@@ -108,7 +107,8 @@ describe('permission', () => {
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.permissionId).toBeTruthy()
|
||||
expect(res.body.uri).toEqual(permission.uri)
|
||||
expect(res.body.path).toEqual(permission.path)
|
||||
expect(res.body.type).toEqual(permission.type)
|
||||
expect(res.body.setting).toEqual(permission.setting)
|
||||
expect(res.body.user).toBeTruthy()
|
||||
})
|
||||
@@ -127,7 +127,8 @@ describe('permission', () => {
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.permissionId).toBeTruthy()
|
||||
expect(res.body.uri).toEqual(permission.uri)
|
||||
expect(res.body.path).toEqual(permission.path)
|
||||
expect(res.body.type).toEqual(permission.type)
|
||||
expect(res.body.setting).toEqual(permission.setting)
|
||||
expect(res.body.group).toBeTruthy()
|
||||
})
|
||||
@@ -142,53 +143,74 @@ describe('permission', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not of an admin account even if user has permission', async () => {
|
||||
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/permission',
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.send(permission)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Admin account required')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if uri is missing', async () => {
|
||||
it('should respond with Bad Request if path is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
uri: undefined
|
||||
path: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"uri" is required`)
|
||||
expect(res.text).toEqual(`"path" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if uri is not valid', async () => {
|
||||
it('should respond with Bad Request if path is not valid', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
uri: '/some/random/api/endpoint'
|
||||
path: '/some/random/api/endpoint'
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if type is not valid', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
type: 'invalid'
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('"type" must be [Route]')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if type is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
type: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"type" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if setting is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
@@ -203,6 +225,20 @@ describe('permission', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if setting is not valid', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
setting: 'invalid'
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if principalType is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
@@ -217,20 +253,6 @@ describe('permission', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if principalId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
principalId: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"principalId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if principal type is not valid', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
@@ -245,17 +267,17 @@ describe('permission', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if setting is not valid', async () => {
|
||||
it('should respond with Bad Request if principalId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
setting: 'invalid'
|
||||
principalId: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
|
||||
expect(res.text).toEqual(`"principalId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -313,7 +335,8 @@ describe('permission', () => {
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
principalType: 'group'
|
||||
principalType: 'group',
|
||||
principalId: 123
|
||||
})
|
||||
.expect(404)
|
||||
|
||||
@@ -334,7 +357,7 @@ describe('permission', () => {
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'Permission already exists with provided URI and User.'
|
||||
'Permission already exists with provided Path, Type and User.'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
@@ -357,7 +380,7 @@ describe('permission', () => {
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ setting: 'Deny' })
|
||||
.send({ setting: PermissionSettingForRoute.deny })
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.setting).toEqual('Deny')
|
||||
@@ -366,7 +389,7 @@ describe('permission', () => {
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||
.send(permission)
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
@@ -400,12 +423,11 @@ describe('permission', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if setting is not valid', async () => {
|
||||
it('should respond with Bad Request if setting is invalid', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/permission')
|
||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...permission,
|
||||
setting: 'invalid'
|
||||
})
|
||||
.expect(400)
|
||||
@@ -414,12 +436,12 @@ describe('permission', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with not found (404) if permission with provided id does not exists', async () => {
|
||||
it('should respond with not found (404) if permission with provided id does not exist', async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/permission/123')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
setting: PermissionSetting.deny
|
||||
setting: PermissionSettingForRoute.deny
|
||||
})
|
||||
.expect(404)
|
||||
|
||||
@@ -458,12 +480,12 @@ describe('permission', () => {
|
||||
beforeAll(async () => {
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
uri: '/test-1',
|
||||
path: '/test-1',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
uri: '/test-2',
|
||||
path: '/test-2',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
})
|
||||
@@ -478,34 +500,37 @@ describe('permission', () => {
|
||||
expect(res.body).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should give a list of all permissions when user is not admin', async () => {
|
||||
const dbUser = await userController.createUser({
|
||||
it(`should give a list of user's own permissions when user is not admin`, async () => {
|
||||
const nonAdminUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'get' + user.username
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const accessToken = await generateAndSaveToken(nonAdminUser.id)
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/permission',
|
||||
path: '/test-1',
|
||||
type: PermissionType.route,
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
principalId: nonAdminUser.id,
|
||||
setting: PermissionSettingForRoute.grant
|
||||
})
|
||||
|
||||
const permissionCount = 1
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/permission/')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toHaveLength(3)
|
||||
expect(res.body).toHaveLength(permissionCount)
|
||||
})
|
||||
})
|
||||
|
||||
describe.only('verify', () => {
|
||||
describe('verify', () => {
|
||||
beforeAll(async () => {
|
||||
await permissionController.createPermission({
|
||||
...permission,
|
||||
uri: '/SASjsApi/drive/deploy',
|
||||
path: '/SASjsApi/drive/deploy',
|
||||
principalId: dbUser.id
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,8 @@ import appPromise from '../../../app'
|
||||
import {
|
||||
UserController,
|
||||
PermissionController,
|
||||
PermissionSetting,
|
||||
PermissionType,
|
||||
PermissionSettingForRoute,
|
||||
PrincipalType
|
||||
} from '../../../controllers/'
|
||||
import {
|
||||
@@ -56,10 +57,11 @@ describe('stp', () => {
|
||||
const dbUser = await userController.createUser(user)
|
||||
accessToken = await generateAndSaveToken(dbUser.id)
|
||||
await permissionController.createPermission({
|
||||
uri: '/SASjsApi/stp/execute',
|
||||
path: '/SASjsApi/stp/execute',
|
||||
type: PermissionType.route,
|
||||
principalType: PrincipalType.user,
|
||||
principalId: dbUser.id,
|
||||
setting: PermissionSetting.grant
|
||||
setting: PermissionSettingForRoute.grant
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -39,12 +39,11 @@ describe('web', () => {
|
||||
|
||||
describe('home', () => {
|
||||
it('should respond with CSRF Token', async () => {
|
||||
await request(app)
|
||||
.get('/')
|
||||
.expect(
|
||||
'set-cookie',
|
||||
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
|
||||
)
|
||||
const res = await request(app).get('/').expect(200)
|
||||
|
||||
expect(res.text).toMatch(
|
||||
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -154,10 +153,10 @@ describe('web', () => {
|
||||
|
||||
const getCSRF = async (app: Express) => {
|
||||
// make request to get CSRF
|
||||
const { header } = await request(app).get('/')
|
||||
const { header, text } = await request(app).get('/')
|
||||
const cookies = header['set-cookie'].join()
|
||||
|
||||
const csrfToken = extractCSRF(cookies)
|
||||
const csrfToken = extractCSRF(text)
|
||||
return { csrfToken, cookies }
|
||||
}
|
||||
|
||||
@@ -177,7 +176,7 @@ const performLogin = async (
|
||||
return { cookies: newCookies }
|
||||
}
|
||||
|
||||
const extractCSRF = (cookies: string) =>
|
||||
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
|
||||
cookies
|
||||
)![2]
|
||||
const extractCSRF = (text: string) =>
|
||||
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
||||
text
|
||||
)![1]
|
||||
|
||||
@@ -26,6 +26,7 @@ export const style = `<style>
|
||||
}
|
||||
.app-container .app img{
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
margin-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@@ -11,11 +11,15 @@ webRouter.get('/', async (req, res) => {
|
||||
try {
|
||||
response = await controller.home()
|
||||
} catch (_) {
|
||||
response = 'Web Build is not present'
|
||||
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||
} finally {
|
||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||
const injectedContent = response?.replace(
|
||||
'</head>',
|
||||
`${codeToInject}</head>`
|
||||
)
|
||||
|
||||
return res.send(response)
|
||||
return res.send(injectedContent)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,5 +2,6 @@ export interface TreeNode {
|
||||
name: string
|
||||
relativePath: string
|
||||
absolutePath: string
|
||||
isFolder: boolean
|
||||
children: Array<TreeNode>
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const StaticAuthorizedRoutes = [
|
||||
'/SASjsApi/drive/file',
|
||||
'/SASjsApi/drive/folder',
|
||||
'/SASjsApi/drive/fileTree',
|
||||
'/SASjsApi/permission'
|
||||
'/SASjsApi/drive/rename'
|
||||
]
|
||||
|
||||
export const getAuthorizedRoutes = () => {
|
||||
@@ -18,7 +18,7 @@ export const getAuthorizedRoutes = () => {
|
||||
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
||||
}
|
||||
|
||||
export const getUri = (req: Request) => {
|
||||
export const getPath = (req: Request) => {
|
||||
const { baseUrl, path: reqPath } = req
|
||||
|
||||
if (baseUrl === '/AppStream') {
|
||||
@@ -32,4 +32,4 @@ export const getUri = (req: Request) => {
|
||||
}
|
||||
|
||||
export const isAuthorizingRoute = (req: Request): boolean =>
|
||||
getAuthorizedRoutes().includes(getUri(req))
|
||||
getAuthorizedRoutes().includes(getPath(req))
|
||||
|
||||
@@ -16,6 +16,7 @@ export * from './getRunTimeAndFilePath'
|
||||
export * from './getServerUrl'
|
||||
export * from './instantiateLogger'
|
||||
export * from './isDebugOn'
|
||||
export * from './isPublicRoute'
|
||||
export * from './zipped'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
|
||||
31
api/src/utils/isPublicRoute.ts
Normal file
31
api/src/utils/isPublicRoute.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Request } from 'express'
|
||||
import { getPath } from './getAuthorizedRoutes'
|
||||
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||
import Permission from '../model/Permission'
|
||||
import { PermissionSettingForRoute } from '../controllers'
|
||||
import { RequestUser } from '../types'
|
||||
|
||||
export const isPublicRoute = async (req: Request): Promise<boolean> => {
|
||||
const group = await Group.findOne({ name: PUBLIC_GROUP_NAME })
|
||||
if (group) {
|
||||
const path = getPath(req)
|
||||
|
||||
const groupPermission = await Permission.findOne({
|
||||
path,
|
||||
group: group?._id
|
||||
})
|
||||
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const publicUser: RequestUser = {
|
||||
userId: 0,
|
||||
clientId: 'public_app',
|
||||
username: 'publicUser',
|
||||
displayName: 'Public User',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Client from '../model/Client'
|
||||
import Group from '../model/Group'
|
||||
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||
import User from '../model/User'
|
||||
import Configuration, { ConfigurationType } from '../model/Configuration'
|
||||
|
||||
@@ -31,6 +31,15 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
||||
console.log(`DB Seed - Group created: ${GROUP.name}`)
|
||||
}
|
||||
|
||||
// Checking if 'Public' Group is already in the database
|
||||
const publicGroupExist = await Group.findOne({ name: PUBLIC_GROUP.name })
|
||||
if (!publicGroupExist) {
|
||||
const group = new Group(PUBLIC_GROUP)
|
||||
await group.save()
|
||||
|
||||
console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
|
||||
}
|
||||
|
||||
// Checking if user is already in the database
|
||||
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||
if (!usernameExist) {
|
||||
@@ -68,6 +77,13 @@ const GROUP = {
|
||||
name: 'AllUsers',
|
||||
description: 'Group contains all users'
|
||||
}
|
||||
|
||||
const PUBLIC_GROUP = {
|
||||
name: PUBLIC_GROUP_NAME,
|
||||
description:
|
||||
'A special group that can be used to bypass authentication for particular routes.'
|
||||
}
|
||||
|
||||
const CLIENT = {
|
||||
clientId: 'clientID1',
|
||||
clientSecret: 'clientSecret'
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import Joi from 'joi'
|
||||
import { PermissionSetting, PrincipalType } from '../controllers/permission'
|
||||
import {
|
||||
PermissionType,
|
||||
PermissionSettingForRoute,
|
||||
PrincipalType
|
||||
} from '../controllers/permission'
|
||||
import { getAuthorizedRoutes } from './getAuthorizedRoutes'
|
||||
|
||||
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||
@@ -89,12 +93,15 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
uri: Joi.string()
|
||||
path: Joi.string()
|
||||
.required()
|
||||
.valid(...getAuthorizedRoutes()),
|
||||
type: Joi.string()
|
||||
.required()
|
||||
.valid(...Object.values(PermissionType)),
|
||||
setting: Joi.string()
|
||||
.required()
|
||||
.valid(...Object.values(PermissionSetting)),
|
||||
.valid(...Object.values(PermissionSettingForRoute)),
|
||||
principalType: Joi.string()
|
||||
.required()
|
||||
.valid(...Object.values(PrincipalType)),
|
||||
@@ -105,7 +112,7 @@ export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
setting: Joi.string()
|
||||
.required()
|
||||
.valid(...Object.values(PermissionSetting))
|
||||
.valid(...Object.values(PermissionSettingForRoute))
|
||||
}).validate(data)
|
||||
|
||||
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||
@@ -138,9 +145,23 @@ export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
||||
_filePath: filePathSchema
|
||||
}).validate(data)
|
||||
|
||||
export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
||||
export const folderParamValidation = (
|
||||
data: any,
|
||||
folderPathRequired?: boolean
|
||||
): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_folderPath: Joi.string()
|
||||
_folderPath: folderPathRequired ? Joi.string().required() : Joi.string()
|
||||
}).validate(data)
|
||||
|
||||
export const folderBodyValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
folderPath: Joi.string().required()
|
||||
}).validate(data)
|
||||
|
||||
export const renameBodyValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
oldPath: Joi.string().required(),
|
||||
newPath: Joi.string().required()
|
||||
}).validate(data)
|
||||
|
||||
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
@@ -125,8 +125,27 @@ const verifyCORS = (): string[] => {
|
||||
|
||||
if (CORS) {
|
||||
const corsTypes = Object.values(CorsType)
|
||||
|
||||
if (!corsTypes.includes(CORS as CorsType))
|
||||
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
|
||||
|
||||
if (CORS === CorsType.ENABLED) {
|
||||
const { WHITELIST } = process.env
|
||||
|
||||
const urls = WHITELIST?.trim()
|
||||
.split(' ')
|
||||
.filter((url) => !!url)
|
||||
if (urls?.length) {
|
||||
urls.forEach((url) => {
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://'))
|
||||
errors.push(
|
||||
`- CORS '${CORS}'\n - provided WHITELIST ${url} is not valid`
|
||||
)
|
||||
})
|
||||
} else {
|
||||
errors.push(`- CORS '${CORS}'\n - provide at least one WHITELIST URL`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { MODE } = process.env
|
||||
process.env.CORS =
|
||||
|
||||
@@ -12,28 +12,16 @@
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Info",
|
||||
"description": "Get Server Information"
|
||||
},
|
||||
{
|
||||
"name": "Session",
|
||||
"description": "Get Session information"
|
||||
},
|
||||
{
|
||||
"name": "User",
|
||||
"description": "Operations with users"
|
||||
},
|
||||
{
|
||||
"name": "Permission",
|
||||
"description": "Operations about permissions"
|
||||
"name": "Auth",
|
||||
"description": "Operations about auth"
|
||||
},
|
||||
{
|
||||
"name": "Client",
|
||||
"description": "Operations about clients"
|
||||
},
|
||||
{
|
||||
"name": "Auth",
|
||||
"description": "Operations about auth"
|
||||
"name": "CODE",
|
||||
"description": "Execution of code (various runtimes are supported)"
|
||||
},
|
||||
{
|
||||
"name": "Drive",
|
||||
@@ -43,13 +31,25 @@
|
||||
"name": "Group",
|
||||
"description": "Operations on groups and group memberships"
|
||||
},
|
||||
{
|
||||
"name": "Info",
|
||||
"description": "Get Server Information"
|
||||
},
|
||||
{
|
||||
"name": "Permission",
|
||||
"description": "Operations about permissions"
|
||||
},
|
||||
{
|
||||
"name": "Session",
|
||||
"description": "Get Session information"
|
||||
},
|
||||
{
|
||||
"name": "STP",
|
||||
"description": "Execution of Stored Programs"
|
||||
},
|
||||
{
|
||||
"name": "CODE",
|
||||
"description": "Execution of code (various runtimes are supported)"
|
||||
"name": "User",
|
||||
"description": "Operations with users"
|
||||
},
|
||||
{
|
||||
"name": "Web",
|
||||
|
||||
572
web/package-lock.json
generated
572
web/package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/icons-material": "^5.0.3",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/lab": "^5.0.0-alpha.50",
|
||||
"@mui/material": "^5.0.3",
|
||||
"@mui/styles": "^5.0.1",
|
||||
@@ -27,7 +27,7 @@
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-monaco-editor": "^0.48.0",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-toastify": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1836,9 +1836,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
|
||||
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
|
||||
"integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
},
|
||||
@@ -2284,6 +2284,58 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
|
||||
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
|
||||
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core": {
|
||||
"version": "5.0.0-alpha.54",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core/-/core-5.0.0-alpha.54.tgz",
|
||||
@@ -2312,19 +2364,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/icons-material": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz",
|
||||
"integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==",
|
||||
"version": "5.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
|
||||
"integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.16.0"
|
||||
"@babel/runtime": "^7.17.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mui/material": "^5.0.0",
|
||||
"@types/react": "^16.8.6 || ^17.0.0",
|
||||
"react": "^17.0.2"
|
||||
"@types/react": "^17.0.0 || ^18.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -3933,11 +3989,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
|
||||
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6518,18 +6572,6 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/espree/node_modules/acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/espree/node_modules/eslint-visitor-keys": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
|
||||
@@ -7128,16 +7170,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/history": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"loose-envify": "^1.2.0",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0",
|
||||
"value-equal": "^1.0.1"
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
@@ -7198,20 +7235,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser/node_modules/acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser/node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
@@ -7221,46 +7244,6 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser/node_modules/source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser/node_modules/terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"acorn": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/html-webpack-plugin": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
|
||||
@@ -7829,11 +7812,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -8392,19 +8370,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-create-react-context": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
||||
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.1",
|
||||
"tiny-warning": "^1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.0.0",
|
||||
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
@@ -8967,14 +8932,6 @@
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
||||
"dependencies": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
@@ -9362,47 +9319,29 @@
|
||||
"react": "^17.x"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
|
||||
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"hoist-non-react-statics": "^3.1.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"mini-create-react-context": "^0.4.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-is": "^16.6.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
|
||||
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-router": "5.2.1",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
"history": "^5.2.0",
|
||||
"react-router": "6.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15"
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
"node_modules/react-router-dom/node_modules/react-router": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
|
||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
||||
"dependencies": {
|
||||
"history": "^5.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-toastify": {
|
||||
"version": "9.0.1",
|
||||
@@ -9679,11 +9618,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pathname": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
||||
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
@@ -10337,6 +10271,28 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.14.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
|
||||
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -10349,11 +10305,6 @@
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
|
||||
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
@@ -10733,11 +10684,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/value-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@@ -11184,17 +11130,6 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/acorn-import-assertions": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
|
||||
@@ -11203,11 +11138,6 @@
|
||||
"acorn": "^8"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/webpack/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -11276,30 +11206,6 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"dependencies": {
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"acorn": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz",
|
||||
@@ -11333,14 +11239,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/terser/node_modules/source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
@@ -12642,9 +12540,9 @@
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
|
||||
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
|
||||
"integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
@@ -12974,6 +12872,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@jridgewell/gen-mapping": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
|
||||
"requires": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
|
||||
},
|
||||
"@jridgewell/set-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
|
||||
},
|
||||
"@jridgewell/source-map": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
|
||||
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
|
||||
"requires": {
|
||||
"@jridgewell/gen-mapping": "^0.3.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
|
||||
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"@mui/core": {
|
||||
"version": "5.0.0-alpha.54",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core/-/core-5.0.0-alpha.54.tgz",
|
||||
@@ -12989,11 +12930,11 @@
|
||||
}
|
||||
},
|
||||
"@mui/icons-material": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz",
|
||||
"integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==",
|
||||
"version": "5.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
|
||||
"integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.16.0"
|
||||
"@babel/runtime": "^7.17.2"
|
||||
}
|
||||
},
|
||||
"@mui/lab": {
|
||||
@@ -14170,11 +14111,9 @@
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
|
||||
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w=="
|
||||
},
|
||||
"acorn-jsx": {
|
||||
"version": "5.3.2",
|
||||
@@ -16121,12 +16060,6 @@
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
|
||||
@@ -16587,16 +16520,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"history": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"loose-envify": "^1.2.0",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0",
|
||||
"value-equal": "^1.0.1"
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
},
|
||||
"hoist-non-react-statics": {
|
||||
@@ -16650,44 +16578,11 @@
|
||||
"terser": "^5.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
||||
"dev": true
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -17084,11 +16979,6 @@
|
||||
"is-docker": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -17530,15 +17420,6 @@
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
|
||||
},
|
||||
"mini-create-react-context": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
|
||||
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.1",
|
||||
"tiny-warning": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
@@ -17961,14 +17842,6 @@
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
|
||||
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
|
||||
"requires": {
|
||||
"isarray": "0.0.1"
|
||||
}
|
||||
},
|
||||
"path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
@@ -18260,44 +18133,25 @@
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
|
||||
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
|
||||
"react-router-dom": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"hoist-non-react-statics": "^3.1.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"mini-create-react-context": "^0.4.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-is": "^16.6.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
"history": "^5.2.0",
|
||||
"react-router": "6.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
"react-router": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
|
||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
||||
"requires": {
|
||||
"history": "^5.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-router-dom": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
|
||||
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-router": "5.2.1",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-toastify": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
|
||||
@@ -18520,11 +18374,6 @@
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
|
||||
},
|
||||
"resolve-pathname": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
|
||||
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||
@@ -19014,6 +18863,24 @@
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.14.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
|
||||
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
|
||||
"requires": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
"acorn": "^8.5.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -19026,11 +18893,6 @@
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"dev": true
|
||||
},
|
||||
"tiny-invariant": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
|
||||
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
|
||||
},
|
||||
"tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
@@ -19320,11 +19182,6 @@
|
||||
"homedir-polyfill": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"value-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@@ -19380,22 +19237,12 @@
|
||||
"webpack-sources": "^3.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
|
||||
},
|
||||
"acorn-import-assertions": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
|
||||
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
|
||||
"requires": {}
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -19442,23 +19289,6 @@
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
||||
"requires": {
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.7.2",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/icons-material": "^5.0.3",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/lab": "^5.0.0-alpha.50",
|
||||
"@mui/material": "^5.0.3",
|
||||
"@mui/styles": "^5.0.1",
|
||||
@@ -26,7 +26,7 @@
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-monaco-editor": "^0.48.0",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-toastify": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Route, HashRouter, Switch } from 'react-router-dom'
|
||||
import { Route, HashRouter, Routes } from 'react-router-dom'
|
||||
import { ThemeProvider } from '@mui/material/styles'
|
||||
import { theme } from './theme'
|
||||
|
||||
import Login from './components/login'
|
||||
import Header from './components/header'
|
||||
import Home from './components/home'
|
||||
import Drive from './containers/Drive'
|
||||
import Studio from './containers/Studio'
|
||||
import Settings from './containers/Settings'
|
||||
|
||||
@@ -22,11 +21,9 @@ function App() {
|
||||
<ThemeProvider theme={theme}>
|
||||
<HashRouter>
|
||||
<Header />
|
||||
<Switch>
|
||||
<Route path="/">
|
||||
<Login />
|
||||
</Route>
|
||||
</Switch>
|
||||
<Routes>
|
||||
<Route path="*" element={<Login />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
)
|
||||
@@ -36,23 +33,12 @@ function App() {
|
||||
<ThemeProvider theme={theme}>
|
||||
<HashRouter>
|
||||
<Header />
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route exact path="/SASjsDrive">
|
||||
<Drive />
|
||||
</Route>
|
||||
<Route exact path="/SASjsStudio">
|
||||
<Studio />
|
||||
</Route>
|
||||
<Route exact path="/SASjsSettings">
|
||||
<Settings />
|
||||
</Route>
|
||||
<Route exact path="/SASjsLogon">
|
||||
<AuthCode />
|
||||
</Route>
|
||||
</Switch>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/SASjsStudio" element={<Studio />} />
|
||||
<Route path="/SASjsSettings" element={<Settings />} />
|
||||
<Route path="/SASjsLogon" element={<AuthCode />} />
|
||||
</Routes>
|
||||
<ToastContainer />
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -18,22 +18,27 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
}
|
||||
}))
|
||||
|
||||
type DeleteModalProps = {
|
||||
type DeleteConfirmationModalProps = {
|
||||
open: boolean
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
deletePermission: () => void
|
||||
message: string
|
||||
_delete: () => void
|
||||
}
|
||||
|
||||
const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => {
|
||||
const DeleteConfirmationModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
message,
|
||||
_delete
|
||||
}: DeleteConfirmationModalProps) => {
|
||||
return (
|
||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
||||
<DialogContent dividers>
|
||||
<Typography gutterBottom>
|
||||
Are you sure you want to delete this permission?
|
||||
</Typography>
|
||||
<Typography gutterBottom>{message}</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button color="error" onClick={() => deletePermission()}>
|
||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||
<Button color="error" onClick={() => _delete()}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
@@ -41,4 +46,4 @@ const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteModal
|
||||
export default DeleteConfirmationModal
|
||||
83
web/src/components/filePathInputModal.tsx
Normal file
83
web/src/components/filePathInputModal.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
|
||||
|
||||
import { BootstrapDialogTitle } from './dialogTitle'
|
||||
import { BootstrapDialog } from './modal'
|
||||
|
||||
type FilePathInputModalProps = {
|
||||
open: boolean
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
saveFile: (filePath: string) => void
|
||||
}
|
||||
|
||||
const FilePathInputModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
saveFile
|
||||
}: FilePathInputModalProps) => {
|
||||
const [filePath, setFilePath] = useState('')
|
||||
const [hasError, setHasError] = useState(false)
|
||||
const [errorText, setErrorText] = useState('')
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value
|
||||
|
||||
const specialChars = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>?~]/
|
||||
const fileExtension = /\.(exe|sh|htaccess)$/i
|
||||
|
||||
if (specialChars.test(value)) {
|
||||
setHasError(true)
|
||||
setErrorText('can not have special characters')
|
||||
} else if (fileExtension.test(value)) {
|
||||
setHasError(true)
|
||||
setErrorText('can not save file with extensions [exe, sh, htaccess]')
|
||||
} else {
|
||||
setHasError(false)
|
||||
setErrorText('')
|
||||
}
|
||||
setFilePath(value)
|
||||
}
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
if (hasError || !filePath) return
|
||||
saveFile(filePath)
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||
Save File
|
||||
</BootstrapDialogTitle>
|
||||
<DialogContent dividers>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
autoFocus
|
||||
variant="outlined"
|
||||
label="File Path"
|
||||
value={filePath}
|
||||
onChange={handleChange}
|
||||
error={hasError}
|
||||
helperText={errorText}
|
||||
/>
|
||||
</form>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => saveFile(filePath)}
|
||||
disabled={hasError || !filePath}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</BootstrapDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilePathInputModal
|
||||
@@ -1,17 +1,19 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
Box,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button,
|
||||
Menu,
|
||||
MenuItem
|
||||
MenuItem,
|
||||
IconButton,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||
import SettingsIcon from '@mui/icons-material/Settings'
|
||||
import { OpenInNew, Settings, Menu as MenuIcon } from '@mui/icons-material'
|
||||
|
||||
import Username from './username'
|
||||
import { AppContext } from '../context/appContext'
|
||||
@@ -24,37 +26,44 @@ const baseUrl =
|
||||
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
||||
|
||||
const Header = (props: any) => {
|
||||
const history = useHistory()
|
||||
const navigate = useNavigate()
|
||||
const { pathname } = useLocation()
|
||||
const appContext = useContext(AppContext)
|
||||
const [tabValue, setTabValue] = useState(
|
||||
validTabs.includes(pathname) ? pathname : '/'
|
||||
)
|
||||
const [anchorEl, setAnchorEl] = useState<
|
||||
(EventTarget & HTMLButtonElement) | null
|
||||
>(null)
|
||||
|
||||
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(null)
|
||||
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
|
||||
null
|
||||
)
|
||||
|
||||
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorElNav(event.currentTarget)
|
||||
}
|
||||
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorElUser(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleCloseNavMenu = () => {
|
||||
setAnchorElNav(null)
|
||||
}
|
||||
|
||||
const handleCloseUserMenu = () => {
|
||||
setAnchorElUser(null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTabValue(validTabs.includes(pathname) ? pathname : '/')
|
||||
}, [pathname])
|
||||
|
||||
const handleMenu = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
||||
setTabValue(value)
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
if (appContext.logout) {
|
||||
handleClose()
|
||||
handleCloseUserMenu()
|
||||
appContext.logout()
|
||||
}
|
||||
}
|
||||
@@ -64,60 +73,129 @@ const Header = (props: any) => {
|
||||
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
>
|
||||
<Toolbar variant="dense">
|
||||
<img
|
||||
src="logo.png"
|
||||
alt="logo"
|
||||
style={{
|
||||
width: '35px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '25px'
|
||||
}}
|
||||
onClick={() => {
|
||||
setTabValue('/')
|
||||
history.push('/')
|
||||
}}
|
||||
/>
|
||||
<Tabs
|
||||
indicatorColor="secondary"
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
>
|
||||
<Tab label="Home" value="/" to="/" component={Link} />
|
||||
<Tab
|
||||
label="Drive"
|
||||
value="/SASjsDrive"
|
||||
to="/SASjsDrive"
|
||||
component={Link}
|
||||
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
||||
<img
|
||||
src="logo.png"
|
||||
alt="logo"
|
||||
style={{
|
||||
width: '35px',
|
||||
height: '35px',
|
||||
marginTop: '9px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '25px'
|
||||
}}
|
||||
onClick={() => {
|
||||
setTabValue('/')
|
||||
navigate('/')
|
||||
}}
|
||||
/>
|
||||
<Tab
|
||||
label="Studio"
|
||||
value="/SASjsStudio"
|
||||
to="/SASjsStudio"
|
||||
component={Link}
|
||||
<Tabs
|
||||
indicatorColor="secondary"
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
>
|
||||
<Tab label="Home" value="/" to="/" component={Link} />
|
||||
<Tab
|
||||
label="Studio"
|
||||
value="/SASjsStudio"
|
||||
to="/SASjsStudio"
|
||||
component={Link}
|
||||
/>
|
||||
</Tabs>
|
||||
<Button
|
||||
href={`${baseUrl}/AppStream`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
endIcon={<OpenInNew />}
|
||||
>
|
||||
Apps
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}>
|
||||
<IconButton size="large" onClick={handleOpenNavMenu} color="inherit">
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElNav}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
open={!!anchorElNav}
|
||||
onClose={handleCloseNavMenu}
|
||||
sx={{
|
||||
display: { xs: 'block', md: 'none' }
|
||||
}}
|
||||
>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/"
|
||||
onClick={handleCloseNavMenu}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Home
|
||||
</Button>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/SASjsStudio"
|
||||
onClick={handleCloseNavMenu}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
Studio
|
||||
</Button>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
href={`${baseUrl}/AppStream`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={handleCloseNavMenu}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
endIcon={<OpenInNew />}
|
||||
>
|
||||
Apps
|
||||
</Button>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: { xs: 'flex', md: 'none' } }}>
|
||||
<img
|
||||
src="logo.png"
|
||||
alt="logo"
|
||||
style={{
|
||||
width: '35px',
|
||||
height: '35px',
|
||||
marginTop: '2px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '25px'
|
||||
}}
|
||||
onClick={() => {
|
||||
setTabValue('/')
|
||||
navigate('/')
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
<Button
|
||||
href={`${baseUrl}/SASjsApi`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
API Docs
|
||||
</Button>
|
||||
<Button
|
||||
href={`${baseUrl}/AppStream`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
App Stream
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -127,11 +205,11 @@ const Header = (props: any) => {
|
||||
>
|
||||
<Username
|
||||
username={appContext.displayName || appContext.username}
|
||||
onClickHandler={handleMenu}
|
||||
onClickHandler={handleOpenUserMenu}
|
||||
/>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorEl={anchorElUser}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center'
|
||||
@@ -141,38 +219,70 @@ const Header = (props: any) => {
|
||||
vertical: 'top',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
open={!!anchorEl}
|
||||
onClose={handleClose}
|
||||
open={!!anchorElUser}
|
||||
onClose={handleCloseUserMenu}
|
||||
>
|
||||
{appContext.loggedIn && (
|
||||
<MenuItem
|
||||
sx={{ justifyContent: 'center', display: { md: 'none' } }}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
sx={{ border: '1px solid black', padding: '5px' }}
|
||||
>
|
||||
{appContext.displayName || appContext.username}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/SASjsSettings"
|
||||
onClick={handleCloseUserMenu}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Settings />}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
href={'https://server.sasjs.io'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
color="primary"
|
||||
endIcon={<OpenInNew />}
|
||||
>
|
||||
Documentation
|
||||
Docs
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/SASjsSettings"
|
||||
onClick={handleClose}
|
||||
href={`${baseUrl}/SASjsApi`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<SettingsIcon />}
|
||||
size="large"
|
||||
endIcon={<OpenInNew />}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||
<Button variant="contained" color="primary">
|
||||
Logout
|
||||
API
|
||||
</Button>
|
||||
</MenuItem>
|
||||
{appContext.loggedIn && (
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
sx={{ justifyContent: 'center' }}
|
||||
>
|
||||
<Button variant="contained" color="primary">
|
||||
Logout
|
||||
</Button>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
</Toolbar>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { styled } from '@mui/material/styles'
|
||||
|
||||
import { BootstrapDialogTitle } from './dialogTitle'
|
||||
|
||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
export const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
@@ -14,7 +14,7 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
}
|
||||
}))
|
||||
|
||||
export interface ModalProps {
|
||||
type ModalProps = {
|
||||
open: boolean
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
title: string
|
||||
|
||||
109
web/src/components/nameInputModal.tsx
Normal file
109
web/src/components/nameInputModal.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
|
||||
|
||||
import { BootstrapDialogTitle } from './dialogTitle'
|
||||
import { BootstrapDialog } from './modal'
|
||||
|
||||
type NameInputModalProps = {
|
||||
open: boolean
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
title: string
|
||||
isFolder: boolean
|
||||
actionLabel: string
|
||||
action: (name: string) => void
|
||||
defaultName?: string
|
||||
}
|
||||
|
||||
const NameInputModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
title,
|
||||
isFolder,
|
||||
actionLabel,
|
||||
action,
|
||||
defaultName
|
||||
}: NameInputModalProps) => {
|
||||
const [name, setName] = useState('')
|
||||
const [hasError, setHasError] = useState(false)
|
||||
const [errorText, setErrorText] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultName) setName(defaultName)
|
||||
}, [defaultName])
|
||||
|
||||
const handleFocus = (
|
||||
event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement, Element>
|
||||
) => {
|
||||
if (defaultName) {
|
||||
event.target.select()
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value
|
||||
|
||||
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
|
||||
const fileNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>/?~]/
|
||||
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
|
||||
|
||||
const specialChars = isFolder ? folderNameRegex : fileNameRegex
|
||||
|
||||
if (specialChars.test(value)) {
|
||||
setHasError(true)
|
||||
setErrorText('can not have special characters')
|
||||
} else if (!isFolder && fileNameExtensionRegex.test(value)) {
|
||||
setHasError(true)
|
||||
setErrorText('can not add file with extensions [exe, sh, htaccess]')
|
||||
} else {
|
||||
setHasError(false)
|
||||
setErrorText('')
|
||||
}
|
||||
|
||||
setName(value)
|
||||
}
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
if (hasError || !name) return
|
||||
action(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||
{title}
|
||||
</BootstrapDialogTitle>
|
||||
<DialogContent dividers>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
id="input-box"
|
||||
fullWidth
|
||||
autoFocus
|
||||
onFocus={handleFocus}
|
||||
variant="outlined"
|
||||
label={isFolder ? 'Folder Name' : 'File Name'}
|
||||
value={name}
|
||||
onChange={handleChange}
|
||||
error={hasError}
|
||||
helperText={errorText}
|
||||
/>
|
||||
</form>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => action(name)}
|
||||
disabled={hasError || !name}
|
||||
>
|
||||
{actionLabel}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</BootstrapDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default NameInputModal
|
||||
247
web/src/components/tree.tsx
Normal file
247
web/src/components/tree.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Menu, MenuItem } from '@mui/material'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||
|
||||
import DeleteConfirmationModal from './deleteConfirmationModal'
|
||||
import NameInputModal from './nameInputModal'
|
||||
|
||||
import { TreeNode } from '../utils/types'
|
||||
|
||||
type Props = {
|
||||
node: TreeNode
|
||||
selectedFilePath: string
|
||||
handleSelect: (filePath: string) => void
|
||||
deleteNode: (path: string, isFolder: boolean) => void
|
||||
addFile: (path: string) => void
|
||||
addFolder: (path: string) => void
|
||||
rename: (oldPath: string, newPath: string) => void
|
||||
defaultExpanded?: string[]
|
||||
}
|
||||
|
||||
const TreeView = ({
|
||||
node,
|
||||
selectedFilePath,
|
||||
handleSelect,
|
||||
deleteNode,
|
||||
addFile,
|
||||
addFolder,
|
||||
rename,
|
||||
defaultExpanded
|
||||
}: Props) => {
|
||||
return (
|
||||
<ul
|
||||
style={{
|
||||
listStyle: 'none',
|
||||
padding: '0.25rem 0.85rem',
|
||||
width: 'max-content'
|
||||
}}
|
||||
>
|
||||
<TreeViewNode
|
||||
node={node}
|
||||
selectedFilePath={selectedFilePath}
|
||||
handleSelect={handleSelect}
|
||||
deleteNode={deleteNode}
|
||||
addFile={addFile}
|
||||
addFolder={addFolder}
|
||||
rename={rename}
|
||||
defaultExpanded={defaultExpanded}
|
||||
/>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default TreeView
|
||||
|
||||
const TreeViewNode = ({
|
||||
node,
|
||||
selectedFilePath,
|
||||
handleSelect,
|
||||
deleteNode,
|
||||
addFile,
|
||||
addFolder,
|
||||
rename,
|
||||
defaultExpanded
|
||||
}: Props) => {
|
||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||
useState(false)
|
||||
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
||||
useState('')
|
||||
const [defaultInputModalName, setDefaultInputModalName] = useState('')
|
||||
const [nameInputModalOpen, setNameInputModalOpen] = useState(false)
|
||||
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
||||
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
||||
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
||||
const [childVisible, setChildVisibility] = useState(false)
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
mouseX: number
|
||||
mouseY: number
|
||||
} | null>(null)
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setContextMenu(
|
||||
contextMenu === null
|
||||
? {
|
||||
mouseX: event.clientX + 2,
|
||||
mouseY: event.clientY - 6
|
||||
}
|
||||
: null
|
||||
)
|
||||
}
|
||||
|
||||
const hasChild = node.children.length ? true : false
|
||||
|
||||
const handleItemClick = () => {
|
||||
if (node.children.length) {
|
||||
setChildVisibility((v) => !v)
|
||||
return
|
||||
}
|
||||
|
||||
handleSelect(node.relativePath)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultExpanded && defaultExpanded[0] === node.relativePath) {
|
||||
setChildVisibility(true)
|
||||
defaultExpanded.shift()
|
||||
}
|
||||
}, [defaultExpanded, node.relativePath])
|
||||
|
||||
const handleDeleteItemClick = () => {
|
||||
setContextMenu(null)
|
||||
setDeleteConfirmationModalOpen(true)
|
||||
setDeleteConfirmationModalMessage(
|
||||
`Are you sure you want to delete ${node.isFolder ? 'folder' : 'file'} "${
|
||||
node.relativePath
|
||||
}"?`
|
||||
)
|
||||
}
|
||||
|
||||
const deleteConfirm = () => {
|
||||
setDeleteConfirmationModalOpen(false)
|
||||
deleteNode(node.relativePath, node.isFolder)
|
||||
}
|
||||
|
||||
const handleNewFolderItemClick = () => {
|
||||
setContextMenu(null)
|
||||
setNameInputModalOpen(true)
|
||||
setNameInputModalTitle('Add Folder')
|
||||
setNameInputModalActionLabel('Add')
|
||||
setNameInputModalForFolder(true)
|
||||
setDefaultInputModalName('')
|
||||
}
|
||||
|
||||
const handleNewFileItemClick = () => {
|
||||
setContextMenu(null)
|
||||
setNameInputModalOpen(true)
|
||||
setNameInputModalTitle('Add File')
|
||||
setNameInputModalActionLabel('Add')
|
||||
setNameInputModalForFolder(false)
|
||||
setDefaultInputModalName('')
|
||||
}
|
||||
|
||||
const addFileFolder = (name: string) => {
|
||||
setNameInputModalOpen(false)
|
||||
const path = node.relativePath + '/' + name
|
||||
if (nameInputModalForFolder) addFolder(path)
|
||||
else addFile(path)
|
||||
}
|
||||
|
||||
const handleRenameItemClick = () => {
|
||||
setContextMenu(null)
|
||||
setNameInputModalOpen(true)
|
||||
setNameInputModalTitle('Rename')
|
||||
setNameInputModalActionLabel('Rename')
|
||||
setNameInputModalForFolder(node.isFolder)
|
||||
setDefaultInputModalName(node.relativePath.split('/').pop() ?? '')
|
||||
}
|
||||
|
||||
const renameFileFolder = (name: string) => {
|
||||
setNameInputModalOpen(false)
|
||||
const oldPath = node.relativePath
|
||||
const splittedPath = node.relativePath.split('/')
|
||||
splittedPath.splice(-1, 1, name)
|
||||
const newPath = splittedPath.join('/')
|
||||
rename(oldPath, newPath)
|
||||
}
|
||||
|
||||
return (
|
||||
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
||||
<li style={{ display: 'list-item' }}>
|
||||
<div
|
||||
className={`tree-item-label ${
|
||||
selectedFilePath === node.relativePath ? 'selected' : ''
|
||||
}`}
|
||||
onClick={() => handleItemClick()}
|
||||
>
|
||||
{hasChild &&
|
||||
(childVisible ? <ExpandMoreIcon /> : <ChevronRightIcon />)}
|
||||
<div>{node.name}</div>
|
||||
</div>
|
||||
|
||||
{hasChild &&
|
||||
childVisible &&
|
||||
node.children.map((child, index) => (
|
||||
<TreeView
|
||||
key={node.relativePath + '-' + index}
|
||||
node={child}
|
||||
selectedFilePath={selectedFilePath}
|
||||
handleSelect={handleSelect}
|
||||
deleteNode={deleteNode}
|
||||
addFile={addFile}
|
||||
addFolder={addFolder}
|
||||
rename={rename}
|
||||
defaultExpanded={defaultExpanded}
|
||||
/>
|
||||
))}
|
||||
</li>
|
||||
<DeleteConfirmationModal
|
||||
open={deleteConfirmationModalOpen}
|
||||
setOpen={setDeleteConfirmationModalOpen}
|
||||
message={deleteConfirmationModalMessage}
|
||||
_delete={deleteConfirm}
|
||||
/>
|
||||
<NameInputModal
|
||||
open={nameInputModalOpen}
|
||||
setOpen={setNameInputModalOpen}
|
||||
title={nameInputModalTitle}
|
||||
isFolder={nameInputModalForFolder}
|
||||
actionLabel={nameInputModalActionLabel}
|
||||
action={
|
||||
nameInputModalActionLabel === 'Add' ? addFileFolder : renameFileFolder
|
||||
}
|
||||
defaultName={defaultInputModalName}
|
||||
/>
|
||||
<Menu
|
||||
open={contextMenu !== null}
|
||||
onClose={() => setContextMenu(null)}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
contextMenu !== null
|
||||
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{node.isFolder && (
|
||||
<div>
|
||||
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!node.relativePath}
|
||||
onClick={handleNewFileItemClick}
|
||||
>
|
||||
Add File
|
||||
</MenuItem>
|
||||
</div>
|
||||
)}
|
||||
<MenuItem disabled={!node.relativePath} onClick={handleRenameItemClick}>
|
||||
Rename
|
||||
</MenuItem>
|
||||
<MenuItem disabled={!node.relativePath} onClick={handleDeleteItemClick}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -20,7 +20,14 @@ const Username = (props: any) => {
|
||||
) : (
|
||||
<AccountCircle></AccountCircle>
|
||||
)}
|
||||
<Typography variant="h6" sx={{ color: 'white', padding: '0 8px' }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
color: 'white',
|
||||
padding: '0 8px',
|
||||
display: { xs: 'none', md: 'flex' }
|
||||
}}
|
||||
>
|
||||
{props.username}
|
||||
</Typography>
|
||||
</IconButton>
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import axios from 'axios'
|
||||
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import Box from '@mui/material/Box'
|
||||
|
||||
import SideBar from './sideBar'
|
||||
import Main from './main'
|
||||
|
||||
export interface TreeNode {
|
||||
name: string
|
||||
relativePath: string
|
||||
absolutePath: string
|
||||
children: Array<TreeNode>
|
||||
}
|
||||
|
||||
const Drive = () => {
|
||||
const location = useLocation()
|
||||
const baseUrl = window.location.origin
|
||||
|
||||
const [selectedFilePath, setSelectedFilePath] = useState('')
|
||||
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
||||
|
||||
const setFilePathOnMount = useCallback(() => {
|
||||
const queryParams = new URLSearchParams(location.search)
|
||||
setSelectedFilePath(queryParams.get('filePath') ?? '')
|
||||
}, [location.search])
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(`/SASjsApi/drive/fileTree`)
|
||||
.then((res: any) => {
|
||||
if (res.data && res.data?.status === 'success') {
|
||||
setDirectoryData(res.data.tree)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
setFilePathOnMount()
|
||||
}, [setFilePathOnMount])
|
||||
|
||||
const handleSelect = (node: TreeNode) => {
|
||||
if (node.children.length) return
|
||||
|
||||
if (!node.name.includes('.')) return
|
||||
|
||||
window.history.pushState(
|
||||
'',
|
||||
'',
|
||||
`${baseUrl}/#/SASjsDrive?filePath=${node.relativePath}`
|
||||
)
|
||||
setSelectedFilePath(node.relativePath)
|
||||
}
|
||||
|
||||
const removeFileFromTree = (path: string) => {
|
||||
if (directoryData) {
|
||||
const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode
|
||||
findAndRemoveNode(newTree, newTree, path)
|
||||
setDirectoryData(newTree)
|
||||
}
|
||||
}
|
||||
|
||||
const findAndRemoveNode = (
|
||||
node: TreeNode,
|
||||
parentNode: TreeNode,
|
||||
path: string
|
||||
) => {
|
||||
if (node.relativePath === path) {
|
||||
removeNodeFromParent(parentNode, path)
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(node.children)) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
if (findAndRemoveNode(node.children[i], node, path)) return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeNodeFromParent = (parent: TreeNode, path: string) => {
|
||||
const index = parent.children.findIndex(
|
||||
(node) => node.relativePath === path
|
||||
)
|
||||
if (index !== -1) {
|
||||
parent.children.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
<SideBar
|
||||
selectedFilePath={selectedFilePath}
|
||||
directoryData={directoryData}
|
||||
handleSelect={handleSelect}
|
||||
/>
|
||||
<Main
|
||||
selectedFilePath={selectedFilePath}
|
||||
removeFileFromTree={removeFileFromTree}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Drive
|
||||
@@ -1,173 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import axios from 'axios'
|
||||
|
||||
import Editor from 'react-monaco-editor'
|
||||
|
||||
import Box from '@mui/material/Box'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import Stack from '@mui/material/Stack'
|
||||
import Button from '@mui/material/Button'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
|
||||
type Props = {
|
||||
selectedFilePath: string
|
||||
removeFileFromTree: (path: string) => void
|
||||
}
|
||||
|
||||
const Main = (props: Props) => {
|
||||
const baseUrl = window.location.origin
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [fileContentBeforeEdit, setFileContentBeforeEdit] = useState('')
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.selectedFilePath) {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.get(`/SASjsApi/drive/file?_filePath=${props.selectedFilePath}`)
|
||||
.then((res: any) => {
|
||||
setFileContent(res.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
}, [props.selectedFilePath])
|
||||
|
||||
const handleDeleteBtnClick = () => {
|
||||
setIsLoading(true)
|
||||
|
||||
const filePath = props.selectedFilePath
|
||||
|
||||
axios
|
||||
.delete(`/SASjsApi/drive/file?_filePath=${filePath}`)
|
||||
.then((res) => {
|
||||
setFileContent('')
|
||||
props.removeFileFromTree(filePath)
|
||||
window.history.pushState('', '', `${baseUrl}/#/SASjsDrive`)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleEditSaveBtnClick = () => {
|
||||
if (!editMode) {
|
||||
setFileContentBeforeEdit(fileContent)
|
||||
setEditMode(true)
|
||||
} else {
|
||||
setIsLoading(true)
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||
formData.append('file', stringBlob, 'filename.sas')
|
||||
formData.append('filePath', props.selectedFilePath)
|
||||
|
||||
axios
|
||||
.patch(`/SASjsApi/drive/file`, formData)
|
||||
.then((res) => {
|
||||
setEditMode(false)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancelExecuteBtnClick = () => {
|
||||
if (editMode) {
|
||||
setFileContent(fileContentBeforeEdit)
|
||||
setEditMode(false)
|
||||
} else {
|
||||
window.open(
|
||||
`${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||
<Toolbar />
|
||||
<Paper
|
||||
sx={{
|
||||
height: '75vh',
|
||||
padding: '10px',
|
||||
overflow: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
{isLoading && (
|
||||
<CircularProgress
|
||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && props?.selectedFilePath && !editMode && (
|
||||
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
|
||||
)}
|
||||
{!isLoading && props?.selectedFilePath && editMode && (
|
||||
<Editor
|
||||
height="95%"
|
||||
language="sas"
|
||||
value={fileContent}
|
||||
onChange={(val) => {
|
||||
if (val) setFileContent(val)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
<Stack
|
||||
spacing={3}
|
||||
direction="row"
|
||||
sx={{ justifyContent: 'center', marginTop: '20px' }}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleDeleteBtnClick}
|
||||
disabled={isLoading || !props?.selectedFilePath}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleEditSaveBtnClick}
|
||||
disabled={isLoading || !props?.selectedFilePath}
|
||||
>
|
||||
{!editMode ? 'Edit' : 'Save'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleCancelExecuteBtnClick}
|
||||
disabled={isLoading || !props?.selectedFilePath}
|
||||
>
|
||||
{editMode ? 'Cancel' : 'Execute'}
|
||||
</Button>
|
||||
{props?.selectedFilePath && (
|
||||
<Button
|
||||
variant="contained"
|
||||
component={Link}
|
||||
to={`/SASjsStudio?_program=${props.selectedFilePath}`}
|
||||
>
|
||||
Open in Studio
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Main
|
||||
@@ -1,100 +0,0 @@
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { makeStyles } from '@mui/styles'
|
||||
|
||||
import Box from '@mui/material/Box'
|
||||
import Drawer from '@mui/material/Drawer'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import ListItemText from '@mui/material/ListItemText'
|
||||
|
||||
import TreeView from '@mui/lab/TreeView'
|
||||
import TreeItem from '@mui/lab/TreeItem'
|
||||
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||
|
||||
import { TreeNode } from '.'
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
'& .MuiTreeItem-content': {
|
||||
width: 'auto'
|
||||
}
|
||||
},
|
||||
listItem: {
|
||||
padding: 0
|
||||
}
|
||||
}))
|
||||
|
||||
const drawerWidth = 240
|
||||
|
||||
type Props = {
|
||||
selectedFilePath: string
|
||||
directoryData: TreeNode | null
|
||||
handleSelect: (node: TreeNode) => void
|
||||
}
|
||||
|
||||
const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const defaultExpanded = useMemo(() => {
|
||||
const splittedPath = selectedFilePath.split('/')
|
||||
const arr = ['']
|
||||
let nodeId = ''
|
||||
splittedPath.forEach((path) => {
|
||||
if (path !== '') {
|
||||
nodeId += '/' + path
|
||||
arr.push(nodeId)
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}, [selectedFilePath])
|
||||
|
||||
const renderTree = (nodes: TreeNode) => (
|
||||
<TreeItem
|
||||
classes={{ root: classes.root }}
|
||||
key={nodes.relativePath}
|
||||
nodeId={nodes.relativePath}
|
||||
label={
|
||||
<ListItem
|
||||
className={classes.listItem}
|
||||
onClick={() => handleSelect(nodes)}
|
||||
>
|
||||
<ListItemText primary={nodes.name} />
|
||||
</ListItem>
|
||||
}
|
||||
>
|
||||
{Array.isArray(nodes.children)
|
||||
? nodes.children.map((node) => renderTree(node))
|
||||
: null}
|
||||
</TreeItem>
|
||||
)
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
{directoryData && (
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
defaultExpanded={defaultExpanded}
|
||||
selected={defaultExpanded.slice(-1)}
|
||||
>
|
||||
{renderTree(directoryData)}
|
||||
</TreeView>
|
||||
)}
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default SideBar
|
||||
@@ -32,7 +32,13 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
type AddPermissionModalProps = {
|
||||
open: boolean
|
||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||
addPermission: (addPermissionPayload: RegisterPermissionPayload) => void
|
||||
addPermission: (
|
||||
permissions: RegisterPermissionPayload[],
|
||||
permissionType: string,
|
||||
principalType: string,
|
||||
principal: string,
|
||||
permissionSetting: string
|
||||
) => void
|
||||
}
|
||||
|
||||
const AddPermissionModal = ({
|
||||
@@ -40,10 +46,11 @@ const AddPermissionModal = ({
|
||||
handleOpen,
|
||||
addPermission
|
||||
}: AddPermissionModalProps) => {
|
||||
const [URIs, setURIs] = useState<string[]>([])
|
||||
const [loadingURIs, setLoadingURIs] = useState(false)
|
||||
const [uri, setUri] = useState<string>()
|
||||
const [principalType, setPrincipalType] = useState('user')
|
||||
const [paths, setPaths] = useState<string[]>([])
|
||||
const [loadingPaths, setLoadingPaths] = useState(false)
|
||||
const [selectedPaths, setSelectedPaths] = useState<string[]>([])
|
||||
const [permissionType, setPermissionType] = useState('Route')
|
||||
const [principalType, setPrincipalType] = useState('Group')
|
||||
const [userPrincipal, setUserPrincipal] = useState<UserResponse>()
|
||||
const [groupPrincipal, setGroupPrincipal] = useState<GroupResponse>()
|
||||
const [permissionSetting, setPermissionSetting] = useState('Grant')
|
||||
@@ -52,29 +59,29 @@ const AddPermissionModal = ({
|
||||
const [groupPrincipals, setGroupPrincipals] = useState<GroupResponse[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingURIs(true)
|
||||
setLoadingPaths(true)
|
||||
axios
|
||||
.get('/SASjsApi/info/authorizedRoutes')
|
||||
.then((res: any) => {
|
||||
if (res.data) {
|
||||
setURIs(res.data.URIs)
|
||||
setPaths(res.data.paths)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingURIs(false)
|
||||
setLoadingPaths(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLoadingPrincipals(true)
|
||||
axios
|
||||
.get(`/SASjsApi/${principalType}`)
|
||||
.get(`/SASjsApi/${principalType.toLowerCase()}`)
|
||||
.then((res: any) => {
|
||||
if (res.data) {
|
||||
if (principalType === 'user') {
|
||||
if (principalType.toLowerCase() === 'user') {
|
||||
const users: UserResponse[] = res.data
|
||||
const nonAdminUsers = users.filter((user) => !user.isAdmin)
|
||||
setUserPrincipals(nonAdminUsers)
|
||||
@@ -92,21 +99,40 @@ const AddPermissionModal = ({
|
||||
}, [principalType])
|
||||
|
||||
const handleAddPermission = () => {
|
||||
const addPermissionPayload: any = {
|
||||
uri,
|
||||
setting: permissionSetting,
|
||||
principalType
|
||||
}
|
||||
if (principalType === 'user' && userPrincipal) {
|
||||
addPermissionPayload.principalId = userPrincipal.id
|
||||
} else if (principalType === 'group' && groupPrincipal) {
|
||||
addPermissionPayload.principalId = groupPrincipal.groupId
|
||||
}
|
||||
addPermission(addPermissionPayload)
|
||||
const permissions: RegisterPermissionPayload[] = []
|
||||
|
||||
selectedPaths.forEach((path) => {
|
||||
const addPermissionPayload: any = {
|
||||
path,
|
||||
type: permissionType,
|
||||
setting: permissionSetting,
|
||||
principalType: principalType.toLowerCase(),
|
||||
principalId:
|
||||
principalType.toLowerCase() === 'user'
|
||||
? userPrincipal?.id
|
||||
: groupPrincipal?.groupId
|
||||
}
|
||||
|
||||
permissions.push(addPermissionPayload)
|
||||
})
|
||||
|
||||
const principal =
|
||||
principalType.toLowerCase() === 'user'
|
||||
? userPrincipal?.username
|
||||
: groupPrincipal?.name
|
||||
|
||||
addPermission(
|
||||
permissions,
|
||||
permissionType,
|
||||
principalType,
|
||||
principal!,
|
||||
permissionSetting
|
||||
)
|
||||
}
|
||||
|
||||
const addButtonDisabled =
|
||||
!uri || (principalType === 'user' ? !userPrincipal : !groupPrincipal)
|
||||
!selectedPaths.length ||
|
||||
(principalType.toLowerCase() === 'user' ? !userPrincipal : !groupPrincipal)
|
||||
|
||||
return (
|
||||
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
||||
@@ -120,22 +146,37 @@ const AddPermissionModal = ({
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
options={URIs}
|
||||
multiple
|
||||
disableClearable
|
||||
value={uri}
|
||||
onChange={(event: any, newValue: string) => setUri(newValue)}
|
||||
options={paths}
|
||||
filterSelectedOptions
|
||||
value={selectedPaths}
|
||||
onChange={(event: any, newValue: string[]) => {
|
||||
setSelectedPaths(newValue)
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} label="Paths" />}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
options={['Route']}
|
||||
disableClearable
|
||||
value={permissionType}
|
||||
onChange={(event: any, newValue: string) =>
|
||||
setPermissionType(newValue)
|
||||
}
|
||||
renderInput={(params) =>
|
||||
loadingURIs ? (
|
||||
loadingPaths ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<TextField {...params} label="Principal" />
|
||||
<TextField {...params} label="Permission Type" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
options={['user', 'group']}
|
||||
options={['Group', 'User']}
|
||||
disableClearable
|
||||
value={principalType}
|
||||
onChange={(event: any, newValue: string) =>
|
||||
@@ -147,7 +188,7 @@ const AddPermissionModal = ({
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
{principalType === 'user' ? (
|
||||
{principalType.toLowerCase() === 'user' ? (
|
||||
<Autocomplete
|
||||
options={userPrincipals}
|
||||
getOptionLabel={(option) => option.displayName}
|
||||
|
||||
120
web/src/containers/Settings/addPermissionResponseModal.tsx
Normal file
120
web/src/containers/Settings/addPermissionResponseModal.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Typography, DialogContent } from '@mui/material'
|
||||
|
||||
import { BootstrapDialog } from '../../components/modal'
|
||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
||||
import { PermissionResponse } from '../../utils/types'
|
||||
|
||||
export interface PermissionResponsePayload {
|
||||
permissionType: string
|
||||
principalType: string
|
||||
principal: string
|
||||
permissionSetting: string
|
||||
existingPermissions: PermissionResponse[]
|
||||
newAddedPermissions: PermissionResponse[]
|
||||
updatedPermissions: PermissionResponse[]
|
||||
errorPaths: string[]
|
||||
}
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
payload: PermissionResponsePayload
|
||||
}
|
||||
|
||||
const PermissionResponseModal = ({ open, setOpen, payload }: Props) => {
|
||||
const newAddedPermissionsLength = payload.newAddedPermissions.length
|
||||
const updatedPermissionsLength = payload.updatedPermissions.length
|
||||
const existingPermissionsLength = payload.existingPermissions.length
|
||||
const appliedPermissionsLength =
|
||||
newAddedPermissionsLength + updatedPermissionsLength
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
||||
<BootstrapDialogTitle
|
||||
id="permission-response-modal"
|
||||
handleOpen={setOpen}
|
||||
>
|
||||
Permission Response
|
||||
</BootstrapDialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography sx={{ fontWeight: 'bold', marginBottom: '15px' }}>
|
||||
{`${appliedPermissionsLength} "${payload.permissionSetting}", "${
|
||||
payload.permissionType
|
||||
}", "${payload.principalType}", "${payload.principal}" ${
|
||||
appliedPermissionsLength > 1 ? 'Rules' : 'Rule'
|
||||
}`}{' '}
|
||||
Applied:
|
||||
</Typography>
|
||||
|
||||
{newAddedPermissionsLength > 0 && (
|
||||
<>
|
||||
<Typography>
|
||||
{`${newAddedPermissionsLength} ${
|
||||
newAddedPermissionsLength > 1 ? 'Rules' : 'Rule'
|
||||
}`}{' '}
|
||||
Added:
|
||||
</Typography>
|
||||
<ul>
|
||||
{payload.newAddedPermissions.map((permission, index) => (
|
||||
<li key={index}>{permission.path}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{updatedPermissionsLength > 0 && (
|
||||
<>
|
||||
<Typography>
|
||||
{` ${updatedPermissionsLength} ${
|
||||
updatedPermissionsLength > 1 ? 'Rules' : 'Rule'
|
||||
}`}{' '}
|
||||
Updated:
|
||||
</Typography>
|
||||
<ul>
|
||||
{payload.updatedPermissions.map((permission, index) => (
|
||||
<li key={index}>{permission.path}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{existingPermissionsLength > 0 && (
|
||||
<>
|
||||
<Typography>
|
||||
{`${existingPermissionsLength} ${
|
||||
existingPermissionsLength > 1 ? 'Rules' : 'Rule'
|
||||
}`}{' '}
|
||||
Unchanged:
|
||||
</Typography>
|
||||
<ul>
|
||||
{payload.existingPermissions.map((permission, index) => (
|
||||
<li key={index}>{permission.path}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{payload.errorPaths.length > 0 && (
|
||||
<>
|
||||
<Typography style={{ color: 'red', marginTop: '10px' }}>
|
||||
Errors occurred for following paths:
|
||||
</Typography>
|
||||
<ul>
|
||||
{payload.errorPaths.map((path, index) => (
|
||||
<li key={index}>
|
||||
<Typography>{path}</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</BootstrapDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PermissionResponseModal
|
||||
@@ -31,11 +31,20 @@ const Settings = () => {
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
marginTop: '65px'
|
||||
}}
|
||||
>
|
||||
<TabContext value={value}>
|
||||
<Box component={Paper} sx={{ margin: '0 5px', height: '92vh' }}>
|
||||
<Box
|
||||
component={Paper}
|
||||
sx={{
|
||||
margin: '0 5px',
|
||||
height: { md: '92vh' },
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<TabList
|
||||
TabIndicatorProps={{
|
||||
style: {
|
||||
@@ -47,7 +56,7 @@ const Settings = () => {
|
||||
>
|
||||
<StyledTab label="Profile" value="profile" />
|
||||
{appContext.mode === ModeType.Server && (
|
||||
<StyledTab label="Uri Access" value="permission" />
|
||||
<StyledTab label="Permissions" value="permission" />
|
||||
)}
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
@@ -27,8 +27,11 @@ import { styled } from '@mui/material/styles'
|
||||
import Modal from '../../components/modal'
|
||||
import PermissionFilterModal from './permissionFilterModal'
|
||||
import AddPermissionModal from './addPermissionModal'
|
||||
import PermissionResponseModal, {
|
||||
PermissionResponsePayload
|
||||
} from './addPermissionResponseModal'
|
||||
import UpdatePermissionModal from './updatePermissionModal'
|
||||
import DeleteModal from './deletePermissionModal'
|
||||
import DeleteConfirmationModal from '../../components/deleteConfirmationModal'
|
||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||
|
||||
import {
|
||||
@@ -36,12 +39,23 @@ import {
|
||||
PermissionResponse,
|
||||
RegisterPermissionPayload
|
||||
} from '../../utils/types'
|
||||
import {
|
||||
findExistingPermission,
|
||||
findUpdatingPermission
|
||||
} from '../../utils/helper'
|
||||
|
||||
import { AppContext } from '../../context/appContext'
|
||||
|
||||
const BootstrapTableCell = styled(TableCell)({
|
||||
textAlign: 'left'
|
||||
})
|
||||
|
||||
const BootstrapGridItem = styled(Grid)({
|
||||
'&.MuiGrid-item': {
|
||||
maxWidth: '100%'
|
||||
}
|
||||
})
|
||||
|
||||
export enum PrincipalType {
|
||||
User = 'User',
|
||||
Group = 'Group'
|
||||
@@ -59,13 +73,30 @@ const Permission = () => {
|
||||
AlertSeverityType.Success
|
||||
)
|
||||
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
||||
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
|
||||
useState(false)
|
||||
const [permissionResponsePayload, setPermissionResponsePayload] =
|
||||
useState<PermissionResponsePayload>({
|
||||
permissionType: '',
|
||||
principalType: '',
|
||||
principal: '',
|
||||
permissionSetting: '',
|
||||
existingPermissions: [],
|
||||
newAddedPermissions: [],
|
||||
updatedPermissions: [],
|
||||
errorPaths: []
|
||||
})
|
||||
|
||||
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
||||
useState(false)
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||
useState(false)
|
||||
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
||||
useState('')
|
||||
const [selectedPermission, setSelectedPermission] =
|
||||
useState<PermissionResponse>()
|
||||
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||
const [uriFilter, setUriFilter] = useState<string[]>([])
|
||||
const [pathFilter, setPathFilter] = useState<string[]>([])
|
||||
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
||||
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
||||
PrincipalType[]
|
||||
@@ -108,8 +139,10 @@ const Permission = () => {
|
||||
setFilterModalOpen(false)
|
||||
|
||||
const uriFilteredPermissions =
|
||||
uriFilter.length > 0
|
||||
? permissions.filter((permission) => uriFilter.includes(permission.uri))
|
||||
pathFilter.length > 0
|
||||
? permissions.filter((permission) =>
|
||||
pathFilter.includes(permission.path)
|
||||
)
|
||||
: permissions
|
||||
|
||||
const principalFilteredPermissions =
|
||||
@@ -169,36 +202,84 @@ const Permission = () => {
|
||||
|
||||
const resetFilter = () => {
|
||||
setFilterModalOpen(false)
|
||||
setUriFilter([])
|
||||
setPathFilter([])
|
||||
setPrincipalFilter([])
|
||||
setSettingFilter([])
|
||||
setFilteredPermissions([])
|
||||
setFilterApplied(false)
|
||||
}
|
||||
|
||||
const addPermission = (addPermissionPayload: RegisterPermissionPayload) => {
|
||||
const addPermission = async (
|
||||
permissionsToAdd: RegisterPermissionPayload[],
|
||||
permissionType: string,
|
||||
principalType: string,
|
||||
principal: string,
|
||||
permissionSetting: string
|
||||
) => {
|
||||
setAddPermissionModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post('/SASjsApi/permission', addPermissionPayload)
|
||||
.then((res: any) => {
|
||||
fetchPermissions()
|
||||
setSnackbarMessage('Permission added!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
|
||||
const newAddedPermissions: PermissionResponse[] = []
|
||||
const updatedPermissions: PermissionResponse[] = []
|
||||
const errorPaths: string[] = []
|
||||
|
||||
const existingPermissions: PermissionResponse[] = []
|
||||
const updatingPermissions: PermissionResponse[] = []
|
||||
const newPermissions: RegisterPermissionPayload[] = []
|
||||
|
||||
permissionsToAdd.forEach((permission) => {
|
||||
const existingPermission = findExistingPermission(permissions, permission)
|
||||
if (existingPermission) {
|
||||
existingPermissions.push(existingPermission)
|
||||
return
|
||||
}
|
||||
|
||||
const updatingPermission = findUpdatingPermission(permissions, permission)
|
||||
if (updatingPermission) {
|
||||
updatingPermissions.push(updatingPermission)
|
||||
return
|
||||
}
|
||||
|
||||
newPermissions.push(permission)
|
||||
})
|
||||
|
||||
for (const permission of newPermissions) {
|
||||
await axios
|
||||
.post('/SASjsApi/permission', permission)
|
||||
.then((res) => {
|
||||
newAddedPermissions.push(res.data)
|
||||
})
|
||||
.catch((error) => {
|
||||
errorPaths.push(permission.path)
|
||||
})
|
||||
}
|
||||
|
||||
for (const permission of updatingPermissions) {
|
||||
await axios
|
||||
.patch(`/SASjsApi/permission/${permission.permissionId}`, {
|
||||
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
|
||||
})
|
||||
.then((res) => {
|
||||
updatedPermissions.push(res.data)
|
||||
})
|
||||
.catch((error) => {
|
||||
errorPaths.push(permission.path)
|
||||
})
|
||||
}
|
||||
|
||||
fetchPermissions()
|
||||
setIsLoading(false)
|
||||
setPermissionResponsePayload({
|
||||
permissionType,
|
||||
principalType,
|
||||
principal,
|
||||
permissionSetting,
|
||||
existingPermissions,
|
||||
updatedPermissions,
|
||||
newAddedPermissions,
|
||||
errorPaths
|
||||
})
|
||||
setOpenPermissionResponseModal(true)
|
||||
}
|
||||
|
||||
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
||||
@@ -236,11 +317,14 @@ const Permission = () => {
|
||||
|
||||
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
||||
setSelectedPermission(permission)
|
||||
setDeleteModalOpen(true)
|
||||
setDeleteConfirmationModalOpen(true)
|
||||
setDeleteConfirmationModalMessage(
|
||||
'Are you sure you want to delete this permission?'
|
||||
)
|
||||
}
|
||||
|
||||
const deletePermission = () => {
|
||||
setDeleteModalOpen(false)
|
||||
setDeleteConfirmationModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
||||
@@ -272,11 +356,11 @@ const Permission = () => {
|
||||
) : (
|
||||
<Box className="permissions-page">
|
||||
<Grid container direction="column" spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<BootstrapGridItem item xs={12}>
|
||||
<Paper elevation={3} sx={{ display: 'flex' }}>
|
||||
<Tooltip title="Filter Permissions">
|
||||
<IconButton>
|
||||
<FilterListIcon onClick={() => setFilterModalOpen(true)} />
|
||||
<IconButton onClick={() => setFilterModalOpen(true)}>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{appContext.isAdmin && (
|
||||
@@ -291,14 +375,14 @@ const Permission = () => {
|
||||
</Tooltip>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
</BootstrapGridItem>
|
||||
<BootstrapGridItem item xs={12}>
|
||||
<PermissionTable
|
||||
permissions={filterApplied ? filteredPermissions : permissions}
|
||||
handleUpdatePermissionClick={handleUpdatePermissionClick}
|
||||
handleDeletePermissionClick={handleDeletePermissionClick}
|
||||
/>
|
||||
</Grid>
|
||||
</BootstrapGridItem>
|
||||
</Grid>
|
||||
<BootstrapSnackbar
|
||||
open={openSnackbar}
|
||||
@@ -316,8 +400,8 @@ const Permission = () => {
|
||||
open={filterModalOpen}
|
||||
handleOpen={setFilterModalOpen}
|
||||
permissions={permissions}
|
||||
uriFilter={uriFilter}
|
||||
setUriFilter={setUriFilter}
|
||||
pathFilter={pathFilter}
|
||||
setPathFilter={setPathFilter}
|
||||
principalFilter={principalFilter}
|
||||
setPrincipalFilter={setPrincipalFilter}
|
||||
principalTypeFilter={principalTypeFilter}
|
||||
@@ -332,16 +416,22 @@ const Permission = () => {
|
||||
handleOpen={setAddPermissionModalOpen}
|
||||
addPermission={addPermission}
|
||||
/>
|
||||
<PermissionResponseModal
|
||||
open={openPermissionResponseModal}
|
||||
setOpen={setOpenPermissionResponseModal}
|
||||
payload={permissionResponsePayload}
|
||||
/>
|
||||
<UpdatePermissionModal
|
||||
open={updatePermissionModalOpen}
|
||||
handleOpen={setUpdatePermissionModalOpen}
|
||||
permission={selectedPermission}
|
||||
updatePermission={updatePermission}
|
||||
/>
|
||||
<DeleteModal
|
||||
open={deleteModalOpen}
|
||||
setOpen={setDeleteModalOpen}
|
||||
deletePermission={deletePermission}
|
||||
<DeleteConfirmationModal
|
||||
open={deleteConfirmationModalOpen}
|
||||
setOpen={setDeleteConfirmationModalOpen}
|
||||
message={deleteConfirmationModalMessage}
|
||||
_delete={deletePermission}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -367,9 +457,10 @@ const PermissionTable = ({
|
||||
<Table sx={{ minWidth: 650 }}>
|
||||
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
||||
<TableRow>
|
||||
<BootstrapTableCell>Uri</BootstrapTableCell>
|
||||
<BootstrapTableCell>Path</BootstrapTableCell>
|
||||
<BootstrapTableCell>Permission Type</BootstrapTableCell>
|
||||
<BootstrapTableCell>Principal</BootstrapTableCell>
|
||||
<BootstrapTableCell>Type</BootstrapTableCell>
|
||||
<BootstrapTableCell>Principal Type</BootstrapTableCell>
|
||||
<BootstrapTableCell>Setting</BootstrapTableCell>
|
||||
{appContext.isAdmin && (
|
||||
<BootstrapTableCell>Action</BootstrapTableCell>
|
||||
@@ -379,7 +470,8 @@ const PermissionTable = ({
|
||||
<TableBody>
|
||||
{permissions.map((permission) => (
|
||||
<TableRow key={permission.permissionId}>
|
||||
<BootstrapTableCell>{permission.uri}</BootstrapTableCell>
|
||||
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
|
||||
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
|
||||
<BootstrapTableCell>
|
||||
{displayPrincipal(permission)}
|
||||
</BootstrapTableCell>
|
||||
@@ -467,8 +559,8 @@ const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
||||
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
||||
Group Members
|
||||
</Typography>
|
||||
{group.users.map((user) => (
|
||||
<Typography sx={{ p: 1 }} component="li">
|
||||
{group.users.map((user, index) => (
|
||||
<Typography key={index} sx={{ p: 1 }} component="li">
|
||||
{user.username}
|
||||
</Typography>
|
||||
))}
|
||||
|
||||
@@ -27,8 +27,8 @@ type FilterModalProps = {
|
||||
open: boolean
|
||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||
permissions: PermissionResponse[]
|
||||
uriFilter: string[]
|
||||
setUriFilter: Dispatch<SetStateAction<string[]>>
|
||||
pathFilter: string[]
|
||||
setPathFilter: Dispatch<SetStateAction<string[]>>
|
||||
principalFilter: string[]
|
||||
setPrincipalFilter: Dispatch<SetStateAction<string[]>>
|
||||
principalTypeFilter: PrincipalType[]
|
||||
@@ -43,8 +43,8 @@ const PermissionFilterModal = ({
|
||||
open,
|
||||
handleOpen,
|
||||
permissions,
|
||||
uriFilter,
|
||||
setUriFilter,
|
||||
pathFilter,
|
||||
setPathFilter,
|
||||
principalFilter,
|
||||
setPrincipalFilter,
|
||||
principalTypeFilter,
|
||||
@@ -54,8 +54,8 @@ const PermissionFilterModal = ({
|
||||
applyFilter,
|
||||
resetFilter
|
||||
}: FilterModalProps) => {
|
||||
const URIs = permissions
|
||||
.map((permission) => permission.uri)
|
||||
const paths = permissions
|
||||
.map((permission) => permission.path)
|
||||
.filter((uri, index, array) => array.indexOf(uri) === index)
|
||||
|
||||
// fetch all the principals from permissions array
|
||||
@@ -86,13 +86,13 @@ const PermissionFilterModal = ({
|
||||
<Grid item xs={12}>
|
||||
<Autocomplete
|
||||
multiple
|
||||
options={URIs}
|
||||
options={paths}
|
||||
filterSelectedOptions
|
||||
value={uriFilter}
|
||||
value={pathFilter}
|
||||
onChange={(event: any, newValue: string[]) => {
|
||||
setUriFilter(newValue)
|
||||
setPathFilter(newValue)
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} label="URIs" />}
|
||||
renderInput={(params) => <TextField {...params} label="Paths" />}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
|
||||
686
web/src/containers/Studio/editor.tsx
Normal file
686
web/src/containers/Studio/editor.tsx
Normal file
@@ -0,0 +1,686 @@
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useContext
|
||||
} from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
import {
|
||||
Backdrop,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Tab,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
import {
|
||||
RocketLaunch,
|
||||
MoreVert,
|
||||
Save,
|
||||
SaveAs,
|
||||
Difference,
|
||||
Edit
|
||||
} from '@mui/icons-material'
|
||||
import Editor, {
|
||||
MonacoDiffEditor,
|
||||
DiffEditorDidMount,
|
||||
EditorDidMount,
|
||||
monaco
|
||||
} from 'react-monaco-editor'
|
||||
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
||||
|
||||
import { AppContext, RunTimeType } from '../../context/appContext'
|
||||
|
||||
import FilePathInputModal from '../../components/filePathInputModal'
|
||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||
import Modal from '../../components/modal'
|
||||
|
||||
import { usePrompt, useStateWithCallback } from '../../utils/hooks'
|
||||
|
||||
const StyledTabPanel = styled(TabPanel)(() => ({
|
||||
padding: '10px'
|
||||
}))
|
||||
|
||||
const StyledTab = styled(Tab)(() => ({
|
||||
fontSize: '1rem',
|
||||
color: 'gray',
|
||||
'&.Mui-selected': {
|
||||
color: 'black'
|
||||
}
|
||||
}))
|
||||
|
||||
type SASjsEditorProps = {
|
||||
selectedFilePath: string
|
||||
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
|
||||
tab: string
|
||||
setTab: Dispatch<SetStateAction<string>>
|
||||
}
|
||||
|
||||
const baseUrl = window.location.origin
|
||||
|
||||
const SASjsEditor = ({
|
||||
selectedFilePath,
|
||||
setSelectedFilePath,
|
||||
tab,
|
||||
setTab
|
||||
}: SASjsEditorProps) => {
|
||||
const appContext = useContext(AppContext)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [openModal, setOpenModal] = useState(false)
|
||||
const [modalTitle, setModalTitle] = useState('')
|
||||
const [modalPayload, setModalPayload] = useState('')
|
||||
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||
AlertSeverityType.Success
|
||||
)
|
||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState('')
|
||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||
const [webout, setWebout] = useState('')
|
||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||
const [showDiff, setShowDiff] = useState(false)
|
||||
|
||||
const editorRef = useRef(null as any)
|
||||
|
||||
const handleEditorDidMount: EditorDidMount = (editor) => {
|
||||
editorRef.current = editor
|
||||
editor.focus()
|
||||
editor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'show-difference',
|
||||
|
||||
// A label of the action that will be presented to the user.
|
||||
label: 'Show Differences',
|
||||
|
||||
// An optional array of keybindings for the action.
|
||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
|
||||
|
||||
contextMenuGroupId: 'navigation',
|
||||
|
||||
contextMenuOrder: 1,
|
||||
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: function (ed) {
|
||||
setShowDiff(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
|
||||
diffEditor.focus()
|
||||
diffEditor.addCommand(monaco.KeyCode.Escape, function () {
|
||||
setShowDiff(false)
|
||||
})
|
||||
}
|
||||
|
||||
usePrompt(
|
||||
'Changes you made may not be saved.',
|
||||
prevFileContent !== fileContent && !!selectedFilePath
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setRunTimes(Object.values(appContext.runTimes))
|
||||
}, [appContext.runTimes])
|
||||
|
||||
useEffect(() => {
|
||||
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
||||
}, [runTimes])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFilePath) {
|
||||
setIsLoading(true)
|
||||
setSelectedFileExtension(selectedFilePath.split('.').pop() ?? '')
|
||||
axios
|
||||
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
|
||||
.then((res: any) => {
|
||||
setPrevFileContent(res.data)
|
||||
setFileContent(res.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
} else {
|
||||
const content = localStorage.getItem('fileContent') ?? ''
|
||||
setFileContent(content)
|
||||
}
|
||||
setLog('')
|
||||
setWebout('')
|
||||
setTab('code')
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedFilePath])
|
||||
|
||||
useEffect(() => {
|
||||
if (fileContent.length && !selectedFilePath) {
|
||||
localStorage.setItem('fileContent', fileContent)
|
||||
}
|
||||
}, [fileContent, selectedFilePath])
|
||||
|
||||
useEffect(() => {
|
||||
if (runTimes.includes(selectedFileExtension))
|
||||
setSelectedRunTime(selectedFileExtension)
|
||||
}, [selectedFileExtension, runTimes])
|
||||
|
||||
const handleTabChange = (_e: any, newValue: string) => {
|
||||
setTab(newValue)
|
||||
}
|
||||
|
||||
const getSelection = () => {
|
||||
const editor = editorRef.current as any
|
||||
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||
return selection ?? ''
|
||||
}
|
||||
|
||||
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
||||
|
||||
const runCode = (code: string) => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||
.then((res: any) => {
|
||||
const parsedLog = res?.data?.log
|
||||
.map((logLine: any) => logLine.line)
|
||||
.join('\n')
|
||||
|
||||
setLog(parsedLog)
|
||||
|
||||
setWebout(`${res.data?._webout}`)
|
||||
setTab('log')
|
||||
|
||||
// Scroll to bottom of log
|
||||
const logElement = document.getElementById('log')
|
||||
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.ctrlKey) {
|
||||
if (event.key === 'v') {
|
||||
setCtrlPressed(false)
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') runCode(getSelection() || fileContent)
|
||||
if (!ctrlPressed) setCtrlPressed(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (event: any) => {
|
||||
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
|
||||
}
|
||||
|
||||
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
||||
setSelectedRunTime(event.target.value as RunTimeType)
|
||||
}
|
||||
|
||||
const handleFilePathInput = (filePath: string) => {
|
||||
setOpenFilePathInputModal(false)
|
||||
saveFile(filePath)
|
||||
}
|
||||
|
||||
const saveFile = (filePath?: string) => {
|
||||
setIsLoading(true)
|
||||
|
||||
if (filePath) {
|
||||
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||
formData.append('file', stringBlob, 'filename.sas')
|
||||
formData.append('filePath', filePath ?? selectedFilePath)
|
||||
|
||||
const axiosPromise = filePath
|
||||
? axios.post('/SASjsApi/drive/file', formData)
|
||||
: axios.patch('/SASjsApi/drive/file', formData)
|
||||
|
||||
axiosPromise
|
||||
.then(() => {
|
||||
if (filePath && fileContent === prevFileContent) {
|
||||
// when fileContent and prevFileContent is same,
|
||||
// callback function in setPrevFileContent method is not called
|
||||
// because behind the scene useEffect hook is being used
|
||||
// for calling callback function, and it's only fired when the
|
||||
// new value is not equal to old value.
|
||||
// So, we'll have to explicitly update the selected file path
|
||||
|
||||
setSelectedFilePath(filePath, true)
|
||||
} else {
|
||||
setPrevFileContent(fileContent, () => {
|
||||
if (filePath) {
|
||||
setSelectedFilePath(filePath, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
setSnackbarMessage('File saved!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={isLoading}
|
||||
>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
{selectedFilePath && !runTimes.includes(selectedFileExtension) ? (
|
||||
<Box sx={{ marginTop: '10px' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<FileMenu
|
||||
showDiff={showDiff}
|
||||
setShowDiff={setShowDiff}
|
||||
prevFileContent={prevFileContent}
|
||||
currentFileContent={fileContent}
|
||||
selectedFilePath={selectedFilePath}
|
||||
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||
saveFile={saveFile}
|
||||
/>
|
||||
</Box>
|
||||
<Paper
|
||||
sx={{
|
||||
height: 'calc(100vh - 140px)',
|
||||
padding: '10px',
|
||||
margin: '0 24px',
|
||||
overflow: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
{showDiff ? (
|
||||
<MonacoDiffEditor
|
||||
height="98%"
|
||||
language={getLanguage(selectedFileExtension)}
|
||||
original={prevFileContent}
|
||||
value={fileContent}
|
||||
editorDidMount={handleDiffEditorDidMount}
|
||||
options={{ readOnly: ctrlPressed }}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
/>
|
||||
) : (
|
||||
<Editor
|
||||
height="98%"
|
||||
language={getLanguage(selectedFileExtension)}
|
||||
value={fileContent}
|
||||
editorDidMount={handleEditorDidMount}
|
||||
options={{ readOnly: ctrlPressed }}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
) : (
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
background: 'white'
|
||||
}}
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<StyledTab label="Code" value="code" />
|
||||
<StyledTab label="Log" value="log" />
|
||||
<StyledTab
|
||||
label={
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Typography>Webout</Typography>
|
||||
</Tooltip>
|
||||
}
|
||||
value="webout"
|
||||
/>
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
<StyledTabPanel sx={{ paddingBottom: 0 }} value="code">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<RunMenu
|
||||
fileContent={fileContent}
|
||||
prevFileContent={prevFileContent}
|
||||
selectedFilePath={selectedFilePath}
|
||||
selectedRunTime={selectedRunTime}
|
||||
runTimes={runTimes}
|
||||
handleChangeRunTime={handleChangeRunTime}
|
||||
handleRunBtnClick={handleRunBtnClick}
|
||||
/>
|
||||
<FileMenu
|
||||
showDiff={showDiff}
|
||||
setShowDiff={setShowDiff}
|
||||
prevFileContent={prevFileContent}
|
||||
currentFileContent={fileContent}
|
||||
selectedFilePath={selectedFilePath}
|
||||
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||
saveFile={saveFile}
|
||||
/>
|
||||
</Box>
|
||||
<Paper
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
sx={{
|
||||
height: 'calc(100vh - 170px)',
|
||||
padding: '10px',
|
||||
overflow: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
{showDiff ? (
|
||||
<MonacoDiffEditor
|
||||
height="98%"
|
||||
language={getLanguage(selectedFileExtension)}
|
||||
original={prevFileContent}
|
||||
value={fileContent}
|
||||
editorDidMount={handleDiffEditorDidMount}
|
||||
options={{ readOnly: ctrlPressed }}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
/>
|
||||
) : (
|
||||
<Editor
|
||||
height="98%"
|
||||
language={getLanguage(selectedFileExtension)}
|
||||
value={fileContent}
|
||||
editorDidMount={handleEditorDidMount}
|
||||
options={{ readOnly: ctrlPressed }}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
/>
|
||||
)}
|
||||
<p
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: -10,
|
||||
textAlign: 'center',
|
||||
fontSize: '13px'
|
||||
}}
|
||||
>
|
||||
Press CTRL + ENTER to run code
|
||||
</p>
|
||||
</Paper>
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value="log">
|
||||
<div>
|
||||
<h2>Log</h2>
|
||||
<pre id="log" style={{ overflow: 'auto', height: '75vh' }}>
|
||||
{log}
|
||||
</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value="webout">
|
||||
<div>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
</TabContext>
|
||||
)}
|
||||
<Modal
|
||||
open={openModal}
|
||||
setOpen={setOpenModal}
|
||||
title={modalTitle}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
<BootstrapSnackbar
|
||||
open={openSnackbar}
|
||||
setOpen={setOpenSnackbar}
|
||||
message={snackbarMessage}
|
||||
severity={snackbarSeverity}
|
||||
/>
|
||||
<FilePathInputModal
|
||||
open={openFilePathInputModal}
|
||||
setOpen={setOpenFilePathInputModal}
|
||||
saveFile={handleFilePathInput}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default SASjsEditor
|
||||
|
||||
type RunMenuProps = {
|
||||
selectedFilePath: string
|
||||
fileContent: string
|
||||
prevFileContent: string
|
||||
selectedRunTime: string
|
||||
runTimes: string[]
|
||||
handleChangeRunTime: (event: SelectChangeEvent) => void
|
||||
handleRunBtnClick: () => void
|
||||
}
|
||||
|
||||
const RunMenu = ({
|
||||
selectedFilePath,
|
||||
fileContent,
|
||||
prevFileContent,
|
||||
selectedRunTime,
|
||||
runTimes,
|
||||
handleChangeRunTime,
|
||||
handleRunBtnClick
|
||||
}: RunMenuProps) => {
|
||||
const launchProgram = () => {
|
||||
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="CTRL+ENTER will also run code">
|
||||
<Button
|
||||
onClick={handleRunBtnClick}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '5px 5px',
|
||||
minWidth: 'unset'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
draggable="false"
|
||||
style={{ width: '25px' }}
|
||||
src="/running-sas.png"
|
||||
></img>
|
||||
<span style={{ fontSize: '12px' }}>RUN</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{selectedFilePath ? (
|
||||
<Box sx={{ marginLeft: '10px' }}>
|
||||
<Tooltip
|
||||
title={
|
||||
fileContent !== prevFileContent
|
||||
? 'Save file before launching program'
|
||||
: 'Launch program in new window'
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={fileContent !== prevFileContent}
|
||||
onClick={launchProgram}
|
||||
>
|
||||
<RocketLaunch />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
||||
<FormControl variant="standard">
|
||||
<Select
|
||||
labelId="run-time-select-label"
|
||||
id="run-time-select"
|
||||
value={selectedRunTime}
|
||||
onChange={handleChangeRunTime}
|
||||
>
|
||||
{runTimes.map((runTime) => (
|
||||
<MenuItem key={runTime} value={runTime}>
|
||||
{runTime}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type FileMenuProps = {
|
||||
showDiff: boolean
|
||||
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>
|
||||
prevFileContent: string
|
||||
currentFileContent: string
|
||||
selectedFilePath: string
|
||||
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||
saveFile: () => void
|
||||
}
|
||||
|
||||
const FileMenu = ({
|
||||
showDiff,
|
||||
setShowDiff,
|
||||
prevFileContent,
|
||||
currentFileContent,
|
||||
selectedFilePath,
|
||||
setOpenFilePathInputModal,
|
||||
saveFile
|
||||
}: FileMenuProps) => {
|
||||
const [anchorEl, setAnchorEl] = useState<
|
||||
(EventTarget & HTMLButtonElement) | null
|
||||
>(null)
|
||||
|
||||
const handleMenu = (
|
||||
event?: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
if (event) setAnchorEl(event.currentTarget)
|
||||
else setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleDiffBtnClick = () => {
|
||||
setAnchorEl(null)
|
||||
setShowDiff(!showDiff)
|
||||
}
|
||||
|
||||
const handleSaveAsBtnClick = () => {
|
||||
setAnchorEl(null)
|
||||
setOpenFilePathInputModal(true)
|
||||
}
|
||||
|
||||
const handleSaveBtnClick = () => {
|
||||
setAnchorEl(null)
|
||||
saveFile()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Save File Menu">
|
||||
<IconButton onClick={handleMenu}>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
id="save-file-menu"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
open={!!anchorEl}
|
||||
onClose={() => handleMenu()}
|
||||
>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
onClick={handleDiffBtnClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={showDiff ? <Edit /> : <Difference />}
|
||||
>
|
||||
{showDiff ? 'Edit' : 'Diff'}
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
onClick={handleSaveBtnClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Save />}
|
||||
disabled={
|
||||
!selectedFilePath || prevFileContent === currentFileContent
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||
<Button
|
||||
onClick={handleSaveAsBtnClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<SaveAs />}
|
||||
>
|
||||
Save As
|
||||
</Button>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const getLanguage = (extension: string) => {
|
||||
if (extension === 'js') return 'javascript'
|
||||
|
||||
if (extension === 'ts') return 'typescript'
|
||||
|
||||
if (extension === 'md' || extension === 'mdx') return 'markdown'
|
||||
|
||||
return extension
|
||||
}
|
||||
@@ -1,253 +1,104 @@
|
||||
import React, { useEffect, useRef, useState, useContext } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import axios from 'axios'
|
||||
|
||||
import {
|
||||
Backdrop,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Tab,
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import Editor, { EditorDidMount } from 'react-monaco-editor'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import Box from '@mui/material/Box'
|
||||
|
||||
import { AppContext, RunTimeType } from '../../context/appContext'
|
||||
import { TreeNode } from '../../utils/types'
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
fontSize: '1rem',
|
||||
color: 'gray',
|
||||
'&.Mui-selected': {
|
||||
color: 'black'
|
||||
}
|
||||
},
|
||||
subMenu: {
|
||||
marginTop: '25px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
runButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '5px 5px',
|
||||
minWidth: 'unset'
|
||||
}
|
||||
}))
|
||||
import SideBar from './sideBar'
|
||||
import SASjsEditor from './editor'
|
||||
|
||||
const Studio = () => {
|
||||
const appContext = useContext(AppContext)
|
||||
const location = useLocation()
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState('')
|
||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||
const [webout, setWebout] = useState('')
|
||||
const [tab, setTab] = useState('1')
|
||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [selectedFilePath, setSelectedFilePath] = useState('')
|
||||
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
||||
const [tab, setTab] = useState('code')
|
||||
|
||||
useEffect(() => {
|
||||
setRunTimes(Object.values(appContext.runTimes))
|
||||
}, [appContext.runTimes])
|
||||
setSelectedFilePath(searchParams.get('filePath') ?? '')
|
||||
}, [searchParams])
|
||||
|
||||
useEffect(() => {
|
||||
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
||||
}, [runTimes])
|
||||
|
||||
const handleTabChange = (_e: any, newValue: string) => {
|
||||
setTab(newValue)
|
||||
}
|
||||
|
||||
const editorRef = useRef(null as any)
|
||||
const handleEditorDidMount: EditorDidMount = (editor) => {
|
||||
editor.focus()
|
||||
editorRef.current = editor
|
||||
}
|
||||
|
||||
const getSelection = () => {
|
||||
const editor = editorRef.current as any
|
||||
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||
return selection ?? ''
|
||||
}
|
||||
|
||||
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
||||
|
||||
const runCode = (code: string) => {
|
||||
setIsRunning(true)
|
||||
const fetchDirectoryData = useCallback(() => {
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||
.get(`/SASjsApi/drive/fileTree`)
|
||||
.then((res: any) => {
|
||||
const parsedLog = res?.data?.log
|
||||
.map((logLine: any) => logLine.line)
|
||||
.join('\n')
|
||||
|
||||
setLog(parsedLog)
|
||||
|
||||
setWebout(`${res.data?._webout}`)
|
||||
setTab('2')
|
||||
|
||||
// Scroll to bottom of log
|
||||
window.scrollTo(0, document.body.scrollHeight)
|
||||
if (res.data && res.data?.status === 'success') {
|
||||
setDirectoryData(res.data.tree)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.catch((err) => console.log(err))
|
||||
.finally(() => setIsRunning(false))
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.ctrlKey) {
|
||||
if (event.key === 'v') {
|
||||
setCtrlPressed(false)
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') runCode(getSelection() || fileContent)
|
||||
if (!ctrlPressed) setCtrlPressed(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (event: any) => {
|
||||
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
|
||||
}
|
||||
|
||||
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
||||
setSelectedRunTime(event.target.value as RunTimeType)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const content = localStorage.getItem('fileContent') ?? ''
|
||||
setFileContent(content)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (fileContent.length) {
|
||||
localStorage.setItem('fileContent', fileContent)
|
||||
fetchDirectoryData()
|
||||
}, [fetchDirectoryData])
|
||||
|
||||
const handleSelect = (filePath: string, refreshSideBar?: boolean) => {
|
||||
setSearchParams({ filePath })
|
||||
if (refreshSideBar) fetchDirectoryData()
|
||||
}
|
||||
|
||||
const removeFileFromTree = (path: string) => {
|
||||
if (directoryData) {
|
||||
const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode
|
||||
findAndRemoveNode(newTree, newTree, path)
|
||||
setDirectoryData(newTree)
|
||||
}
|
||||
}, [fileContent])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const programPath = params.get('_program')
|
||||
const findAndRemoveNode = (
|
||||
node: TreeNode,
|
||||
parentNode: TreeNode,
|
||||
path: string
|
||||
) => {
|
||||
if (node.relativePath === path) {
|
||||
removeNodeFromParent(parentNode, path)
|
||||
// reset selected file path and file path query param
|
||||
if (
|
||||
node.relativePath === selectedFilePath ||
|
||||
selectedFilePath.startsWith(node.relativePath)
|
||||
)
|
||||
setSearchParams({})
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(node.children)) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
if (findAndRemoveNode(node.children[i], node, path)) return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (programPath?.length)
|
||||
axios
|
||||
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
|
||||
.then((res: any) => setFileContent(res.data.fileContent))
|
||||
.catch((err) => console.log(err))
|
||||
}, [location.search])
|
||||
|
||||
const classes = useStyles()
|
||||
const removeNodeFromParent = (parent: TreeNode, path: string) => {
|
||||
const index = parent.children.findIndex(
|
||||
(node) => node.relativePath === path
|
||||
)
|
||||
if (index !== -1) {
|
||||
parent.children.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
|
||||
>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<Tab className={classes.root} label="Code" value="1" />
|
||||
<Tab className={classes.root} label="Log" value="2" />
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Tab className={classes.root} label="Webout" value="3" />
|
||||
</Tooltip>
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
<TabPanel sx={{ paddingBottom: 0 }} value="1">
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={isRunning}
|
||||
>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
<div className={classes.subMenu}>
|
||||
<Tooltip title="CTRL+ENTER will also run SAS code">
|
||||
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
||||
<img
|
||||
alt=""
|
||||
draggable="false"
|
||||
style={{ width: '25px' }}
|
||||
src="/running-sas.png"
|
||||
></img>
|
||||
<span style={{ fontSize: '12px' }}>RUN</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
||||
<FormControl variant="standard">
|
||||
<Select
|
||||
labelId="run-time-select-label"
|
||||
id="run-time-select"
|
||||
value={selectedRunTime}
|
||||
onChange={handleChangeRunTime}
|
||||
>
|
||||
{runTimes.map((runTime) => (
|
||||
<MenuItem key={runTime} value={runTime}>
|
||||
{runTime}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</div>
|
||||
<Paper
|
||||
sx={{
|
||||
height: 'calc(100vh - 170px)',
|
||||
padding: '10px',
|
||||
overflow: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
<Editor
|
||||
height="98%"
|
||||
language="sas"
|
||||
value={fileContent}
|
||||
editorDidMount={handleEditorDidMount}
|
||||
options={{ readOnly: ctrlPressed }}
|
||||
onChange={(val) => {
|
||||
if (val) setFileContent(val)
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: -10,
|
||||
textAlign: 'center',
|
||||
fontSize: '13px'
|
||||
}}
|
||||
>
|
||||
Press CTRL + ENTER to run SAS code
|
||||
</p>
|
||||
</Paper>
|
||||
</TabPanel>
|
||||
<TabPanel value="2">
|
||||
<div style={{ marginTop: '50px' }}>
|
||||
<h2>SAS Log</h2>
|
||||
<pre>{log}</pre>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel value="3">
|
||||
<div style={{ marginTop: '50px' }}>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
{tab === 'code' && (
|
||||
<SideBar
|
||||
selectedFilePath={selectedFilePath}
|
||||
directoryData={directoryData}
|
||||
handleSelect={handleSelect}
|
||||
removeFileFromTree={removeFileFromTree}
|
||||
refreshSideBar={fetchDirectoryData}
|
||||
/>
|
||||
)}
|
||||
<SASjsEditor
|
||||
selectedFilePath={selectedFilePath}
|
||||
setSelectedFilePath={handleSelect}
|
||||
tab={tab}
|
||||
setTab={setTab}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
267
web/src/containers/Studio/sideBar.tsx
Normal file
267
web/src/containers/Studio/sideBar.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Backdrop,
|
||||
Box,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
Drawer,
|
||||
Toolbar,
|
||||
IconButton
|
||||
} from '@mui/material'
|
||||
import { FolderOpen } from '@mui/icons-material'
|
||||
|
||||
import TreeView from '../../components/tree'
|
||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||
import Modal from '../../components/modal'
|
||||
import { TreeNode } from '../../utils/types'
|
||||
|
||||
const drawerWidth = '15%'
|
||||
|
||||
type Props = {
|
||||
selectedFilePath: string
|
||||
directoryData: TreeNode | null
|
||||
handleSelect: (filePath: string) => void
|
||||
removeFileFromTree: (filePath: string) => void
|
||||
refreshSideBar: () => void
|
||||
}
|
||||
|
||||
const SideBar = ({
|
||||
selectedFilePath,
|
||||
directoryData,
|
||||
handleSelect,
|
||||
removeFileFromTree,
|
||||
refreshSideBar
|
||||
}: Props) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [openModal, setOpenModal] = useState(false)
|
||||
const [modalTitle, setModalTitle] = useState('')
|
||||
const [modalPayload, setModalPayload] = useState('')
|
||||
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||
AlertSeverityType.Success
|
||||
)
|
||||
const [mobileOpen, setMobileOpen] = React.useState(false)
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen)
|
||||
}
|
||||
|
||||
const handleFileSelect = (filePath: string) => {
|
||||
setMobileOpen(false)
|
||||
handleSelect(filePath)
|
||||
}
|
||||
|
||||
const defaultExpanded = useMemo(() => {
|
||||
const splittedPath = selectedFilePath.split('/')
|
||||
const arr = ['']
|
||||
let nodeId = ''
|
||||
splittedPath.forEach((path) => {
|
||||
if (path !== '') {
|
||||
nodeId += '/' + path
|
||||
arr.push(nodeId)
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}, [selectedFilePath])
|
||||
|
||||
const deleteNode = (path: string, isFolder: boolean) => {
|
||||
setIsLoading(true)
|
||||
const axiosPromise = axios.delete(
|
||||
`/SASjsApi/drive/${
|
||||
isFolder ? `folder?_folderPath=${path}` : `file?_filePath=${path}`
|
||||
}`
|
||||
)
|
||||
|
||||
axiosPromise
|
||||
.then(() => {
|
||||
removeFileFromTree(path)
|
||||
setSnackbarMessage('Deleted!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
const addFile = (filePath: string) => {
|
||||
const formData = new FormData()
|
||||
const stringBlob = new Blob([''], { type: 'text/plain' })
|
||||
formData.append('file', stringBlob)
|
||||
formData.append('filePath', filePath)
|
||||
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post('/SASjsApi/drive/file', formData)
|
||||
.then(() => {
|
||||
setSnackbarMessage('File added!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
refreshSideBar()
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
const addFolder = (folderPath: string) => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post('/SASjsApi/drive/folder', { folderPath })
|
||||
.then(() => {
|
||||
setSnackbarMessage('Folder added!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
refreshSideBar()
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
const rename = (oldPath: string, newPath: string) => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post('/SASjsApi/drive/rename', { oldPath, newPath })
|
||||
.then(() => {
|
||||
setSnackbarMessage('Successfully Renamed')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
if (oldPath === selectedFilePath) handleSelect(newPath)
|
||||
else if (selectedFilePath.startsWith(oldPath))
|
||||
handleSelect(selectedFilePath.replace(oldPath, newPath))
|
||||
refreshSideBar()
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
const drawer = (
|
||||
<div>
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={isLoading}
|
||||
>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
<Toolbar />
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
{directoryData && (
|
||||
<TreeView
|
||||
node={directoryData}
|
||||
selectedFilePath={selectedFilePath}
|
||||
handleSelect={handleFileSelect}
|
||||
deleteNode={deleteNode}
|
||||
addFile={addFile}
|
||||
addFolder={addFolder}
|
||||
rename={rename}
|
||||
defaultExpanded={defaultExpanded}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<BootstrapSnackbar
|
||||
open={openSnackbar}
|
||||
setOpen={setOpenSnackbar}
|
||||
message={snackbarMessage}
|
||||
severity={snackbarSeverity}
|
||||
/>
|
||||
<Modal
|
||||
open={openModal}
|
||||
setOpen={setOpenModal}
|
||||
title={modalTitle}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
component={Paper}
|
||||
sx={{
|
||||
margin: '5px',
|
||||
height: '97vh',
|
||||
paddingTop: '45px',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="large"
|
||||
aria-label="open drawer"
|
||||
edge="start"
|
||||
onClick={handleDrawerToggle}
|
||||
sx={{ left: '5px', display: { md: 'none' } }}
|
||||
>
|
||||
<FolderOpen />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Drawer
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onClose={handleDrawerToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true // Better open performance on mobile.
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: 'block', md: 'none' },
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: {
|
||||
width: 240,
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'block' },
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: {
|
||||
width: drawerWidth,
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SideBar
|
||||
@@ -80,7 +80,18 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
})
|
||||
.catch(() => {
|
||||
setLoggedIn(false)
|
||||
axios.get('/') // get CSRF TOKEN
|
||||
// get CSRF TOKEN and set cookie
|
||||
axios
|
||||
.get('/')
|
||||
.then((res) => res.data)
|
||||
.then((data: string) => {
|
||||
const result =
|
||||
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
|
||||
data
|
||||
)?.[1]
|
||||
|
||||
if (result) document.cookie = result
|
||||
})
|
||||
})
|
||||
|
||||
axios
|
||||
|
||||
@@ -13,7 +13,7 @@ code {
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-top: 50px;
|
||||
margin: 50px 10px 0 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -25,3 +25,15 @@ code {
|
||||
padding: '5px 10px';
|
||||
margin-top: '10px';
|
||||
}
|
||||
|
||||
.tree-item-label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tree-item-label.selected {
|
||||
background: lightgoldenrodyellow;
|
||||
}
|
||||
|
||||
.tree-item-label:hover {
|
||||
background: lightgray;
|
||||
}
|
||||
|
||||
59
web/src/utils/helper.ts
Normal file
59
web/src/utils/helper.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { PermissionResponse, RegisterPermissionPayload } from './types'
|
||||
|
||||
export const findExistingPermission = (
|
||||
existingPermissions: PermissionResponse[],
|
||||
newPermission: RegisterPermissionPayload
|
||||
) => {
|
||||
for (const permission of existingPermissions) {
|
||||
if (
|
||||
permission.user?.id === newPermission.principalId &&
|
||||
hasSameCombination(permission, newPermission)
|
||||
)
|
||||
return permission
|
||||
|
||||
if (
|
||||
permission.group?.groupId === newPermission.principalId &&
|
||||
hasSameCombination(permission, newPermission)
|
||||
)
|
||||
return permission
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const findUpdatingPermission = (
|
||||
existingPermissions: PermissionResponse[],
|
||||
newPermission: RegisterPermissionPayload
|
||||
) => {
|
||||
for (const permission of existingPermissions) {
|
||||
if (
|
||||
permission.user?.id === newPermission.principalId &&
|
||||
hasDifferentSetting(permission, newPermission)
|
||||
)
|
||||
return permission
|
||||
|
||||
if (
|
||||
permission.group?.groupId === newPermission.principalId &&
|
||||
hasDifferentSetting(permission, newPermission)
|
||||
)
|
||||
return permission
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const hasSameCombination = (
|
||||
existingPermission: PermissionResponse,
|
||||
newPermission: RegisterPermissionPayload
|
||||
) =>
|
||||
existingPermission.path === newPermission.path &&
|
||||
existingPermission.type === newPermission.type &&
|
||||
existingPermission.setting === newPermission.setting
|
||||
|
||||
const hasDifferentSetting = (
|
||||
existingPermission: PermissionResponse,
|
||||
newPermission: RegisterPermissionPayload
|
||||
) =>
|
||||
existingPermission.path === newPermission.path &&
|
||||
existingPermission.type === newPermission.type &&
|
||||
existingPermission.setting !== newPermission.setting
|
||||
2
web/src/utils/hooks/index.ts
Normal file
2
web/src/utils/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './usePrompt'
|
||||
export * from './useStateWithCallback'
|
||||
36
web/src/utils/hooks/usePrompt.ts
Normal file
36
web/src/utils/hooks/usePrompt.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useCallback, useContext } from 'react'
|
||||
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'
|
||||
import { History, Blocker, Transition } from 'history'
|
||||
|
||||
const useBlocker = (blocker: Blocker, when = true) => {
|
||||
const navigator = useContext(NavigationContext).navigator as History
|
||||
|
||||
useEffect(() => {
|
||||
if (!when) return
|
||||
|
||||
const unblock = navigator.block((tx: Transition) => {
|
||||
const autoUnblockingTx = {
|
||||
...tx,
|
||||
retry() {
|
||||
unblock()
|
||||
tx.retry()
|
||||
}
|
||||
}
|
||||
|
||||
blocker(autoUnblockingTx)
|
||||
})
|
||||
|
||||
return unblock
|
||||
}, [navigator, blocker, when])
|
||||
}
|
||||
|
||||
export const usePrompt = (message: string, when = true) => {
|
||||
const blocker = useCallback(
|
||||
(tx) => {
|
||||
if (window.confirm(message)) tx.retry()
|
||||
},
|
||||
[message]
|
||||
)
|
||||
|
||||
useBlocker(blocker, when)
|
||||
}
|
||||
27
web/src/utils/hooks/useStateWithCallback.ts
Normal file
27
web/src/utils/hooks/useStateWithCallback.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
export const useStateWithCallback = <T>(
|
||||
initialValue: T
|
||||
): [T, (newValue: T, callback?: () => void) => void] => {
|
||||
const callbackRef = useRef<any>(null)
|
||||
|
||||
const [value, setValue] = useState(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof callbackRef.current === 'function') {
|
||||
callbackRef.current()
|
||||
|
||||
callbackRef.current = null
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const setValueWithCallback = (newValue: T, callback?: () => void) => {
|
||||
callbackRef.current = callback
|
||||
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
return [value, setValueWithCallback]
|
||||
}
|
||||
|
||||
export default useStateWithCallback
|
||||
@@ -18,15 +18,24 @@ export interface GroupDetailsResponse extends GroupResponse {
|
||||
|
||||
export interface PermissionResponse {
|
||||
permissionId: number
|
||||
uri: string
|
||||
path: string
|
||||
type: string
|
||||
setting: string
|
||||
user?: UserResponse
|
||||
group?: GroupDetailsResponse
|
||||
}
|
||||
|
||||
export interface RegisterPermissionPayload {
|
||||
uri: string
|
||||
path: string
|
||||
type: string
|
||||
setting: string
|
||||
principalType: string
|
||||
principalId: number
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
name: string
|
||||
relativePath: string
|
||||
isFolder: boolean
|
||||
children: Array<TreeNode>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user