mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 19:44:35 +00:00
Compare commits
1 Commits
v0.13.2
...
testing-ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1a02c0da5 |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,3 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: [sasjs]
|
|
||||||
182
CHANGELOG.md
182
CHANGELOG.md
@@ -1,185 +1,3 @@
|
|||||||
## [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)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* filePath fix in code.js file for windows ([2995121](https://github.com/sasjs/server/commit/299512135d77c2ac9e34853cf35aee6f2e1d4da4))
|
|
||||||
|
|
||||||
## [0.11.2](https://github.com/sasjs/server/compare/v0.11.1...v0.11.2) (2022-07-18)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* apply icon option only for sas.exe ([d2ddd8a](https://github.com/sasjs/server/commit/d2ddd8aacadfdd143026881f2c6ae8c6b277610a))
|
|
||||||
|
|
||||||
## [0.11.1](https://github.com/sasjs/server/compare/v0.11.0...v0.11.1) (2022-07-18)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* bank operator ([aa02741](https://github.com/sasjs/server/commit/aa027414ed3ce51f1014ef36c4191e064b2e963d))
|
|
||||||
* ensuring nosplash option only applies for sas.exe ([65e6de9](https://github.com/sasjs/server/commit/65e6de966383fe49a919b1f901d77c7f1e402c9b)), closes [#229](https://github.com/sasjs/server/issues/229)
|
|
||||||
|
|
||||||
# [0.11.0](https://github.com/sasjs/server/compare/v0.10.0...v0.11.0) (2022-07-16)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **logs:** logs location is configurable ([e024a92](https://github.com/sasjs/server/commit/e024a92f165990e08db8aa26ee326dbcb30e2e46))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **logs:** logs to file with rotating + code split into files ([92fda18](https://github.com/sasjs/server/commit/92fda183f3f0f3956b7c791669eb8dd52c389d1b))
|
|
||||||
|
|
||||||
# [0.10.0](https://github.com/sasjs/server/compare/v0.9.0...v0.10.0) (2022-07-06)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* add authorize middleware for appStreams ([e54a09d](https://github.com/sasjs/server/commit/e54a09db19ec8690e54a40760531a4e06d250974))
|
|
||||||
* add isAdmin attribute to return response of get session and login requests ([bdf63df](https://github.com/sasjs/server/commit/bdf63df1d915892486005ec904807749786b1c0c))
|
|
||||||
* add permission authorization middleware to only specific routes ([f3dfc70](https://github.com/sasjs/server/commit/f3dfc7083fbfb4b447521341b1a86730fb90b4c0))
|
|
||||||
* bumping core and running lint ([a2d1396](https://github.com/sasjs/server/commit/a2d13960578014312d2cb5e03145bfd1829d99ec))
|
|
||||||
* controller fixed for deleting permission ([b5f595a](https://github.com/sasjs/server/commit/b5f595a25c50550d62482409353c7629c5a5c3e0))
|
|
||||||
* do not show admin users in add permission modal ([a75edba](https://github.com/sasjs/server/commit/a75edbaa327ec2af49523c13996ac283061da7d8))
|
|
||||||
* export GroupResponse interface ([38a7db8](https://github.com/sasjs/server/commit/38a7db8514de0acd94d74ba96bc1efb732add30c))
|
|
||||||
* move permission filter modal to separate file and icons for different actions ([d000f75](https://github.com/sasjs/server/commit/d000f7508f6d7384afffafee4179151fca802ca8))
|
|
||||||
* principalId type changed to number from any ([4fcc191](https://github.com/sasjs/server/commit/4fcc191ce9edc7e4dcd8821fb8019f4eea5db4ea))
|
|
||||||
* remove clientId from principal types ([0781ddd](https://github.com/sasjs/server/commit/0781ddd64e3b5e5ca39647bb4e4e1a9332a0f4f8))
|
|
||||||
* remove duplicates principals from permission filter modal ([5b319f9](https://github.com/sasjs/server/commit/5b319f9ad1f941b306db6b9473a2128b2e42bf76))
|
|
||||||
* show loading spinner in studio while executing code ([496247d](https://github.com/sasjs/server/commit/496247d0b9975097a008cf4d3a999d77648fd930))
|
|
||||||
* show permission component only in server mode ([f863b81](https://github.com/sasjs/server/commit/f863b81a7d40a1296a061ec93946f204382af2c3))
|
|
||||||
* update permission model ([39fc908](https://github.com/sasjs/server/commit/39fc908de1945f2aaea18d14e6bce703f6bf0c06))
|
|
||||||
* update permission response ([e516b77](https://github.com/sasjs/server/commit/e516b7716da5ff7e23350a5f77cfa073b1171175))
|
|
||||||
* **web:** only admin should be able to add, update or delete permission ([be8635c](https://github.com/sasjs/server/commit/be8635ccc5eb34c3f0a5951c8a0421292ef69c97))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* add api endpoint for deleting permission ([0171344](https://github.com/sasjs/server/commit/01713440a4fa661b76368785c0ca731f096ac70a))
|
|
||||||
* add api endpoint for updating permission setting ([540f54f](https://github.com/sasjs/server/commit/540f54fb77b364822da7889dbe75c02242f48a59))
|
|
||||||
* add authorize middleware for validating permissions ([7d916ec](https://github.com/sasjs/server/commit/7d916ec3e9ef579dde1b73015715cd01098c2018))
|
|
||||||
* add basic UI for settings and permissions ([5652325](https://github.com/sasjs/server/commit/56523254525a66e756196e90b39a2b8cdadc1518))
|
|
||||||
* add documentation link under usename dropdown menu ([eeb63b3](https://github.com/sasjs/server/commit/eeb63b330c292afcdd5c8f006882b224c4235068))
|
|
||||||
* add permission model ([6bea1f7](https://github.com/sasjs/server/commit/6bea1f76668ddb070ad95b3e02c31238af67c346))
|
|
||||||
* add UI for updating permission ([e8c21a4](https://github.com/sasjs/server/commit/e8c21a43b215f5fced0463b70747cda1191a4e01))
|
|
||||||
* add validation for registering permission ([e5200c1](https://github.com/sasjs/server/commit/e5200c1000903185dfad9ee49c99583e473c4388))
|
|
||||||
* add, remove and update permissions from web component ([97ecfdc](https://github.com/sasjs/server/commit/97ecfdc95563c72dbdecaebcb504e5194250a763))
|
|
||||||
* added get authorizedRoutes api endpoint ([b10e932](https://github.com/sasjs/server/commit/b10e9326058193dd65a57fab2d2f05b7b06096e7))
|
|
||||||
* created modal for adding permission ([1413b18](https://github.com/sasjs/server/commit/1413b1850838ecc988ab289da4541bde36a9a346))
|
|
||||||
* defined register permission and get all permissions api endpoints ([1103ffe](https://github.com/sasjs/server/commit/1103ffe07b88496967cb03683b08f058ca3bbb9f))
|
|
||||||
* update swagger docs ([797c2bc](https://github.com/sasjs/server/commit/797c2bcc39005a05a995be15a150d584fecae259))
|
|
||||||
|
|
||||||
# [0.9.0](https://github.com/sasjs/server/compare/v0.8.3...v0.9.0) (2022-07-03)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* removed secrets from env variables ([9c3da56](https://github.com/sasjs/server/commit/9c3da56901672a818f54267f9defc9f4701ab7fb))
|
|
||||||
|
|
||||||
## [0.8.3](https://github.com/sasjs/server/compare/v0.8.2...v0.8.3) (2022-07-02)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **deploy:** extract first json from zip file ([e290751](https://github.com/sasjs/server/commit/e290751c872d24009482871a8c398e834357dcde))
|
|
||||||
|
|
||||||
## [0.8.2](https://github.com/sasjs/server/compare/v0.8.1...v0.8.2) (2022-06-22)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* getRuntimeAndFilePath function to handle the scenarion when path is provided with an extension other than runtimes ([5cc85b5](https://github.com/sasjs/server/commit/5cc85b57f80b13296156811fe966d7b37d45f213))
|
|
||||||
|
|
||||||
## [0.8.1](https://github.com/sasjs/server/compare/v0.8.0...v0.8.1) (2022-06-21)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* make CA_ROOT optional in getCertificates method ([1b5859e](https://github.com/sasjs/server/commit/1b5859ee37ae73c419115b9debfd5141a79733de))
|
|
||||||
* update /logout route to /SASLogon/logout ([65380be](https://github.com/sasjs/server/commit/65380be2f3945bae559f1749064845b514447a53))
|
|
||||||
|
|
||||||
# [0.8.0](https://github.com/sasjs/server/compare/v0.7.3...v0.8.0) (2022-06-21)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **certs:** ENV variables updated and set CA Root for HTTPS server ([2119e9d](https://github.com/sasjs/server/commit/2119e9de9ab1e5ce1222658f554ac74f4f35cf4d))
|
|
||||||
|
|
||||||
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)
|
## [0.7.3](https://github.com/sasjs/server/compare/v0.7.2...v0.7.3) (2022-06-20)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
## Issue
|
|
||||||
|
|
||||||
Link any related issue(s) in this section.
|
|
||||||
|
|
||||||
## Intent
|
|
||||||
|
|
||||||
What this PR intends to achieve.
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
What code changes have been made to achieve the intent.
|
|
||||||
|
|
||||||
## Checks
|
|
||||||
|
|
||||||
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
|
||||||
- [ ] Any new functionality has been unit tested.
|
|
||||||
- [ ] All unit tests are passing (`npm test`).
|
|
||||||
- [ ] All CI checks are green.
|
|
||||||
- [ ] Reviewer is assigned.
|
|
||||||
14
README.md
14
README.md
@@ -99,12 +99,15 @@ SASV9_OPTIONS= -NOXCMD
|
|||||||
## Additional Web Server Options
|
## Additional Web Server Options
|
||||||
#
|
#
|
||||||
|
|
||||||
# ENV variables for PROTOCOL: `https`
|
# ENV variables required for PROTOCOL: `https`
|
||||||
PRIVATE_KEY=privkey.pem (required)
|
PRIVATE_KEY=privkey.pem
|
||||||
CERT_CHAIN=certificate.pem (required)
|
FULL_CHAIN=fullchain.pem
|
||||||
CA_ROOT=fullchain.pem (optional)
|
|
||||||
|
|
||||||
# ENV variables required for MODE: `server`
|
# ENV variables required for MODE: `server`
|
||||||
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
|
REFRESH_TOKEN_SECRET=<secret>
|
||||||
|
AUTH_CODE_SECRET=<secret>
|
||||||
|
SESSION_SECRET=<secret>
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||||
@@ -136,9 +139,6 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json
|
|||||||
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
||||||
LOG_FORMAT_MORGAN=
|
LOG_FORMAT_MORGAN=
|
||||||
|
|
||||||
# This location is for server logs with classical UNIX logrotate behavior
|
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
|
||||||
|
|
||||||
# A comma separated string that defines the available runTimes.
|
# A comma separated string that defines the available runTimes.
|
||||||
# Priority is given to the runtime that comes first in the string.
|
# Priority is given to the runtime that comes first in the string.
|
||||||
# Possible options at the moment are sas and js
|
# Possible options at the moment are sas and js
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
|||||||
|
|
||||||
PROTOCOL=[http|https] default considered as http
|
PROTOCOL=[http|https] default considered as http
|
||||||
PRIVATE_KEY=privkey.pem
|
PRIVATE_KEY=privkey.pem
|
||||||
CERT_CHAIN=certificate.pem
|
FULL_CHAIN=fullchain.pem
|
||||||
CA_ROOT=fullchain.pem
|
|
||||||
|
|
||||||
PORT=[5000] default value is 5000
|
PORT=[5000] default value is 5000
|
||||||
|
|
||||||
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
||||||
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
||||||
|
|
||||||
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
|
REFRESH_TOKEN_SECRET=<secret>
|
||||||
|
AUTH_CODE_SECRET=<secret>
|
||||||
|
SESSION_SECRET=<secret>
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
|
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
|
||||||
@@ -20,5 +23,4 @@ NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
|||||||
|
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
LOG_FORMAT_MORGAN=common
|
LOG_FORMAT_MORGAN=common
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
|
||||||
1266
api/package-lock.json
generated
1266
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
|||||||
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
||||||
"prestart": "npm run initial",
|
"prestart": "npm run initial",
|
||||||
"prebuild": "npm run initial",
|
"prebuild": "npm run initial",
|
||||||
"start": "NODE_ENV=development nodemon ./src/server.ts",
|
"start": "nodemon ./src/server.ts",
|
||||||
"start:prod": "node ./build/src/server.js",
|
"start:prod": "node ./build/src/server.js",
|
||||||
"build": "rimraf build && tsc",
|
"build": "rimraf build && tsc",
|
||||||
"postbuild": "npm run copy:files",
|
"postbuild": "npm run copy:files",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
},
|
},
|
||||||
"author": "4GL Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^4.31.3",
|
"@sasjs/core": "^4.27.3",
|
||||||
"@sasjs/utils": "2.42.1",
|
"@sasjs/utils": "2.42.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"connect-mongo": "^4.6.0",
|
"connect-mongo": "^4.6.0",
|
||||||
@@ -63,7 +63,6 @@
|
|||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
"rotating-file-stream": "^3.0.4",
|
|
||||||
"swagger-ui-express": "4.3.0",
|
"swagger-ui-express": "4.3.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
"url": "^0.10.3"
|
"url": "^0.10.3"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,12 @@ import {
|
|||||||
readFile,
|
readFile,
|
||||||
SASJsFileType
|
SASJsFileType
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
import { apiRoot, sysInitCompiledPath } from '../src/utils/file'
|
import { apiRoot, sysInitCompiledPath } from '../src/utils'
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
const compiledSystemInit = async (systemInit: string) =>
|
const compiledSystemInit = async (systemInit: string) =>
|
||||||
'options ls=max ps=max;\n' +
|
'options ps=max;\n' +
|
||||||
(await loadDependenciesFile({
|
(await loadDependenciesFile({
|
||||||
fileContent: systemInit,
|
fileContent: systemInit,
|
||||||
type: SASJsFileType.job,
|
type: SASJsFileType.job,
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import {
|
|||||||
listFilesInFolder
|
listFilesInFolder
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
import {
|
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
|
||||||
apiRoot,
|
|
||||||
sasJSCoreMacros,
|
|
||||||
sasJSCoreMacrosInfo
|
|
||||||
} from '../src/utils/file'
|
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Express } from 'express'
|
|
||||||
import cors from 'cors'
|
|
||||||
import { CorsType } from '../utils'
|
|
||||||
|
|
||||||
export const configureCors = (app: Express) => {
|
|
||||||
const { CORS, WHITELIST } = process.env
|
|
||||||
|
|
||||||
if (CORS === CorsType.ENABLED) {
|
|
||||||
const whiteList: string[] = []
|
|
||||||
WHITELIST?.split(' ')
|
|
||||||
?.filter((url) => !!url)
|
|
||||||
.forEach((url) => {
|
|
||||||
if (url.startsWith('http'))
|
|
||||||
// removing trailing slash of URLs listing for CORS
|
|
||||||
whiteList.push(url.replace(/\/$/, ''))
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('All CORS Requests are enabled for:', whiteList)
|
|
||||||
app.use(cors({ credentials: true, origin: whiteList }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Express } from 'express'
|
|
||||||
import mongoose from 'mongoose'
|
|
||||||
import session from 'express-session'
|
|
||||||
import MongoStore from 'connect-mongo'
|
|
||||||
|
|
||||||
import { ModeType } from '../utils'
|
|
||||||
import { cookieOptions } from '../app'
|
|
||||||
|
|
||||||
export const configureExpressSession = (app: Express) => {
|
|
||||||
const { MODE } = process.env
|
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
|
||||||
let store: MongoStore | undefined
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
|
||||||
store = MongoStore.create({
|
|
||||||
client: mongoose.connection!.getClient() as any,
|
|
||||||
collectionName: 'sessions'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
session({
|
|
||||||
secret: process.secrets.SESSION_SECRET,
|
|
||||||
saveUninitialized: false, // don't create session until something stored
|
|
||||||
resave: false, //don't save session if unmodified
|
|
||||||
store,
|
|
||||||
cookie: cookieOptions
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
import { Express } from 'express'
|
|
||||||
import morgan from 'morgan'
|
|
||||||
import { createStream } from 'rotating-file-stream'
|
|
||||||
import { generateTimestamp } from '@sasjs/utils'
|
|
||||||
import { getLogFolder } from '../utils'
|
|
||||||
|
|
||||||
export const configureLogger = (app: Express) => {
|
|
||||||
const { LOG_FORMAT_MORGAN } = process.env
|
|
||||||
|
|
||||||
let options
|
|
||||||
if (
|
|
||||||
process.env.NODE_ENV !== 'development' &&
|
|
||||||
process.env.NODE_ENV !== 'test'
|
|
||||||
) {
|
|
||||||
const timestamp = generateTimestamp()
|
|
||||||
const filename = `${timestamp}.log`
|
|
||||||
const logsFolder = getLogFolder()
|
|
||||||
|
|
||||||
// create a rotating write stream
|
|
||||||
var accessLogStream = createStream(filename, {
|
|
||||||
interval: '1d', // rotate daily
|
|
||||||
path: logsFolder
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('Writing Logs to :', path.join(logsFolder, filename))
|
|
||||||
|
|
||||||
options = { stream: accessLogStream }
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup the logger
|
|
||||||
app.use(morgan(LOG_FORMAT_MORGAN as string, options))
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Express } from 'express'
|
|
||||||
import { getEnvCSPDirectives } from '../utils/parseHelmetConfig'
|
|
||||||
import { HelmetCoepType, ProtocolType } from '../utils'
|
|
||||||
import helmet from 'helmet'
|
|
||||||
|
|
||||||
export const configureSecurity = (app: Express) => {
|
|
||||||
const { PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } = process.env
|
|
||||||
|
|
||||||
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
|
||||||
HELMET_CSP_CONFIG_PATH
|
|
||||||
)
|
|
||||||
if (PROTOCOL === ProtocolType.HTTP)
|
|
||||||
cspConfigJson['upgrade-insecure-requests'] = null
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
helmet({
|
|
||||||
contentSecurityPolicy: {
|
|
||||||
directives: {
|
|
||||||
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
|
||||||
...cspConfigJson
|
|
||||||
}
|
|
||||||
},
|
|
||||||
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './configureCors'
|
|
||||||
export * from './configureExpressSession'
|
|
||||||
export * from './configureLogger'
|
|
||||||
export * from './configureSecurity'
|
|
||||||
122
api/src/app.ts
122
api/src/app.ts
@@ -1,26 +1,30 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { ErrorRequestHandler } from 'express'
|
import express, { ErrorRequestHandler } from 'express'
|
||||||
import csrf from 'csurf'
|
import csrf from 'csurf'
|
||||||
|
import session from 'express-session'
|
||||||
|
import MongoStore from 'connect-mongo'
|
||||||
|
import morgan from 'morgan'
|
||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
import cors from 'cors'
|
||||||
|
import helmet from 'helmet'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
connectDB,
|
||||||
copySASjsCore,
|
copySASjsCore,
|
||||||
|
CorsType,
|
||||||
getWebBuildFolder,
|
getWebBuildFolder,
|
||||||
|
HelmetCoepType,
|
||||||
instantiateLogger,
|
instantiateLogger,
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
|
ModeType,
|
||||||
ProtocolType,
|
ProtocolType,
|
||||||
ReturnCode,
|
ReturnCode,
|
||||||
setProcessVariables,
|
setProcessVariables,
|
||||||
setupFolders,
|
setupFolders,
|
||||||
verifyEnvVariables
|
verifyEnvVariables
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import {
|
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
|
||||||
configureCors,
|
|
||||||
configureExpressSession,
|
|
||||||
configureLogger,
|
|
||||||
configureSecurity
|
|
||||||
} from './app-modules'
|
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
@@ -30,7 +34,19 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
|||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { PROTOCOL } = process.env
|
app.use(cookieParser())
|
||||||
|
|
||||||
|
const {
|
||||||
|
MODE,
|
||||||
|
CORS,
|
||||||
|
WHITELIST,
|
||||||
|
PROTOCOL,
|
||||||
|
HELMET_CSP_CONFIG_PATH,
|
||||||
|
HELMET_COEP,
|
||||||
|
LOG_FORMAT_MORGAN
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
app.use(morgan(LOG_FORMAT_MORGAN as string))
|
||||||
|
|
||||||
export const cookieOptions = {
|
export const cookieOptions = {
|
||||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||||
@@ -38,11 +54,79 @@ export const cookieOptions = {
|
|||||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
||||||
|
HELMET_CSP_CONFIG_PATH
|
||||||
|
)
|
||||||
|
if (PROTOCOL === ProtocolType.HTTP)
|
||||||
|
cspConfigJson['upgrade-insecure-requests'] = null
|
||||||
|
|
||||||
/***********************************
|
/***********************************
|
||||||
* CSRF Protection *
|
* CSRF Protection *
|
||||||
***********************************/
|
***********************************/
|
||||||
export const csrfProtection = csrf({ cookie: cookieOptions })
|
export const csrfProtection = csrf({ cookie: cookieOptions })
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Handle security and origin *
|
||||||
|
***********************************/
|
||||||
|
app.use(
|
||||||
|
helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
||||||
|
...cspConfigJson
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Enabling CORS *
|
||||||
|
***********************************/
|
||||||
|
if (CORS === CorsType.ENABLED) {
|
||||||
|
const whiteList: string[] = []
|
||||||
|
WHITELIST?.split(' ')
|
||||||
|
?.filter((url) => !!url)
|
||||||
|
.forEach((url) => {
|
||||||
|
if (url.startsWith('http'))
|
||||||
|
// removing trailing slash of URLs listing for CORS
|
||||||
|
whiteList.push(url.replace(/\/$/, ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('All CORS Requests are enabled for:', whiteList)
|
||||||
|
app.use(cors({ credentials: true, origin: whiteList }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* DB Connection & *
|
||||||
|
* Express Sessions *
|
||||||
|
* With Mongo Store *
|
||||||
|
***********************************/
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
let store: MongoStore | undefined
|
||||||
|
|
||||||
|
// NOTE: when exporting app.js as agent for supertest
|
||||||
|
// we should exclude connecting to the real database
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
||||||
|
|
||||||
|
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: process.env.SESSION_SECRET as string,
|
||||||
|
saveUninitialized: false, // don't create session until something stored
|
||||||
|
resave: false, //don't save session if unmodified
|
||||||
|
store,
|
||||||
|
cookie: cookieOptions
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(express.json({ limit: '100mb' }))
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
if (err.code === 'EBADCSRFTOKEN')
|
if (err.code === 'EBADCSRFTOKEN')
|
||||||
return res.status(400).send('Invalid CSRF token!')
|
return res.status(400).send('Invalid CSRF token!')
|
||||||
@@ -52,30 +136,6 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default setProcessVariables().then(async () => {
|
export default setProcessVariables().then(async () => {
|
||||||
app.use(cookieParser())
|
|
||||||
|
|
||||||
configureLogger(app)
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* Handle security and origin *
|
|
||||||
***********************************/
|
|
||||||
configureSecurity(app)
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* Enabling CORS *
|
|
||||||
***********************************/
|
|
||||||
configureCors(app)
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* DB Connection & *
|
|
||||||
* Express Sessions *
|
|
||||||
* With Mongo Store *
|
|
||||||
***********************************/
|
|
||||||
configureExpressSession(app)
|
|
||||||
|
|
||||||
app.use(express.json({ limit: '100mb' }))
|
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
|
||||||
|
|
||||||
await setupFolders()
|
await setupFolders()
|
||||||
await copySASjsCore()
|
await copySASjsCore()
|
||||||
|
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ const verifyAuthCode = async (
|
|||||||
clientId: string,
|
clientId: string,
|
||||||
code: string
|
code: string
|
||||||
): Promise<InfoJWT | undefined> => {
|
): Promise<InfoJWT | undefined> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
jwt.verify(code, process.secrets.AUTH_CODE_SECRET, (err, data) => {
|
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
|
||||||
if (err) return resolve(undefined)
|
if (err) return resolve(undefined)
|
||||||
|
|
||||||
const clientInfo: InfoJWT = {
|
const clientInfo: InfoJWT = {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
moveFile,
|
moveFile,
|
||||||
createFolder,
|
createFolder,
|
||||||
deleteFile as deleteFileOnSystem,
|
deleteFile as deleteFileOnSystem,
|
||||||
deleteFolder as deleteFolderOnSystem,
|
|
||||||
folderExists,
|
folderExists,
|
||||||
listFilesInFolder,
|
listFilesInFolder,
|
||||||
listSubFoldersInFolder,
|
listSubFoldersInFolder,
|
||||||
@@ -59,32 +58,11 @@ interface GetFileTreeResponse {
|
|||||||
tree: TreeNode
|
tree: TreeNode
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileFolderResponse {
|
interface UpdateFileResponse {
|
||||||
status: string
|
status: string
|
||||||
message?: 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 fileTreeExample = getTreeExample()
|
||||||
|
|
||||||
const successDeployResponse: DeployResponse = {
|
const successDeployResponse: DeployResponse = {
|
||||||
@@ -165,7 +143,7 @@ export class DriveController {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @summary Delete file from SASjs Drive
|
* @summary Delete file from SASjs Drive
|
||||||
* @query _filePath Location of file
|
* @query _filePath Location of SAS program
|
||||||
* @example _filePath "/Public/somefolder/some.file"
|
* @example _filePath "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
@Delete('/file')
|
@Delete('/file')
|
||||||
@@ -173,31 +151,20 @@ export class DriveController {
|
|||||||
return deleteFile(_filePath)
|
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
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
* Or provide `filePath` in body as form field.
|
* Or provide `filePath` in body as form field.
|
||||||
* But it's required to provide else API will respond with Bad Request.
|
* But it's required to provide else API will respond with Bad Request.
|
||||||
*
|
*
|
||||||
* @summary Create a file in SASjs Drive
|
* @summary Create a file in SASjs Drive
|
||||||
* @param _filePath Location of file
|
* @param _filePath Location of SAS program
|
||||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<FileFolderResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<FileFolderResponse>(403, 'File already exists', {
|
@Response<UpdateFileResponse>(403, 'File already exists', {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@@ -206,28 +173,10 @@ export class DriveController {
|
|||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Query() _filePath?: string,
|
@Query() _filePath?: string,
|
||||||
@FormField() filePath?: string
|
@FormField() filePath?: string
|
||||||
): Promise<FileFolderResponse> {
|
): Promise<UpdateFileResponse> {
|
||||||
return saveFile((_filePath ?? filePath)!, file)
|
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
|
* It's optional to either provide `_filePath` in url as query parameter
|
||||||
* Or provide `filePath` in body as form field.
|
* Or provide `filePath` in body as form field.
|
||||||
@@ -238,10 +187,10 @@ export class DriveController {
|
|||||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<FileFolderResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<FileFolderResponse>(403, `File doesn't exist`, {
|
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@@ -250,28 +199,10 @@ export class DriveController {
|
|||||||
@UploadedFile() file: Express.Multer.File,
|
@UploadedFile() file: Express.Multer.File,
|
||||||
@Query() _filePath?: string,
|
@Query() _filePath?: string,
|
||||||
@FormField() filePath?: string
|
@FormField() filePath?: string
|
||||||
): Promise<FileFolderResponse> {
|
): Promise<UpdateFileResponse> {
|
||||||
return updateFile((_filePath ?? filePath)!, file)
|
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.
|
* @summary Fetch file tree within SASjs Drive.
|
||||||
*
|
*
|
||||||
@@ -318,26 +249,20 @@ const getFile = async (req: express.Request, filePath: string) => {
|
|||||||
.join(getFilesFolder(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath))
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
throw {
|
throw new Error('Cannot get file outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't get file outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull)))
|
if (!(await fileExists(filePathFull))) {
|
||||||
throw {
|
throw new Error("File doesn't exist.")
|
||||||
code: 404,
|
}
|
||||||
status: 'Not Found',
|
|
||||||
message: `File doesn't exist.`
|
|
||||||
}
|
|
||||||
|
|
||||||
const extension = path.extname(filePathFull).toLowerCase()
|
const extension = path.extname(filePathFull).toLowerCase()
|
||||||
if (extension === '.sas') {
|
if (extension === '.sas') {
|
||||||
req.res?.setHeader('Content-type', 'text/plain')
|
req.res?.setHeader('Content-type', 'text/plain')
|
||||||
}
|
}
|
||||||
|
|
||||||
req.res?.sendFile(path.resolve(filePathFull), { dotfiles: 'allow' })
|
req.res?.sendFile(path.resolve(filePathFull))
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFolder = async (folderPath?: string) => {
|
const getFolder = async (folderPath?: string) => {
|
||||||
@@ -348,26 +273,17 @@ const getFolder = async (folderPath?: string) => {
|
|||||||
.join(getFilesFolder(), folderPath)
|
.join(getFilesFolder(), folderPath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!folderPathFull.includes(driveFilesPath))
|
if (!folderPathFull.includes(driveFilesPath)) {
|
||||||
throw {
|
throw new Error('Cannot get folder outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't get folder outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await folderExists(folderPathFull)))
|
if (!(await folderExists(folderPathFull))) {
|
||||||
throw {
|
throw new Error("Folder doesn't exist.")
|
||||||
code: 404,
|
}
|
||||||
status: 'Not Found',
|
|
||||||
message: `Folder doesn't exist.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await isFolder(folderPathFull)))
|
if (!(await isFolder(folderPathFull))) {
|
||||||
throw {
|
throw new Error('Not a Folder.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: 'Not a Folder.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const files: string[] = await listFilesInFolder(folderPathFull)
|
const files: string[] = await listFilesInFolder(folderPathFull)
|
||||||
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
||||||
@@ -386,51 +302,19 @@ const deleteFile = async (filePath: string) => {
|
|||||||
.join(getFilesFolder(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath))
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
throw {
|
throw new Error('Cannot delete file outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't delete file outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull)))
|
if (!(await fileExists(filePathFull))) {
|
||||||
throw {
|
throw new Error('File does not exist.')
|
||||||
code: 404,
|
}
|
||||||
status: 'Not Found',
|
|
||||||
message: `File doesn't exist.`
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteFileOnSystem(filePathFull)
|
await deleteFileOnSystem(filePathFull)
|
||||||
|
|
||||||
return { status: 'success' }
|
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 (
|
const saveFile = async (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
@@ -441,19 +325,13 @@ const saveFile = async (
|
|||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath))
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
throw {
|
throw new Error('Cannot put file outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't put file outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await fileExists(filePathFull))
|
if (await fileExists(filePathFull)) {
|
||||||
throw {
|
throw new Error('File already exists.')
|
||||||
code: 409,
|
}
|
||||||
status: 'Conflict',
|
|
||||||
message: 'File already exists.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const folderPath = path.dirname(filePathFull)
|
const folderPath = path.dirname(filePathFull)
|
||||||
await createFolder(folderPath)
|
await createFolder(folderPath)
|
||||||
@@ -462,88 +340,6 @@ const saveFile = async (
|
|||||||
return { status: 'success' }
|
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 (
|
const updateFile = async (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
@@ -554,19 +350,13 @@ const updateFile = async (
|
|||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath))
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
throw {
|
throw new Error('Cannot modify file outside drive.')
|
||||||
code: 400,
|
}
|
||||||
status: 'Bad Request',
|
|
||||||
message: `Can't modify file outside drive.`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull)))
|
if (!(await fileExists(filePathFull))) {
|
||||||
throw {
|
throw new Error(`File doesn't exist.`)
|
||||||
code: 404,
|
}
|
||||||
status: 'Not Found',
|
|
||||||
message: `File doesn't exist.`
|
|
||||||
}
|
|
||||||
|
|
||||||
await moveFile(multerFile.path, filePathFull)
|
await moveFile(multerFile.path, filePathFull)
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface GroupResponse {
|
|||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupDetailsResponse {
|
interface GroupDetailsResponse {
|
||||||
groupId: number
|
groupId: number
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
@@ -198,7 +198,7 @@ const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
|
|||||||
'groupId name description isActive users -_id'
|
'groupId name description isActive users -_id'
|
||||||
).populate(
|
).populate(
|
||||||
'users',
|
'users',
|
||||||
'id username displayName isAdmin -_id'
|
'id username displayName -_id'
|
||||||
)) as unknown as GroupDetailsResponse
|
)) as unknown as GroupDetailsResponse
|
||||||
if (!group)
|
if (!group)
|
||||||
throw {
|
throw {
|
||||||
@@ -249,10 +249,9 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'User not found.'
|
message: 'User not found.'
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedGroup =
|
const updatedGroup = (action === 'addUser'
|
||||||
action === 'addUser'
|
? await group.addUser(user._id)
|
||||||
? await group.addUser(user)
|
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
|
||||||
: await group.removeUser(user)
|
|
||||||
|
|
||||||
if (!updatedGroup)
|
if (!updatedGroup)
|
||||||
throw {
|
throw {
|
||||||
@@ -261,6 +260,9 @@ const updateUsersListInGroup = async (
|
|||||||
message: 'Unable to update group.'
|
message: 'Unable to update group.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === 'addUser') user.addGroup(group._id)
|
||||||
|
else user.removeGroup(group._id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupId: updatedGroup.groupId,
|
groupId: updatedGroup.groupId,
|
||||||
name: updatedGroup.name,
|
name: updatedGroup.name,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export * from './code'
|
|||||||
export * from './drive'
|
export * from './drive'
|
||||||
export * from './group'
|
export * from './group'
|
||||||
export * from './info'
|
export * from './info'
|
||||||
export * from './permission'
|
|
||||||
export * from './session'
|
export * from './session'
|
||||||
export * from './stp'
|
export * from './stp'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { Route, Tags, Example, Get } from 'tsoa'
|
import { Route, Tags, Example, Get } from 'tsoa'
|
||||||
import { getAuthorizedRoutes } from '../utils'
|
|
||||||
export interface AuthorizedRoutesResponse {
|
|
||||||
URIs: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InfoResponse {
|
export interface InfoResponse {
|
||||||
mode: string
|
mode: string
|
||||||
@@ -40,19 +36,4 @@ export class InfoController {
|
|||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get authorized routes.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<AuthorizedRoutesResponse>({
|
|
||||||
URIs: ['/AppStream', '/SASjsApi/stp/execute']
|
|
||||||
})
|
|
||||||
@Get('/authorizedRoutes')
|
|
||||||
public authorizedRoutes(): AuthorizedRoutesResponse {
|
|
||||||
const response = {
|
|
||||||
URIs: getAuthorizedRoutes()
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { getSessionController, processProgram } from './'
|
import {
|
||||||
|
getSASSessionController,
|
||||||
|
getJSSessionController,
|
||||||
|
processProgram
|
||||||
|
} from './'
|
||||||
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
|
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
|
||||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||||
import {
|
import {
|
||||||
@@ -72,7 +76,10 @@ export class ExecutionController {
|
|||||||
session: sessionByFileUpload,
|
session: sessionByFileUpload,
|
||||||
runTime
|
runTime
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||||
const sessionController = getSessionController(runTime)
|
const sessionController =
|
||||||
|
runTime === RunTimeType.SAS
|
||||||
|
? getSASSessionController()
|
||||||
|
: getJSSessionController()
|
||||||
|
|
||||||
const session =
|
const session =
|
||||||
sessionByFileUpload ?? (await sessionController.getSession())
|
sessionByFileUpload ?? (await sessionController.getSession())
|
||||||
@@ -143,7 +150,6 @@ export class ExecutionController {
|
|||||||
name: 'files',
|
name: 'files',
|
||||||
relativePath: '',
|
relativePath: '',
|
||||||
absolutePath: getFilesFolder(),
|
absolutePath: getFilesFolder(),
|
||||||
isFolder: true,
|
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,22 +159,15 @@ export class ExecutionController {
|
|||||||
const currentNode = stack.pop()
|
const currentNode = stack.pop()
|
||||||
|
|
||||||
if (currentNode) {
|
if (currentNode) {
|
||||||
currentNode.isFolder = fs
|
|
||||||
.statSync(currentNode.absolutePath)
|
|
||||||
.isDirectory()
|
|
||||||
|
|
||||||
const children = fs.readdirSync(currentNode.absolutePath)
|
const children = fs.readdirSync(currentNode.absolutePath)
|
||||||
|
|
||||||
for (let child of children) {
|
for (let child of children) {
|
||||||
const absoluteChildPath = path.join(currentNode.absolutePath, child)
|
const absoluteChildPath = `${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 relativeChildPath = `${currentNode.relativePath}/${child}`
|
||||||
const childNode: TreeNode = {
|
const childNode: TreeNode = {
|
||||||
name: child,
|
name: child,
|
||||||
relativePath: relativeChildPath,
|
relativePath: relativeChildPath,
|
||||||
absolutePath: absoluteChildPath,
|
absolutePath: absoluteChildPath,
|
||||||
isFolder: false,
|
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
currentNode.children.push(childNode)
|
currentNode.children.push(childNode)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, RequestHandler } from 'express'
|
import { Request, RequestHandler } from 'express'
|
||||||
import multer from 'multer'
|
import multer from 'multer'
|
||||||
import { uuidv4 } from '@sasjs/utils'
|
import { uuidv4 } from '@sasjs/utils'
|
||||||
import { getSessionController } from '.'
|
import { getSASSessionController, getJSSessionController } from '.'
|
||||||
import {
|
import {
|
||||||
executeProgramRawValidation,
|
executeProgramRawValidation,
|
||||||
getRunTimeAndFilePath,
|
getRunTimeAndFilePath,
|
||||||
@@ -37,23 +37,17 @@ export class FileUploadController {
|
|||||||
try {
|
try {
|
||||||
;({ runTime } = await getRunTimeAndFilePath(programPath))
|
;({ runTime } = await getRunTimeAndFilePath(programPath))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return res.status(400).send({
|
res.status(400).send({
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'Job execution failed',
|
message: 'Job execution failed',
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
error: typeof err === 'object' ? err.toString() : err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let sessionController
|
const sessionController =
|
||||||
try {
|
runTime === RunTimeType.SAS
|
||||||
sessionController = getSessionController(runTime)
|
? getSASSessionController()
|
||||||
} catch (err: any) {
|
: getJSSessionController()
|
||||||
return res.status(400).send({
|
|
||||||
status: 'failure',
|
|
||||||
message: err.message,
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await sessionController.getSession()
|
const session = await sessionController.getSession()
|
||||||
// marking consumed true, so that it's not available
|
// marking consumed true, so that it's not available
|
||||||
|
|||||||
@@ -5,16 +5,14 @@ import { execFile } from 'child_process'
|
|||||||
import {
|
import {
|
||||||
getSessionsFolder,
|
getSessionsFolder,
|
||||||
generateUniqueFileName,
|
generateUniqueFileName,
|
||||||
sysInitCompiledPath,
|
sysInitCompiledPath
|
||||||
RunTimeType
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import {
|
import {
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
createFile,
|
createFile,
|
||||||
fileExists,
|
fileExists,
|
||||||
generateTimestamp,
|
generateTimestamp,
|
||||||
readFile,
|
readFile
|
||||||
isWindows
|
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
@@ -90,7 +88,7 @@ ${autoExecContent}`
|
|||||||
|
|
||||||
// Additional windows specific options to avoid the desktop popups.
|
// Additional windows specific options to avoid the desktop popups.
|
||||||
|
|
||||||
execFilePromise(process.sasLoc!, [
|
execFilePromise(process.sasLoc, [
|
||||||
'-SYSIN',
|
'-SYSIN',
|
||||||
codePath,
|
codePath,
|
||||||
'-LOG',
|
'-LOG',
|
||||||
@@ -101,12 +99,9 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
process.platform === 'win32' ? '-nosplash' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
process.platform === 'win32' ? '-icon' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
process.platform === 'win32' ? '-nologo' : ''
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
|
||||||
isWindows() ? '-nologo' : ''
|
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
session.completed = true
|
session.completed = true
|
||||||
@@ -197,21 +192,7 @@ export class JSSessionController extends SessionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSessionController = (
|
export const getSASSessionController = (): SASSessionController => {
|
||||||
runTime: RunTimeType
|
|
||||||
): SASSessionController | JSSessionController => {
|
|
||||||
if (runTime === RunTimeType.SAS) {
|
|
||||||
return getSASSessionController()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runTime === RunTimeType.JS) {
|
|
||||||
return getJSSessionController()
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('No Runtime is configured')
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSASSessionController = (): SASSessionController => {
|
|
||||||
if (process.sasSessionController) return process.sasSessionController
|
if (process.sasSessionController) return process.sasSessionController
|
||||||
|
|
||||||
process.sasSessionController = new SASSessionController()
|
process.sasSessionController = new SASSessionController()
|
||||||
@@ -219,7 +200,7 @@ const getSASSessionController = (): SASSessionController => {
|
|||||||
return process.sasSessionController
|
return process.sasSessionController
|
||||||
}
|
}
|
||||||
|
|
||||||
const getJSSessionController = (): JSSessionController => {
|
export const getJSSessionController = (): JSSessionController => {
|
||||||
if (process.jsSessionController) return process.jsSessionController
|
if (process.jsSessionController) return process.jsSessionController
|
||||||
|
|
||||||
process.jsSessionController = new JSSessionController()
|
process.jsSessionController = new JSSessionController()
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { isWindows } from '@sasjs/utils'
|
|
||||||
import { PreProgramVars, Session } from '../../types'
|
import { PreProgramVars, Session } from '../../types'
|
||||||
import { generateFileUploadJSCode } from '../../utils'
|
import { generateFileUploadJSCode } from '../../utils'
|
||||||
import { ExecutionVars } from './'
|
import { ExecutionVars } from './'
|
||||||
@@ -21,11 +20,11 @@ export const createJSProgram = async (
|
|||||||
const preProgramVarStatments = `
|
const preProgramVarStatments = `
|
||||||
let _webout = '';
|
let _webout = '';
|
||||||
const weboutPath = '${
|
const weboutPath = '${
|
||||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
process.platform === 'win32'
|
||||||
|
? weboutPath.replace(/\\/g, '\\\\')
|
||||||
|
: weboutPath
|
||||||
}';
|
}';
|
||||||
const _sasjs_tokenfile = '${
|
const _sasjs_tokenfile = '${tokenFile}';
|
||||||
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
|
||||||
}';
|
|
||||||
const _sasjs_username = '${preProgramVariables?.username}';
|
const _sasjs_username = '${preProgramVariables?.username}';
|
||||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
const _sasjs_userid = '${preProgramVariables?.userId}';
|
||||||
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const processProgram = async (
|
|||||||
// waiting for the open event so that we can have underlying file descriptor
|
// waiting for the open event so that we can have underlying file descriptor
|
||||||
await once(writeStream, 'open')
|
await once(writeStream, 'open')
|
||||||
|
|
||||||
execFileSync(process.nodeLoc!, [codePath], {
|
execFileSync(process.nodeLoc, [codePath], {
|
||||||
stdio: ['ignore', writeStream, writeStream]
|
stdio: ['ignore', writeStream, writeStream]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,331 +0,0 @@
|
|||||||
import {
|
|
||||||
Security,
|
|
||||||
Route,
|
|
||||||
Tags,
|
|
||||||
Path,
|
|
||||||
Example,
|
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Patch,
|
|
||||||
Delete,
|
|
||||||
Body
|
|
||||||
} from 'tsoa'
|
|
||||||
|
|
||||||
import Permission from '../model/Permission'
|
|
||||||
import User from '../model/User'
|
|
||||||
import Group from '../model/Group'
|
|
||||||
import { UserResponse } from './user'
|
|
||||||
import { GroupDetailsResponse } from './group'
|
|
||||||
|
|
||||||
export enum PrincipalType {
|
|
||||||
user = 'user',
|
|
||||||
group = 'group'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PermissionSetting {
|
|
||||||
grant = 'Grant',
|
|
||||||
deny = 'Deny'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegisterPermissionPayload {
|
|
||||||
/**
|
|
||||||
* Name of affected resource
|
|
||||||
* @example "/SASjsApi/code/execute"
|
|
||||||
*/
|
|
||||||
uri: string
|
|
||||||
/**
|
|
||||||
* The indication of whether (and to what extent) access is provided
|
|
||||||
* @example "Grant"
|
|
||||||
*/
|
|
||||||
setting: PermissionSetting
|
|
||||||
/**
|
|
||||||
* Indicates the type of principal
|
|
||||||
* @example "user"
|
|
||||||
*/
|
|
||||||
principalType: PrincipalType
|
|
||||||
/**
|
|
||||||
* The id of user or group to which a rule is assigned.
|
|
||||||
* @example 123
|
|
||||||
*/
|
|
||||||
principalId: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdatePermissionPayload {
|
|
||||||
/**
|
|
||||||
* The indication of whether (and to what extent) access is provided
|
|
||||||
* @example "Grant"
|
|
||||||
*/
|
|
||||||
setting: PermissionSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PermissionDetailsResponse {
|
|
||||||
permissionId: number
|
|
||||||
uri: string
|
|
||||||
setting: string
|
|
||||||
user?: UserResponse
|
|
||||||
group?: GroupDetailsResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
|
||||||
@Route('SASjsApi/permission')
|
|
||||||
@Tags('Permission')
|
|
||||||
export class PermissionController {
|
|
||||||
/**
|
|
||||||
* @summary Get list of all permissions (uri, setting and userDetail).
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<PermissionDetailsResponse[]>([
|
|
||||||
{
|
|
||||||
permissionId: 123,
|
|
||||||
uri: '/SASjsApi/code/execute',
|
|
||||||
setting: 'Grant',
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
username: 'johnSnow01',
|
|
||||||
displayName: 'John Snow',
|
|
||||||
isAdmin: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
permissionId: 124,
|
|
||||||
uri: '/SASjsApi/code/execute',
|
|
||||||
setting: 'Grant',
|
|
||||||
group: {
|
|
||||||
groupId: 1,
|
|
||||||
name: 'DCGroup',
|
|
||||||
description: 'This group represents Data Controller Users',
|
|
||||||
isActive: true,
|
|
||||||
users: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
@Get('/')
|
|
||||||
public async getAllPermissions(): Promise<PermissionDetailsResponse[]> {
|
|
||||||
return getAllPermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Create a new permission. Admin only.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<PermissionDetailsResponse>({
|
|
||||||
permissionId: 123,
|
|
||||||
uri: '/SASjsApi/code/execute',
|
|
||||||
setting: 'Grant',
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
username: 'johnSnow01',
|
|
||||||
displayName: 'John Snow',
|
|
||||||
isAdmin: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@Post('/')
|
|
||||||
public async createPermission(
|
|
||||||
@Body() body: RegisterPermissionPayload
|
|
||||||
): Promise<PermissionDetailsResponse> {
|
|
||||||
return createPermission(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Update permission setting. Admin only
|
|
||||||
* @param permissionId The permission's identifier
|
|
||||||
* @example permissionId 1234
|
|
||||||
*/
|
|
||||||
@Example<PermissionDetailsResponse>({
|
|
||||||
permissionId: 123,
|
|
||||||
uri: '/SASjsApi/code/execute',
|
|
||||||
setting: 'Grant',
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
username: 'johnSnow01',
|
|
||||||
displayName: 'John Snow',
|
|
||||||
isAdmin: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@Patch('{permissionId}')
|
|
||||||
public async updatePermission(
|
|
||||||
@Path() permissionId: number,
|
|
||||||
@Body() body: UpdatePermissionPayload
|
|
||||||
): Promise<PermissionDetailsResponse> {
|
|
||||||
return updatePermission(permissionId, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Delete a permission. Admin only.
|
|
||||||
* @param permissionId The user's identifier
|
|
||||||
* @example permissionId 1234
|
|
||||||
*/
|
|
||||||
@Delete('{permissionId}')
|
|
||||||
public async deletePermission(@Path() permissionId: number) {
|
|
||||||
return deletePermission(permissionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
})) as unknown as PermissionDetailsResponse[]
|
|
||||||
|
|
||||||
const createPermission = async ({
|
|
||||||
uri,
|
|
||||||
setting,
|
|
||||||
principalType,
|
|
||||||
principalId
|
|
||||||
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
|
|
||||||
const permission = new Permission({
|
|
||||||
uri,
|
|
||||||
setting
|
|
||||||
})
|
|
||||||
|
|
||||||
let user: UserResponse | undefined
|
|
||||||
let group: GroupDetailsResponse | undefined
|
|
||||||
|
|
||||||
switch (principalType) {
|
|
||||||
case PrincipalType.user: {
|
|
||||||
const userInDB = await User.findOne({ id: principalId })
|
|
||||||
if (!userInDB)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'User not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userInDB.isAdmin)
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: 'Can not add permission for admin user.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const alreadyExists = await Permission.findOne({
|
|
||||||
uri,
|
|
||||||
user: userInDB._id
|
|
||||||
})
|
|
||||||
|
|
||||||
if (alreadyExists)
|
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
status: 'Conflict',
|
|
||||||
message: 'Permission already exists with provided URI and User.'
|
|
||||||
}
|
|
||||||
|
|
||||||
permission.user = userInDB._id
|
|
||||||
|
|
||||||
user = {
|
|
||||||
id: userInDB.id,
|
|
||||||
username: userInDB.username,
|
|
||||||
displayName: userInDB.displayName,
|
|
||||||
isAdmin: userInDB.isAdmin
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case PrincipalType.group: {
|
|
||||||
const groupInDB = await Group.findOne({ groupId: principalId })
|
|
||||||
if (!groupInDB)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'Group not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const alreadyExists = await Permission.findOne({
|
|
||||||
uri,
|
|
||||||
group: groupInDB._id
|
|
||||||
})
|
|
||||||
if (alreadyExists)
|
|
||||||
throw {
|
|
||||||
code: 409,
|
|
||||||
status: 'Conflict',
|
|
||||||
message: 'Permission already exists with provided URI and Group.'
|
|
||||||
}
|
|
||||||
|
|
||||||
permission.group = groupInDB._id
|
|
||||||
|
|
||||||
group = {
|
|
||||||
groupId: groupInDB.groupId,
|
|
||||||
name: groupInDB.name,
|
|
||||||
description: groupInDB.description,
|
|
||||||
isActive: groupInDB.isActive,
|
|
||||||
users: groupInDB.populate({
|
|
||||||
path: 'users',
|
|
||||||
select: 'id username displayName isAdmin -_id',
|
|
||||||
options: { limit: 15 }
|
|
||||||
}) as unknown as UserResponse[]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'Bad Request',
|
|
||||||
message: 'Invalid principal type. Valid types are user or group.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedPermission = await permission.save()
|
|
||||||
|
|
||||||
return {
|
|
||||||
permissionId: savedPermission.permissionId,
|
|
||||||
uri: savedPermission.uri,
|
|
||||||
setting: savedPermission.setting,
|
|
||||||
user,
|
|
||||||
group
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePermission = async (
|
|
||||||
id: number,
|
|
||||||
data: UpdatePermissionPayload
|
|
||||||
): Promise<PermissionDetailsResponse> => {
|
|
||||||
const { setting } = data
|
|
||||||
|
|
||||||
const updatedPermission = (await Permission.findOneAndUpdate(
|
|
||||||
{ permissionId: id },
|
|
||||||
{ setting },
|
|
||||||
{ new: true }
|
|
||||||
)
|
|
||||||
.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'
|
|
||||||
})) as unknown as PermissionDetailsResponse
|
|
||||||
if (!updatedPermission)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'Permission not found.'
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedPermission
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletePermission = async (id: number) => {
|
|
||||||
const permission = await Permission.findOne({ permissionId: id })
|
|
||||||
if (!permission)
|
|
||||||
throw {
|
|
||||||
code: 404,
|
|
||||||
status: 'Not Found',
|
|
||||||
message: 'Permission not found.'
|
|
||||||
}
|
|
||||||
await Permission.deleteOne({ permissionId: id })
|
|
||||||
}
|
|
||||||
@@ -13,8 +13,7 @@ export class SessionController {
|
|||||||
@Example<UserResponse>({
|
@Example<UserResponse>({
|
||||||
id: 123,
|
id: 123,
|
||||||
username: 'johnusername',
|
username: 'johnusername',
|
||||||
displayName: 'John',
|
displayName: 'John'
|
||||||
isAdmin: false
|
|
||||||
})
|
})
|
||||||
@Get('/')
|
@Get('/')
|
||||||
public async session(
|
public async session(
|
||||||
@@ -27,6 +26,5 @@ export class SessionController {
|
|||||||
const session = (req: express.Request) => ({
|
const session = (req: express.Request) => ({
|
||||||
id: req.user!.userId,
|
id: req.user!.userId,
|
||||||
username: req.user!.username,
|
username: req.user!.username,
|
||||||
displayName: req.user!.displayName,
|
displayName: req.user!.displayName
|
||||||
isAdmin: req.user!.isAdmin
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,10 +24,9 @@ export interface UserResponse {
|
|||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
isAdmin: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserDetailsResponse {
|
interface UserDetailsResponse {
|
||||||
id: number
|
id: number
|
||||||
displayName: string
|
displayName: string
|
||||||
username: string
|
username: string
|
||||||
@@ -49,14 +48,12 @@ export class UserController {
|
|||||||
{
|
{
|
||||||
id: 123,
|
id: 123,
|
||||||
username: 'johnusername',
|
username: 'johnusername',
|
||||||
displayName: 'John',
|
displayName: 'John'
|
||||||
isAdmin: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 456,
|
id: 456,
|
||||||
username: 'starkusername',
|
username: 'starkusername',
|
||||||
displayName: 'Stark',
|
displayName: 'Stark'
|
||||||
isAdmin: true
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@Get('/')
|
@Get('/')
|
||||||
@@ -203,7 +200,7 @@ export class UserController {
|
|||||||
|
|
||||||
const getAllUsers = async (): Promise<UserResponse[]> =>
|
const getAllUsers = async (): Promise<UserResponse[]> =>
|
||||||
await User.find({})
|
await User.find({})
|
||||||
.select({ _id: 0, id: 1, username: 1, displayName: 1, isAdmin: 1 })
|
.select({ _id: 0, id: 1, username: 1, displayName: 1 })
|
||||||
.exec()
|
.exec()
|
||||||
|
|
||||||
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export class WebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Destroy the session stored in cookies
|
* @summary Accept a valid username/password
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Get('/SASLogon/logout')
|
@Get('/logout')
|
||||||
public async logout(@Request() req: express.Request) {
|
public async logout(@Request() req: express.Request) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
req.session.destroy(() => {
|
req.session.destroy(() => {
|
||||||
@@ -99,8 +99,7 @@ const login = async (
|
|||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName
|
||||||
isAdmin: user.isAdmin
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { csrfProtection } from '../app'
|
import { csrfProtection } from '../app'
|
||||||
import {
|
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
|
||||||
fetchLatestAutoExec,
|
|
||||||
ModeType,
|
|
||||||
verifyTokenInDB,
|
|
||||||
isAuthorizingRoute
|
|
||||||
} from '../utils'
|
|
||||||
import { desktopUser } from './desktop'
|
import { desktopUser } from './desktop'
|
||||||
import { authorize } from './authorize'
|
|
||||||
|
|
||||||
export const authenticateAccessToken: RequestHandler = async (
|
export const authenticateAccessToken: RequestHandler = async (
|
||||||
req,
|
req,
|
||||||
@@ -21,10 +15,6 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextFunction = isAuthorizingRoute(req)
|
|
||||||
? () => authorize(req, res, next)
|
|
||||||
: next
|
|
||||||
|
|
||||||
// if request is coming from web and has valid session
|
// if request is coming from web and has valid session
|
||||||
// it can be validated.
|
// it can be validated.
|
||||||
if (req.session?.loggedIn) {
|
if (req.session?.loggedIn) {
|
||||||
@@ -34,7 +24,7 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
if (user) {
|
if (user) {
|
||||||
if (user.isActive) {
|
if (user.isActive) {
|
||||||
req.user = user
|
req.user = user
|
||||||
return csrfProtection(req, res, nextFunction)
|
return csrfProtection(req, res, next)
|
||||||
} else return res.sendStatus(401)
|
} else return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,8 +34,8 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
authenticateToken(
|
authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
nextFunction,
|
next,
|
||||||
process.secrets.ACCESS_TOKEN_SECRET,
|
process.env.ACCESS_TOKEN_SECRET as string,
|
||||||
'accessToken'
|
'accessToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -55,7 +45,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
|||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next,
|
next,
|
||||||
process.secrets.REFRESH_TOKEN_SECRET,
|
process.env.REFRESH_TOKEN_SECRET as string,
|
||||||
'refreshToken'
|
'refreshToken'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -68,7 +58,7 @@ const authenticateToken = (
|
|||||||
tokenType: 'accessToken' | 'refreshToken'
|
tokenType: 'accessToken' | 'refreshToken'
|
||||||
) => {
|
) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE === ModeType.Desktop) {
|
if (MODE?.trim() !== 'server') {
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: 1234,
|
userId: 1234,
|
||||||
clientId: 'desktopModeClientId',
|
clientId: 'desktopModeClientId',
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { RequestHandler } from 'express'
|
|
||||||
import User from '../model/User'
|
|
||||||
import Permission from '../model/Permission'
|
|
||||||
import { PermissionSetting } from '../controllers/permission'
|
|
||||||
import { getUri } from '../utils'
|
|
||||||
|
|
||||||
export const authorize: RequestHandler = async (req, res, next) => {
|
|
||||||
const { user } = req
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
// no need to check for permissions when user is admin
|
|
||||||
if (user.isAdmin) return next()
|
|
||||||
|
|
||||||
const dbUser = await User.findOne({ id: user.userId })
|
|
||||||
if (!dbUser) return res.sendStatus(401)
|
|
||||||
|
|
||||||
const uri = getUri(req)
|
|
||||||
|
|
||||||
// find permission w.r.t user
|
|
||||||
const permission = await Permission.findOne({ uri, user: dbUser._id })
|
|
||||||
|
|
||||||
if (permission) {
|
|
||||||
if (permission.setting === PermissionSetting.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()
|
|
||||||
}
|
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
@@ -2,4 +2,3 @@ export * from './authenticateToken'
|
|||||||
export * from './desktop'
|
export * from './desktop'
|
||||||
export * from './verifyAdmin'
|
export * from './verifyAdmin'
|
||||||
export * from './verifyAdminIfNeeded'
|
export * from './verifyAdminIfNeeded'
|
||||||
export * from './authorize'
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { RequestHandler } from 'express'
|
import { RequestHandler } from 'express'
|
||||||
import { ModeType } from '../utils'
|
|
||||||
|
|
||||||
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE === ModeType.Desktop) return next()
|
if (MODE?.trim() !== 'server') return next()
|
||||||
|
|
||||||
const { user } = req
|
const { user } = req
|
||||||
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import mongoose, { Schema } from 'mongoose'
|
|
||||||
|
|
||||||
export interface ConfigurationType {
|
|
||||||
/**
|
|
||||||
* SecretOrPrivateKey to sign Access Token
|
|
||||||
* @example "someRandomCryptoString"
|
|
||||||
*/
|
|
||||||
ACCESS_TOKEN_SECRET: string
|
|
||||||
/**
|
|
||||||
* SecretOrPrivateKey to sign Refresh Token
|
|
||||||
* @example "someRandomCryptoString"
|
|
||||||
*/
|
|
||||||
REFRESH_TOKEN_SECRET: string
|
|
||||||
/**
|
|
||||||
* SecretOrPrivateKey to sign Auth Code
|
|
||||||
* @example "someRandomCryptoString"
|
|
||||||
*/
|
|
||||||
AUTH_CODE_SECRET: string
|
|
||||||
/**
|
|
||||||
* Secret used to sign the session cookie
|
|
||||||
* @example "someRandomCryptoString"
|
|
||||||
*/
|
|
||||||
SESSION_SECRET: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigurationSchema = new Schema<ConfigurationType>({
|
|
||||||
ACCESS_TOKEN_SECRET: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
REFRESH_TOKEN_SECRET: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
AUTH_CODE_SECRET: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
SESSION_SECRET: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default mongoose.model('Configuration', ConfigurationSchema)
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||||
import { GroupDetailsResponse } from '../controllers'
|
import User from './User'
|
||||||
import User, { IUser } from './User'
|
|
||||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||||
|
|
||||||
export interface GroupPayload {
|
export interface GroupPayload {
|
||||||
@@ -28,9 +27,8 @@ interface IGroupDocument extends GroupPayload, Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IGroup extends IGroupDocument {
|
interface IGroup extends IGroupDocument {
|
||||||
addUser(user: IUser): Promise<GroupDetailsResponse>
|
addUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
||||||
removeUser(user: IUser): Promise<GroupDetailsResponse>
|
removeUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
||||||
hasUser(user: IUser): boolean
|
|
||||||
}
|
}
|
||||||
interface IGroupModel extends Model<IGroup> {}
|
interface IGroupModel extends Model<IGroup> {}
|
||||||
|
|
||||||
@@ -72,31 +70,28 @@ groupSchema.pre('remove', async function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Instance Methods
|
// Instance Methods
|
||||||
groupSchema.method('addUser', async function (user: IUser) {
|
groupSchema.method(
|
||||||
const userObjectId = user._id
|
'addUser',
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
async function (userObjectId: Schema.Types.ObjectId) {
|
||||||
if (userIdIndex === -1) {
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
this.users.push(userObjectId)
|
if (userIdIndex === -1) {
|
||||||
user.addGroup(this._id)
|
this.users.push(userObjectId)
|
||||||
|
}
|
||||||
|
this.markModified('users')
|
||||||
|
return this.save()
|
||||||
}
|
}
|
||||||
this.markModified('users')
|
)
|
||||||
return this.save()
|
groupSchema.method(
|
||||||
})
|
'removeUser',
|
||||||
groupSchema.method('removeUser', async function (user: IUser) {
|
async function (userObjectId: Schema.Types.ObjectId) {
|
||||||
const userObjectId = user._id
|
const userIdIndex = this.users.indexOf(userObjectId)
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
if (userIdIndex > -1) {
|
||||||
if (userIdIndex > -1) {
|
this.users.splice(userIdIndex, 1)
|
||||||
this.users.splice(userIdIndex, 1)
|
}
|
||||||
user.removeGroup(this._id)
|
this.markModified('users')
|
||||||
|
return this.save()
|
||||||
}
|
}
|
||||||
this.markModified('users')
|
)
|
||||||
return this.save()
|
|
||||||
})
|
|
||||||
groupSchema.method('hasUser', function (user: IUser) {
|
|
||||||
const userObjectId = user._id
|
|
||||||
const userIdIndex = this.users.indexOf(userObjectId)
|
|
||||||
return userIdIndex > -1
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
||||||
'Group',
|
'Group',
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
|
||||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
|
||||||
|
|
||||||
interface IPermissionDocument extends Document {
|
|
||||||
uri: string
|
|
||||||
setting: string
|
|
||||||
permissionId: number
|
|
||||||
user: Schema.Types.ObjectId
|
|
||||||
group: Schema.Types.ObjectId
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPermission extends IPermissionDocument {}
|
|
||||||
|
|
||||||
interface IPermissionModel extends Model<IPermission> {}
|
|
||||||
|
|
||||||
const permissionSchema = new Schema<IPermissionDocument>({
|
|
||||||
uri: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
setting: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
user: { type: Schema.Types.ObjectId, ref: 'User' },
|
|
||||||
group: { type: Schema.Types.ObjectId, ref: 'Group' }
|
|
||||||
})
|
|
||||||
|
|
||||||
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
|
|
||||||
|
|
||||||
export const Permission: IPermissionModel = model<
|
|
||||||
IPermission,
|
|
||||||
IPermissionModel
|
|
||||||
>('Permission', permissionSchema)
|
|
||||||
|
|
||||||
export default Permission
|
|
||||||
@@ -35,7 +35,6 @@ export interface UserPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IUserDocument extends UserPayload, Document {
|
interface IUserDocument extends UserPayload, Document {
|
||||||
_id: Schema.Types.ObjectId
|
|
||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
@@ -44,7 +43,7 @@ interface IUserDocument extends UserPayload, Document {
|
|||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUser extends IUserDocument {
|
interface IUser extends IUserDocument {
|
||||||
comparePassword(password: string): boolean
|
comparePassword(password: string): boolean
|
||||||
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ import {
|
|||||||
extractName,
|
extractName,
|
||||||
fileBodyValidation,
|
fileBodyValidation,
|
||||||
fileParamValidation,
|
fileParamValidation,
|
||||||
folderBodyValidation,
|
|
||||||
folderParamValidation,
|
folderParamValidation,
|
||||||
isZipFile,
|
isZipFile
|
||||||
renameBodyValidation
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|
||||||
const controller = new DriveController()
|
const controller = new DriveController()
|
||||||
@@ -121,11 +119,7 @@ driveRouter.get('/file', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await controller.getFile(req, query._filePath)
|
await controller.getFile(req, query._filePath)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -138,11 +132,7 @@ driveRouter.get('/folder', async (req, res) => {
|
|||||||
const response = await controller.getFolder(query._folderPath)
|
const response = await controller.getFolder(query._folderPath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -155,28 +145,7 @@ driveRouter.delete('/file', async (req, res) => {
|
|||||||
const response = await controller.deleteFile(query._filePath)
|
const response = await controller.deleteFile(query._filePath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -203,33 +172,11 @@ driveRouter.post(
|
|||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await deleteFile(req.file.path)
|
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(
|
driveRouter.patch(
|
||||||
'/file',
|
'/file',
|
||||||
(...arg) => multerSingle('file', arg),
|
(...arg) => multerSingle('file', arg),
|
||||||
@@ -253,33 +200,11 @@ driveRouter.patch(
|
|||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
await deleteFile(req.file.path)
|
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) => {
|
driveRouter.get('/fileTree', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const response = await controller.getFileTree()
|
const response = await controller.getFileTree()
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import groupRouter from './group'
|
|||||||
import clientRouter from './client'
|
import clientRouter from './client'
|
||||||
import authRouter from './auth'
|
import authRouter from './auth'
|
||||||
import sessionRouter from './session'
|
import sessionRouter from './session'
|
||||||
import permissionRouter from './permission'
|
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
@@ -36,12 +35,6 @@ router.use('/group', desktopRestrict, groupRouter)
|
|||||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||||
router.use('/code', authenticateAccessToken, codeRouter)
|
router.use('/code', authenticateAccessToken, codeRouter)
|
||||||
router.use('/user', desktopRestrict, userRouter)
|
router.use('/user', desktopRestrict, userRouter)
|
||||||
router.use(
|
|
||||||
'/permission',
|
|
||||||
desktopRestrict,
|
|
||||||
authenticateAccessToken,
|
|
||||||
permissionRouter
|
|
||||||
)
|
|
||||||
|
|
||||||
router.use(
|
router.use(
|
||||||
'/',
|
'/',
|
||||||
|
|||||||
@@ -13,14 +13,4 @@ infoRouter.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
infoRouter.get('/authorizedRoutes', async (req, res) => {
|
|
||||||
const controller = new InfoController()
|
|
||||||
try {
|
|
||||||
const response = controller.authorizedRoutes()
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default infoRouter
|
export default infoRouter
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import { PermissionController } from '../../controllers/'
|
|
||||||
import { verifyAdmin } from '../../middlewares'
|
|
||||||
import {
|
|
||||||
registerPermissionValidation,
|
|
||||||
updatePermissionValidation
|
|
||||||
} from '../../utils'
|
|
||||||
|
|
||||||
const permissionRouter = express.Router()
|
|
||||||
const controller = new PermissionController()
|
|
||||||
|
|
||||||
permissionRouter.get('/', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const response = await controller.getAllPermissions()
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
delete err.code
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
permissionRouter.post('/', verifyAdmin, async (req, res) => {
|
|
||||||
const { error, value: body } = registerPermissionValidation(req.body)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.createPermission(body)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
delete err.code
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
permissionRouter.patch('/:permissionId', verifyAdmin, async (req: any, res) => {
|
|
||||||
const { permissionId } = req.params
|
|
||||||
|
|
||||||
const { error, value: body } = updatePermissionValidation(req.body)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await controller.updatePermission(permissionId, body)
|
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
delete err.code
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
permissionRouter.delete(
|
|
||||||
'/:permissionId',
|
|
||||||
verifyAdmin,
|
|
||||||
async (req: any, res) => {
|
|
||||||
const { permissionId } = req.params
|
|
||||||
|
|
||||||
try {
|
|
||||||
await controller.deletePermission(permissionId)
|
|
||||||
res.status(200).send('Permission Deleted!')
|
|
||||||
} catch (err: any) {
|
|
||||||
const statusCode = err.code
|
|
||||||
delete err.code
|
|
||||||
res.status(statusCode).send(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
export default permissionRouter
|
|
||||||
@@ -29,12 +29,7 @@ jest
|
|||||||
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||||
|
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import {
|
import { UserController } from '../../../controllers/'
|
||||||
UserController,
|
|
||||||
PermissionController,
|
|
||||||
PermissionSetting,
|
|
||||||
PrincipalType
|
|
||||||
} from '../../../controllers/'
|
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||||
const { getFilesFolder } = fileUtilModules
|
const { getFilesFolder } = fileUtilModules
|
||||||
@@ -53,7 +48,6 @@ describe('drive', () => {
|
|||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
const permissionController = new PermissionController()
|
|
||||||
|
|
||||||
let accessToken: string
|
let accessToken: string
|
||||||
|
|
||||||
@@ -64,37 +58,11 @@ describe('drive', () => {
|
|||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
accessToken = await generateAndSaveToken(dbUser.id)
|
accessToken = generateAccessToken({
|
||||||
await permissionController.createPermission({
|
clientId,
|
||||||
uri: '/SASjsApi/drive/deploy',
|
userId: dbUser.id
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: dbUser.id,
|
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
uri: '/SASjsApi/drive/deploy/upload',
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: dbUser.id,
|
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
uri: '/SASjsApi/drive/file',
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: dbUser.id,
|
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
uri: '/SASjsApi/drive/folder',
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: dbUser.id,
|
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
uri: '/SASjsApi/drive/rename',
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: dbUser.id,
|
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
})
|
||||||
|
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -549,29 +517,29 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if folder is not present', async () => {
|
it('should respond with Forbidden if folder is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Folder doesn't exist.`)
|
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if folderPath outside Drive', async () => {
|
it('should respond with Forbidden if folderPath outside Drive', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: '/../path/code.sas' })
|
.query({ _folderPath: '/../path/code.sas' })
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't get folder outside drive.`)
|
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if folderPath is of a file', async () => {
|
it('should respond with Forbidden if folderPath is of a file', async () => {
|
||||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const filePath = '/my/path/code.sas'
|
const filePath = '/my/path/code.sas'
|
||||||
|
|
||||||
@@ -582,96 +550,12 @@ describe('drive', () => {
|
|||||||
.get(getFolderApi)
|
.get(getFolderApi)
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _folderPath: filePath })
|
.query({ _folderPath: filePath })
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Not a Folder.')
|
expect(res.text).toEqual('Error: Not a Folder.')
|
||||||
expect(res.body).toEqual({})
|
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', () => {
|
describe('file', () => {
|
||||||
@@ -717,7 +601,7 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Conflict if file is already present', async () => {
|
it('should respond with Forbidden if file is already present', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||||
|
|
||||||
@@ -732,13 +616,13 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(409)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('File already exists.')
|
expect(res.text).toEqual('Error: File already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if filePath outside Drive', async () => {
|
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = '/../path/code.sas'
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
@@ -747,9 +631,9 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't put file outside drive.`)
|
expect(res.text).toEqual('Error: Cannot put file outside drive.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -884,19 +768,19 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if file is not present', async () => {
|
it('should respond with Forbidden if file is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/drive/file')
|
.patch('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', `/my/path/code-3.sas`)
|
.field('filePath', `/my/path/code-3.sas`)
|
||||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`File doesn't exist.`)
|
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if filePath outside Drive', async () => {
|
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const pathToUpload = '/../path/code.sas'
|
const pathToUpload = '/../path/code.sas'
|
||||||
|
|
||||||
@@ -905,9 +789,9 @@ describe('drive', () => {
|
|||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', pathToUpload)
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't modify file outside drive.`)
|
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1012,25 +896,25 @@ describe('drive', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Not Found if file is not present', async () => {
|
it('should respond with Forbidden if file is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/drive/file')
|
.get('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _filePath: `/my/path/code-4.sas` })
|
.query({ _filePath: `/my/path/code-4.sas` })
|
||||||
.expect(404)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`File doesn't exist.`)
|
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if filePath outside Drive', async () => {
|
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get('/SASjsApi/drive/file')
|
.get('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _filePath: '/../path/code.sas' })
|
.query({ _filePath: '/../path/code.sas' })
|
||||||
.expect(400)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual(`Can't get file outside drive.`)
|
expect(res.text).toEqual('Error: Cannot get file outside drive.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1056,150 +940,8 @@ 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 =>
|
const getExampleService = (): ServiceMember =>
|
||||||
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
||||||
.members[0] as ServiceMember
|
.members[0] as ServiceMember
|
||||||
|
|
||||||
const generateAndSaveToken = async (userId: number) => {
|
|
||||||
const adminAccessToken = generateAccessToken({
|
|
||||||
clientId,
|
|
||||||
userId
|
|
||||||
})
|
|
||||||
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
|
||||||
return adminAccessToken
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,571 +0,0 @@
|
|||||||
import { Express } from 'express'
|
|
||||||
import mongoose, { Mongoose } from 'mongoose'
|
|
||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
|
||||||
import request from 'supertest'
|
|
||||||
import appPromise from '../../../app'
|
|
||||||
import {
|
|
||||||
DriveController,
|
|
||||||
UserController,
|
|
||||||
GroupController,
|
|
||||||
ClientController,
|
|
||||||
PermissionController,
|
|
||||||
PrincipalType,
|
|
||||||
PermissionSetting
|
|
||||||
} from '../../../controllers/'
|
|
||||||
import {
|
|
||||||
UserDetailsResponse,
|
|
||||||
PermissionDetailsResponse
|
|
||||||
} from '../../../controllers'
|
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
|
||||||
|
|
||||||
const deployPayload = {
|
|
||||||
appLoc: 'string',
|
|
||||||
streamWebFolder: 'string',
|
|
||||||
fileTree: {
|
|
||||||
members: [
|
|
||||||
{
|
|
||||||
name: 'string',
|
|
||||||
type: 'folder',
|
|
||||||
members: [
|
|
||||||
'string',
|
|
||||||
{
|
|
||||||
name: 'string',
|
|
||||||
type: 'service',
|
|
||||||
code: 'string'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
|
||||||
const adminUser = {
|
|
||||||
displayName: 'Test Admin',
|
|
||||||
username: 'testAdminUsername',
|
|
||||||
password: '12345678',
|
|
||||||
isAdmin: true,
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
const user = {
|
|
||||||
displayName: 'Test User',
|
|
||||||
username: 'testUsername',
|
|
||||||
password: '87654321',
|
|
||||||
isAdmin: false,
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const permission = {
|
|
||||||
uri: '/SASjsApi/code/execute',
|
|
||||||
setting: PermissionSetting.grant,
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: 123
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = {
|
|
||||||
name: 'DCGroup1',
|
|
||||||
description: 'DC group for testing purposes.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const userController = new UserController()
|
|
||||||
const groupController = new GroupController()
|
|
||||||
const clientController = new ClientController()
|
|
||||||
const permissionController = new PermissionController()
|
|
||||||
|
|
||||||
describe('permission', () => {
|
|
||||||
let app: Express
|
|
||||||
let con: Mongoose
|
|
||||||
let mongoServer: MongoMemoryServer
|
|
||||||
let adminAccessToken: string
|
|
||||||
let dbUser: UserDetailsResponse
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await appPromise
|
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
|
||||||
|
|
||||||
adminAccessToken = await generateSaveTokenAndCreateUser()
|
|
||||||
dbUser = await userController.createUser(user)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await con.connection.dropDatabase()
|
|
||||||
await con.connection.close()
|
|
||||||
await mongoServer.stop()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('create', () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
await deleteAllPermissions()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with new permission when principalType is user', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ ...permission, principalId: dbUser.id })
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.permissionId).toBeTruthy()
|
|
||||||
expect(res.body.uri).toEqual(permission.uri)
|
|
||||||
expect(res.body.setting).toEqual(permission.setting)
|
|
||||||
expect(res.body.user).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with new permission when principalType is group', async () => {
|
|
||||||
const dbGroup = await groupController.createGroup(group)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalType: 'group',
|
|
||||||
principalId: dbGroup.groupId
|
|
||||||
})
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.permissionId).toBeTruthy()
|
|
||||||
expect(res.body.uri).toEqual(permission.uri)
|
|
||||||
expect(res.body.setting).toEqual(permission.setting)
|
|
||||||
expect(res.body.group).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.send(permission)
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not of an admin account even if user has permission', 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()
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if uri is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
uri: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"uri" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if uri is not valid', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
uri: '/some/random/api/endpoint'
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
setting: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"setting" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if principalType is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalType: undefined
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"principalType" is required`)
|
|
||||||
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')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalType: 'invalid'
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('"principalType" must be one of [user, group]')
|
|
||||||
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 principalId is not a number', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalId: 'someCharacters'
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('"principalId" must be a number')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if adding permission for admin user', async () => {
|
|
||||||
const adminUser = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'adminUser',
|
|
||||||
isAdmin: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalId: adminUser.id
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Can not add permission for admin user.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found (404) if user is not found', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalId: 123
|
|
||||||
})
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('User not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Not Found (404) if group is not found', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
...permission,
|
|
||||||
principalType: 'group'
|
|
||||||
})
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Group not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Conflict (409) if permission already exists', async () => {
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/permission')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ ...permission, principalId: dbUser.id })
|
|
||||||
.expect(409)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
|
||||||
'Permission already exists with provided URI and User.'
|
|
||||||
)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
let dbPermission: PermissionDetailsResponse | undefined
|
|
||||||
beforeAll(async () => {
|
|
||||||
dbPermission = await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await deleteAllPermissions()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with updated permission', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ setting: 'Deny' })
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.setting).toEqual('Deny')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.send(permission)
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
|
||||||
const accessToken = await generateSaveTokenAndCreateUser({
|
|
||||||
...user,
|
|
||||||
username: 'update' + user.username
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(401)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if setting is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"setting" is required`)
|
|
||||||
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 not found (404) if permission with provided id does not exists', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.patch('/SASjsApi/permission/123')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({
|
|
||||||
setting: PermissionSetting.deny
|
|
||||||
})
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Permission not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should delete permission', async () => {
|
|
||||||
const dbPermission = await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
const res = await request(app)
|
|
||||||
.delete(`/SASjsApi/permission/${dbPermission?.permissionId}`)
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Permission Deleted!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with not found (404) if permission with provided id does not exists', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.delete('/SASjsApi/permission/123')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(404)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Permission not found.')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('get', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
uri: '/test-1',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
uri: '/test-2',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should give a list of all permissions when user is admin', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.get('/SASjsApi/permission/')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body).toHaveLength(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should give a list of all permissions when user is not admin', async () => {
|
|
||||||
const dbUser = await userController.createUser({
|
|
||||||
...user,
|
|
||||||
username: 'get' + user.username
|
|
||||||
})
|
|
||||||
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)
|
|
||||||
.get('/SASjsApi/permission/')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body).toHaveLength(3)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe.only('verify', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await permissionController.createPermission({
|
|
||||||
...permission,
|
|
||||||
uri: '/SASjsApi/drive/deploy',
|
|
||||||
principalId: dbUser.id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest
|
|
||||||
.spyOn(DriveController.prototype, 'deploy')
|
|
||||||
.mockImplementation((deployPayload) =>
|
|
||||||
Promise.resolve({
|
|
||||||
status: 'success',
|
|
||||||
message: 'Files deployed successfully to @sasjs/server.'
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should create files in SASJS drive', async () => {
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get('/SASjsApi/drive/deploy')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send(deployPayload)
|
|
||||||
.expect(200)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond unauthorized', async () => {
|
|
||||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get('/SASjsApi/drive/deploy/upload')
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(401)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const generateSaveTokenAndCreateUser = async (
|
|
||||||
someUser?: any
|
|
||||||
): Promise<string> => {
|
|
||||||
const dbUser = await userController.createUser(someUser ?? adminUser)
|
|
||||||
|
|
||||||
return generateAndSaveToken(dbUser.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateAndSaveToken = async (userId: number) => {
|
|
||||||
const adminAccessToken = generateAccessToken({
|
|
||||||
clientId,
|
|
||||||
userId
|
|
||||||
})
|
|
||||||
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
|
||||||
return adminAccessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteAllPermissions = async () => {
|
|
||||||
const { collections } = mongoose.connection
|
|
||||||
const collection = collections['permissions']
|
|
||||||
await collection.deleteMany({})
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,7 @@ import mongoose, { Mongoose } from 'mongoose'
|
|||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import {
|
import { UserController } from '../../../controllers/'
|
||||||
UserController,
|
|
||||||
PermissionController,
|
|
||||||
PermissionSetting,
|
|
||||||
PrincipalType
|
|
||||||
} from '../../../controllers/'
|
|
||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
saveTokensInDB,
|
saveTokensInDB,
|
||||||
@@ -46,21 +41,12 @@ describe('stp', () => {
|
|||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
let accessToken: string
|
let accessToken: string
|
||||||
const userController = new UserController()
|
|
||||||
const permissionController = new PermissionController()
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await appPromise
|
app = await appPromise
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
const dbUser = await userController.createUser(user)
|
accessToken = await generateSaveTokenAndCreateUser(user)
|
||||||
accessToken = await generateAndSaveToken(dbUser.id)
|
|
||||||
await permissionController.createPermission({
|
|
||||||
uri: '/SASjsApi/stp/execute',
|
|
||||||
principalType: PrincipalType.user,
|
|
||||||
principalId: dbUser.id,
|
|
||||||
setting: PermissionSetting.grant
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -770,14 +770,12 @@ describe('user', () => {
|
|||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: adminUser.username,
|
username: adminUser.username,
|
||||||
displayName: adminUser.displayName,
|
displayName: adminUser.displayName
|
||||||
isAdmin: adminUser.isAdmin
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName
|
||||||
isAdmin: user.isAdmin
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -798,14 +796,12 @@ describe('user', () => {
|
|||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: adminUser.username,
|
username: adminUser.username,
|
||||||
displayName: adminUser.displayName,
|
displayName: adminUser.displayName
|
||||||
isAdmin: adminUser.isAdmin
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: expect.anything(),
|
id: expect.anything(),
|
||||||
username: 'randomUser',
|
username: 'randomUser',
|
||||||
displayName: user.displayName,
|
displayName: user.displayName
|
||||||
isAdmin: user.isAdmin
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -79,8 +79,7 @@ describe('web', () => {
|
|||||||
expect(res.body.user).toEqual({
|
expect(res.body.user).toEqual({
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName
|
||||||
isAdmin: user.isAdmin
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { Request } from 'express'
|
import express, { Request } from 'express'
|
||||||
import { authenticateAccessToken } from '../../middlewares'
|
|
||||||
import { folderExists } from '@sasjs/utils'
|
import { folderExists } from '@sasjs/utils'
|
||||||
|
|
||||||
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
||||||
@@ -10,7 +9,7 @@ const appStreams: { [key: string]: string } = {}
|
|||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.get('/', authenticateAccessToken, async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const content = appStreamHtml(process.appStreamConfig)
|
const content = appStreamHtml(process.appStreamConfig)
|
||||||
|
|
||||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||||
@@ -67,7 +66,7 @@ export const publishAppStream = async (
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get(`/*`, authenticateAccessToken, function (req: Request, res, next) {
|
router.get(`/*`, function (req: Request, res, next) {
|
||||||
const reqPath = req.path.replace(/^\//, '')
|
const reqPath = req.path.replace(/^\//, '')
|
||||||
|
|
||||||
// Redirecting to url with trailing slash for appStream base URL only
|
// Redirecting to url with trailing slash for appStream base URL only
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ webRouter.post(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
webRouter.get('/SASLogon/logout', desktopRestrict, async (req, res) => {
|
webRouter.get('/logout', desktopRestrict, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await controller.logout(req)
|
await controller.logout(req)
|
||||||
res.status(200).send('OK!')
|
res.status(200).send('OK!')
|
||||||
|
|||||||
@@ -2,6 +2,5 @@ export interface TreeNode {
|
|||||||
name: string
|
name: string
|
||||||
relativePath: string
|
relativePath: string
|
||||||
absolutePath: string
|
absolutePath: string
|
||||||
isFolder: boolean
|
|
||||||
children: Array<TreeNode>
|
children: Array<TreeNode>
|
||||||
}
|
}
|
||||||
|
|||||||
6
api/src/types/system/process.d.ts
vendored
6
api/src/types/system/process.d.ts
vendored
@@ -1,14 +1,12 @@
|
|||||||
declare namespace NodeJS {
|
declare namespace NodeJS {
|
||||||
export interface Process {
|
export interface Process {
|
||||||
sasLoc?: string
|
sasLoc: string
|
||||||
nodeLoc?: string
|
nodeLoc: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
logsLoc: string
|
|
||||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
jsSessionController?: import('../../controllers/internal').JSSessionController
|
||||||
appStreamConfig: import('../').AppStreamConfig
|
appStreamConfig: import('../').AppStreamConfig
|
||||||
logger: import('@sasjs/utils/logger').Logger
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
runTimes: import('../../utils').RunTimeType[]
|
runTimes: import('../../utils').RunTimeType[]
|
||||||
secrets: import('../../model/Configuration').ConfigurationType
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { AppStreamConfig } from '../types'
|
|||||||
import { getAppStreamConfigPath } from './file'
|
import { getAppStreamConfigPath } from './file'
|
||||||
|
|
||||||
export const loadAppStreamConfig = async () => {
|
export const loadAppStreamConfig = async () => {
|
||||||
process.appStreamConfig = {}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
const appStreamConfigPath = getAppStreamConfigPath()
|
const appStreamConfigPath = getAppStreamConfigPath()
|
||||||
@@ -23,6 +21,7 @@ export const loadAppStreamConfig = async () => {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
appStreamConfig = {}
|
appStreamConfig = {}
|
||||||
}
|
}
|
||||||
|
process.appStreamConfig = {}
|
||||||
|
|
||||||
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
|
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
|
||||||
const { appLoc, streamWebFolder, streamLogo } = entry
|
const { appLoc, streamWebFolder, streamLogo } = entry
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ export const connectDB = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Connected to DB!')
|
console.log('Connected to DB!')
|
||||||
return seedDB()
|
await seedDB()
|
||||||
|
|
||||||
|
return mongoose.connection
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ export const getDesktopUserAutoExecPath = () =>
|
|||||||
|
|
||||||
export const getSasjsRootFolder = () => process.driveLoc
|
export const getSasjsRootFolder = () => process.driveLoc
|
||||||
|
|
||||||
export const getLogFolder = () => process.logsLoc
|
|
||||||
|
|
||||||
export const getAppStreamConfigPath = () =>
|
export const getAppStreamConfigPath = () =>
|
||||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
||||||
|
|
||||||
@@ -34,6 +32,8 @@ export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
|||||||
|
|
||||||
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
||||||
|
|
||||||
|
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
|
||||||
|
|
||||||
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||||
|
|
||||||
export const getSessionsFolder = () =>
|
export const getSessionsFolder = () =>
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateAccessToken = (data: InfoJWT) =>
|
export const generateAccessToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
|
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
|
||||||
expiresIn: '1day'
|
expiresIn: '1day'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateAuthCode = (data: InfoJWT) =>
|
export const generateAuthCode = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.secrets.AUTH_CODE_SECRET, {
|
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, {
|
||||||
expiresIn: '30s'
|
expiresIn: '30s'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import jwt from 'jsonwebtoken'
|
|||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
|
|
||||||
export const generateRefreshToken = (data: InfoJWT) =>
|
export const generateRefreshToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
|
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
|
||||||
expiresIn: '30 days'
|
expiresIn: '30 days'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Request } from 'express'
|
|
||||||
|
|
||||||
const StaticAuthorizedRoutes = [
|
|
||||||
'/AppStream',
|
|
||||||
'/SASjsApi/code/execute',
|
|
||||||
'/SASjsApi/stp/execute',
|
|
||||||
'/SASjsApi/drive/deploy',
|
|
||||||
'/SASjsApi/drive/deploy/upload',
|
|
||||||
'/SASjsApi/drive/file',
|
|
||||||
'/SASjsApi/drive/folder',
|
|
||||||
'/SASjsApi/drive/fileTree',
|
|
||||||
'/SASjsApi/drive/rename',
|
|
||||||
'/SASjsApi/permission'
|
|
||||||
]
|
|
||||||
|
|
||||||
export const getAuthorizedRoutes = () => {
|
|
||||||
const streamingApps = Object.keys(process.appStreamConfig)
|
|
||||||
const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`)
|
|
||||||
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getUri = (req: Request) => {
|
|
||||||
const { baseUrl, path: reqPath } = req
|
|
||||||
|
|
||||||
if (baseUrl === '/AppStream') {
|
|
||||||
const appStream = reqPath.split('/')[1]
|
|
||||||
|
|
||||||
// removing trailing slash of URLs
|
|
||||||
return (baseUrl + '/' + appStream).replace(/\/$/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (baseUrl + reqPath).replace(/\/$/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isAuthorizingRoute = (req: Request): boolean =>
|
|
||||||
getAuthorizedRoutes().includes(getUri(req))
|
|
||||||
@@ -2,32 +2,25 @@ import path from 'path'
|
|||||||
import { fileExists, getString, readFile } from '@sasjs/utils'
|
import { fileExists, getString, readFile } from '@sasjs/utils'
|
||||||
|
|
||||||
export const getCertificates = async () => {
|
export const getCertificates = async () => {
|
||||||
const { PRIVATE_KEY, CERT_CHAIN, CA_ROOT } = process.env
|
const { PRIVATE_KEY, FULL_CHAIN, CA } = process.env
|
||||||
|
|
||||||
let ca
|
|
||||||
|
|
||||||
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
|
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
|
||||||
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
|
const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)'))
|
||||||
const caPath = CA_ROOT
|
const caPath = CA ?? (await getFileInput('Full Chain (PEM)'))
|
||||||
|
|
||||||
console.log('keyPath: ', keyPath)
|
console.log('keyPath: ', keyPath)
|
||||||
console.log('certPath: ', certPath)
|
console.log('certPath: ', certPath)
|
||||||
if (caPath) console.log('caPath: ', caPath)
|
console.log('caPath: ', caPath)
|
||||||
|
|
||||||
const key = await readFile(keyPath)
|
const key = await readFile(keyPath)
|
||||||
const cert = await readFile(certPath)
|
const cert = await readFile(certPath)
|
||||||
if (caPath) ca = await readFile(caPath)
|
const ca = await readFile(caPath)
|
||||||
|
|
||||||
return { key, cert, ca }
|
return { key, cert, ca }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFileInput = async (
|
const getFileInput = async (filename: string): Promise<string> => {
|
||||||
filename: string,
|
|
||||||
required: boolean = true
|
|
||||||
): Promise<string> => {
|
|
||||||
const validator = async (filePath: string) => {
|
const validator = async (filePath: string) => {
|
||||||
if (!required) return true
|
|
||||||
|
|
||||||
if (!filePath) return `Path to ${filename} is required.`
|
if (!filePath) return `Path to ${filename} is required.`
|
||||||
|
|
||||||
if (!(await fileExists(path.join(process.cwd(), filePath)))) {
|
if (!(await fileExists(path.join(process.cwd(), filePath)))) {
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getString } from '@sasjs/utils/input'
|
import { getString } from '@sasjs/utils/input'
|
||||||
import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
|
import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
||||||
import { RunTimeType } from './verifyEnvVariables'
|
|
||||||
|
const isWindows = () => process.platform === 'win32'
|
||||||
|
|
||||||
export const getDesktopFields = async () => {
|
export const getDesktopFields = async () => {
|
||||||
const { SAS_PATH, NODE_PATH } = process.env
|
const { SAS_PATH, NODE_PATH } = process.env
|
||||||
|
|
||||||
let sasLoc, nodeLoc
|
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||||
|
const nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||||
if (process.runTimes.includes(RunTimeType.SAS)) {
|
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
||||||
sasLoc = SAS_PATH ?? (await getSASLocation())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.runTimes.includes(RunTimeType.JS)) {
|
|
||||||
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
|
||||||
}
|
|
||||||
|
|
||||||
return { sasLoc, nodeLoc }
|
return { sasLoc, nodeLoc }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import { RunTimeType } from '.'
|
|||||||
|
|
||||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||||
const ext = path.extname(programPath)
|
const ext = path.extname(programPath)
|
||||||
// If programPath (_program) is provided with a ".sas" or ".js" extension
|
// if program path is provided with extension we should split that into code path and ext as run time
|
||||||
// we should use that extension to determine the appropriate runTime
|
if (ext) {
|
||||||
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
|
||||||
const runTime = ext.slice(1)
|
const runTime = ext.slice(1)
|
||||||
|
const runTimeTypes = Object.values(RunTimeType)
|
||||||
|
|
||||||
|
if (!runTimeTypes.includes(runTime as RunTimeType)) {
|
||||||
|
throw `The '${runTime}' runtime is not supported.`
|
||||||
|
}
|
||||||
|
|
||||||
const codePath = path
|
const codePath = path
|
||||||
.join(getFilesFolder(), programPath)
|
.join(getFilesFolder(), programPath)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export * from './file'
|
|||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
export * from './generateAuthCode'
|
export * from './generateAuthCode'
|
||||||
export * from './generateRefreshToken'
|
export * from './generateRefreshToken'
|
||||||
export * from './getAuthorizedRoutes'
|
|
||||||
export * from './getCertificates'
|
export * from './getCertificates'
|
||||||
export * from './getDesktopFields'
|
export * from './getDesktopFields'
|
||||||
export * from './getPreProgramVariables'
|
export * from './getPreProgramVariables'
|
||||||
|
|||||||
@@ -1,73 +1,6 @@
|
|||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
import Group from '../model/Group'
|
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import Configuration, { ConfigurationType } from '../model/Configuration'
|
|
||||||
|
|
||||||
import { randomBytes } from 'crypto'
|
|
||||||
|
|
||||||
export const SECRETS: ConfigurationType = {
|
|
||||||
ACCESS_TOKEN_SECRET: randomBytes(64).toString('hex'),
|
|
||||||
REFRESH_TOKEN_SECRET: randomBytes(64).toString('hex'),
|
|
||||||
AUTH_CODE_SECRET: randomBytes(64).toString('hex'),
|
|
||||||
SESSION_SECRET: randomBytes(64).toString('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const seedDB = async (): Promise<ConfigurationType> => {
|
|
||||||
// Checking if client is already in the database
|
|
||||||
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
|
||||||
if (!clientExist) {
|
|
||||||
const client = new Client(CLIENT)
|
|
||||||
await client.save()
|
|
||||||
|
|
||||||
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checking if 'AllUsers' Group is already in the database
|
|
||||||
let groupExist = await Group.findOne({ name: GROUP.name })
|
|
||||||
if (!groupExist) {
|
|
||||||
const group = new Group(GROUP)
|
|
||||||
groupExist = await group.save()
|
|
||||||
|
|
||||||
console.log(`DB Seed - Group created: ${GROUP.name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checking if user is already in the database
|
|
||||||
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
|
||||||
if (!usernameExist) {
|
|
||||||
const user = new User(ADMIN_USER)
|
|
||||||
usernameExist = await user.save()
|
|
||||||
|
|
||||||
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!groupExist.hasUser(usernameExist)) {
|
|
||||||
groupExist.addUser(usernameExist)
|
|
||||||
console.log(
|
|
||||||
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checking if configuration is present in the database
|
|
||||||
let configExist = await Configuration.findOne()
|
|
||||||
if (!configExist) {
|
|
||||||
const configuration = new Configuration(SECRETS)
|
|
||||||
configExist = await configuration.save()
|
|
||||||
|
|
||||||
console.log('DB Seed - configuration added')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
ACCESS_TOKEN_SECRET: configExist.ACCESS_TOKEN_SECRET,
|
|
||||||
REFRESH_TOKEN_SECRET: configExist.REFRESH_TOKEN_SECRET,
|
|
||||||
AUTH_CODE_SECRET: configExist.AUTH_CODE_SECRET,
|
|
||||||
SESSION_SECRET: configExist.SESSION_SECRET
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const GROUP = {
|
|
||||||
name: 'AllUsers',
|
|
||||||
description: 'Group contains all users'
|
|
||||||
}
|
|
||||||
const CLIENT = {
|
const CLIENT = {
|
||||||
clientId: 'clientID1',
|
clientId: 'clientID1',
|
||||||
clientSecret: 'clientSecret'
|
clientSecret: 'clientSecret'
|
||||||
@@ -80,3 +13,23 @@ const ADMIN_USER = {
|
|||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const seedDB = async () => {
|
||||||
|
// Checking if client is already in the database
|
||||||
|
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
||||||
|
if (!clientExist) {
|
||||||
|
const client = new Client(CLIENT)
|
||||||
|
await client.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if user is already in the database
|
||||||
|
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||||
|
if (!usernameExist) {
|
||||||
|
const user = new User(ADMIN_USER)
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,33 +1,19 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||||
|
|
||||||
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
|
import { getDesktopFields, ModeType, RunTimeType } from '.'
|
||||||
|
|
||||||
export const setProcessVariables = async () => {
|
export const setProcessVariables = async () => {
|
||||||
const { MODE, RUN_TIMES } = process.env
|
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
|
||||||
// NOTE: when exporting app.js as agent for supertest
|
|
||||||
// it should prevent connecting to the real database
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
|
||||||
const secrets = await connectDB()
|
|
||||||
|
|
||||||
process.secrets = secrets
|
|
||||||
} else {
|
|
||||||
process.secrets = SECRETS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
process.runTimes = (RUN_TIMES?.split(',') as RunTimeType[]) ?? []
|
const { MODE } = process.env
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
process.sasLoc = process.env.SAS_PATH
|
process.sasLoc = process.env.SAS_PATH as string
|
||||||
process.nodeLoc = process.env.NODE_PATH
|
process.nodeLoc = process.env.NODE_PATH as string
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc, nodeLoc } = await getDesktopFields()
|
const { sasLoc, nodeLoc } = await getDesktopFields()
|
||||||
|
|
||||||
@@ -40,16 +26,10 @@ export const setProcessVariables = async () => {
|
|||||||
await createFolder(absPath)
|
await createFolder(absPath)
|
||||||
process.driveLoc = getRealPath(absPath)
|
process.driveLoc = getRealPath(absPath)
|
||||||
|
|
||||||
const { LOG_LOCATION } = process.env
|
const { RUN_TIMES } = process.env
|
||||||
const absLogsPath = getAbsolutePath(
|
process.runTimes = (RUN_TIMES as string).split(',') as RunTimeType[]
|
||||||
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
|
|
||||||
process.cwd()
|
|
||||||
)
|
|
||||||
await createFolder(absLogsPath)
|
|
||||||
process.logsLoc = getRealPath(absLogsPath)
|
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
console.log('sasLogs: ', process.logsLoc)
|
|
||||||
console.log('runTimes: ', process.runTimes)
|
console.log('runTimes: ', process.runTimes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { extractHeaders } from '../extractHeaders'
|
import { extractHeaders } from '..'
|
||||||
|
|
||||||
describe('extractHeaders', () => {
|
describe('extractHeaders', () => {
|
||||||
it('should return valid http headers', () => {
|
it('should return valid http headers', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseLogToArray } from '../parseLogToArray'
|
import { parseLogToArray } from '..'
|
||||||
|
|
||||||
describe('parseLogToArray', () => {
|
describe('parseLogToArray', () => {
|
||||||
it('should parse log to array type', () => {
|
it('should parse log to array type', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
import { listFilesInFolder, readFileBinary, isWindows } from '@sasjs/utils'
|
import { listFilesInFolder, readFileBinary } from '@sasjs/utils'
|
||||||
|
|
||||||
interface FilenameMapSingle {
|
interface FilenameMapSingle {
|
||||||
fieldName: string
|
fieldName: string
|
||||||
@@ -118,9 +118,7 @@ export const generateFileUploadJSCode = async (
|
|||||||
if (fileName.includes('req_file')) {
|
if (fileName.includes('req_file')) {
|
||||||
fileCount++
|
fileCount++
|
||||||
const filePath = path.join(sessionFolder, fileName)
|
const filePath = path.join(sessionFolder, fileName)
|
||||||
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${
|
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${filePath}')`
|
||||||
isWindows() ? filePath.replace(/\\/g, '\\\\') : filePath
|
|
||||||
}')`
|
|
||||||
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
||||||
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import Joi from 'joi'
|
import Joi from 'joi'
|
||||||
import { PermissionSetting, PrincipalType } from '../controllers/permission'
|
import { RunTimeType } from '.'
|
||||||
import { getAuthorizedRoutes } from './getAuthorizedRoutes'
|
|
||||||
|
|
||||||
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||||
const passwordSchema = Joi.string().min(6).max(1024)
|
const passwordSchema = Joi.string().min(6).max(1024)
|
||||||
@@ -87,27 +86,6 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
|||||||
clientSecret: Joi.string().required()
|
clientSecret: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
|
|
||||||
Joi.object({
|
|
||||||
uri: Joi.string()
|
|
||||||
.required()
|
|
||||||
.valid(...getAuthorizedRoutes()),
|
|
||||||
setting: Joi.string()
|
|
||||||
.required()
|
|
||||||
.valid(...Object.values(PermissionSetting)),
|
|
||||||
principalType: Joi.string()
|
|
||||||
.required()
|
|
||||||
.valid(...Object.values(PrincipalType)),
|
|
||||||
principalId: Joi.number().required()
|
|
||||||
}).validate(data)
|
|
||||||
|
|
||||||
export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
|
|
||||||
Joi.object({
|
|
||||||
setting: Joi.string()
|
|
||||||
.required()
|
|
||||||
.valid(...Object.values(PermissionSetting))
|
|
||||||
}).validate(data)
|
|
||||||
|
|
||||||
export const deployValidation = (data: any): Joi.ValidationResult =>
|
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
||||||
@@ -138,23 +116,9 @@ export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
|||||||
_filePath: filePathSchema
|
_filePath: filePathSchema
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const folderParamValidation = (
|
export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
||||||
data: any,
|
|
||||||
folderPathRequired?: boolean
|
|
||||||
): Joi.ValidationResult =>
|
|
||||||
Joi.object({
|
Joi.object({
|
||||||
_folderPath: folderPathRequired ? Joi.string().required() : Joi.string()
|
_folderPath: 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)
|
}).validate(data)
|
||||||
|
|
||||||
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
|||||||
@@ -78,7 +78,33 @@ const verifyMODE = (): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Server) {
|
if (process.env.MODE === ModeType.Server) {
|
||||||
const { DB_CONNECT } = process.env
|
const {
|
||||||
|
ACCESS_TOKEN_SECRET,
|
||||||
|
REFRESH_TOKEN_SECRET,
|
||||||
|
AUTH_CODE_SECRET,
|
||||||
|
SESSION_SECRET,
|
||||||
|
DB_CONNECT
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
if (!ACCESS_TOKEN_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!REFRESH_TOKEN_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!AUTH_CODE_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!SESSION_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test')
|
if (process.env.NODE_ENV !== 'test')
|
||||||
if (!DB_CONNECT)
|
if (!DB_CONNECT)
|
||||||
@@ -103,16 +129,16 @@ const verifyPROTOCOL = (): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
|
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
|
||||||
const { PRIVATE_KEY, CERT_CHAIN } = process.env
|
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
||||||
|
|
||||||
if (!PRIVATE_KEY)
|
if (!PRIVATE_KEY)
|
||||||
errors.push(
|
errors.push(
|
||||||
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!CERT_CHAIN)
|
if (!FULL_CHAIN)
|
||||||
errors.push(
|
errors.push(
|
||||||
`- CERT_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,5 +258,5 @@ const DEFAULTS = {
|
|||||||
PORT: '5000',
|
PORT: '5000',
|
||||||
HELMET_COEP: HelmetCoepType.TRUE,
|
HELMET_COEP: HelmetCoepType.TRUE,
|
||||||
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
||||||
RUN_TIMES: RunTimeType.SAS
|
RUN_TIMES: `${RunTimeType.SAS}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ export const extractJSONFromZip = async (zipFile: Express.Multer.File) => {
|
|||||||
|
|
||||||
for await (const entry of zip) {
|
for await (const entry of zip) {
|
||||||
const fileName = entry.path as string
|
const fileName = entry.path as string
|
||||||
// grab the first json found in .zip
|
if (fileName.toUpperCase().endsWith('.JSON') && fileName === fileInZip) {
|
||||||
if (fileName.toUpperCase().endsWith('.JSON')) {
|
|
||||||
fileContent = await entry.buffer()
|
fileContent = await entry.buffer()
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,45 +11,41 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
{
|
|
||||||
"name": "Auth",
|
|
||||||
"description": "Operations about auth"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Client",
|
|
||||||
"description": "Operations about clients"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "CODE",
|
|
||||||
"description": "Execution of code (various runtimes are supported)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Drive",
|
|
||||||
"description": "Operations on SASjs Drive"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Group",
|
|
||||||
"description": "Operations on groups and group memberships"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Info",
|
"name": "Info",
|
||||||
"description": "Get Server Information"
|
"description": "Get Server Info"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Permission",
|
|
||||||
"description": "Operations about permissions"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Session",
|
"name": "Session",
|
||||||
"description": "Get Session information"
|
"description": "Get Session information"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "STP",
|
"name": "User",
|
||||||
"description": "Execution of Stored Programs"
|
"description": "Operations about users"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "User",
|
"name": "Client",
|
||||||
"description": "Operations with users"
|
"description": "Operations about clients"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Auth",
|
||||||
|
"description": "Operations about auth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Drive",
|
||||||
|
"description": "Operations about drive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group",
|
||||||
|
"description": "Operations about group"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "STP",
|
||||||
|
"description": "Operations about STP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CODE",
|
||||||
|
"description": "Operations on SAS code"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Web",
|
"name": "Web",
|
||||||
|
|||||||
235
web/package-lock.json
generated
235
web/package-lock.json
generated
@@ -10,7 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@mui/icons-material": "^5.8.4",
|
"@mui/icons-material": "^5.0.3",
|
||||||
"@mui/lab": "^5.0.0-alpha.50",
|
"@mui/lab": "^5.0.0-alpha.50",
|
||||||
"@mui/material": "^5.0.3",
|
"@mui/material": "^5.0.3",
|
||||||
"@mui/styles": "^5.0.1",
|
"@mui/styles": "^5.0.1",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-monaco-editor": "^0.48.0",
|
"react-monaco-editor": "^0.48.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^5.3.0",
|
||||||
"react-toastify": "^9.0.1"
|
"react-toastify": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1836,9 +1836,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.18.6",
|
"version": "7.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
|
||||||
"integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
|
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.13.4"
|
"regenerator-runtime": "^0.13.4"
|
||||||
},
|
},
|
||||||
@@ -2312,23 +2312,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/icons-material": {
|
"node_modules/@mui/icons-material": {
|
||||||
"version": "5.8.4",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz",
|
||||||
"integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
|
"integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.17.2"
|
"@babel/runtime": "^7.16.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/mui"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mui/material": "^5.0.0",
|
"@mui/material": "^5.0.0",
|
||||||
"@types/react": "^17.0.0 || ^18.0.0",
|
"@types/react": "^16.8.6 || ^17.0.0",
|
||||||
"react": "^17.0.0 || ^18.0.0"
|
"react": "^17.0.2"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
@@ -7132,11 +7128,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/history": {
|
"node_modules/history": {
|
||||||
"version": "5.3.0",
|
"version": "4.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.7.6"
|
"@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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hoist-non-react-statics": {
|
"node_modules/hoist-non-react-statics": {
|
||||||
@@ -7828,6 +7829,11 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -8386,6 +8392,19 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
@@ -8948,6 +8967,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
"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": {
|
"node_modules/path-type": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
@@ -9335,30 +9362,48 @@
|
|||||||
"react": "^17.x"
|
"react": "^17.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router": {
|
||||||
"version": "6.3.0",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
|
||||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"history": "^5.2.0",
|
"@babel/runtime": "^7.12.13",
|
||||||
"react-router": "6.3.0"
|
"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": {
|
"peerDependencies": {
|
||||||
"react": ">=16.8",
|
"react": ">=15"
|
||||||
"react-dom": ">=16.8"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom/node_modules/react-router": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "6.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
|
||||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"history": "^5.2.0"
|
"@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"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16.8"
|
"react": ">=15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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-toastify": {
|
"node_modules/react-toastify": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
|
||||||
@@ -9634,6 +9679,11 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/retry": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||||
@@ -10299,6 +10349,11 @@
|
|||||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
@@ -10678,6 +10733,11 @@
|
|||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@@ -12582,9 +12642,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.18.6",
|
"version": "7.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
|
||||||
"integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
|
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.13.4"
|
"regenerator-runtime": "^0.13.4"
|
||||||
}
|
}
|
||||||
@@ -12929,11 +12989,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@mui/icons-material": {
|
"@mui/icons-material": {
|
||||||
"version": "5.8.4",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz",
|
||||||
"integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
|
"integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.17.2"
|
"@babel/runtime": "^7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@mui/lab": {
|
"@mui/lab": {
|
||||||
@@ -16527,11 +16587,16 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"version": "5.3.0",
|
"version": "4.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.7.6"
|
"@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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hoist-non-react-statics": {
|
"hoist-non-react-statics": {
|
||||||
@@ -17019,6 +17084,11 @@
|
|||||||
"is-docker": "^2.0.0"
|
"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": {
|
"isexe": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
@@ -17460,6 +17530,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
|
"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": {
|
"minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||||
@@ -17882,6 +17961,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
"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": {
|
"path-type": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||||
@@ -18173,25 +18260,44 @@
|
|||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-router-dom": {
|
"react-router": {
|
||||||
"version": "6.3.0",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
|
||||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"history": "^5.2.0",
|
"@babel/runtime": "^7.12.13",
|
||||||
"react-router": "6.3.0"
|
"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"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": {
|
"react-is": {
|
||||||
"version": "6.3.0",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
"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": {
|
"react-toastify": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
|
||||||
@@ -18414,6 +18520,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
|
"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": {
|
"retry": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||||
@@ -18915,6 +19026,11 @@
|
|||||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||||
"dev": true
|
"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": {
|
"tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
@@ -19204,6 +19320,11 @@
|
|||||||
"homedir-polyfill": "^1.0.1"
|
"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": {
|
"vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server --config webpack.dev.ts --hot",
|
"start": "npx webpack-dev-server --config webpack.dev.ts --hot",
|
||||||
"build": "webpack --config webpack.prod.ts"
|
"build": "npx webpack --config webpack.prod.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@mui/icons-material": "^5.8.4",
|
"@mui/icons-material": "^5.0.3",
|
||||||
"@mui/lab": "^5.0.0-alpha.50",
|
"@mui/lab": "^5.0.0-alpha.50",
|
||||||
"@mui/material": "^5.0.3",
|
"@mui/material": "^5.0.3",
|
||||||
"@mui/styles": "^5.0.1",
|
"@mui/styles": "^5.0.1",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-monaco-editor": "^0.48.0",
|
"react-monaco-editor": "^0.48.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^5.3.0",
|
||||||
"react-toastify": "^9.0.1"
|
"react-toastify": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Route, HashRouter, Routes } from 'react-router-dom'
|
import { Route, HashRouter, Switch } from 'react-router-dom'
|
||||||
import { ThemeProvider } from '@mui/material/styles'
|
import { ThemeProvider } from '@mui/material/styles'
|
||||||
import { theme } from './theme'
|
import { theme } from './theme'
|
||||||
|
|
||||||
import Login from './components/login'
|
import Login from './components/login'
|
||||||
import Header from './components/header'
|
import Header from './components/header'
|
||||||
import Home from './components/home'
|
import Home from './components/home'
|
||||||
|
import Drive from './containers/Drive'
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
import Settings from './containers/Settings'
|
import Settings from './containers/Settings'
|
||||||
|
|
||||||
@@ -21,9 +22,11 @@ function App() {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Header />
|
<Header />
|
||||||
<Routes>
|
<Switch>
|
||||||
<Route path="/" element={<Login />} />
|
<Route path="/">
|
||||||
</Routes>
|
<Login />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
@@ -33,12 +36,23 @@ function App() {
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Header />
|
<Header />
|
||||||
<Routes>
|
<Switch>
|
||||||
<Route path="/" element={<Home />} />
|
<Route exact path="/">
|
||||||
<Route path="/SASjsStudio" element={<Studio />} />
|
<Home />
|
||||||
<Route path="/SASjsSettings" element={<Settings />} />
|
</Route>
|
||||||
<Route path="/SASjsLogon" element={<AuthCode />} />
|
<Route exact path="/SASjsDrive">
|
||||||
</Routes>
|
<Drive />
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/SASjsStudio">
|
||||||
|
<Studio />
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/SASjsSettings">
|
||||||
|
<Settings />
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/SASjsLogon">
|
||||||
|
<AuthCode />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
Typography
|
|
||||||
} from '@mui/material'
|
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
'& .MuiDialogContent-root': {
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
'& .MuiDialogActions-root': {
|
|
||||||
padding: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
type DeleteConfirmationModalProps = {
|
|
||||||
open: boolean
|
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
message: string
|
|
||||||
_delete: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeleteConfirmationModal = ({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
message,
|
|
||||||
_delete
|
|
||||||
}: DeleteConfirmationModalProps) => {
|
|
||||||
return (
|
|
||||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Typography gutterBottom>{message}</Typography>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
|
||||||
<Button color="error" onClick={() => _delete()}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</BootstrapDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DeleteConfirmationModal
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import React, { Dispatch, SetStateAction } from 'react'
|
|
||||||
|
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
|
||||||
import IconButton from '@mui/material/IconButton'
|
|
||||||
import CloseIcon from '@mui/icons-material/Close'
|
|
||||||
|
|
||||||
export interface DialogTitleProps {
|
|
||||||
id: string
|
|
||||||
children?: React.ReactNode
|
|
||||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BootstrapDialogTitle = (props: DialogTitleProps) => {
|
|
||||||
const { children, handleOpen, ...other } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogTitle sx={{ m: 0, p: 2 }} {...other}>
|
|
||||||
{children}
|
|
||||||
{handleOpen ? (
|
|
||||||
<IconButton
|
|
||||||
aria-label="close"
|
|
||||||
onClick={() => handleOpen(false)}
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
right: 8,
|
|
||||||
top: 8,
|
|
||||||
color: (theme) => theme.palette.grey[500]
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
) : null}
|
|
||||||
</DialogTitle>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
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,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react'
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
@@ -24,7 +24,7 @@ const baseUrl =
|
|||||||
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
||||||
|
|
||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
const navigate = useNavigate()
|
const history = useHistory()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
const [tabValue, setTabValue] = useState(
|
const [tabValue, setTabValue] = useState(
|
||||||
@@ -74,7 +74,7 @@ const Header = (props: any) => {
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTabValue('/')
|
setTabValue('/')
|
||||||
navigate('/')
|
history.push('/')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -83,6 +83,12 @@ const Header = (props: any) => {
|
|||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
>
|
>
|
||||||
<Tab label="Home" value="/" to="/" component={Link} />
|
<Tab label="Home" value="/" to="/" component={Link} />
|
||||||
|
<Tab
|
||||||
|
label="Drive"
|
||||||
|
value="/SASjsDrive"
|
||||||
|
to="/SASjsDrive"
|
||||||
|
component={Link}
|
||||||
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
label="Studio"
|
label="Studio"
|
||||||
value="/SASjsStudio"
|
value="/SASjsStudio"
|
||||||
@@ -90,6 +96,17 @@ const Header = (props: any) => {
|
|||||||
component={Link}
|
component={Link}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
<Button
|
||||||
|
href={`${baseUrl}/SASjsApi`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
endIcon={<OpenInNewIcon />}
|
||||||
|
>
|
||||||
|
API Docs
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
href={`${baseUrl}/AppStream`}
|
href={`${baseUrl}/AppStream`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -99,7 +116,7 @@ const Header = (props: any) => {
|
|||||||
size="large"
|
size="large"
|
||||||
endIcon={<OpenInNewIcon />}
|
endIcon={<OpenInNewIcon />}
|
||||||
>
|
>
|
||||||
Apps
|
App Stream
|
||||||
</Button>
|
</Button>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -139,32 +156,6 @@ const Header = (props: any) => {
|
|||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
href={'https://server.sasjs.io'}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
variant="contained"
|
|
||||||
size="large"
|
|
||||||
color="primary"
|
|
||||||
endIcon={<OpenInNewIcon />}
|
|
||||||
>
|
|
||||||
Docs
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
href={`${baseUrl}/SASjsApi`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
endIcon={<OpenInNewIcon />}
|
|
||||||
>
|
|
||||||
API
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||||
<Button variant="contained" color="primary">
|
<Button variant="contained" color="primary">
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const Login = () => {
|
|||||||
username,
|
username,
|
||||||
password
|
password
|
||||||
}).catch((err: any) => {
|
}).catch((err: any) => {
|
||||||
setErrorMessage(err.response?.data || err.toString())
|
setErrorMessage(err.response.data)
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -30,7 +30,6 @@ const Login = () => {
|
|||||||
appContext.setUserId?.(user.id)
|
appContext.setUserId?.(user.id)
|
||||||
appContext.setUsername?.(user.username)
|
appContext.setUsername?.(user.username)
|
||||||
appContext.setDisplayName?.(user.displayName)
|
appContext.setDisplayName?.(user.displayName)
|
||||||
appContext.setIsAdmin?.(user.isAdmin)
|
|
||||||
appContext.setLoggedIn?.(loggedIn)
|
appContext.setLoggedIn?.(loggedIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { Typography, Dialog, DialogContent } from '@mui/material'
|
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
|
|
||||||
import { BootstrapDialogTitle } from './dialogTitle'
|
|
||||||
|
|
||||||
export const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
'& .MuiDialogContent-root': {
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
'& .MuiDialogActions-root': {
|
|
||||||
padding: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
type ModalProps = {
|
|
||||||
open: boolean
|
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
title: string
|
|
||||||
payload: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Modal = (props: ModalProps) => {
|
|
||||||
const { open, setOpen, title, payload } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
|
||||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
|
||||||
{title}
|
|
||||||
</BootstrapDialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Typography gutterBottom>
|
|
||||||
<span style={{ fontFamily: 'monospace' }}>{payload}</span>
|
|
||||||
</Typography>
|
|
||||||
</DialogContent>
|
|
||||||
</BootstrapDialog>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Modal
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import React, { Dispatch, SetStateAction } from 'react'
|
|
||||||
import Snackbar from '@mui/material/Snackbar'
|
|
||||||
import MuiAlert, { AlertProps } from '@mui/material/Alert'
|
|
||||||
import Slide, { SlideProps } from '@mui/material/Slide'
|
|
||||||
|
|
||||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
|
|
||||||
props,
|
|
||||||
ref
|
|
||||||
) {
|
|
||||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
|
|
||||||
})
|
|
||||||
|
|
||||||
const Transition = (props: SlideProps) => {
|
|
||||||
return <Slide {...props} direction="up" />
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AlertSeverityType {
|
|
||||||
Success = 'success',
|
|
||||||
Warning = 'warning',
|
|
||||||
Info = 'info',
|
|
||||||
Error = 'error'
|
|
||||||
}
|
|
||||||
|
|
||||||
type BootstrapSnackbarProps = {
|
|
||||||
open: boolean
|
|
||||||
setOpen: Dispatch<SetStateAction<boolean>>
|
|
||||||
message: string
|
|
||||||
severity: AlertSeverityType
|
|
||||||
}
|
|
||||||
|
|
||||||
const BootstrapSnackbar = ({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
message,
|
|
||||||
severity
|
|
||||||
}: BootstrapSnackbarProps) => {
|
|
||||||
const handleClose = (
|
|
||||||
event: React.SyntheticEvent | Event,
|
|
||||||
reason?: string
|
|
||||||
) => {
|
|
||||||
if (reason === 'clickaway') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Snackbar
|
|
||||||
open={open}
|
|
||||||
autoHideDuration={3000}
|
|
||||||
onClose={handleClose}
|
|
||||||
TransitionComponent={Transition}
|
|
||||||
>
|
|
||||||
<Alert onClose={handleClose} severity={severity} sx={{ width: '100%' }}>
|
|
||||||
{message}
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BootstrapSnackbar
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
106
web/src/containers/Drive/index.tsx
Normal file
106
web/src/containers/Drive/index.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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
|
||||||
173
web/src/containers/Drive/main.tsx
Normal file
173
web/src/containers/Drive/main.tsx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
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
|
||||||
100
web/src/containers/Drive/sideBar.tsx
Normal file
100
web/src/containers/Drive/sideBar.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
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
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import React, { useState, useEffect, Dispatch, SetStateAction } from 'react'
|
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
TextField,
|
|
||||||
CircularProgress,
|
|
||||||
Autocomplete
|
|
||||||
} from '@mui/material'
|
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
|
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
|
||||||
|
|
||||||
import {
|
|
||||||
UserResponse,
|
|
||||||
GroupResponse,
|
|
||||||
RegisterPermissionPayload
|
|
||||||
} from '../../utils/types'
|
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
'& .MuiDialogContent-root': {
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
'& .MuiDialogActions-root': {
|
|
||||||
padding: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
type AddPermissionModalProps = {
|
|
||||||
open: boolean
|
|
||||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
|
||||||
addPermission: (addPermissionPayload: RegisterPermissionPayload) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddPermissionModal = ({
|
|
||||||
open,
|
|
||||||
handleOpen,
|
|
||||||
addPermission
|
|
||||||
}: AddPermissionModalProps) => {
|
|
||||||
const [URIs, setURIs] = useState<string[]>([])
|
|
||||||
const [loadingURIs, setLoadingURIs] = useState(false)
|
|
||||||
const [uri, setUri] = useState<string>()
|
|
||||||
const [principalType, setPrincipalType] = useState('user')
|
|
||||||
const [userPrincipal, setUserPrincipal] = useState<UserResponse>()
|
|
||||||
const [groupPrincipal, setGroupPrincipal] = useState<GroupResponse>()
|
|
||||||
const [permissionSetting, setPermissionSetting] = useState('Grant')
|
|
||||||
const [loadingPrincipals, setLoadingPrincipals] = useState(false)
|
|
||||||
const [userPrincipals, setUserPrincipals] = useState<UserResponse[]>([])
|
|
||||||
const [groupPrincipals, setGroupPrincipals] = useState<GroupResponse[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoadingURIs(true)
|
|
||||||
axios
|
|
||||||
.get('/SASjsApi/info/authorizedRoutes')
|
|
||||||
.then((res: any) => {
|
|
||||||
if (res.data) {
|
|
||||||
setURIs(res.data.URIs)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoadingURIs(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoadingPrincipals(true)
|
|
||||||
axios
|
|
||||||
.get(`/SASjsApi/${principalType}`)
|
|
||||||
.then((res: any) => {
|
|
||||||
if (res.data) {
|
|
||||||
if (principalType === 'user') {
|
|
||||||
const users: UserResponse[] = res.data
|
|
||||||
const nonAdminUsers = users.filter((user) => !user.isAdmin)
|
|
||||||
setUserPrincipals(nonAdminUsers)
|
|
||||||
} else {
|
|
||||||
setGroupPrincipals(res.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoadingPrincipals(false)
|
|
||||||
})
|
|
||||||
}, [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 addButtonDisabled =
|
|
||||||
!uri || (principalType === 'user' ? !userPrincipal : !groupPrincipal)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
|
||||||
<BootstrapDialogTitle
|
|
||||||
id="add-permission-dialog-title"
|
|
||||||
handleOpen={handleOpen}
|
|
||||||
>
|
|
||||||
Add Permission
|
|
||||||
</BootstrapDialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Autocomplete
|
|
||||||
options={URIs}
|
|
||||||
disableClearable
|
|
||||||
value={uri}
|
|
||||||
onChange={(event: any, newValue: string) => setUri(newValue)}
|
|
||||||
renderInput={(params) =>
|
|
||||||
loadingURIs ? (
|
|
||||||
<CircularProgress />
|
|
||||||
) : (
|
|
||||||
<TextField {...params} label="Principal" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Autocomplete
|
|
||||||
options={['user', 'group']}
|
|
||||||
disableClearable
|
|
||||||
value={principalType}
|
|
||||||
onChange={(event: any, newValue: string) =>
|
|
||||||
setPrincipalType(newValue)
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField {...params} label="Principal Type" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
{principalType === 'user' ? (
|
|
||||||
<Autocomplete
|
|
||||||
options={userPrincipals}
|
|
||||||
getOptionLabel={(option) => option.displayName}
|
|
||||||
disableClearable
|
|
||||||
value={userPrincipal}
|
|
||||||
onChange={(event: any, newValue: UserResponse) =>
|
|
||||||
setUserPrincipal(newValue)
|
|
||||||
}
|
|
||||||
renderInput={(params) =>
|
|
||||||
loadingPrincipals ? (
|
|
||||||
<CircularProgress />
|
|
||||||
) : (
|
|
||||||
<TextField {...params} label="Principal" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Autocomplete
|
|
||||||
options={groupPrincipals}
|
|
||||||
getOptionLabel={(option) => option.name}
|
|
||||||
disableClearable
|
|
||||||
value={groupPrincipal}
|
|
||||||
onChange={(event: any, newValue: GroupResponse) =>
|
|
||||||
setGroupPrincipal(newValue)
|
|
||||||
}
|
|
||||||
renderInput={(params) =>
|
|
||||||
loadingPrincipals ? (
|
|
||||||
<CircularProgress />
|
|
||||||
) : (
|
|
||||||
<TextField {...params} label="Principal" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Autocomplete
|
|
||||||
options={['Grant', 'Deny']}
|
|
||||||
disableClearable
|
|
||||||
value={permissionSetting}
|
|
||||||
onChange={(event: any, newValue: string) =>
|
|
||||||
setPermissionSetting(newValue)
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField {...params} label="Settings" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleAddPermission}
|
|
||||||
disabled={addButtonDisabled}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</BootstrapDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AddPermissionModal
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import React, { useState, useContext } from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { Box, Paper, Tab, styled } from '@mui/material'
|
import { Box, Paper, Tab, styled } from '@mui/material'
|
||||||
import TabContext from '@mui/lab/TabContext'
|
import TabContext from '@mui/lab/TabContext'
|
||||||
import TabList from '@mui/lab/TabList'
|
import TabList from '@mui/lab/TabList'
|
||||||
import TabPanel from '@mui/lab/TabPanel'
|
import TabPanel from '@mui/lab/TabPanel'
|
||||||
|
|
||||||
import Permission from './permission'
|
|
||||||
import Profile from './profile'
|
import Profile from './profile'
|
||||||
|
|
||||||
import { AppContext, ModeType } from '../../context/appContext'
|
|
||||||
|
|
||||||
const StyledTab = styled(Tab)({
|
const StyledTab = styled(Tab)({
|
||||||
background: 'black',
|
background: 'black',
|
||||||
margin: '0 5px 5px 0'
|
margin: '0 5px 5px 0'
|
||||||
@@ -20,8 +17,7 @@ const StyledTabpanel = styled(TabPanel)({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const appContext = useContext(AppContext)
|
const [value, setValue] = React.useState('profile')
|
||||||
const [value, setValue] = useState('profile')
|
|
||||||
|
|
||||||
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
setValue(newValue)
|
setValue(newValue)
|
||||||
@@ -46,17 +42,11 @@ const Settings = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
<StyledTab label="Profile" value="profile" />
|
<StyledTab label="Profile" value="profile" />
|
||||||
{appContext.mode === ModeType.Server && (
|
|
||||||
<StyledTab label="Uri Access" value="permission" />
|
|
||||||
)}
|
|
||||||
</TabList>
|
</TabList>
|
||||||
</Box>
|
</Box>
|
||||||
<StyledTabpanel value="profile">
|
<StyledTabpanel value="profile">
|
||||||
<Profile />
|
<Profile />
|
||||||
</StyledTabpanel>
|
</StyledTabpanel>
|
||||||
<StyledTabpanel value="permission">
|
|
||||||
<Permission />
|
|
||||||
</StyledTabpanel>
|
|
||||||
</TabContext>
|
</TabContext>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,490 +0,0 @@
|
|||||||
import React, { useState, useEffect, useContext, useCallback } from 'react'
|
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Paper,
|
|
||||||
Grid,
|
|
||||||
CircularProgress,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
Popover
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
import FilterListIcon from '@mui/icons-material/FilterList'
|
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
|
||||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
|
||||||
|
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
|
|
||||||
import Modal from '../../components/modal'
|
|
||||||
import PermissionFilterModal from './permissionFilterModal'
|
|
||||||
import AddPermissionModal from './addPermissionModal'
|
|
||||||
import UpdatePermissionModal from './updatePermissionModal'
|
|
||||||
import DeleteConfirmationModal from '../../components/deleteConfirmationModal'
|
|
||||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
|
||||||
|
|
||||||
import {
|
|
||||||
GroupDetailsResponse,
|
|
||||||
PermissionResponse,
|
|
||||||
RegisterPermissionPayload
|
|
||||||
} from '../../utils/types'
|
|
||||||
import { AppContext } from '../../context/appContext'
|
|
||||||
|
|
||||||
const BootstrapTableCell = styled(TableCell)({
|
|
||||||
textAlign: 'left'
|
|
||||||
})
|
|
||||||
|
|
||||||
export enum PrincipalType {
|
|
||||||
User = 'User',
|
|
||||||
Group = 'Group'
|
|
||||||
}
|
|
||||||
|
|
||||||
const Permission = () => {
|
|
||||||
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 [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
|
||||||
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
|
||||||
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 [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
|
||||||
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
|
||||||
PrincipalType[]
|
|
||||||
>([])
|
|
||||||
const [settingFilter, setSettingFilter] = useState<string[]>([])
|
|
||||||
const [permissions, setPermissions] = useState<PermissionResponse[]>([])
|
|
||||||
const [filteredPermissions, setFilteredPermissions] = useState<
|
|
||||||
PermissionResponse[]
|
|
||||||
>([])
|
|
||||||
const [filterApplied, setFilterApplied] = useState(false)
|
|
||||||
|
|
||||||
const fetchPermissions = useCallback(() => {
|
|
||||||
axios
|
|
||||||
.get(`/SASjsApi/permission`)
|
|
||||||
.then((res: any) => {
|
|
||||||
if (res.data?.length > 0) {
|
|
||||||
setPermissions(res.data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setModalTitle('Abort')
|
|
||||||
setModalPayload(
|
|
||||||
typeof err.response.data === 'object'
|
|
||||||
? JSON.stringify(err.response.data)
|
|
||||||
: err.response.data
|
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPermissions()
|
|
||||||
}, [fetchPermissions])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* first find the permissions w.r.t each filter type
|
|
||||||
* take intersection of resultant arrays
|
|
||||||
*/
|
|
||||||
const applyFilter = () => {
|
|
||||||
setFilterModalOpen(false)
|
|
||||||
|
|
||||||
const uriFilteredPermissions =
|
|
||||||
uriFilter.length > 0
|
|
||||||
? permissions.filter((permission) => uriFilter.includes(permission.uri))
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
const principalFilteredPermissions =
|
|
||||||
principalFilter.length > 0
|
|
||||||
? permissions.filter((permission) => {
|
|
||||||
if (permission.user) {
|
|
||||||
return principalFilter.includes(permission.user.username)
|
|
||||||
}
|
|
||||||
if (permission.group) {
|
|
||||||
return principalFilter.includes(permission.group.name)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
const principalTypeFilteredPermissions =
|
|
||||||
principalTypeFilter.length > 0
|
|
||||||
? permissions.filter((permission) => {
|
|
||||||
if (permission.user) {
|
|
||||||
return principalTypeFilter.includes(PrincipalType.User)
|
|
||||||
}
|
|
||||||
if (permission.group) {
|
|
||||||
return principalTypeFilter.includes(PrincipalType.Group)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
const settingFilteredPermissions =
|
|
||||||
settingFilter.length > 0
|
|
||||||
? permissions.filter((permission) =>
|
|
||||||
settingFilter.includes(permission.setting)
|
|
||||||
)
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
|
||||||
principalFilteredPermissions.some(
|
|
||||||
(item) => item.permissionId === permission.permissionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
filteredArray = filteredArray.filter((permission) =>
|
|
||||||
principalTypeFilteredPermissions.some(
|
|
||||||
(item) => item.permissionId === permission.permissionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
filteredArray = filteredArray.filter((permission) =>
|
|
||||||
settingFilteredPermissions.some(
|
|
||||||
(item) => item.permissionId === permission.permissionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
setFilteredPermissions(filteredArray)
|
|
||||||
setFilterApplied(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetFilter = () => {
|
|
||||||
setFilterModalOpen(false)
|
|
||||||
setUriFilter([])
|
|
||||||
setPrincipalFilter([])
|
|
||||||
setSettingFilter([])
|
|
||||||
setFilteredPermissions([])
|
|
||||||
setFilterApplied(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addPermission = (addPermissionPayload: RegisterPermissionPayload) => {
|
|
||||||
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 handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
|
||||||
setSelectedPermission(permission)
|
|
||||||
setUpdatePermissionModalOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePermission = (setting: string) => {
|
|
||||||
setUpdatePermissionModalOpen(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
|
|
||||||
setting
|
|
||||||
})
|
|
||||||
.then((res: any) => {
|
|
||||||
fetchPermissions()
|
|
||||||
setSnackbarMessage('Permission updated!')
|
|
||||||
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)
|
|
||||||
setSelectedPermission(undefined)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
|
||||||
setSelectedPermission(permission)
|
|
||||||
setDeleteConfirmationModalOpen(true)
|
|
||||||
setDeleteConfirmationModalMessage(
|
|
||||||
'Are you sure you want to delete this permission?'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletePermission = () => {
|
|
||||||
setDeleteConfirmationModalOpen(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
|
||||||
.then((res: any) => {
|
|
||||||
fetchPermissions()
|
|
||||||
setSnackbarMessage('Permission 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)
|
|
||||||
setSelectedPermission(undefined)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return isLoading ? (
|
|
||||||
<CircularProgress
|
|
||||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Box className="permissions-page">
|
|
||||||
<Grid container direction="column" spacing={1}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Paper elevation={3} sx={{ display: 'flex' }}>
|
|
||||||
<Tooltip title="Filter Permissions">
|
|
||||||
<IconButton>
|
|
||||||
<FilterListIcon onClick={() => setFilterModalOpen(true)} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
{appContext.isAdmin && (
|
|
||||||
<Tooltip
|
|
||||||
sx={{ marginLeft: 'auto' }}
|
|
||||||
title="Add Permission"
|
|
||||||
placement="bottom-end"
|
|
||||||
>
|
|
||||||
<IconButton onClick={() => setAddPermissionModalOpen(true)}>
|
|
||||||
<AddIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<PermissionTable
|
|
||||||
permissions={filterApplied ? filteredPermissions : permissions}
|
|
||||||
handleUpdatePermissionClick={handleUpdatePermissionClick}
|
|
||||||
handleDeletePermissionClick={handleDeletePermissionClick}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<BootstrapSnackbar
|
|
||||||
open={openSnackbar}
|
|
||||||
setOpen={setOpenSnackbar}
|
|
||||||
message={snackbarMessage}
|
|
||||||
severity={snackbarSeverity}
|
|
||||||
/>
|
|
||||||
<Modal
|
|
||||||
open={openModal}
|
|
||||||
setOpen={setOpenModal}
|
|
||||||
title={modalTitle}
|
|
||||||
payload={modalPayload}
|
|
||||||
/>
|
|
||||||
<PermissionFilterModal
|
|
||||||
open={filterModalOpen}
|
|
||||||
handleOpen={setFilterModalOpen}
|
|
||||||
permissions={permissions}
|
|
||||||
uriFilter={uriFilter}
|
|
||||||
setUriFilter={setUriFilter}
|
|
||||||
principalFilter={principalFilter}
|
|
||||||
setPrincipalFilter={setPrincipalFilter}
|
|
||||||
principalTypeFilter={principalTypeFilter}
|
|
||||||
setPrincipalTypeFilter={setPrincipalTypeFilter}
|
|
||||||
settingFilter={settingFilter}
|
|
||||||
setSettingFilter={setSettingFilter}
|
|
||||||
applyFilter={applyFilter}
|
|
||||||
resetFilter={resetFilter}
|
|
||||||
/>
|
|
||||||
<AddPermissionModal
|
|
||||||
open={addPermissionModalOpen}
|
|
||||||
handleOpen={setAddPermissionModalOpen}
|
|
||||||
addPermission={addPermission}
|
|
||||||
/>
|
|
||||||
<UpdatePermissionModal
|
|
||||||
open={updatePermissionModalOpen}
|
|
||||||
handleOpen={setUpdatePermissionModalOpen}
|
|
||||||
permission={selectedPermission}
|
|
||||||
updatePermission={updatePermission}
|
|
||||||
/>
|
|
||||||
<DeleteConfirmationModal
|
|
||||||
open={deleteConfirmationModalOpen}
|
|
||||||
setOpen={setDeleteConfirmationModalOpen}
|
|
||||||
message={deleteConfirmationModalMessage}
|
|
||||||
_delete={deletePermission}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Permission
|
|
||||||
|
|
||||||
type PermissionTableProps = {
|
|
||||||
permissions: PermissionResponse[]
|
|
||||||
handleUpdatePermissionClick: (permission: PermissionResponse) => void
|
|
||||||
handleDeletePermissionClick: (permission: PermissionResponse) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PermissionTable = ({
|
|
||||||
permissions,
|
|
||||||
handleUpdatePermissionClick,
|
|
||||||
handleDeletePermissionClick
|
|
||||||
}: PermissionTableProps) => {
|
|
||||||
const appContext = useContext(AppContext)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table sx={{ minWidth: 650 }}>
|
|
||||||
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
|
||||||
<TableRow>
|
|
||||||
<BootstrapTableCell>Uri</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Principal</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Type</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Setting</BootstrapTableCell>
|
|
||||||
{appContext.isAdmin && (
|
|
||||||
<BootstrapTableCell>Action</BootstrapTableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{permissions.map((permission) => (
|
|
||||||
<TableRow key={permission.permissionId}>
|
|
||||||
<BootstrapTableCell>{permission.uri}</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>
|
|
||||||
{displayPrincipal(permission)}
|
|
||||||
</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>
|
|
||||||
{displayPrincipalType(permission)}
|
|
||||||
</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>{permission.setting}</BootstrapTableCell>
|
|
||||||
{appContext.isAdmin && (
|
|
||||||
<BootstrapTableCell>
|
|
||||||
<Tooltip title="Edit Permission">
|
|
||||||
<IconButton
|
|
||||||
onClick={() => handleUpdatePermissionClick(permission)}
|
|
||||||
>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Delete Permission">
|
|
||||||
<IconButton
|
|
||||||
color="error"
|
|
||||||
onClick={() => handleDeletePermissionClick(permission)}
|
|
||||||
>
|
|
||||||
<DeleteForeverIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</BootstrapTableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayPrincipal = (permission: PermissionResponse) => {
|
|
||||||
if (permission.user) return permission.user.username
|
|
||||||
if (permission.group) return <DisplayGroup group={permission.group} />
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisplayGroupProps = {
|
|
||||||
group: GroupDetailsResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePopoverClose = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = Boolean(anchorEl)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Typography
|
|
||||||
aria-owns={open ? 'mouse-over-popover' : undefined}
|
|
||||||
aria-haspopup="true"
|
|
||||||
onMouseEnter={handlePopoverOpen}
|
|
||||||
onMouseLeave={handlePopoverClose}
|
|
||||||
>
|
|
||||||
{group.name}
|
|
||||||
</Typography>
|
|
||||||
<Popover
|
|
||||||
id="mouse-over-popover"
|
|
||||||
sx={{
|
|
||||||
pointerEvents: 'none'
|
|
||||||
}}
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'left'
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'left'
|
|
||||||
}}
|
|
||||||
onClose={handlePopoverClose}
|
|
||||||
disableRestoreFocus
|
|
||||||
>
|
|
||||||
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
|
||||||
Group Members
|
|
||||||
</Typography>
|
|
||||||
{group.users.map((user) => (
|
|
||||||
<Typography sx={{ p: 1 }} component="li">
|
|
||||||
{user.username}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayPrincipalType = (permission: PermissionResponse) => {
|
|
||||||
if (permission.user) return PrincipalType.User
|
|
||||||
if (permission.group) return PrincipalType.Group
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import React, { Dispatch, SetStateAction } from 'react'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
TextField
|
|
||||||
} from '@mui/material'
|
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
import Autocomplete from '@mui/material/Autocomplete'
|
|
||||||
|
|
||||||
import { PermissionResponse } from '../../utils/types'
|
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
|
||||||
import { PrincipalType } from './permission'
|
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
'& .MuiDialogContent-root': {
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
'& .MuiDialogActions-root': {
|
|
||||||
padding: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
type FilterModalProps = {
|
|
||||||
open: boolean
|
|
||||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
|
||||||
permissions: PermissionResponse[]
|
|
||||||
uriFilter: string[]
|
|
||||||
setUriFilter: Dispatch<SetStateAction<string[]>>
|
|
||||||
principalFilter: string[]
|
|
||||||
setPrincipalFilter: Dispatch<SetStateAction<string[]>>
|
|
||||||
principalTypeFilter: PrincipalType[]
|
|
||||||
setPrincipalTypeFilter: Dispatch<SetStateAction<PrincipalType[]>>
|
|
||||||
settingFilter: string[]
|
|
||||||
setSettingFilter: Dispatch<SetStateAction<string[]>>
|
|
||||||
applyFilter: () => void
|
|
||||||
resetFilter: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PermissionFilterModal = ({
|
|
||||||
open,
|
|
||||||
handleOpen,
|
|
||||||
permissions,
|
|
||||||
uriFilter,
|
|
||||||
setUriFilter,
|
|
||||||
principalFilter,
|
|
||||||
setPrincipalFilter,
|
|
||||||
principalTypeFilter,
|
|
||||||
setPrincipalTypeFilter,
|
|
||||||
settingFilter,
|
|
||||||
setSettingFilter,
|
|
||||||
applyFilter,
|
|
||||||
resetFilter
|
|
||||||
}: FilterModalProps) => {
|
|
||||||
const URIs = permissions
|
|
||||||
.map((permission) => permission.uri)
|
|
||||||
.filter((uri, index, array) => array.indexOf(uri) === index)
|
|
||||||
|
|
||||||
// fetch all the principals from permissions array
|
|
||||||
let principals = permissions.map((permission) => {
|
|
||||||
if (permission.user) return permission.user.username
|
|
||||||
if (permission.group) return permission.group.name
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// removes empty strings
|
|
||||||
principals = principals.filter((principal) => principal !== '')
|
|
||||||
|
|
||||||
// removes the duplicates
|
|
||||||
principals = principals.filter(
|
|
||||||
(principal, index, array) => array.indexOf(principal) === index
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
|
||||||
<BootstrapDialogTitle
|
|
||||||
id="permission-filter-dialog-title"
|
|
||||||
handleOpen={handleOpen}
|
|
||||||
>
|
|
||||||
Permission Filter
|
|
||||||
</BootstrapDialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Grid container spacing={1}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
options={URIs}
|
|
||||||
filterSelectedOptions
|
|
||||||
value={uriFilter}
|
|
||||||
onChange={(event: any, newValue: string[]) => {
|
|
||||||
setUriFilter(newValue)
|
|
||||||
}}
|
|
||||||
renderInput={(params) => <TextField {...params} label="URIs" />}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
options={principals}
|
|
||||||
filterSelectedOptions
|
|
||||||
value={principalFilter}
|
|
||||||
onChange={(event: any, newValue: string[]) => {
|
|
||||||
setPrincipalFilter(newValue)
|
|
||||||
}}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField {...params} label="Principals" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
options={Object.values(PrincipalType)}
|
|
||||||
filterSelectedOptions
|
|
||||||
value={principalTypeFilter}
|
|
||||||
onChange={(event: any, newValue: PrincipalType[]) => {
|
|
||||||
setPrincipalTypeFilter(newValue)
|
|
||||||
}}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField {...params} label="Principal Type" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
options={['Grant', 'Deny']}
|
|
||||||
filterSelectedOptions
|
|
||||||
value={settingFilter}
|
|
||||||
onChange={(event: any, newValue: string[]) => {
|
|
||||||
setSettingFilter(newValue)
|
|
||||||
}}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField {...params} label="Settings" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button variant="outlined" color="error" onClick={resetFilter}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
<Button variant="outlined" onClick={applyFilter}>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</BootstrapDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PermissionFilterModal
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import React, { useState, Dispatch, SetStateAction, useEffect } from 'react'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
TextField
|
|
||||||
} from '@mui/material'
|
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
import Autocomplete from '@mui/material/Autocomplete'
|
|
||||||
|
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
|
||||||
|
|
||||||
import { PermissionResponse } from '../../utils/types'
|
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
'& .MuiDialogContent-root': {
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
'& .MuiDialogActions-root': {
|
|
||||||
padding: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
type UpdatePermissionModalProps = {
|
|
||||||
open: boolean
|
|
||||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
|
||||||
permission: PermissionResponse | undefined
|
|
||||||
updatePermission: (setting: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const UpdatePermissionModal = ({
|
|
||||||
open,
|
|
||||||
handleOpen,
|
|
||||||
permission,
|
|
||||||
updatePermission
|
|
||||||
}: UpdatePermissionModalProps) => {
|
|
||||||
const [permissionSetting, setPermissionSetting] = useState('Grant')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (permission) setPermissionSetting(permission.setting)
|
|
||||||
}, [permission])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
|
|
||||||
<BootstrapDialogTitle
|
|
||||||
id="add-permission-dialog-title"
|
|
||||||
handleOpen={handleOpen}
|
|
||||||
>
|
|
||||||
Update Permission
|
|
||||||
</BootstrapDialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Autocomplete
|
|
||||||
sx={{ width: 300 }}
|
|
||||||
options={['Grant', 'Deny']}
|
|
||||||
disableClearable
|
|
||||||
value={permissionSetting}
|
|
||||||
onChange={(event: any, newValue: string) =>
|
|
||||||
setPermissionSetting(newValue)
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField {...params} label="Settings" />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => updatePermission(permissionSetting)}
|
|
||||||
disabled={permission?.setting === permissionSetting}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</BootstrapDialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UpdatePermissionModal
|
|
||||||
@@ -1,678 +0,0 @@
|
|||||||
import React, { 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
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrl = window.location.origin
|
|
||||||
|
|
||||||
const SASjsEditor = ({
|
|
||||||
selectedFilePath,
|
|
||||||
setSelectedFilePath
|
|
||||||
}: 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 [tab, setTab] = useState('1')
|
|
||||||
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('1')
|
|
||||||
// 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('2')
|
|
||||||
|
|
||||||
// Scroll to bottom of log
|
|
||||||
window.scrollTo(0, document.body.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',
|
|
||||||
position: 'fixed',
|
|
||||||
background: 'white',
|
|
||||||
width: '85%'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TabList onChange={handleTabChange} centered>
|
|
||||||
<StyledTab label="Code" value="1" />
|
|
||||||
<StyledTab label="Log" value="2" />
|
|
||||||
<StyledTab
|
|
||||||
label={
|
|
||||||
<Tooltip title="Displays content from the _webout fileref">
|
|
||||||
<Typography>Webout</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
value="3"
|
|
||||||
/>
|
|
||||||
</TabList>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<StyledTabPanel
|
|
||||||
sx={{ paddingBottom: 0, marginTop: '45px' }}
|
|
||||||
value="1"
|
|
||||||
>
|
|
||||||
<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="2">
|
|
||||||
<div style={{ marginTop: '50px' }}>
|
|
||||||
<h2>SAS Log</h2>
|
|
||||||
<pre>{log}</pre>
|
|
||||||
</div>
|
|
||||||
</StyledTabPanel>
|
|
||||||
<StyledTabPanel value="3">
|
|
||||||
<div style={{ marginTop: '50px' }}>
|
|
||||||
<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,99 +1,242 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useEffect, useRef, useState, useContext } from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import CssBaseline from '@mui/material/CssBaseline'
|
import {
|
||||||
import Box from '@mui/material/Box'
|
Box,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
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 { TreeNode } from '../../utils/types'
|
import { AppContext, RunTimeType } from '../../context/appContext'
|
||||||
|
|
||||||
import SideBar from './sideBar'
|
const useStyles = makeStyles(() => ({
|
||||||
import SASjsEditor from './editor'
|
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'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
const Studio = () => {
|
const Studio = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const appContext = useContext(AppContext)
|
||||||
const [selectedFilePath, setSelectedFilePath] = useState('')
|
const location = useLocation()
|
||||||
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
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('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedFilePath(searchParams.get('filePath') ?? '')
|
setRunTimes(Object.values(appContext.runTimes))
|
||||||
}, [searchParams])
|
}, [appContext.runTimes])
|
||||||
|
|
||||||
const fetchDirectoryData = useCallback(() => {
|
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) => {
|
||||||
axios
|
axios
|
||||||
.get(`/SASjsApi/drive/fileTree`)
|
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
if (res.data && res.data?.status === 'success') {
|
const parsedLog = res?.data?.log
|
||||||
setDirectoryData(res.data.tree)
|
.map((logLine: any) => logLine.line)
|
||||||
}
|
.join('\n')
|
||||||
})
|
|
||||||
.catch((err) => {
|
setLog(parsedLog)
|
||||||
console.log(err)
|
|
||||||
|
setWebout(`${res.data?._webout}`)
|
||||||
|
setTab('2')
|
||||||
|
|
||||||
|
// Scroll to bottom of log
|
||||||
|
window.scrollTo(0, document.body.scrollHeight)
|
||||||
})
|
})
|
||||||
|
.catch((err) => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
fetchDirectoryData()
|
if (fileContent.length) {
|
||||||
}, [fetchDirectoryData])
|
localStorage.setItem('fileContent', fileContent)
|
||||||
|
|
||||||
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])
|
||||||
|
|
||||||
const findAndRemoveNode = (
|
useEffect(() => {
|
||||||
node: TreeNode,
|
const params = new URLSearchParams(location.search)
|
||||||
parentNode: TreeNode,
|
const programPath = params.get('_program')
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeNodeFromParent = (parent: TreeNode, path: string) => {
|
if (programPath?.length)
|
||||||
const index = parent.children.findIndex(
|
axios
|
||||||
(node) => node.relativePath === path
|
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
|
||||||
)
|
.then((res: any) => setFileContent(res.data.fileContent))
|
||||||
if (index !== -1) {
|
.catch((err) => console.log(err))
|
||||||
parent.children.splice(index, 1)
|
}, [location.search])
|
||||||
}
|
|
||||||
}
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box
|
||||||
<CssBaseline />
|
onKeyUp={handleKeyUp}
|
||||||
<SideBar
|
onKeyDown={handleKeyDown}
|
||||||
selectedFilePath={selectedFilePath}
|
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
|
||||||
directoryData={directoryData}
|
>
|
||||||
handleSelect={handleSelect}
|
<TabContext value={tab}>
|
||||||
removeFileFromTree={removeFileFromTree}
|
<Box
|
||||||
refreshSideBar={fetchDirectoryData}
|
sx={{
|
||||||
/>
|
borderBottom: 1,
|
||||||
<SASjsEditor
|
borderColor: 'divider'
|
||||||
selectedFilePath={selectedFilePath}
|
}}
|
||||||
setSelectedFilePath={handleSelect}
|
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">
|
||||||
|
<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>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
import React, { useState, useMemo } from 'react'
|
|
||||||
import axios from 'axios'
|
|
||||||
import { Backdrop, Box, CircularProgress, Drawer, Toolbar } from '@mui/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 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
variant="permanent"
|
|
||||||
sx={{
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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={handleSelect}
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SideBar
|
|
||||||
@@ -29,8 +29,6 @@ interface AppContextProps {
|
|||||||
setUsername: Dispatch<SetStateAction<string>> | null
|
setUsername: Dispatch<SetStateAction<string>> | null
|
||||||
displayName: string
|
displayName: string
|
||||||
setDisplayName: Dispatch<SetStateAction<string>> | null
|
setDisplayName: Dispatch<SetStateAction<string>> | null
|
||||||
isAdmin: boolean
|
|
||||||
setIsAdmin: Dispatch<SetStateAction<boolean>> | null
|
|
||||||
mode: ModeType
|
mode: ModeType
|
||||||
runTimes: RunTimeType[]
|
runTimes: RunTimeType[]
|
||||||
logout: (() => void) | null
|
logout: (() => void) | null
|
||||||
@@ -46,8 +44,6 @@ export const AppContext = createContext<AppContextProps>({
|
|||||||
setUsername: null,
|
setUsername: null,
|
||||||
displayName: '',
|
displayName: '',
|
||||||
setDisplayName: null,
|
setDisplayName: null,
|
||||||
isAdmin: false,
|
|
||||||
setIsAdmin: null,
|
|
||||||
mode: ModeType.Server,
|
mode: ModeType.Server,
|
||||||
runTimes: [],
|
runTimes: [],
|
||||||
logout: null
|
logout: null
|
||||||
@@ -60,7 +56,6 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
const [userId, setUserId] = useState(0)
|
const [userId, setUserId] = useState(0)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [displayName, setDisplayName] = useState('')
|
const [displayName, setDisplayName] = useState('')
|
||||||
const [isAdmin, setIsAdmin] = useState(false)
|
|
||||||
const [mode, setMode] = useState(ModeType.Server)
|
const [mode, setMode] = useState(ModeType.Server)
|
||||||
const [runTimes, setRunTimes] = useState<RunTimeType[]>([])
|
const [runTimes, setRunTimes] = useState<RunTimeType[]>([])
|
||||||
|
|
||||||
@@ -75,7 +70,6 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
setUserId(data.id)
|
setUserId(data.id)
|
||||||
setUsername(data.username)
|
setUsername(data.username)
|
||||||
setDisplayName(data.displayName)
|
setDisplayName(data.displayName)
|
||||||
setIsAdmin(data.isAdmin)
|
|
||||||
setLoggedIn(true)
|
setLoggedIn(true)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -94,7 +88,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
axios.get('/SASLogon/logout').then(() => {
|
axios.get('/logout').then(() => {
|
||||||
setLoggedIn(false)
|
setLoggedIn(false)
|
||||||
setUsername('')
|
setUsername('')
|
||||||
setDisplayName('')
|
setDisplayName('')
|
||||||
@@ -113,8 +107,6 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
setUsername,
|
setUsername,
|
||||||
displayName,
|
displayName,
|
||||||
setDisplayName,
|
setDisplayName,
|
||||||
isAdmin,
|
|
||||||
setIsAdmin,
|
|
||||||
mode,
|
mode,
|
||||||
runTimes,
|
runTimes,
|
||||||
logout
|
logout
|
||||||
|
|||||||
@@ -18,22 +18,3 @@ code {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.permissions-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: '5px 10px';
|
|
||||||
margin-top: '10px';
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-label {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-label.selected {
|
|
||||||
background: lightgoldenrodyellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-item-label:hover {
|
|
||||||
background: lightgray;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './usePrompt'
|
|
||||||
export * from './useStateWithCallback'
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
export interface UserResponse {
|
|
||||||
id: number
|
|
||||||
username: string
|
|
||||||
displayName: string
|
|
||||||
isAdmin: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GroupResponse {
|
|
||||||
groupId: number
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GroupDetailsResponse extends GroupResponse {
|
|
||||||
isActive: boolean
|
|
||||||
users: UserResponse[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PermissionResponse {
|
|
||||||
permissionId: number
|
|
||||||
uri: string
|
|
||||||
setting: string
|
|
||||||
user?: UserResponse
|
|
||||||
group?: GroupDetailsResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RegisterPermissionPayload {
|
|
||||||
uri: 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