1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +00:00

Compare commits

...

131 Commits

Author SHA1 Message Date
semantic-release-bot
e355276e44 chore(release): 0.17.4 [skip ci]
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)

### Bug Fixes

* invalid JS logic ([9f06080](9f06080348))
2022-09-01 12:50:14 +00:00
Allan Bowe
a3a9e3bd9f Merge pull request #269 from sasjs/allanbowe/error-unrecognized-sas-267
fix: invalid JS logic
2022-09-01 13:41:52 +01:00
Allan Bowe
9f06080348 fix: invalid JS logic 2022-09-01 12:35:58 +00:00
semantic-release-bot
4bbf9cfdb3 chore(release): 0.17.3 [skip ci]
## [0.17.3](https://github.com/sasjs/server/compare/v0.17.2...v0.17.3) (2022-09-01)

### Bug Fixes

* making SASINITIALFOLDER option windows only.  Closes [#267](https://github.com/sasjs/server/issues/267) ([e63271a](e63271a67a))
2022-09-01 12:25:33 +00:00
Allan Bowe
e8e71fcde9 Merge pull request #268 from sasjs/allanbowe/error-unrecognized-sas-267
fix: making SASINITIALFOLDER option windows only.  Closes #267
2022-09-01 13:21:25 +01:00
Allan Bowe
e63271a67a fix: making SASINITIALFOLDER option windows only. Closes #267 2022-09-01 12:18:53 +00:00
semantic-release-bot
e67d27d264 chore(release): 0.17.2 [skip ci]
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)

### Bug Fixes

* addition of SASINITIALFOLDER startup option.  Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](a5ee2f2923))
2022-08-31 09:35:51 +00:00
Allan Bowe
53033ccc96 Merge pull request #262 from sasjs/allanbowe/sas-default-folder-should-260
fix: addition of SASINITIALFOLDER startup option.  Closes #260
2022-08-31 10:32:14 +01:00
semantic-release-bot
6131ed1cbe chore(release): 0.17.1 [skip ci]
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)

### Bug Fixes

* typo mistake ([ee17d37](ee17d37aa1))
2022-08-30 18:15:33 +00:00
Allan Bowe
5d624e3399 Merge pull request #266 from sasjs/issue-265
fix: typo mistake
2022-08-30 19:10:47 +01:00
ee17d37aa1 fix: typo mistake 2022-08-30 22:42:11 +05:00
semantic-release-bot
476f834a80 chore(release): 0.17.0 [skip ci]
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)

### Bug Fixes

* allow underscores in file name ([bce83cb](bce83cb6fb))

### Features

* add the functionality of saving file by ctrl + s in editor ([3a3c90d](3a3c90d9e6))
2022-08-25 09:47:48 +00:00
Sabir Hassan
8b8739a873 Merge pull request #263 from sasjs/ctrl-save
feat: add the functionality of saving file by ctrl + s in editor
2022-08-25 14:43:50 +05:00
bce83cb6fb fix: allow underscores in file name 2022-08-25 14:27:42 +05:00
3a3c90d9e6 feat: add the functionality of saving file by ctrl + s in editor 2022-08-25 14:12:51 +05:00
semantic-release-bot
e63eaa5302 chore(release): 0.16.1 [skip ci]
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)

### Bug Fixes

* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](98ea2ac9b9))
2022-08-24 16:07:11 +00:00
Sabir Hassan
65de1bb175 Merge pull request #261 from sasjs/issue-258
fix: update response of /SASjsApi/stp/execute and /SASjsApi/code/execute
2022-08-24 21:02:57 +05:00
Allan Bowe
a5ee2f2923 fix: addition of SASINITIALFOLDER startup option. Closes #260 2022-08-19 15:20:36 +00:00
98ea2ac9b9 fix: update response of /SASjsApi/stp/execute and /SASjsApi/code/execute 2022-08-19 15:06:39 +05:00
semantic-release-bot
e94c56b23f chore(release): 0.16.0 [skip ci]
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)

### Bug Fixes

* add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py ([882bedd](882bedd5d5))
* update content for code.sas file ([02e88ae](02e88ae728))
* update default content type for python and js runtimes ([8780b80](8780b800a3))

### Features

* implement the logic for running python stored programs ([b06993a](b06993ab9e))
2022-08-17 20:49:53 +00:00
Allan Bowe
64f80e958d Merge pull request #259 from sasjs/python-runtime
feat: implement the logic for running python stored programs
2022-08-17 21:45:55 +01:00
bd97363c13 chore: quick fixes 2022-08-18 01:39:03 +05:00
02e88ae728 fix: update content for code.sas file 2022-08-18 01:20:33 +05:00
882bedd5d5 fix: add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py 2022-08-18 01:19:47 +05:00
8780b800a3 fix: update default content type for python and js runtimes 2022-08-18 01:17:17 +05:00
4c11082796 chore: addressed comments 2022-08-17 21:24:30 +05:00
a9b25b8880 chore: added specs for stp 2022-08-16 22:39:15 +05:00
b06993ab9e feat: implement the logic for running python stored programs 2022-08-16 15:51:37 +05:00
semantic-release-bot
f736e67517 chore(release): 0.15.3 [skip ci]
## [0.15.3](https://github.com/sasjs/server/compare/v0.15.2...v0.15.3) (2022-08-11)

### Bug Fixes

* adding proc printto in precode to enable print output in log.  Closes [#253](https://github.com/sasjs/server/issues/253) ([f8bb732](f8bb7327a8))
2022-08-11 15:07:31 +00:00
Allan Bowe
0f4a60c0c7 Merge pull request #254 from sasjs/allanbowe/sasjs-server-does-not-253
fix: adding proc printto in precode to enable print output in log.  Closes #253
2022-08-11 16:03:13 +01:00
Allan Bowe
f8bb7327a8 fix: adding proc printto in precode to enable print output in log. Closes #253 2022-08-11 15:01:46 +00:00
semantic-release-bot
abce135da2 chore(release): 0.15.2 [skip ci]
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)

### Bug Fixes

* remove vulnerabitities ([f27ac51](f27ac51fc4))
2022-08-10 11:28:07 +00:00
Allan Bowe
a6c014946a Merge pull request #252 from sasjs/fix-vulnerabilities
fix: remove vulnerabitities
2022-08-10 12:23:23 +01:00
f27ac51fc4 fix: remove vulnerabitities 2022-08-10 16:10:37 +05:00
semantic-release-bot
cb5be1be21 chore(release): 0.15.1 [skip ci]
## [0.15.1](https://github.com/sasjs/server/compare/v0.15.0...v0.15.1) (2022-08-10)

### Bug Fixes

* **web:** fix UI responsiveness ([d99fdd1](d99fdd1ec7))
2022-08-10 10:34:36 +00:00
Allan Bowe
d90fa9e5dd Merge pull request #251 from sasjs/issue-250
fix(web): fix UI responsiveness
2022-08-10 11:29:41 +01:00
d99fdd1ec7 fix(web): fix UI responsiveness 2022-08-10 15:18:05 +05:00
semantic-release-bot
399b5edad0 chore(release): 0.15.0 [skip ci]
# [0.15.0](https://github.com/sasjs/server/compare/v0.14.1...v0.15.0) (2022-08-05)

### Bug Fixes

* after selecting file in sidebar collapse sidebar in mobile view ([e215958](e215958b8b))
* improve mobile view for studio page ([c67d3ee](c67d3ee2f1))
* improve responsiveness for mobile view ([6ef40b9](6ef40b954a))
* improve user experience for adding permissions ([7a162ed](7a162eda8f))
* show logout button only when user is logged in ([9227cd4](9227cd449d))

### Features

* add multiple permission for same combination of type and principal at once ([754704b](754704bca8))
2022-08-05 09:59:19 +00:00
Allan Bowe
1dbc12e96b Merge pull request #249 from sasjs/issue-225
feat: add multiple permission for same combination of type and principal at once
2022-08-05 10:55:32 +01:00
e215958b8b fix: after selecting file in sidebar collapse sidebar in mobile view 2022-08-05 14:18:59 +05:00
9227cd449d fix: show logout button only when user is logged in 2022-08-05 01:22:27 +05:00
c67d3ee2f1 fix: improve mobile view for studio page 2022-08-05 01:10:15 +05:00
6ef40b954a fix: improve responsiveness for mobile view 2022-08-04 22:57:21 +05:00
semantic-release-bot
0d913baff1 chore(release): 0.14.1 [skip ci]
## [0.14.1](https://github.com/sasjs/server/compare/v0.14.0...v0.14.1) (2022-08-04)

### Bug Fixes

* **apps:** App Stream logo fix ([87c03c5](87c03c5f8d))
* **cookie:** XSRF cookie is removed and passed token in head section ([77f8d30](77f8d30baf))
* **env:** check added for not providing WHITELIST ([5966016](5966016853))
* **web:** show login on logged-out state ([f7fcc77](f7fcc7741a))
2022-08-04 12:10:31 +00:00
Allan Bowe
3671736c3d Merge pull request #248 from sasjs/cookies-management
fix(cookie): XSRF cookie is removed and passed token in head section
2022-08-04 13:06:30 +01:00
34cd84d8a9 chore: improve interface for add permission response 2022-08-04 16:34:15 +05:00
Saad Jutt
f7fcc7741a fix(web): show login on logged-out state 2022-08-04 05:39:28 +05:00
Saad Jutt
18052fdbf6 test: fixed failed specs 2022-08-04 04:01:51 +05:00
Saad Jutt
5966016853 fix(env): check added for not providing WHITELIST 2022-08-04 03:32:04 +05:00
Saad Jutt
87c03c5f8d fix(apps): App Stream logo fix 2022-08-04 03:03:27 +05:00
7a162eda8f fix: improve user experience for adding permissions 2022-08-04 02:51:59 +05:00
754704bca8 feat: add multiple permission for same combination of type and principal at once 2022-08-03 23:26:31 +05:00
Saad Jutt
77f8d30baf fix(cookie): XSRF cookie is removed and passed token in head section 2022-08-03 03:38:11 +05:00
semantic-release-bot
78bea7c154 chore(release): 0.14.0 [skip ci]
# [0.14.0](https://github.com/sasjs/server/compare/v0.13.3...v0.14.0) (2022-08-02)

### Bug Fixes

* add restriction on  add/remove user to public group ([d3a516c](d3a516c36e))
* call jwt.verify in synchronous way ([254bc07](254bc07da7))

### Features

* add public group to DB on seed ([c3e3bef](c3e3befc17))
* bypass authentication when route is enabled for public group ([68515f9](68515f95a6))
2022-08-02 19:08:38 +00:00
Saad Jutt
9c3b155c12 Merge pull request #246 from sasjs/issue-240
feat: bypass authentication when route is enabled for public group
2022-08-03 00:03:43 +05:00
Allan Bowe
98e501334f Update seedDB.ts 2022-08-02 19:33:16 +01:00
Allan Bowe
bbfd53e79e Update group.spec.ts 2022-08-02 19:32:44 +01:00
254bc07da7 fix: call jwt.verify in synchronous way 2022-08-02 23:05:42 +05:00
f978814ca7 chore: code refactor 2022-08-02 22:16:41 +05:00
68515f95a6 feat: bypass authentication when route is enabled for public group 2022-08-02 18:06:33 +05:00
d3a516c36e fix: add restriction on add/remove user to public group 2022-08-02 18:05:28 +05:00
c3e3befc17 feat: add public group to DB on seed 2022-08-02 18:04:00 +05:00
semantic-release-bot
275de9478e chore(release): 0.13.3 [skip ci]
## [0.13.3](https://github.com/sasjs/server/compare/v0.13.2...v0.13.3) (2022-08-02)

### Bug Fixes

* show non-admin user his own permissions only ([8a3054e](8a3054e19a))
* update schema of Permission ([5d5a9d3](5d5a9d3788))
2022-08-02 12:01:53 +00:00
Allan Bowe
1a3ef62cb2 Merge pull request #243 from sasjs/issue-241
fix: show non-admin user his own permissions only
2022-08-02 12:57:57 +01:00
semantic-release-bot
9eb5f3ca4d chore(release): 0.13.2 [skip ci]
## [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](916947dffa))
2022-08-01 22:42:31 +00:00
Allan Bowe
916947dffa fix: adding ls=max to reduce log size and improve readability 2022-08-01 22:38:31 +00:00
79b7827b7c chore: update tabs label in setting page 2022-08-01 23:01:05 +05:00
37e1aa9b61 chore: spec fixed 2022-08-01 22:54:31 +05:00
7e504008b7 chore: quick fix 2022-08-01 22:50:18 +05:00
5d5a9d3788 fix: update schema of Permission 2022-08-01 21:33:10 +05:00
semantic-release-bot
7c79d6479c chore(release): 0.13.1 [skip ci]
## [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](77db14c690))
2022-07-31 17:09:11 +00:00
Allan Bowe
3e635f422a Merge pull request #245 from sasjs/allanbowe/avoid-batch-sas-window-244
fix: adding options to prevent unwanted windows on windows.  Closes #244
2022-07-31 18:05:05 +01:00
Allan Bowe
77db14c690 fix: adding options to prevent unwanted windows on windows. Closes #244 2022-07-31 16:58:33 +00:00
b7dff341f0 chore: fix specs 2022-07-30 00:18:02 +05:00
8a3054e19a fix: show non-admin user his own permissions only 2022-07-30 00:01:15 +05:00
semantic-release-bot
a531de2adb chore(release): 0.13.0 [skip ci]
# [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](7681722e5a))
* move api button to user menu ([8de032b](8de032b543))

### Features

* add action and command to editor ([706e228](706e228a8e))
2022-07-28 19:27:12 +00:00
Allan Bowe
c458d94493 Merge pull request #239 from sasjs/issue-238
fix: improve user experience in the studio
2022-07-28 20:21:48 +01:00
706e228a8e feat: add action and command to editor 2022-07-28 23:56:44 +05:00
7681722e5a fix: autofocus input field and submit on enter 2022-07-28 23:55:59 +05:00
8de032b543 fix: move api button to user menu 2022-07-28 23:54:40 +05:00
semantic-release-bot
998ef213e9 chore(release): 0.12.1 [skip ci]
## [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](c574b42235))
* **web:** saveAs functionality fixed in studio page ([3c987c6](3c987c61dd))
* **web:** show original name as default name in rename file/folder modal ([9640f65](9640f65264))
* **web:** webout tab item fixed in studio page ([7cdffe3](7cdffe30e3))
* **web:** when no file is selected save the editor content to local storage ([3b1fcb9](3b1fcb937d))
2022-07-26 20:52:05 +00:00
Allan Bowe
f8b0f98678 Merge pull request #236 from sasjs/fix-studio
fix: issues fixed in studio page
2022-07-26 21:48:20 +01:00
9640f65264 fix(web): show original name as default name in rename file/folder modal 2022-07-27 01:44:13 +05:00
c574b42235 fix(web): disable launch icon button when file content is not saved 2022-07-27 01:42:46 +05:00
468d1a929d chore(web): quick fixes 2022-07-27 00:47:38 +05:00
7cdffe30e3 fix(web): webout tab item fixed in studio page 2022-07-26 23:53:07 +05:00
3b1fcb937d fix(web): when no file is selected save the editor content to local storage 2022-07-26 23:30:41 +05:00
3c987c61dd fix(web): saveAs functionality fixed in studio page 2022-07-26 23:15:42 +05:00
0a780697da chore(web): move hooks to hooks folder 2022-07-26 23:14:29 +05:00
83d819df53 chore(web): created custom useStateWithCallback hook 2022-07-26 23:12:55 +05:00
semantic-release-bot
95df2b21d6 chore(release): 0.12.0 [skip ci]
# [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](0f19384999))
* remove drive component ([06d7c91](06d7c91fc3))

### Features

* add api end point for delete folder ([08e0c61](08e0c61e0f))
* add sidebar(drive) to left of studio ([6c35412](6c35412d2f))
* created api endpoint for adding empty folder in drive ([941917e](941917e508))
* implemented api for renaming file/folder ([fdcaba9](fdcaba9d56))
* implemented delete file/folder functionality ([177675b](177675bc89))
* implemented functionality for adding file/folder from sidebar context menu ([0ce94a5](0ce94a553e))
* implemented the functionality for renaming file/folder from context menu ([7010a6a](7010a6a120))
* prevent user from leaving studio page when there are unsaved changes ([6c75502](6c7550286b))
* **web:** add difference view editor in studio ([420a61a](420a61a5a6))
2022-07-26 14:29:41 +00:00
Allan Bowe
accdf914f1 Merge pull request #235 from sasjs/issue-198
feat: deprecate drive and add sidebar for file navigation to studio
2022-07-26 15:25:26 +01:00
15bdd2d7f0 chore: close file menu after clicking on diff editor menu item 2022-07-26 14:54:06 +05:00
2ce947d216 chore: code fixes 2022-07-26 14:16:27 +05:00
ce2114e3f6 chore: code fixes 2022-07-26 00:07:11 +05:00
6c7550286b feat: prevent user from leaving studio page when there are unsaved changes 2022-07-25 22:41:05 +05:00
2360e104bd chore: reduce the padding between tree items 2022-07-25 15:11:02 +05:00
420a61a5a6 feat(web): add difference view editor in studio 2022-07-25 15:01:04 +05:00
04e0f9efe3 chore: merge main into issue-198 2022-07-22 22:31:32 +05:00
99172cd9ed chore: add specs 2022-07-22 22:18:03 +05:00
57daad0c26 chore: error response codes for drive api 2022-07-22 16:58:26 +05:00
cc1e4543fc chore: add specs 2022-07-21 23:03:56 +05:00
03cb89d14f chore: code fixes 2022-07-21 23:03:40 +05:00
72140d73c2 chore: modified folderParamValidation method 2022-07-21 14:08:44 +05:00
efcefd2a42 chore: quick fix 2022-07-21 13:25:46 +05:00
06d7c91fc3 fix: remove drive component 2022-07-20 23:53:42 +05:00
7010a6a120 feat: implemented the functionality for renaming file/folder from context menu 2022-07-20 23:46:39 +05:00
fdcaba9d56 feat: implemented api for renaming file/folder 2022-07-20 23:45:11 +05:00
48688a6547 chore: update swagger docs 2022-07-20 16:52:49 +05:00
0ce94a553e feat: implemented functionality for adding file/folder from sidebar context menu 2022-07-20 16:45:45 +05:00
941917e508 feat: created api endpoint for adding empty folder in drive 2022-07-20 16:43:43 +05:00
semantic-release-bot
5706371ffd chore(release): 0.11.5 [skip ci]
## [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](ce5218a227))
2022-07-19 23:03:43 +00:00
Saad Jutt
ce5218a227 fix: Revert "fix(security): missing cookie flags are added"
This reverts commit 526402fd73.
2022-07-20 03:58:25 +05:00
semantic-release-bot
8b62755f39 chore(release): 0.11.4 [skip ci]
## [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](526402fd73))
2022-07-19 21:06:05 +00:00
Allan Bowe
cb84c3ebbb Merge pull request #234 from sasjs/issue147
fix(security): missing cookie flags are added
2022-07-19 22:02:05 +01:00
Saad Jutt
526402fd73 fix(security): missing cookie flags are added 2022-07-20 01:40:31 +05:00
177675bc89 feat: implemented delete file/folder functionality 2022-07-19 22:49:34 +05:00
721165ff12 chore: add delete confirmation modal and use it in permission component 2022-07-19 22:48:22 +05:00
08e0c61e0f feat: add api end point for delete folder 2022-07-19 22:41:03 +05:00
semantic-release-bot
1b234eb2b1 chore(release): 0.11.3 [skip ci]
## [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](299512135d))
2022-07-19 14:50:19 +00:00
Allan Bowe
ef25eec11f Merge pull request #233 from sasjs/issue-227
fix: filePath fix in code.js file for windows
2022-07-19 15:46:18 +01:00
3e53f70928 chore: update swagger docs 2022-07-19 16:14:40 +05:00
0f19384999 fix: fileTree api response to include an additional attribute isFolder 2022-07-19 16:13:46 +05:00
63dd6813c0 chore: lint fix 2022-07-19 13:07:34 +05:00
299512135d fix: filePath fix in code.js file for windows 2022-07-19 13:00:33 +05:00
6c35412d2f feat: add sidebar(drive) to left of studio 2022-07-18 22:39:09 +05:00
27410bc32b chore: add file path input modal 2022-07-18 22:37:32 +05:00
849b2dd468 chore: add custom tree view component 2022-07-18 22:32:10 +05:00
30d7a65358 chore: fix breaking changes caused by react-router-dom update 2022-07-15 18:42:59 +05:00
5e930f14d2 chore: bump mui/icons-material and react-router-dom versions 2022-07-15 18:41:11 +05:00
9bc68b1cdc chore: update swagger docs 2022-07-15 18:40:02 +05:00
76 changed files with 6491 additions and 3848 deletions

View File

@@ -1,3 +1,213 @@
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)
### Bug Fixes
* invalid JS logic ([9f06080](https://github.com/sasjs/server/commit/9f06080348aed076f8188a26fb4890d38a5a3510))
## [0.17.3](https://github.com/sasjs/server/compare/v0.17.2...v0.17.3) (2022-09-01)
### Bug Fixes
* making SASINITIALFOLDER option windows only. Closes [#267](https://github.com/sasjs/server/issues/267) ([e63271a](https://github.com/sasjs/server/commit/e63271a67a0deb3059a5f2bec1854efee5a6e5a5))
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)
### Bug Fixes
* addition of SASINITIALFOLDER startup option. Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](https://github.com/sasjs/server/commit/a5ee2f292384f90e9d95d003d652311c0d91a7a7))
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)
### Bug Fixes
* typo mistake ([ee17d37](https://github.com/sasjs/server/commit/ee17d37aa188b0ca43cea0e89d6cd1a566b765cb))
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)
### Bug Fixes
* allow underscores in file name ([bce83cb](https://github.com/sasjs/server/commit/bce83cb6fbc98f8198564c9399821f5829acc767))
### Features
* add the functionality of saving file by ctrl + s in editor ([3a3c90d](https://github.com/sasjs/server/commit/3a3c90d9e690ac5267bf1acc834b5b5c5b4dadb6))
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)
### Bug Fixes
* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](https://github.com/sasjs/server/commit/98ea2ac9b98631605e39e5900e533727ea0e3d85))
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)
### Bug Fixes
* add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py ([882bedd](https://github.com/sasjs/server/commit/882bedd5d5da22de6ed45c03d0a261aadfb3a33c))
* update content for code.sas file ([02e88ae](https://github.com/sasjs/server/commit/02e88ae7280d020a753bc2c095a931c79ac392d1))
* update default content type for python and js runtimes ([8780b80](https://github.com/sasjs/server/commit/8780b800a34aa618631821e5d97e26e8b0f15806))
### Features
* implement the logic for running python stored programs ([b06993a](https://github.com/sasjs/server/commit/b06993ab9ea24b28d9e553763187387685aaa666))
## [0.15.3](https://github.com/sasjs/server/compare/v0.15.2...v0.15.3) (2022-08-11)
### Bug Fixes
* adding proc printto in precode to enable print output in log. Closes [#253](https://github.com/sasjs/server/issues/253) ([f8bb732](https://github.com/sasjs/server/commit/f8bb7327a8a4649ac77bb6237e31cea075d46bb9))
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)
### Bug Fixes
* remove vulnerabitities ([f27ac51](https://github.com/sasjs/server/commit/f27ac51fc4beb21070d0ab551cfdaec1f6ba39e0))
## [0.15.1](https://github.com/sasjs/server/compare/v0.15.0...v0.15.1) (2022-08-10)
### Bug Fixes
* **web:** fix UI responsiveness ([d99fdd1](https://github.com/sasjs/server/commit/d99fdd1ec7991b94a0d98338d7a7a6216f46ce45))
# [0.15.0](https://github.com/sasjs/server/compare/v0.14.1...v0.15.0) (2022-08-05)
### Bug Fixes
* after selecting file in sidebar collapse sidebar in mobile view ([e215958](https://github.com/sasjs/server/commit/e215958b8b05d7a8ce9d82395e0640b5b37fb40d))
* improve mobile view for studio page ([c67d3ee](https://github.com/sasjs/server/commit/c67d3ee2f102155e2e9781e13d5d33c1ab227cb4))
* improve responsiveness for mobile view ([6ef40b9](https://github.com/sasjs/server/commit/6ef40b954a87ebb0a2621119064f38d58ea85148))
* improve user experience for adding permissions ([7a162ed](https://github.com/sasjs/server/commit/7a162eda8fc60383ff647d93e6611799e2e6af7a))
* show logout button only when user is logged in ([9227cd4](https://github.com/sasjs/server/commit/9227cd449dc46fd960a488eb281804a9b9ffc284))
### Features
* add multiple permission for same combination of type and principal at once ([754704b](https://github.com/sasjs/server/commit/754704bca89ecbdbcc3bd4ef04b94124c4f24167))
## [0.14.1](https://github.com/sasjs/server/compare/v0.14.0...v0.14.1) (2022-08-04)
### Bug Fixes
* **apps:** App Stream logo fix ([87c03c5](https://github.com/sasjs/server/commit/87c03c5f8dbdfc151d4ff3722ecbcd3f7e409aea))
* **cookie:** XSRF cookie is removed and passed token in head section ([77f8d30](https://github.com/sasjs/server/commit/77f8d30baf9b1077279c29f1c3e5ca02a5436bc0))
* **env:** check added for not providing WHITELIST ([5966016](https://github.com/sasjs/server/commit/5966016853369146b27ac5781808cb51d65c887f))
* **web:** show login on logged-out state ([f7fcc77](https://github.com/sasjs/server/commit/f7fcc7741aa2af93a4a2b1e651003704c9bbff0c))
# [0.14.0](https://github.com/sasjs/server/compare/v0.13.3...v0.14.0) (2022-08-02)
### Bug Fixes
* add restriction on add/remove user to public group ([d3a516c](https://github.com/sasjs/server/commit/d3a516c36e45aa1cc76c30c744e6a0e5bd553165))
* call jwt.verify in synchronous way ([254bc07](https://github.com/sasjs/server/commit/254bc07da744a9708109bfb792be70aa3f6284f4))
### Features
* add public group to DB on seed ([c3e3bef](https://github.com/sasjs/server/commit/c3e3befc17102ee1754e1403193040b4f79fb2a7))
* bypass authentication when route is enabled for public group ([68515f9](https://github.com/sasjs/server/commit/68515f95a65d422e29c0ed6028f3ea0ae8d9b1bf))
## [0.13.3](https://github.com/sasjs/server/compare/v0.13.2...v0.13.3) (2022-08-02)
### Bug Fixes
* show non-admin user his own permissions only ([8a3054e](https://github.com/sasjs/server/commit/8a3054e19ade82e2792cfb0f2a8af9e502c5eb52))
* update schema of Permission ([5d5a9d3](https://github.com/sasjs/server/commit/5d5a9d3788281d75c56f68f0dff231abc9c9c275))
## [0.13.2](https://github.com/sasjs/server/compare/v0.13.1...v0.13.2) (2022-08-01)
### Bug Fixes
* adding ls=max to reduce log size and improve readability ([916947d](https://github.com/sasjs/server/commit/916947dffacd902ff23ac3e899d1bf5ab6238b75))
## [0.13.1](https://github.com/sasjs/server/compare/v0.13.0...v0.13.1) (2022-07-31)
### Bug Fixes
* adding options to prevent unwanted windows on windows. Closes [#244](https://github.com/sasjs/server/issues/244) ([77db14c](https://github.com/sasjs/server/commit/77db14c690e18145d733ac2b0d646ab0dbe4d521))
# [0.13.0](https://github.com/sasjs/server/compare/v0.12.1...v0.13.0) (2022-07-28)
### Bug Fixes
* autofocus input field and submit on enter ([7681722](https://github.com/sasjs/server/commit/7681722e5afdc2df0c9eed201b05add3beda92a7))
* move api button to user menu ([8de032b](https://github.com/sasjs/server/commit/8de032b5431b47daabcf783c47ff078bf817247d))
### Features
* add action and command to editor ([706e228](https://github.com/sasjs/server/commit/706e228a8e1924786fd9dc97de387974eda504b1))
## [0.12.1](https://github.com/sasjs/server/compare/v0.12.0...v0.12.1) (2022-07-26)
### Bug Fixes
* **web:** disable launch icon button when file content is not saved ([c574b42](https://github.com/sasjs/server/commit/c574b4223591c4a6cd3ef5e146ce99cd8f7c9190))
* **web:** saveAs functionality fixed in studio page ([3c987c6](https://github.com/sasjs/server/commit/3c987c61ddc258f991e2bf38c1f16a0c4248d6ae))
* **web:** show original name as default name in rename file/folder modal ([9640f65](https://github.com/sasjs/server/commit/9640f6526496f3564664ccb1f834d0f659dcad4e))
* **web:** webout tab item fixed in studio page ([7cdffe3](https://github.com/sasjs/server/commit/7cdffe30e36e5cad0284f48ea97925958e12704c))
* **web:** when no file is selected save the editor content to local storage ([3b1fcb9](https://github.com/sasjs/server/commit/3b1fcb937d06d02ab99c9e8dbe307012d48a7a3a))
# [0.12.0](https://github.com/sasjs/server/compare/v0.11.5...v0.12.0) (2022-07-26)
### Bug Fixes
* fileTree api response to include an additional attribute isFolder ([0f19384](https://github.com/sasjs/server/commit/0f193849994f1ac8a071afa8f10af5b46f86663d))
* remove drive component ([06d7c91](https://github.com/sasjs/server/commit/06d7c91fc34620a954df1fd1c682eff370f79ca6))
### Features
* add api end point for delete folder ([08e0c61](https://github.com/sasjs/server/commit/08e0c61e0fd7041d6cded6f4d71fbb410e5615ce))
* add sidebar(drive) to left of studio ([6c35412](https://github.com/sasjs/server/commit/6c35412d2f5180d4e49b12e616576d8b8dacb7d8))
* created api endpoint for adding empty folder in drive ([941917e](https://github.com/sasjs/server/commit/941917e508ece5009135f9dddf99775dd4002f78))
* implemented api for renaming file/folder ([fdcaba9](https://github.com/sasjs/server/commit/fdcaba9d56cddea5d56d7de5a172f1bb49be3db5))
* implemented delete file/folder functionality ([177675b](https://github.com/sasjs/server/commit/177675bc897416f7994dd849dc7bb11ba072efe9))
* implemented functionality for adding file/folder from sidebar context menu ([0ce94a5](https://github.com/sasjs/server/commit/0ce94a553e53bfcdbd6273b26b322095a080a341))
* implemented the functionality for renaming file/folder from context menu ([7010a6a](https://github.com/sasjs/server/commit/7010a6a1201720d0eb4093267a344fb828b90a2f))
* prevent user from leaving studio page when there are unsaved changes ([6c75502](https://github.com/sasjs/server/commit/6c7550286b5f505e9dfe8ca63c62fa1db1b60b2e))
* **web:** add difference view editor in studio ([420a61a](https://github.com/sasjs/server/commit/420a61a5a6b11dcb5eb0a652ea9cecea5c3bee5f))
## [0.11.5](https://github.com/sasjs/server/compare/v0.11.4...v0.11.5) (2022-07-19)
### Bug Fixes
* Revert "fix(security): missing cookie flags are added" ([ce5218a](https://github.com/sasjs/server/commit/ce5218a2278cc750f2b1032024685dc6cd72f796))
## [0.11.4](https://github.com/sasjs/server/compare/v0.11.3...v0.11.4) (2022-07-19)
### Bug Fixes
* **security:** missing cookie flags are added ([526402f](https://github.com/sasjs/server/commit/526402fd73407ee4fa2d31092111a7e6a1741487))
## [0.11.3](https://github.com/sasjs/server/compare/v0.11.2...v0.11.3) (2022-07-19)
### 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)

View File

@@ -64,12 +64,27 @@ Example contents of a `.env` file:
# Server mode is multi-user and suitable for intranet / internet use
MODE=
# A comma separated string that defines the available runTimes.
# Priority is given to the runtime that comes first in the string.
# Possible options at the moment are sas and js
# This string sets the priority of the available analytic runtimes
# Valid runtimes are SAS (sas), JavaScript (js) and Python (py)
# For each option provided, there should be a corresponding path,
# eg SAS_PATH, NODE_PATH or PYTHON_PATH
# Priority is given to runtimes earlier in the string
# Example options: [sas,js,py | js,py | sas | sas,js]
RUN_TIMES=
# Path to SAS executable (sas.exe / sas.sh)
SAS_PATH=/path/to/sas/executable.exe
# Path to Node.js executable
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
# Path to Python executable
PYTHON_PATH=/usr/bin/python
# Path to working directory
# This location is for SAS WORK, staged files, DRIVE, configuration etc
SASJS_ROOT=./sasjs_root
@@ -139,13 +154,6 @@ 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.
# Priority is given to the runtime that comes first in the string.
# Possible options at the moment are sas and js
# options: [sas,js|js,sas|sas|js] default:sas
RUN_TIMES=
```
## Persisting the Session

View File

@@ -14,9 +14,10 @@ HELMET_COEP=[true|false] if omitted HELMET default will be used
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,py | js,py | sas | sas,js] default considered as sas
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
PYTHON_PATH=/usr/bin/python
SASJS_ROOT=./sasjs_root

503
api/package-lock.json generated
View File

@@ -23,7 +23,7 @@
"mongoose": "^6.0.12",
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"multer": "^1.4.5-lts.1",
"rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
@@ -2184,9 +2184,9 @@
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
},
"node_modules/@types/whatwg-url": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz",
"integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==",
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
"dependencies": {
"@types/node": "*",
"@types/webidl-conversions": "*"
@@ -2834,9 +2834,9 @@
}
},
"node_modules/bson": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/bson/-/bson-4.5.4.tgz",
"integrity": "sha512-wIt0bPACnx8Ju9r6IsS2wVtGDHBr9Dxb+U29A1YED2pu8XOhS8aKjOnLZ8sxyXkPwanoK7iWWVhS1+coxde6xA==",
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/bson/-/bson-4.6.5.tgz",
"integrity": "sha512-uqrgcjyOaZsHfz7ea8zLRCLe1u+QGUSzMZmvXqO24CDW7DWoW1qiN9folSwa7hSneTSgM2ykDIzF5kcQQ8cwNw==",
"dependencies": {
"buffer": "^5.6.0"
},
@@ -2903,38 +2903,16 @@
}
},
"node_modules/busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=0.8.0"
"node": ">=10.16.0"
}
},
"node_modules/busboy/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"node_modules/busboy/node_modules/readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/busboy/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
},
"node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -3566,39 +3544,6 @@
"node": ">=8"
}
},
"node_modules/dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"dependencies": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/dicer/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"node_modules/dicer/node_modules/readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/dicer/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -4946,6 +4891,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -6788,9 +6738,9 @@
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"node_modules/kareem": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ=="
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz",
"integrity": "sha512-aJ9opVoXroQUPfovYP5kaj2lM7Jn02Gw13bL0lg9v0V7SaUc0qavPs0Eue7d2DcC3NjqI6QAUElXNsuZSeM+EA=="
},
"node_modules/kleur": {
"version": "3.0.3",
@@ -7093,13 +7043,14 @@
"dev": true
},
"node_modules/mongodb": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.8.1.tgz",
"integrity": "sha512-/NyiM3Ox9AwP5zrfT9TXjRKDJbXlLaUDQ9Rg//2lbg8D2A8GXV0VidYYnA/gfdK6uwbnL4FnAflH7FbGw3TS7w==",
"dependencies": {
"bson": "^4.5.4",
"bson": "^4.6.5",
"denque": "^2.0.1",
"mongodb-connection-string-url": "^2.1.0"
"mongodb-connection-string-url": "^2.5.2",
"socks": "^2.6.2"
},
"engines": {
"node": ">=12.9.0"
@@ -7109,21 +7060,40 @@
}
},
"node_modules/mongodb-connection-string-url": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.1.0.tgz",
"integrity": "sha512-Qf9Zw7KGiRljWvMrrUFDdVqo46KIEiDuCzvEN97rh/PcKzk2bd6n9KuzEwBwW9xo5glwx69y1mI6s+jFUD/aIQ==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.3.tgz",
"integrity": "sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ==",
"dependencies": {
"@types/whatwg-url": "^8.2.1",
"whatwg-url": "^9.1.0"
"whatwg-url": "^11.0.0"
}
},
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"dependencies": {
"punycode": "^2.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"engines": {
"node": ">=12"
}
},
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz",
"integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==",
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
"dependencies": {
"tr46": "^2.1.0",
"webidl-conversions": "^6.1.0"
"tr46": "^3.0.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=12"
@@ -7221,19 +7191,17 @@
}
},
"node_modules/mongoose": {
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.0.12.tgz",
"integrity": "sha512-BvsZk7zEEhb1AgQFLtxN9C+7qgy5edRuA3ZDDwHU+kHG/HM44vI6FdKV5m6HVdAUeCHHQTiVv+YQh8BRsToSHw==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.2.tgz",
"integrity": "sha512-3CFDrSLtK2qjM1pZeZpLTUyqPRkc11Iuh74ZrwS4IwEJ3K2PqGnmyPLw7ex4Kzu37ujIMp3MAuiBlUjfrcb6hw==",
"dependencies": {
"bson": "^4.2.2",
"kareem": "2.3.2",
"mongodb": "4.1.3",
"mpath": "0.8.4",
"mquery": "4.0.0",
"ms": "2.1.2",
"regexp-clone": "1.0.0",
"sift": "13.5.2",
"sliced": "1.0.1"
"bson": "^4.6.5",
"kareem": "2.4.1",
"mongodb": "4.8.1",
"mpath": "0.9.0",
"mquery": "4.0.3",
"ms": "2.1.3",
"sift": "16.0.0"
},
"engines": {
"node": ">=12.0.0"
@@ -7255,26 +7223,10 @@
"mongoose": ">=4"
}
},
"node_modules/mongoose/node_modules/mongodb": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.3.tgz",
"integrity": "sha512-lHvTqODBiSpuqjpCj48DOyYWS6Iq6ElJNUiH9HWdQtONyOfjgsKzJULipWduMGsSzaNO4nFi/kmlMFCLvjox/Q==",
"dependencies": {
"bson": "^4.5.2",
"denque": "^2.0.1",
"mongodb-connection-string-url": "^2.0.0"
},
"engines": {
"node": ">=12.9.0"
},
"optionalDependencies": {
"saslprep": "^1.0.3"
}
},
"node_modules/mongoose/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/morgan": {
"version": "1.10.0",
@@ -7300,30 +7252,28 @@
}
},
"node_modules/mpath": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz",
"integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==",
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/mquery": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.0.tgz",
"integrity": "sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz",
"integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==",
"dependencies": {
"debug": "4.x",
"regexp-clone": "^1.0.0",
"sliced": "1.0.1"
"debug": "4.x"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/mquery/node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
@@ -7347,22 +7297,20 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/multer": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
"deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.",
"version": "1.4.5-lts.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 0.10.0"
"node": ">= 6.0.0"
}
},
"node_modules/multer/node_modules/mkdirp": {
@@ -8353,11 +8301,6 @@
"node": ">=8.10.0"
}
},
"node_modules/regexp-clone": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw=="
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -8606,9 +8549,9 @@
}
},
"node_modules/sift": {
"version": "13.5.2",
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz",
"integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ=="
},
"node_modules/signal-exit": {
"version": "3.0.3",
@@ -8706,10 +8649,27 @@
"node": ">=8"
}
},
"node_modules/sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==",
"dependencies": {
"ip": "^2.0.0",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.13.0",
"npm": ">= 3.0.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
@@ -8808,11 +8768,11 @@
}
},
"node_modules/streamsearch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=0.8.0"
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
@@ -9296,6 +9256,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
"dev": true,
"dependencies": {
"punycode": "^2.1.1"
},
@@ -9719,6 +9680,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
"dev": true,
"engines": {
"node": ">=10.4"
}
@@ -11724,9 +11686,9 @@
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
},
"@types/whatwg-url": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz",
"integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==",
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
"requires": {
"@types/node": "*",
"@types/webidl-conversions": "*"
@@ -12240,9 +12202,9 @@
}
},
"bson": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/bson/-/bson-4.5.4.tgz",
"integrity": "sha512-wIt0bPACnx8Ju9r6IsS2wVtGDHBr9Dxb+U29A1YED2pu8XOhS8aKjOnLZ8sxyXkPwanoK7iWWVhS1+coxde6xA==",
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/bson/-/bson-4.6.5.tgz",
"integrity": "sha512-uqrgcjyOaZsHfz7ea8zLRCLe1u+QGUSzMZmvXqO24CDW7DWoW1qiN9folSwa7hSneTSgM2ykDIzF5kcQQ8cwNw==",
"requires": {
"buffer": "^5.6.0"
}
@@ -12283,35 +12245,11 @@
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"requires": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
"streamsearch": "^1.1.0"
}
},
"bytes": {
@@ -12813,38 +12751,6 @@
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
"dev": true
},
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"requires": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -13868,6 +13774,11 @@
"p-is-promise": "^3.0.0"
}
},
"ip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -15248,9 +15159,9 @@
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"kareem": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ=="
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz",
"integrity": "sha512-aJ9opVoXroQUPfovYP5kaj2lM7Jn02Gw13bL0lg9v0V7SaUc0qavPs0Eue7d2DcC3NjqI6QAUElXNsuZSeM+EA=="
},
"kleur": {
"version": "3.0.3",
@@ -15486,32 +15397,46 @@
"dev": true
},
"mongodb": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.8.1.tgz",
"integrity": "sha512-/NyiM3Ox9AwP5zrfT9TXjRKDJbXlLaUDQ9Rg//2lbg8D2A8GXV0VidYYnA/gfdK6uwbnL4FnAflH7FbGw3TS7w==",
"requires": {
"bson": "^4.5.4",
"bson": "^4.6.5",
"denque": "^2.0.1",
"mongodb-connection-string-url": "^2.1.0",
"saslprep": "^1.0.3"
"mongodb-connection-string-url": "^2.5.2",
"saslprep": "^1.0.3",
"socks": "^2.6.2"
}
},
"mongodb-connection-string-url": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.1.0.tgz",
"integrity": "sha512-Qf9Zw7KGiRljWvMrrUFDdVqo46KIEiDuCzvEN97rh/PcKzk2bd6n9KuzEwBwW9xo5glwx69y1mI6s+jFUD/aIQ==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.3.tgz",
"integrity": "sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ==",
"requires": {
"@types/whatwg-url": "^8.2.1",
"whatwg-url": "^9.1.0"
"whatwg-url": "^11.0.0"
},
"dependencies": {
"whatwg-url": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz",
"integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==",
"tr46": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
"requires": {
"tr46": "^2.1.0",
"webidl-conversions": "^6.1.0"
"punycode": "^2.1.1"
}
},
"webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
},
"whatwg-url": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
"requires": {
"tr46": "^3.0.0",
"webidl-conversions": "^7.0.0"
}
}
}
@@ -15583,36 +15508,23 @@
}
},
"mongoose": {
"version": "6.0.12",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.0.12.tgz",
"integrity": "sha512-BvsZk7zEEhb1AgQFLtxN9C+7qgy5edRuA3ZDDwHU+kHG/HM44vI6FdKV5m6HVdAUeCHHQTiVv+YQh8BRsToSHw==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.2.tgz",
"integrity": "sha512-3CFDrSLtK2qjM1pZeZpLTUyqPRkc11Iuh74ZrwS4IwEJ3K2PqGnmyPLw7ex4Kzu37ujIMp3MAuiBlUjfrcb6hw==",
"requires": {
"bson": "^4.2.2",
"kareem": "2.3.2",
"mongodb": "4.1.3",
"mpath": "0.8.4",
"mquery": "4.0.0",
"ms": "2.1.2",
"regexp-clone": "1.0.0",
"sift": "13.5.2",
"sliced": "1.0.1"
"bson": "^4.6.5",
"kareem": "2.4.1",
"mongodb": "4.8.1",
"mpath": "0.9.0",
"mquery": "4.0.3",
"ms": "2.1.3",
"sift": "16.0.0"
},
"dependencies": {
"mongodb": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.3.tgz",
"integrity": "sha512-lHvTqODBiSpuqjpCj48DOyYWS6Iq6ElJNUiH9HWdQtONyOfjgsKzJULipWduMGsSzaNO4nFi/kmlMFCLvjox/Q==",
"requires": {
"bson": "^4.5.2",
"denque": "^2.0.1",
"mongodb-connection-string-url": "^2.0.0",
"saslprep": "^1.0.3"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
@@ -15645,24 +15557,22 @@
}
},
"mpath": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz",
"integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g=="
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="
},
"mquery": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.0.tgz",
"integrity": "sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz",
"integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==",
"requires": {
"debug": "4.x",
"regexp-clone": "^1.0.0",
"sliced": "1.0.1"
"debug": "4.x"
},
"dependencies": {
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
@@ -15680,16 +15590,15 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multer": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
"version": "1.4.5-lts.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"requires": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
@@ -16416,11 +16325,6 @@
"picomatch": "^2.2.1"
}
},
"regexp-clone": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw=="
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -16605,9 +16509,9 @@
}
},
"sift": {
"version": "13.5.2",
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz",
"integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ=="
},
"signal-exit": {
"version": "3.0.3",
@@ -16677,10 +16581,19 @@
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
"smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
},
"socks": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==",
"requires": {
"ip": "^2.0.0",
"smart-buffer": "^4.2.0"
}
},
"source-map": {
"version": "0.6.1",
@@ -16771,9 +16684,9 @@
}
},
"streamsearch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
},
"string_decoder": {
"version": "1.3.0",
@@ -17140,6 +17053,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
"dev": true,
"requires": {
"punycode": "^2.1.1"
}
@@ -17456,7 +17370,8 @@
"webidl-conversions": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w=="
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
"dev": true
},
"whatwg-encoding": {
"version": "1.0.5",

View File

@@ -62,7 +62,7 @@
"mongoose": "^6.0.12",
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"multer": "^1.4.5-lts.1",
"rotating-file-stream": "^3.0.4",
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ import { apiRoot, sysInitCompiledPath } from '../src/utils/file'
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
const compiledSystemInit = async (systemInit: string) =>
'options ps=max;\n' +
'options ls=max ps=max;\n' +
(await loadDependenciesFile({
fileContent: systemInit,
type: SASJsFileType.job,

View File

@@ -1,6 +1,6 @@
import path from 'path'
import express, { ErrorRequestHandler } from 'express'
import csrf from 'csurf'
import csrf, { CookieOptions } from 'csurf'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
@@ -32,9 +32,10 @@ const app = express()
const { PROTOCOL } = process.env
export const cookieOptions = {
export const cookieOptions: CookieOptions = {
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}

View File

@@ -1,7 +1,6 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal'
import { ExecuteReturnJsonResponse } from '.'
import { ExecutionController } from './internal'
import {
getPreProgramVariables,
getUserAutoExec,
@@ -35,7 +34,7 @@ export class CodeController {
public async executeCode(
@Request() request: express.Request,
@Body() body: ExecuteCodePayload
): Promise<ExecuteReturnJsonResponse> {
): Promise<string | Buffer> {
return executeCode(request, body)
}
}
@@ -51,22 +50,15 @@ const executeCode = async (
: await getUserAutoExec()
try {
const { webout, log, httpHeaders } =
(await new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
returnJson: true,
runTime: runTime
})) as ExecuteReturnJson
const { result } = await new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime
})
return {
status: 'success',
_webout: webout as string,
log: parseLogToArray(log),
httpHeaders
}
return result
} catch (err: any) {
throw {
code: 400,

View File

@@ -22,6 +22,7 @@ import {
moveFile,
createFolder,
deleteFile as deleteFileOnSystem,
deleteFolder as deleteFolderOnSystem,
folderExists,
listFilesInFolder,
listSubFoldersInFolder,
@@ -58,11 +59,32 @@ interface GetFileTreeResponse {
tree: TreeNode
}
interface UpdateFileResponse {
interface FileFolderResponse {
status: string
message?: string
}
interface AddFolderPayload {
/**
* Location of folder
* @example "/Public/someFolder"
*/
folderPath: string
}
interface RenamePayload {
/**
* Old path of file/folder
* @example "/Public/someFolder"
*/
oldPath: string
/**
* New path of file/folder
* @example "/Public/newFolder"
*/
newPath: string
}
const fileTreeExample = getTreeExample()
const successDeployResponse: DeployResponse = {
@@ -143,7 +165,7 @@ export class DriveController {
/**
*
* @summary Delete file from SASjs Drive
* @query _filePath Location of SAS program
* @query _filePath Location of file
* @example _filePath "/Public/somefolder/some.file"
*/
@Delete('/file')
@@ -151,20 +173,31 @@ export class DriveController {
return deleteFile(_filePath)
}
/**
*
* @summary Delete folder from SASjs Drive
* @query _folderPath Location of folder
* @example _folderPath "/Public/somefolder/"
*/
@Delete('/folder')
public async deleteFolder(@Query() _folderPath: string) {
return deleteFolder(_folderPath)
}
/**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
*
* @summary Create a file in SASjs Drive
* @param _filePath Location of SAS program
* @param _filePath Location of file
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<UpdateFileResponse>({
@Example<FileFolderResponse>({
status: 'success'
})
@Response<UpdateFileResponse>(403, 'File already exists', {
@Response<FileFolderResponse>(403, 'File already exists', {
status: 'failure',
message: 'File request failed.'
})
@@ -173,10 +206,28 @@ export class DriveController {
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<UpdateFileResponse> {
): Promise<FileFolderResponse> {
return saveFile((_filePath ?? filePath)!, file)
}
/**
* @summary Create an empty folder in SASjs Drive
*
*/
@Example<FileFolderResponse>({
status: 'success'
})
@Response<FileFolderResponse>(409, 'Folder already exists', {
status: 'failure',
message: 'Add folder request failed.'
})
@Post('/folder')
public async addFolder(
@Body() body: AddFolderPayload
): Promise<FileFolderResponse> {
return addFolder(body.folderPath)
}
/**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
@@ -187,10 +238,10 @@ export class DriveController {
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<UpdateFileResponse>({
@Example<FileFolderResponse>({
status: 'success'
})
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
@Response<FileFolderResponse>(403, `File doesn't exist`, {
status: 'failure',
message: 'File request failed.'
})
@@ -199,10 +250,28 @@ export class DriveController {
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<UpdateFileResponse> {
): Promise<FileFolderResponse> {
return updateFile((_filePath ?? filePath)!, file)
}
/**
* @summary Renames a file/folder in SASjs Drive
*
*/
@Example<FileFolderResponse>({
status: 'success'
})
@Response<FileFolderResponse>(409, 'Folder already exists', {
status: 'failure',
message: 'rename request failed.'
})
@Post('/rename')
public async rename(
@Body() body: RenamePayload
): Promise<FileFolderResponse> {
return rename(body.oldPath, body.newPath)
}
/**
* @summary Fetch file tree within SASjs Drive.
*
@@ -249,20 +318,26 @@ const getFile = async (req: express.Request, filePath: string) => {
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot get file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't get file outside drive.`
}
if (!(await fileExists(filePathFull))) {
throw new Error("File doesn't exist.")
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
const extension = path.extname(filePathFull).toLowerCase()
if (extension === '.sas') {
req.res?.setHeader('Content-type', 'text/plain')
}
req.res?.sendFile(path.resolve(filePathFull))
req.res?.sendFile(path.resolve(filePathFull), { dotfiles: 'allow' })
}
const getFolder = async (folderPath?: string) => {
@@ -273,17 +348,26 @@ const getFolder = async (folderPath?: string) => {
.join(getFilesFolder(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(driveFilesPath)) {
throw new Error('Cannot get folder outside drive.')
}
if (!folderPathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't get folder outside drive.`
}
if (!(await folderExists(folderPathFull))) {
throw new Error("Folder doesn't exist.")
}
if (!(await folderExists(folderPathFull)))
throw {
code: 404,
status: 'Not Found',
message: `Folder doesn't exist.`
}
if (!(await isFolder(folderPathFull))) {
throw new Error('Not a Folder.')
}
if (!(await isFolder(folderPathFull)))
throw {
code: 400,
status: 'Bad Request',
message: 'Not a Folder.'
}
const files: string[] = await listFilesInFolder(folderPathFull)
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
@@ -302,19 +386,51 @@ const deleteFile = async (filePath: string) => {
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot delete file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't delete file outside drive.`
}
if (!(await fileExists(filePathFull))) {
throw new Error('File does not exist.')
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
await deleteFileOnSystem(filePathFull)
return { status: 'success' }
}
const deleteFolder = async (folderPath: string) => {
const driveFolderPath = getFilesFolder()
const folderPathFull = path
.join(getFilesFolder(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(driveFolderPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't delete folder outside drive.`
}
if (!(await folderExists(folderPathFull)))
throw {
code: 404,
status: 'Not Found',
message: `Folder doesn't exist.`
}
await deleteFolderOnSystem(folderPathFull)
return { status: 'success' }
}
const saveFile = async (
filePath: string,
multerFile: Express.Multer.File
@@ -325,13 +441,19 @@ const saveFile = async (
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot put file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't put file outside drive.`
}
if (await fileExists(filePathFull)) {
throw new Error('File already exists.')
}
if (await fileExists(filePathFull))
throw {
code: 409,
status: 'Conflict',
message: 'File already exists.'
}
const folderPath = path.dirname(filePathFull)
await createFolder(folderPath)
@@ -340,6 +462,88 @@ const saveFile = async (
return { status: 'success' }
}
const addFolder = async (folderPath: string): Promise<FileFolderResponse> => {
const drivePath = getFilesFolder()
const folderPathFull = path
.join(drivePath, folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(drivePath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't put folder outside drive.`
}
if (await folderExists(folderPathFull))
throw {
code: 409,
status: 'Conflict',
message: 'Folder already exists.'
}
await createFolder(folderPathFull)
return { status: 'success' }
}
const rename = async (
oldPath: string,
newPath: string
): Promise<FileFolderResponse> => {
const drivePath = getFilesFolder()
const oldPathFull = path
.join(drivePath, oldPath)
.replace(new RegExp('/', 'g'), path.sep)
const newPathFull = path
.join(drivePath, newPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!oldPathFull.includes(drivePath))
throw {
code: 400,
status: 'Bad Request',
message: `Old path can't be outside of drive.`
}
if (!newPathFull.includes(drivePath))
throw {
code: 400,
status: 'Bad Request',
message: `New path can't be outside of drive.`
}
if (await isFolder(oldPathFull)) {
if (await folderExists(newPathFull))
throw {
code: 409,
status: 'Conflict',
message: 'Folder with new name already exists.'
}
else moveFile(oldPathFull, newPathFull)
return { status: 'success' }
} else if (await fileExists(oldPathFull)) {
if (await fileExists(newPathFull))
throw {
code: 409,
status: 'Conflict',
message: 'File with new name already exists.'
}
else moveFile(oldPathFull, newPathFull)
return { status: 'success' }
}
throw {
code: 404,
status: 'Not Found',
message: 'No file/folder found for provided path.'
}
}
const updateFile = async (
filePath: string,
multerFile: Express.Multer.File
@@ -350,13 +554,19 @@ const updateFile = async (
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot modify file outside drive.')
}
if (!filePathFull.includes(driveFilesPath))
throw {
code: 400,
status: 'Bad Request',
message: `Can't modify file outside drive.`
}
if (!(await fileExists(filePathFull))) {
throw new Error(`File doesn't exist.`)
}
if (!(await fileExists(filePathFull)))
throw {
code: 404,
status: 'Not Found',
message: `File doesn't exist.`
}
await moveFile(multerFile.path, filePathFull)

View File

@@ -10,7 +10,7 @@ import {
Body
} from 'tsoa'
import Group, { GroupPayload } from '../model/Group'
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User'
import { UserResponse } from './user'
@@ -241,6 +241,13 @@ const updateUsersListInGroup = async (
message: 'Group not found.'
}
if (group.name === PUBLIC_GROUP_NAME)
throw {
code: 400,
status: 'Bad Request',
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
}
const user = await User.findOne({ id: userId })
if (!user)
throw {

View File

@@ -1,7 +1,7 @@
import { Route, Tags, Example, Get } from 'tsoa'
import { getAuthorizedRoutes } from '../utils'
export interface AuthorizedRoutesResponse {
URIs: string[]
paths: string[]
}
export interface InfoResponse {
@@ -42,16 +42,16 @@ export class InfoController {
}
/**
* @summary Get authorized routes.
* @summary Get the list of available routes to which permissions can be applied. Used to populate the dialog in the URI Permissions feature.
*
*/
@Example<AuthorizedRoutesResponse>({
URIs: ['/AppStream', '/SASjsApi/stp/execute']
paths: ['/AppStream', '/SASjsApi/stp/execute']
})
@Get('/authorizedRoutes')
public authorizedRoutes(): AuthorizedRoutesResponse {
const response = {
URIs: getAuthorizedRoutes()
paths: getAuthorizedRoutes()
}
return response
}

View File

@@ -20,12 +20,6 @@ export interface ExecuteReturnRaw {
result: string | Buffer
}
export interface ExecuteReturnJson {
httpHeaders: HTTPHeaders
webout: string | Buffer
log?: string
}
interface ExecuteFileParams {
programPath: string
preProgramVariables: PreProgramVars
@@ -68,10 +62,9 @@ export class ExecutionController {
preProgramVariables,
vars,
otherArgs,
returnJson,
session: sessionByFileUpload,
runTime
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
const sessionController = getSessionController(runTime)
const session =
@@ -96,6 +89,7 @@ export class ExecutionController {
vars,
session,
weboutPath,
headersPath,
tokenFile,
runTime,
logPath,
@@ -107,10 +101,7 @@ export class ExecutionController {
? await readFile(headersPath)
: ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
const fileResponse: boolean =
httpHeaders.hasOwnProperty('content-type') &&
!returnJson && // not a POST Request
!isDebugOn(vars) // Debug is not enabled
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
const webout = (await fileExists(weboutPath))
? fileResponse
@@ -121,19 +112,11 @@ export class ExecutionController {
// it should be deleted by scheduleSessionDestroy
session.inUse = false
if (returnJson) {
return {
httpHeaders,
webout,
log: isDebugOn(vars) || session.crashed ? log : undefined
}
}
return {
httpHeaders,
result:
isDebugOn(vars) || session.crashed
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
? `${webout}\n${process.logsUUID}\n${log}`
: webout
}
}
@@ -143,6 +126,7 @@ export class ExecutionController {
name: 'files',
relativePath: '',
absolutePath: getFilesFolder(),
isFolder: true,
children: []
}
@@ -152,15 +136,22 @@ export class ExecutionController {
const currentNode = stack.pop()
if (currentNode) {
currentNode.isFolder = fs
.statSync(currentNode.absolutePath)
.isDirectory()
const children = fs.readdirSync(currentNode.absolutePath)
for (let child of children) {
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
const absoluteChildPath = path.join(currentNode.absolutePath, child)
// relative path will only be used in frontend component
// so, no need to convert '/' to platform specific separator
const relativeChildPath = `${currentNode.relativePath}/${child}`
const childNode: TreeNode = {
name: child,
relativePath: relativeChildPath,
absolutePath: absoluteChildPath,
isFolder: false,
children: []
}
currentNode.children.push(childNode)

View File

@@ -103,6 +103,12 @@ ${autoExecContent}`
autoExecPath,
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
process.sasLoc!.endsWith('sas.exe')
? '-SASINITIALFOLDER ' + session.path
: '',
isWindows() ? '-nologo' : ''
])
.then(() => {
@@ -187,7 +193,39 @@ export class JSSessionController extends SessionController {
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'Content-type: application/json')
await createFile(headersPath, 'Content-type: text/plain')
this.sessions.push(session)
return session
}
}
export class PythonSessionController extends SessionController {
protected async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getSessionsFolder(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString()
const session: Session = {
id: sessionId,
ready: true,
inUse: true,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
path: sessionFolder
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'Content-type: text/plain')
this.sessions.push(session)
return session
@@ -196,7 +234,7 @@ export class JSSessionController extends SessionController {
export const getSessionController = (
runTime: RunTimeType
): SASSessionController | JSSessionController => {
): SASSessionController | JSSessionController | PythonSessionController => {
if (runTime === RunTimeType.SAS) {
return getSASSessionController()
}
@@ -205,6 +243,10 @@ export const getSessionController = (
return getJSSessionController()
}
if (runTime === RunTimeType.PY) {
return getPythonSessionController()
}
throw new Error('No Runtime is configured')
}
@@ -224,6 +266,14 @@ const getJSSessionController = (): JSSessionController => {
return process.jsSessionController
}
const getPythonSessionController = (): PythonSessionController => {
if (process.pythonSessionController) return process.pythonSessionController
process.pythonSessionController = new PythonSessionController()
return process.pythonSessionController
}
const autoExecContent = `
data _null_;
/* remove the dummy SYSIN */

View File

@@ -9,6 +9,7 @@ export const createJSProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
@@ -23,13 +24,16 @@ let _webout = '';
const weboutPath = '${
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
}';
const _sasjs_tokenfile = '${tokenFile}';
const _sasjs_username = '${preProgramVariables?.username}';
const _sasjs_userid = '${preProgramVariables?.userId}';
const _sasjs_displayname = '${preProgramVariables?.displayName}';
const _metaperson = _sasjs_displayname;
const _metauser = _sasjs_username;
const sasjsprocessmode = 'Stored Program';
const _SASJS_TOKENFILE = '${
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
}';
const _SASJS_WEBOUT_HEADERS = '${headersPath}';
const _SASJS_USERNAME = '${preProgramVariables?.username}';
const _SASJS_USERID = '${preProgramVariables?.userId}';
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
const _METAPERSON = _SASJS_DISPLAYNAME;
const _METAUSER = _SASJS_USERNAME;
const SASJSPROCESSMODE = 'Stored Program';
`
const requiredModules = `const fs = require('fs')`
@@ -53,14 +57,15 @@ if (_webout) {
`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadJSCode = await generateFileUploadJSCode(
const uploadJsCode = await generateFileUploadJSCode(
otherArgs.filesNamesMap,
session.path
)
//If js code for the file is generated it will be appended to the top of jsCode
if (uploadJSCode.length > 0) {
program = `${uploadJSCode}\n` + program
// If any files are uploaded, the program needs to be updated with some
// dynamically generated variables (pointers) for ease of ingestion
if (uploadJsCode.length > 0) {
program = `${uploadJsCode}\n` + program
}
}
return requiredModules + program

View File

@@ -0,0 +1,68 @@
import { isWindows } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadPythonCode } from '../../utils'
import { ExecutionVars } from './'
export const createPythonProgram = async (
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => `${computed}${key} = '${vars[key]}';\n`,
''
)
const preProgramVarStatments = `
_SASJS_SESSION_PATH = '${
isWindows() ? session.path.replace(/\\/g, '\\\\') : session.path
}';
_WEBOUT = '${isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath}';
_SASJS_WEBOUT_HEADERS = '${headersPath}';
_SASJS_TOKENFILE = '${
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
}';
_SASJS_USERNAME = '${preProgramVariables?.username}';
_SASJS_USERID = '${preProgramVariables?.userId}';
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
_METAPERSON = _SASJS_DISPLAYNAME;
_METAUSER = _SASJS_USERNAME;
SASJSPROCESSMODE = 'Stored Program';
`
const requiredModules = `import os`
program = `
# runtime vars
${varStatments}
# dynamic user-provided vars
${preProgramVarStatments}
# change working directory to session folder
os.chdir(_SASJS_SESSION_PATH)
# actual job code
${program}
`
// if no files are uploaded filesNamesMap will be undefined
if (otherArgs?.filesNamesMap) {
const uploadPythonCode = await generateFileUploadPythonCode(
otherArgs.filesNamesMap,
session.path
)
// If any files are uploaded, the program needs to be updated with some
// dynamically generated variables (pointers) for ease of ingestion
if (uploadPythonCode.length > 0) {
program = `${uploadPythonCode}\n` + program
}
}
return requiredModules + program
}

View File

@@ -23,10 +23,14 @@ export const createSASProgram = async (
%let _sasjs_displayname=${preProgramVariables?.displayName};
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _sasjs_webout_headers=%sysfunc(pathname(work))/../stpsrv_header.txt;
%let _metaperson=&_sasjs_displayname;
%let _metauser=&_sasjs_username;
/* the below is here for compatibility and will be removed in a future release */
%let sasjs_stpsrv_header_loc=&_sasjs_webout_headers;
%let sasjsprocessmode=Stored Program;
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
%macro _sasjs_server_init();
@@ -34,6 +38,9 @@ export const createSASProgram = async (
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
%mend;
%_sasjs_server_init()
proc printto print="%sysfunc(getoption(log))";
run;
`
program = `
@@ -60,7 +67,8 @@ ${program}`
session.path
)
//If sas code for the file is generated it will be appended to the top of sasCode
// If any files are uploaded, the program needs to be updated with some
// dynamically generated variables (pointers) for ease of ingestion
if (uploadSasCode.length > 0) {
program = `${uploadSasCode}` + program
}

View File

@@ -4,4 +4,5 @@ export * from './Execution'
export * from './FileUploadController'
export * from './createSASProgram'
export * from './createJSProgram'
export * from './createPythonProgram'
export * from './processProgram'

View File

@@ -5,7 +5,12 @@ import { once } from 'stream'
import { createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { RunTimeType } from '../../utils'
import { ExecutionVars, createSASProgram, createJSProgram } from './'
import {
ExecutionVars,
createSASProgram,
createJSProgram,
createPythonProgram
} from './'
export const processProgram = async (
program: string,
@@ -13,6 +18,7 @@ export const processProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
runTime: RunTimeType,
logPath: string,
@@ -25,6 +31,7 @@ export const processProgram = async (
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
@@ -47,6 +54,43 @@ export const processProgram = async (
// copy the code.js program to log and end write stream
writeStream.end(program)
session.completed = true
console.log('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
}
} else if (runTime === RunTimeType.PY) {
program = await createPythonProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.py')
try {
await createFile(codePath, program)
// create a stream that will write to console outputs to log file
const writeStream = fs.createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
execFileSync(process.pythonLoc!, [codePath], {
stdio: ['ignore', writeStream, writeStream]
})
// copy the code.py program to log and end write stream
writeStream.end(program)
session.completed = true
console.log('session completed', session)
} catch (err: any) {

View File

@@ -1,3 +1,4 @@
import express from 'express'
import {
Security,
Route,
@@ -8,7 +9,8 @@ import {
Post,
Patch,
Delete,
Body
Body,
Request
} from 'tsoa'
import Permission from '../model/Permission'
@@ -17,12 +19,16 @@ import Group from '../model/Group'
import { UserResponse } from './user'
import { GroupDetailsResponse } from './group'
export enum PermissionType {
route = 'Route'
}
export enum PrincipalType {
user = 'user',
group = 'group'
}
export enum PermissionSetting {
export enum PermissionSettingForRoute {
grant = 'Grant',
deny = 'Deny'
}
@@ -32,12 +38,17 @@ interface RegisterPermissionPayload {
* Name of affected resource
* @example "/SASjsApi/code/execute"
*/
uri: string
path: string
/**
* Type of affected resource
* @example "Route"
*/
type: PermissionType
/**
* The indication of whether (and to what extent) access is provided
* @example "Grant"
*/
setting: PermissionSetting
setting: PermissionSettingForRoute
/**
* Indicates the type of principal
* @example "user"
@@ -55,12 +66,13 @@ interface UpdatePermissionPayload {
* The indication of whether (and to what extent) access is provided
* @example "Grant"
*/
setting: PermissionSetting
setting: PermissionSettingForRoute
}
export interface PermissionDetailsResponse {
permissionId: number
uri: string
path: string
type: string
setting: string
user?: UserResponse
group?: GroupDetailsResponse
@@ -71,13 +83,17 @@ export interface PermissionDetailsResponse {
@Tags('Permission')
export class PermissionController {
/**
* @summary Get list of all permissions (uri, setting and userDetail).
* Get the list of permission rules applicable the authenticated user.
* If the user is an admin, all rules are returned.
*
* @summary Get the list of permission rules. If the user is admin, all rules are returned.
*
*/
@Example<PermissionDetailsResponse[]>([
{
permissionId: 123,
uri: '/SASjsApi/code/execute',
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
user: {
id: 1,
@@ -88,7 +104,8 @@ export class PermissionController {
},
{
permissionId: 124,
uri: '/SASjsApi/code/execute',
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
group: {
groupId: 1,
@@ -100,8 +117,10 @@ export class PermissionController {
}
])
@Get('/')
public async getAllPermissions(): Promise<PermissionDetailsResponse[]> {
return getAllPermissions()
public async getAllPermissions(
@Request() request: express.Request
): Promise<PermissionDetailsResponse[]> {
return getAllPermissions(request)
}
/**
@@ -110,7 +129,8 @@ export class PermissionController {
*/
@Example<PermissionDetailsResponse>({
permissionId: 123,
uri: '/SASjsApi/code/execute',
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
user: {
id: 1,
@@ -133,7 +153,8 @@ export class PermissionController {
*/
@Example<PermissionDetailsResponse>({
permissionId: 123,
uri: '/SASjsApi/code/execute',
path: '/SASjsApi/code/execute',
type: 'Route',
setting: 'Grant',
user: {
id: 1,
@@ -161,33 +182,43 @@ export class PermissionController {
}
}
const getAllPermissions = async (): Promise<PermissionDetailsResponse[]> =>
(await Permission.find({})
.select({
_id: 0,
permissionId: 1,
uri: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
.populate({
path: 'group',
select: 'groupId name description -_id',
populate: {
path: 'users',
select: 'id username displayName isAdmin -_id',
options: { limit: 15 }
const getAllPermissions = async (
req: express.Request
): Promise<PermissionDetailsResponse[]> => {
const { user } = req
if (user?.isAdmin) return await Permission.get({})
else {
const permissions: PermissionDetailsResponse[] = []
const dbUser = await User.findOne({ id: user?.userId })
if (!dbUser)
throw {
code: 404,
status: 'Not Found',
message: 'User not found.'
}
})) as unknown as PermissionDetailsResponse[]
permissions.push(...(await Permission.get({ user: dbUser._id })))
for (const group of dbUser.groups) {
permissions.push(...(await Permission.get({ group })))
}
return permissions
}
}
const createPermission = async ({
uri,
path,
type,
setting,
principalType,
principalId
}: RegisterPermissionPayload): Promise<PermissionDetailsResponse> => {
const permission = new Permission({
uri,
path,
type,
setting
})
@@ -212,7 +243,8 @@ const createPermission = async ({
}
const alreadyExists = await Permission.findOne({
uri,
path,
type,
user: userInDB._id
})
@@ -220,7 +252,8 @@ const createPermission = async ({
throw {
code: 409,
status: 'Conflict',
message: 'Permission already exists with provided URI and User.'
message:
'Permission already exists with provided Path, Type and User.'
}
permission.user = userInDB._id
@@ -243,14 +276,16 @@ const createPermission = async ({
}
const alreadyExists = await Permission.findOne({
uri,
path,
type,
group: groupInDB._id
})
if (alreadyExists)
throw {
code: 409,
status: 'Conflict',
message: 'Permission already exists with provided URI and Group.'
message:
'Permission already exists with provided Path, Type and Group.'
}
permission.group = groupInDB._id
@@ -280,7 +315,8 @@ const createPermission = async ({
return {
permissionId: savedPermission.permissionId,
uri: savedPermission.uri,
path: savedPermission.path,
type: savedPermission.type,
setting: savedPermission.setting,
user,
group
@@ -301,7 +337,8 @@ const updatePermission = async (
.select({
_id: 0,
permissionId: 1,
uri: 1,
path: 1,
type: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })

View File

@@ -1,33 +1,16 @@
import express from 'express'
import {
Request,
Security,
Route,
Tags,
Post,
Body,
Get,
Query,
Example
} from 'tsoa'
import {
ExecuteReturnJson,
ExecuteReturnRaw,
ExecutionController,
ExecutionVars
} from './internal'
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
import { ExecutionController, ExecutionVars } from './internal'
import {
getPreProgramVariables,
HTTPHeaders,
isDebugOn,
LogLine,
makeFilesNamesMap,
parseLogToArray,
getRunTimeAndFilePath
} from '../utils'
import { MulterFile } from '../types/Upload'
interface ExecuteReturnJsonPayload {
interface ExecutePostRequestPayload {
/**
* Location of SAS program
* @example "/Public/somefolder/some.file"
@@ -35,17 +18,6 @@ interface ExecuteReturnJsonPayload {
_program?: string
}
interface IRecordOfAny {
[key: string]: any
}
export interface ExecuteReturnJsonResponse {
status: string
_webout: string | IRecordOfAny
log: LogLine[]
message?: string
httpHeaders: HTTPHeaders
}
@Security('bearerAuth')
@Route('SASjsApi/stp')
@Tags('STP')
@@ -62,11 +34,12 @@ export class STPController {
* @example _program "/Projects/myApp/some/program"
*/
@Get('/execute')
public async executeReturnRaw(
public async executeGetRequest(
@Request() request: express.Request,
@Query() _program: string
): Promise<string | Buffer> {
return executeReturnRaw(request, _program)
const vars = request.query as ExecutionVars
return execute(request, _program, vars)
}
/**
@@ -87,50 +60,42 @@ export class STPController {
* @param _program Location of SAS or JS code
* @example _program "/Projects/myApp/some/program"
*/
@Example<ExecuteReturnJsonResponse>({
status: 'success',
_webout: 'webout content',
log: [],
httpHeaders: {
'Content-type': 'application/zip',
'Cache-Control': 'public, max-age=1000'
}
})
@Post('/execute')
public async executeReturnJson(
public async executePostRequest(
@Request() request: express.Request,
@Body() body?: ExecuteReturnJsonPayload,
@Body() body?: ExecutePostRequestPayload,
@Query() _program?: string
): Promise<ExecuteReturnJsonResponse> {
): Promise<string | Buffer> {
const program = _program ?? body?._program
return executeReturnJson(request, program!)
const vars = { ...request.query, ...request.body }
const filesNamesMap = request.files?.length
? makeFilesNamesMap(request.files as MulterFile[])
: null
const otherArgs = { filesNamesMap: filesNamesMap }
return execute(request, program!, vars, otherArgs)
}
}
const executeReturnRaw = async (
const execute = async (
req: express.Request,
_program: string
_program: string,
vars: ExecutionVars,
otherArgs?: any
): Promise<string | Buffer> => {
const query = req.query as ExecutionVars
try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { result, httpHeaders } =
(await new ExecutionController().executeFile({
const { result, httpHeaders } = await new ExecutionController().executeFile(
{
programPath: codePath,
runTime,
preProgramVariables: getPreProgramVariables(req),
vars: query,
runTime
})) as ExecuteReturnRaw
// Should over-ride response header for debug
// on GET request to see entire log rendering on browser.
if (isDebugOn(query)) {
httpHeaders['content-type'] = 'text/plain'
}
req.res?.set(httpHeaders)
vars,
otherArgs,
session: req.sasjsSession
}
)
if (result instanceof Buffer) {
;(req as any).sasHeaders = httpHeaders
@@ -146,48 +111,3 @@ const executeReturnRaw = async (
}
}
}
const executeReturnJson = async (
req: express.Request,
_program: string
): Promise<ExecuteReturnJsonResponse> => {
const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[])
: null
try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { webout, log, httpHeaders } =
(await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, ...req.body },
otherArgs: { filesNamesMap: filesNamesMap },
returnJson: true,
session: req.sasjsSession,
runTime
})) as ExecuteReturnJson
let weboutRes: string | IRecordOfAny = webout
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
try {
weboutRes = JSON.parse(webout as string)
} catch (_) {}
}
return {
status: 'success',
_webout: weboutRes,
log: parseLogToArray(log),
httpHeaders
}
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -5,7 +5,9 @@ import {
fetchLatestAutoExec,
ModeType,
verifyTokenInDB,
isAuthorizingRoute
isAuthorizingRoute,
isPublicRoute,
publicUser
} from '../utils'
import { desktopUser } from './desktop'
import { authorize } from './authorize'
@@ -41,7 +43,7 @@ export const authenticateAccessToken: RequestHandler = async (
return res.sendStatus(401)
}
authenticateToken(
await authenticateToken(
req,
res,
nextFunction,
@@ -50,8 +52,12 @@ export const authenticateAccessToken: RequestHandler = async (
)
}
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
authenticateToken(
export const authenticateRefreshToken: RequestHandler = async (
req,
res,
next
) => {
await authenticateToken(
req,
res,
next,
@@ -60,7 +66,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
)
}
const authenticateToken = (
const authenticateToken = async (
req: Request,
res: Response,
next: NextFunction,
@@ -83,12 +89,12 @@ const authenticateToken = (
const authHeader = req.headers['authorization']
const token = authHeader?.split(' ')[1]
if (!token) return res.sendStatus(401)
jwt.verify(token, key, async (err: any, data: any) => {
if (err) return res.sendStatus(401)
try {
if (!token) throw 'Unauthorized'
const data: any = jwt.verify(token, key)
// verify this valid token's entry in DB
const user = await verifyTokenInDB(
data?.userId,
data?.clientId,
@@ -101,8 +107,16 @@ const authenticateToken = (
req.user = user
if (tokenType === 'accessToken') req.accessToken = token
return next()
} else return res.sendStatus(401)
} else throw 'Unauthorized'
}
return res.sendStatus(401)
})
throw 'Unauthorized'
} catch (error) {
if (await isPublicRoute(req)) {
req.user = publicUser
return next()
}
res.sendStatus(401)
}
}

View File

@@ -1,8 +1,11 @@
import { RequestHandler } from 'express'
import User from '../model/User'
import Permission from '../model/Permission'
import { PermissionSetting } from '../controllers/permission'
import { getUri } from '../utils'
import {
PermissionSettingForRoute,
PermissionType
} from '../controllers/permission'
import { getPath, isPublicRoute } from '../utils'
export const authorize: RequestHandler = async (req, res, next) => {
const { user } = req
@@ -14,23 +17,35 @@ export const authorize: RequestHandler = async (req, res, next) => {
// no need to check for permissions when user is admin
if (user.isAdmin) return next()
// no need to check for permissions when route is Public
if (await isPublicRoute(req)) return next()
const dbUser = await User.findOne({ id: user.userId })
if (!dbUser) return res.sendStatus(401)
const uri = getUri(req)
const path = getPath(req)
// find permission w.r.t user
const permission = await Permission.findOne({ uri, user: dbUser._id })
const permission = await Permission.findOne({
path,
type: PermissionType.route,
user: dbUser._id
})
if (permission) {
if (permission.setting === PermissionSetting.grant) return next()
if (permission.setting === PermissionSettingForRoute.grant) return next()
else return res.sendStatus(401)
}
// find permission w.r.t user's groups
for (const group of dbUser.groups) {
const groupPermission = await Permission.findOne({ uri, group })
if (groupPermission?.setting === PermissionSetting.grant) return next()
const groupPermission = await Permission.findOne({
path,
type: PermissionType.route,
group
})
if (groupPermission?.setting === PermissionSettingForRoute.grant)
return next()
}
return res.sendStatus(401)
}

View File

@@ -3,6 +3,8 @@ import { GroupDetailsResponse } from '../controllers'
import User, { IUser } from './User'
const AutoIncrement = require('mongoose-sequence')(mongoose)
export const PUBLIC_GROUP_NAME = 'Public'
export interface GroupPayload {
/**
* Name of the group

View File

@@ -1,8 +1,15 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
const AutoIncrement = require('mongoose-sequence')(mongoose)
import { PermissionDetailsResponse } from '../controllers'
interface GetPermissionBy {
user?: Schema.Types.ObjectId
group?: Schema.Types.ObjectId
}
interface IPermissionDocument extends Document {
uri: string
path: string
type: string
setting: string
permissionId: number
user: Schema.Types.ObjectId
@@ -11,10 +18,16 @@ interface IPermissionDocument extends Document {
interface IPermission extends IPermissionDocument {}
interface IPermissionModel extends Model<IPermission> {}
interface IPermissionModel extends Model<IPermission> {
get(getBy: GetPermissionBy): Promise<PermissionDetailsResponse[]>
}
const permissionSchema = new Schema<IPermissionDocument>({
uri: {
path: {
type: String,
required: true
},
type: {
type: String,
required: true
},
@@ -28,6 +41,30 @@ const permissionSchema = new Schema<IPermissionDocument>({
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
// Static Methods
permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
PermissionDetailsResponse[]
> {
return (await this.find(getBy)
.select({
_id: 0,
permissionId: 1,
path: 1,
type: 1,
setting: 1
})
.populate({ path: 'user', select: 'id username displayName isAdmin -_id' })
.populate({
path: 'group',
select: 'groupId name description -_id',
populate: {
path: 'users',
select: 'id username displayName isAdmin -_id',
options: { limit: 15 }
}
})) as unknown as PermissionDetailsResponse[]
})
export const Permission: IPermissionModel = model<
IPermission,
IPermissionModel

View File

@@ -11,8 +11,10 @@ import {
extractName,
fileBodyValidation,
fileParamValidation,
folderBodyValidation,
folderParamValidation,
isZipFile
isZipFile,
renameBodyValidation
} from '../../utils'
const controller = new DriveController()
@@ -119,7 +121,11 @@ driveRouter.get('/file', async (req, res) => {
try {
await controller.getFile(req, query._filePath)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
@@ -132,7 +138,11 @@ driveRouter.get('/folder', async (req, res) => {
const response = await controller.getFolder(query._folderPath)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
@@ -145,7 +155,28 @@ driveRouter.delete('/file', async (req, res) => {
const response = await controller.deleteFile(query._filePath)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
driveRouter.delete('/folder', async (req, res) => {
const { error: errQ, value: query } = folderParamValidation(req.query, true)
if (errQ) return res.status(400).send(errQ.details[0].message)
try {
const response = await controller.deleteFolder(query._folderPath)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
@@ -172,11 +203,33 @@ driveRouter.post(
res.send(response)
} catch (err: any) {
await deleteFile(req.file.path)
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
driveRouter.post('/folder', async (req, res) => {
const { error, value: body } = folderBodyValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.addFolder(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
driveRouter.patch(
'/file',
(...arg) => multerSingle('file', arg),
@@ -200,11 +253,33 @@ driveRouter.patch(
res.send(response)
} catch (err: any) {
await deleteFile(req.file.path)
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
driveRouter.post('/rename', async (req, res) => {
const { error, value: body } = renameBodyValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.rename(body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
driveRouter.get('/fileTree', async (req, res) => {
try {
const response = await controller.getFileTree()

View File

@@ -11,7 +11,7 @@ const controller = new PermissionController()
permissionRouter.get('/', async (req, res) => {
try {
const response = await controller.getAllPermissions()
const response = await controller.getAllPermissions(req)
res.send(response)
} catch (err: any) {
const statusCode = err.code

View File

@@ -32,7 +32,8 @@ import appPromise from '../../../app'
import {
UserController,
PermissionController,
PermissionSetting,
PermissionType,
PermissionSettingForRoute,
PrincipalType
} from '../../../controllers/'
import { getTreeExample } from '../../../controllers/internal'
@@ -48,6 +49,12 @@ const user = {
isActive: true
}
const permission = {
type: PermissionType.route,
principalType: PrincipalType.user,
setting: PermissionSettingForRoute.grant
}
describe('drive', () => {
let app: Express
let con: Mongoose
@@ -66,28 +73,29 @@ describe('drive', () => {
const dbUser = await controller.createUser(user)
accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
uri: '/SASjsApi/drive/deploy',
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
...permission,
path: '/SASjsApi/drive/deploy',
principalId: dbUser.id
})
await permissionController.createPermission({
uri: '/SASjsApi/drive/deploy/upload',
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
...permission,
path: '/SASjsApi/drive/deploy/upload',
principalId: dbUser.id
})
await permissionController.createPermission({
uri: '/SASjsApi/drive/file',
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
...permission,
path: '/SASjsApi/drive/file',
principalId: dbUser.id
})
await permissionController.createPermission({
uri: '/SASjsApi/drive/folder',
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
...permission,
path: '/SASjsApi/drive/folder',
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
path: '/SASjsApi/drive/rename',
principalId: dbUser.id
})
})
@@ -543,29 +551,29 @@ describe('drive', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folder is not present', async () => {
it('should respond with Not Found if folder is not present', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
.expect(403)
.expect(404)
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
expect(res.text).toEqual(`Folder doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath outside Drive', async () => {
it('should respond with Bad Request if folderPath outside Drive', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: '/../path/code.sas' })
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
expect(res.text).toEqual(`Can't get folder outside drive.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath is of a file', async () => {
it('should respond with Bad Request if folderPath is of a file', async () => {
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
const filePath = '/my/path/code.sas'
@@ -576,12 +584,96 @@ describe('drive', () => {
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: filePath })
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Not a Folder.')
expect(res.text).toEqual('Not a Folder.')
expect(res.body).toEqual({})
})
})
describe('post', () => {
const folderApi = '/SASjsApi/drive/folder'
const pathToDrive = fileUtilModules.getFilesFolder()
afterEach(async () => {
await deleteFolder(path.join(pathToDrive, 'post'))
})
it('should create a folder on drive', async () => {
const res = await request(app)
.post(folderApi)
.auth(accessToken, { type: 'bearer' })
.send({ folderPath: '/post/folder' })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Conflict if the folder already exists', async () => {
await createFolder(path.join(pathToDrive, '/post/folder'))
const res = await request(app)
.post(folderApi)
.auth(accessToken, { type: 'bearer' })
.send({ folderPath: '/post/folder' })
.expect(409)
expect(res.text).toEqual(`Folder already exists.`)
expect(res.statusCode).toEqual(409)
})
it('should respond with Bad Request if the folderPath is outside drive', async () => {
const res = await request(app)
.post(folderApi)
.auth(accessToken, { type: 'bearer' })
.send({ folderPath: '../sample' })
.expect(400)
expect(res.text).toEqual(`Can't put folder outside drive.`)
})
})
describe('delete', () => {
const folderApi = '/SASjsApi/drive/folder'
const pathToDrive = fileUtilModules.getFilesFolder()
it('should delete a folder on drive', async () => {
await createFolder(path.join(pathToDrive, 'delete'))
const res = await request(app)
.delete(folderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: 'delete' })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Not Found if the folder does not exists', async () => {
const res = await request(app)
.delete(folderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: 'notExists' })
.expect(404)
expect(res.text).toEqual(`Folder doesn't exist.`)
})
it('should respond with Bad Request if the folderPath is outside drive', async () => {
const res = await request(app)
.delete(folderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: '../outsideDrive' })
.expect(400)
expect(res.text).toEqual(`Can't delete folder outside drive.`)
})
})
})
describe('file', () => {
@@ -627,7 +719,7 @@ describe('drive', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is already present', async () => {
it('should respond with Conflict if file is already present', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
@@ -642,13 +734,13 @@ describe('drive', () => {
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
.expect(409)
expect(res.text).toEqual('Error: File already exists.')
expect(res.text).toEqual('File already exists.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
it('should respond with Bad Request if filePath outside Drive', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/../path/code.sas'
@@ -657,9 +749,9 @@ describe('drive', () => {
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Cannot put file outside drive.')
expect(res.text).toEqual(`Can't put file outside drive.`)
expect(res.body).toEqual({})
})
@@ -794,19 +886,19 @@ describe('drive', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is not present', async () => {
it('should respond with Not Found if file is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', `/my/path/code-3.sas`)
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
.expect(403)
.expect(404)
expect(res.text).toEqual(`Error: File doesn't exist.`)
expect(res.text).toEqual(`File doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
it('should respond with Bad Request if filePath outside Drive', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/../path/code.sas'
@@ -815,9 +907,9 @@ describe('drive', () => {
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
expect(res.text).toEqual(`Can't modify file outside drive.`)
expect(res.body).toEqual({})
})
@@ -922,25 +1014,25 @@ describe('drive', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is not present', async () => {
it('should respond with Not Found if file is not present', async () => {
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: `/my/path/code-4.sas` })
.expect(403)
.expect(404)
expect(res.text).toEqual(`Error: File doesn't exist.`)
expect(res.text).toEqual(`File doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
it('should respond with Bad Request if filePath outside Drive', async () => {
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/../path/code.sas' })
.expect(403)
.expect(400)
expect(res.text).toEqual('Error: Cannot get file outside drive.')
expect(res.text).toEqual(`Can't get file outside drive.`)
expect(res.body).toEqual({})
})
@@ -966,6 +1058,139 @@ describe('drive', () => {
})
})
})
describe('rename', () => {
const renameApi = '/SASjsApi/drive/rename'
const pathToDrive = fileUtilModules.getFilesFolder()
afterEach(async () => {
await deleteFolder(path.join(pathToDrive, 'rename'))
})
it('should rename a folder', async () => {
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/folder', newPath: '/rename/renamed' })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should rename a file', async () => {
await createFile(
path.join(pathToDrive, 'rename', 'file.txt'),
'some file content'
)
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({
oldPath: '/rename/file.txt',
newPath: '/rename/renamed.txt'
})
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Bad Request if the oldPath is missing', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ newPath: 'newPath' })
.expect(400)
expect(res.text).toEqual(`\"oldPath\" is required`)
})
it('should respond with Bad Request if the newPath is missing', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: 'oldPath' })
.expect(400)
expect(res.text).toEqual(`\"newPath\" is required`)
})
it('should respond with Bad Request if the oldPath is outside drive', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '../outside', newPath: 'renamed' })
.expect(400)
expect(res.text).toEqual(`Old path can't be outside of drive.`)
})
it('should respond with Bad Request if the newPath is outside drive', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: 'older', newPath: '../outside' })
.expect(400)
expect(res.text).toEqual(`New path can't be outside of drive.`)
})
it('should respond with Not Found if the folder does not exist', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/not exists', newPath: '/rename/renamed' })
.expect(404)
expect(res.text).toEqual('No file/folder found for provided path.')
})
it('should respond with Conflict if the folder already exists', async () => {
await createFolder(path.join(pathToDrive, 'rename', 'folder'))
await createFolder(path.join(pathToDrive, 'rename', 'exists'))
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/folder', newPath: '/rename/exists' })
.expect(409)
expect(res.text).toEqual('Folder with new name already exists.')
})
it('should respond with Not Found if the file does not exist', async () => {
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/file.txt', newPath: '/rename/renamed.txt' })
.expect(404)
expect(res.text).toEqual('No file/folder found for provided path.')
})
it('should respond with Conflict if the file already exists', async () => {
await createFile(
path.join(pathToDrive, 'rename', 'file.txt'),
'some file content'
)
await createFile(
path.join(pathToDrive, 'rename', 'exists.txt'),
'some existing content'
)
const res = await request(app)
.post(renameApi)
.auth(accessToken, { type: 'bearer' })
.send({ oldPath: '/rename/file.txt', newPath: '/rename/exists.txt' })
.expect(409)
expect(res.text).toEqual('File with new name already exists.')
})
})
})
const getExampleService = (): ServiceMember =>

View File

@@ -5,6 +5,7 @@ import request from 'supertest'
import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { PUBLIC_GROUP_NAME } from '../../../model/Group'
const clientId = 'someclientID'
const adminUser = {
@@ -27,6 +28,12 @@ const group = {
description: 'DC group for testing purposes.'
}
const PUBLIC_GROUP = {
name: PUBLIC_GROUP_NAME,
description:
'A special group that can be used to bypass authentication for particular routes.'
}
const userController = new UserController()
const groupController = new GroupController()
@@ -535,6 +542,24 @@ describe('group', () => {
expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request when adding user to Public group', async () => {
const dbGroup = await groupController.createGroup(PUBLIC_GROUP)
const dbUser = await userController.createUser({
...user,
username: 'publicUser'
})
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(400)
expect(res.text).toEqual(
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
)
})
})
describe('RemoveUser', () => {

View File

@@ -7,10 +7,10 @@ import {
DriveController,
UserController,
GroupController,
ClientController,
PermissionController,
PrincipalType,
PermissionSetting
PermissionType,
PermissionSettingForRoute
} from '../../../controllers/'
import {
UserDetailsResponse,
@@ -56,10 +56,10 @@ const user = {
}
const permission = {
uri: '/SASjsApi/code/execute',
setting: PermissionSetting.grant,
principalType: PrincipalType.user,
principalId: 123
path: '/SASjsApi/code/execute',
type: PermissionType.route,
setting: PermissionSettingForRoute.grant,
principalType: PrincipalType.user
}
const group = {
@@ -69,7 +69,6 @@ const group = {
const userController = new UserController()
const groupController = new GroupController()
const clientController = new ClientController()
const permissionController = new PermissionController()
describe('permission', () => {
@@ -108,7 +107,8 @@ describe('permission', () => {
.expect(200)
expect(res.body.permissionId).toBeTruthy()
expect(res.body.uri).toEqual(permission.uri)
expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting)
expect(res.body.user).toBeTruthy()
})
@@ -127,7 +127,8 @@ describe('permission', () => {
.expect(200)
expect(res.body.permissionId).toBeTruthy()
expect(res.body.uri).toEqual(permission.uri)
expect(res.body.path).toEqual(permission.path)
expect(res.body.type).toEqual(permission.type)
expect(res.body.setting).toEqual(permission.setting)
expect(res.body.group).toBeTruthy()
})
@@ -142,53 +143,74 @@ describe('permission', () => {
expect(res.body).toEqual({})
})
it('should respond with Unauthorized if access token is not of an admin account even if user has permission', async () => {
it('should respond with Unauthorized if access token is not of an admin account', async () => {
const accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
uri: '/SASjsApi/permission',
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
})
const res = await request(app)
.post('/SASjsApi/permission')
.auth(accessToken, { type: 'bearer' })
.send()
.send(permission)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if uri is missing', async () => {
it('should respond with Bad Request if path is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
uri: undefined
path: undefined
})
.expect(400)
expect(res.text).toEqual(`"uri" is required`)
expect(res.text).toEqual(`"path" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if uri is not valid', async () => {
it('should respond with Bad Request if path is not valid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
uri: '/some/random/api/endpoint'
path: '/some/random/api/endpoint'
})
.expect(400)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if type is not valid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
type: 'invalid'
})
.expect(400)
expect(res.text).toEqual('"type" must be [Route]')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if type is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
type: undefined
})
.expect(400)
expect(res.text).toEqual(`"type" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if setting is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
@@ -203,6 +225,20 @@ describe('permission', () => {
expect(res.body).toEqual({})
})
it('should respond with Bad Request if setting is not valid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
setting: 'invalid'
})
.expect(400)
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if principalType is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
@@ -217,20 +253,6 @@ describe('permission', () => {
expect(res.body).toEqual({})
})
it('should respond with Bad Request if principalId is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalId: undefined
})
.expect(400)
expect(res.text).toEqual(`"principalId" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if principal type is not valid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
@@ -245,17 +267,17 @@ describe('permission', () => {
expect(res.body).toEqual({})
})
it('should respond with Bad Request if setting is not valid', async () => {
it('should respond with Bad Request if principalId is missing', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
setting: 'invalid'
principalId: undefined
})
.expect(400)
expect(res.text).toEqual('"setting" must be one of [Grant, Deny]')
expect(res.text).toEqual(`"principalId" is required`)
expect(res.body).toEqual({})
})
@@ -313,7 +335,8 @@ describe('permission', () => {
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
principalType: 'group'
principalType: 'group',
principalId: 123
})
.expect(404)
@@ -334,7 +357,7 @@ describe('permission', () => {
.expect(409)
expect(res.text).toEqual(
'Permission already exists with provided URI and User.'
'Permission already exists with provided Path, Type and User.'
)
expect(res.body).toEqual({})
})
@@ -357,7 +380,7 @@ describe('permission', () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ setting: 'Deny' })
.send({ setting: PermissionSettingForRoute.deny })
.expect(200)
expect(res.body.setting).toEqual('Deny')
@@ -366,7 +389,7 @@ describe('permission', () => {
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.send(permission)
.send()
.expect(401)
expect(res.text).toEqual('Unauthorized')
@@ -400,12 +423,11 @@ describe('permission', () => {
expect(res.body).toEqual({})
})
it('should respond with Bad Request if setting is not valid', async () => {
it('should respond with Bad Request if setting is invalid', async () => {
const res = await request(app)
.post('/SASjsApi/permission')
.patch(`/SASjsApi/permission/${dbPermission?.permissionId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({
...permission,
setting: 'invalid'
})
.expect(400)
@@ -414,12 +436,12 @@ describe('permission', () => {
expect(res.body).toEqual({})
})
it('should respond with not found (404) if permission with provided id does not exists', async () => {
it('should respond with not found (404) if permission with provided id does not exist', async () => {
const res = await request(app)
.patch('/SASjsApi/permission/123')
.auth(adminAccessToken, { type: 'bearer' })
.send({
setting: PermissionSetting.deny
setting: PermissionSettingForRoute.deny
})
.expect(404)
@@ -458,12 +480,12 @@ describe('permission', () => {
beforeAll(async () => {
await permissionController.createPermission({
...permission,
uri: '/test-1',
path: '/test-1',
principalId: dbUser.id
})
await permissionController.createPermission({
...permission,
uri: '/test-2',
path: '/test-2',
principalId: dbUser.id
})
})
@@ -478,34 +500,37 @@ describe('permission', () => {
expect(res.body).toHaveLength(2)
})
it('should give a list of all permissions when user is not admin', async () => {
const dbUser = await userController.createUser({
it(`should give a list of user's own permissions when user is not admin`, async () => {
const nonAdminUser = await userController.createUser({
...user,
username: 'get' + user.username
})
const accessToken = await generateAndSaveToken(dbUser.id)
const accessToken = await generateAndSaveToken(nonAdminUser.id)
await permissionController.createPermission({
uri: '/SASjsApi/permission',
path: '/test-1',
type: PermissionType.route,
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
principalId: nonAdminUser.id,
setting: PermissionSettingForRoute.grant
})
const permissionCount = 1
const res = await request(app)
.get('/SASjsApi/permission/')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toHaveLength(3)
expect(res.body).toHaveLength(permissionCount)
})
})
describe.only('verify', () => {
describe('verify', () => {
beforeAll(async () => {
await permissionController.createPermission({
...permission,
uri: '/SASjsApi/drive/deploy',
path: '/SASjsApi/drive/deploy',
principalId: dbUser.id
})
})

View File

@@ -7,7 +7,8 @@ import appPromise from '../../../app'
import {
UserController,
PermissionController,
PermissionSetting,
PermissionType,
PermissionSettingForRoute,
PrincipalType
} from '../../../controllers/'
import {
@@ -21,7 +22,8 @@ import {
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
import {
SASSessionController,
JSSessionController
JSSessionController,
PythonSessionController
} from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } from '../../../types'
@@ -38,14 +40,17 @@ const user = {
const sampleSasProgram = '%put hello world!;'
const sampleJsProgram = `console.log('hello world!/')`
const samplePyProgram = `print('hello world!/')`
const filesFolder = getFilesFolder()
const testFilesFolder = `test-stp-${generateTimestamp()}`
let app: Express
let accessToken: string
describe('stp', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let accessToken: string
const userController = new UserController()
const permissionController = new PermissionController()
@@ -56,10 +61,11 @@ describe('stp', () => {
const dbUser = await userController.createUser(user)
accessToken = await generateAndSaveToken(dbUser.id)
await permissionController.createPermission({
uri: '/SASjsApi/stp/execute',
path: '/SASjsApi/stp/execute',
type: PermissionType.route,
principalType: PrincipalType.user,
principalId: dbUser.id,
setting: PermissionSetting.grant
setting: PermissionSettingForRoute.grant
})
})
@@ -70,8 +76,6 @@ describe('stp', () => {
})
describe('execute', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
describe('get', () => {
describe('with runtime js', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
@@ -91,41 +95,45 @@ describe('stp', () => {
})
it('should execute js program when both js and sas program are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
await makeRequestAndAssert(
[RunTimeType.JS, RunTimeType.SAS],
200,
RunTimeType.JS
)
})
it('should throw error when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
await createFile(sasProgramPath, sampleSasProgram)
await makeRequestAndAssert([], 400)
})
})
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
describe('with runtime py', () => {
const testFilesFolder = `test-stp-${generateTimestamp()}`
beforeAll(() => {
process.runTimes = [RunTimeType.PY]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute python program when python, js and sas programs are present', async () => {
await makeRequestAndAssert(
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.PY
)
})
it('should throw error when py program is not present but js or sas program exists', async () => {
await makeRequestAndAssert([], 400)
})
})
@@ -145,41 +153,11 @@ describe('stp', () => {
})
it('should execute sas program when both sas and js programs are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
})
it('should throw error when sas program do not exit but js exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
await makeRequestAndAssert([], 400)
})
})
@@ -199,63 +177,51 @@ describe('stp', () => {
})
it('should execute js program when both js and sas program are present', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.JS
)
})
it('should execute sas program when js program is not present but sas program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
await createFile(sasProgramPath, sampleSasProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
)
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await makeRequestAndAssert([], 400)
})
})
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
describe('with runtime py and sas', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.PY, RunTimeType.SAS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute python program when both python and sas program are present', async () => {
await makeRequestAndAssert(
[RunTimeType.PY, RunTimeType.SAS],
200,
RunTimeType.PY
)
})
it('should execute sas program when python program is not present but sas program exists', async () => {
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
})
it('should throw error when both sas and js programs do not exist', async () => {
await makeRequestAndAssert([], 400)
})
})
@@ -275,76 +241,220 @@ describe('stp', () => {
})
it('should execute sas program when both sas and js programs exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(sasProgramPath, sampleSasProgram)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.SAS,
expect.anything(),
undefined
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.SAS
)
})
it('should execute js program when sas program is not present but js program exists', async () => {
const programPath = path.join(testFilesFolder, 'program')
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
await createFile(jsProgramPath, sampleJsProgram)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
RunTimeType.JS,
expect.anything(),
undefined
)
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
})
it('should throw error when both sas and js programs do not exist', async () => {
const programPath = path.join(testFilesFolder, 'program')
await makeRequestAndAssert([], 400)
})
})
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
describe('with runtime sas and py', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.SAS, RunTimeType.PY]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute sas program when both sas and python programs exist', async () => {
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.PY],
200,
RunTimeType.SAS
)
})
it('should execute python program when sas program is not present but python program exists', async () => {
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
})
it('should throw error when both sas and python programs do not exist', async () => {
await makeRequestAndAssert([], 400)
})
})
describe('with runtime sas, js and py', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.SAS, RunTimeType.JS, RunTimeType.PY]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute sas program when it exists, no matter js and python programs exist or not', async () => {
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.PY, RunTimeType.JS],
200,
RunTimeType.SAS
)
})
it('should execute js program when sas program is absent but js and python programs are present', async () => {
await makeRequestAndAssert(
[RunTimeType.JS, RunTimeType.PY],
200,
RunTimeType.JS
)
})
it('should execute python program when both sas and js programs are not present', async () => {
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
})
it('should throw error when no program exists', async () => {
await makeRequestAndAssert([], 400)
})
})
describe('with runtime js, sas and py', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute js program when it exists, no matter sas and python programs exist or not', async () => {
await makeRequestAndAssert(
[RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY],
200,
RunTimeType.JS
)
})
it('should execute sas program when js program is absent but sas and python programs are present', async () => {
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.PY],
200,
RunTimeType.SAS
)
})
it('should execute python program when both sas and js programs are not present', async () => {
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
})
it('should throw error when no program exists', async () => {
await makeRequestAndAssert([], 400)
})
})
describe('with runtime py, sas and js', () => {
beforeAll(() => {
process.runTimes = [RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS]
})
beforeEach(() => {
jest.resetModules() // it clears the cache
setupMocks()
})
afterEach(async () => {
jest.resetAllMocks()
await deleteFolder(path.join(filesFolder, testFilesFolder))
})
it('should execute python program when it exists, no matter sas and js programs exist or not', async () => {
await makeRequestAndAssert(
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.PY
)
})
it('should execute sas program when python program is absent but sas and js programs are present', async () => {
await makeRequestAndAssert(
[RunTimeType.SAS, RunTimeType.JS],
200,
RunTimeType.SAS
)
})
it('should execute js program when both sas and python programs are not present', async () => {
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
})
it('should throw error when no program exists', async () => {
await makeRequestAndAssert([], 400)
})
})
})
})
})
const generateSaveTokenAndCreateUser = async (
someUser: any
): Promise<string> => {
const userController = new UserController()
const dbUser = await userController.createUser(someUser)
const makeRequestAndAssert = async (
programTypes: RunTimeType[],
expectedStatusCode: number,
expectedRuntime?: RunTimeType
) => {
const programPath = path.join(testFilesFolder, 'program')
for (const programType of programTypes) {
if (programType === RunTimeType.JS)
await createFile(
path.join(filesFolder, `${programPath}.js`),
sampleJsProgram
)
else if (programType === RunTimeType.PY)
await createFile(
path.join(filesFolder, `${programPath}.py`),
samplePyProgram
)
else if (programType === RunTimeType.SAS)
await createFile(
path.join(filesFolder, `${programPath}.sas`),
sampleSasProgram
)
}
return generateAndSaveToken(dbUser.id)
await request(app)
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(expectedStatusCode)
if (expectedRuntime)
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expect.anything(),
expectedRuntime,
expect.anything(),
undefined
)
}
const generateAndSaveToken = async (userId: number) => {
@@ -365,6 +475,10 @@ const setupMocks = async () => {
.spyOn(JSSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
.spyOn(PythonSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
.spyOn(ProcessProgramModule, 'processProgram')
.mockImplementation(() => Promise.resolve())

View File

@@ -39,12 +39,11 @@ describe('web', () => {
describe('home', () => {
it('should respond with CSRF Token', async () => {
await request(app)
.get('/')
.expect(
'set-cookie',
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
)
const res = await request(app).get('/').expect(200)
expect(res.text).toMatch(
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/
)
})
})
@@ -154,10 +153,10 @@ describe('web', () => {
const getCSRF = async (app: Express) => {
// make request to get CSRF
const { header } = await request(app).get('/')
const { header, text } = await request(app).get('/')
const cookies = header['set-cookie'].join()
const csrfToken = extractCSRF(cookies)
const csrfToken = extractCSRF(text)
return { csrfToken, cookies }
}
@@ -177,7 +176,7 @@ const performLogin = async (
return { cookies: newCookies }
}
const extractCSRF = (cookies: string) =>
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
cookies
)![2]
const extractCSRF = (text: string) =>
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
text
)![1]

View File

@@ -13,7 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeReturnRaw(req, query._program)
const response = await controller.executeGetRequest(req, query._program)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
@@ -42,7 +42,7 @@ stpRouter.post(
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
try {
const response = await controller.executeReturnJson(
const response = await controller.executePostRequest(
req,
req.body,
req.query?._program as string

View File

@@ -26,6 +26,7 @@ export const style = `<style>
}
.app-container .app img{
width: 100%;
height: calc(100% - 30px);
margin-bottom: 10px;
border-radius: 10px;
}

View File

@@ -11,11 +11,15 @@ webRouter.get('/', async (req, res) => {
try {
response = await controller.home()
} catch (_) {
response = 'Web Build is not present'
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
res.cookie('XSRF-TOKEN', req.csrfToken())
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
)
return res.send(response)
return res.send(injectedContent)
}
})

View File

@@ -2,5 +2,6 @@ export interface TreeNode {
name: string
relativePath: string
absolutePath: string
isFolder: boolean
children: Array<TreeNode>
}

View File

@@ -2,10 +2,13 @@ declare namespace NodeJS {
export interface Process {
sasLoc?: string
nodeLoc?: string
pythonLoc?: string
driveLoc: string
logsLoc: string
logsUUID: string
sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController
pythonSessionController?: import('../../controllers/internal').PythonSessionController
appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[]

View File

@@ -9,7 +9,7 @@ const StaticAuthorizedRoutes = [
'/SASjsApi/drive/file',
'/SASjsApi/drive/folder',
'/SASjsApi/drive/fileTree',
'/SASjsApi/permission'
'/SASjsApi/drive/rename'
]
export const getAuthorizedRoutes = () => {
@@ -18,7 +18,7 @@ export const getAuthorizedRoutes = () => {
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
}
export const getUri = (req: Request) => {
export const getPath = (req: Request) => {
const { baseUrl, path: reqPath } = req
if (baseUrl === '/AppStream') {
@@ -32,4 +32,4 @@ export const getUri = (req: Request) => {
}
export const isAuthorizingRoute = (req: Request): boolean =>
getAuthorizedRoutes().includes(getUri(req))
getAuthorizedRoutes().includes(getPath(req))

View File

@@ -4,9 +4,9 @@ import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
import { RunTimeType } from './verifyEnvVariables'
export const getDesktopFields = async () => {
const { SAS_PATH, NODE_PATH } = process.env
const { SAS_PATH, NODE_PATH, PYTHON_PATH } = process.env
let sasLoc, nodeLoc
let sasLoc, nodeLoc, pythonLoc
if (process.runTimes.includes(RunTimeType.SAS)) {
sasLoc = SAS_PATH ?? (await getSASLocation())
@@ -16,7 +16,11 @@ export const getDesktopFields = async () => {
nodeLoc = NODE_PATH ?? (await getNodeLocation())
}
return { sasLoc, nodeLoc }
if (process.runTimes.includes(RunTimeType.PY)) {
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
}
return { sasLoc, nodeLoc, pythonLoc }
}
const getDriveLocation = async (): Promise<string> => {
@@ -91,3 +95,25 @@ const getNodeLocation = async (): Promise<string> => {
return targetName
}
const getPythonLocation = async (): Promise<string> => {
const validator = async (filePath: string) => {
if (!filePath) return 'Path to Python executable is required.'
if (!(await fileExists(filePath))) {
return 'No file found at provided path.'
}
return true
}
const defaultLocation = isWindows() ? 'C:\\Python' : '/usr/bin/python'
const targetName = await getString(
'Please enter full path to a Python executable: ',
validator,
defaultLocation
)
return targetName
}

View File

@@ -5,7 +5,7 @@ import { RunTimeType } from '.'
export const getRunTimeAndFilePath = async (programPath: string) => {
const ext = path.extname(programPath)
// If programPath (_program) is provided with a ".sas" or ".js" extension
// If programPath (_program) is provided with a ".sas", ".js" or ".py" extension
// we should use that extension to determine the appropriate runTime
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
const runTime = ext.slice(1)

View File

@@ -16,6 +16,7 @@ export * from './getRunTimeAndFilePath'
export * from './getServerUrl'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './isPublicRoute'
export * from './zipped'
export * from './parseLogToArray'
export * from './removeTokensInDB'

View File

@@ -0,0 +1,31 @@
import { Request } from 'express'
import { getPath } from './getAuthorizedRoutes'
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
import Permission from '../model/Permission'
import { PermissionSettingForRoute } from '../controllers'
import { RequestUser } from '../types'
export const isPublicRoute = async (req: Request): Promise<boolean> => {
const group = await Group.findOne({ name: PUBLIC_GROUP_NAME })
if (group) {
const path = getPath(req)
const groupPermission = await Permission.findOne({
path,
group: group?._id
})
if (groupPermission?.setting === PermissionSettingForRoute.grant)
return true
}
return false
}
export const publicUser: RequestUser = {
userId: 0,
clientId: 'public_app',
username: 'publicUser',
displayName: 'Public User',
isAdmin: false,
isActive: true
}

View File

@@ -1,5 +1,5 @@
import Client from '../model/Client'
import Group from '../model/Group'
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User'
import Configuration, { ConfigurationType } from '../model/Configuration'
@@ -31,6 +31,15 @@ export const seedDB = async (): Promise<ConfigurationType> => {
console.log(`DB Seed - Group created: ${GROUP.name}`)
}
// Checking if 'Public' Group is already in the database
const publicGroupExist = await Group.findOne({ name: PUBLIC_GROUP.name })
if (!publicGroupExist) {
const group = new Group(PUBLIC_GROUP)
await group.save()
console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
}
// Checking if user is already in the database
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
if (!usernameExist) {
@@ -68,6 +77,13 @@ const GROUP = {
name: 'AllUsers',
description: 'Group contains all users'
}
const PUBLIC_GROUP = {
name: PUBLIC_GROUP_NAME,
description:
'A special group that can be used to bypass authentication for particular routes.'
}
const CLIENT = {
clientId: 'clientID1',
clientSecret: 'clientSecret'

View File

@@ -28,11 +28,13 @@ export const setProcessVariables = async () => {
if (MODE === ModeType.Server) {
process.sasLoc = process.env.SAS_PATH
process.nodeLoc = process.env.NODE_PATH
process.pythonLoc = process.env.PYTHON_PATH
} else {
const { sasLoc, nodeLoc } = await getDesktopFields()
const { sasLoc, nodeLoc, pythonLoc } = await getDesktopFields()
process.sasLoc = sasLoc
process.nodeLoc = nodeLoc
process.pythonLoc = pythonLoc
}
const { SASJS_ROOT } = process.env
@@ -48,6 +50,8 @@ export const setProcessVariables = async () => {
await createFolder(absLogsPath)
process.logsLoc = getRealPath(absLogsPath)
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc)
console.log('sasLogs: ', process.logsLoc)

View File

@@ -1,6 +1,6 @@
import path from 'path'
import { MulterFile } from '../types/Upload'
import { listFilesInFolder, readFileBinary } from '@sasjs/utils'
import { listFilesInFolder, readFileBinary, isWindows } from '@sasjs/utils'
interface FilenameMapSingle {
fieldName: string
@@ -118,15 +118,42 @@ export const generateFileUploadJSCode = async (
if (fileName.includes('req_file')) {
fileCount++
const filePath = path.join(sessionFolder, fileName)
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${filePath}')`
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${
isWindows() ? filePath.replace(/\\/g, '\\\\') : filePath
}')`
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
}
})
if (fileCount) {
uploadCode = `\nconst _WEBIN_FILE_COUNT = ${fileCount}` + uploadCode
}
uploadCode += `\nconst _WEBIN_FILE_COUNT = ${fileCount}`
return uploadCode
}
/**
* Generates the python code that references uploaded files in the concurrent request
* @param filesNamesMap object that maps hashed file names and original file names
* @param sessionFolder name of the folder that is created for the purpose of files in concurrent request
* @returns generated python code
*/
export const generateFileUploadPythonCode = async (
filesNamesMap: FilenamesMap,
sessionFolder: string
) => {
let uploadCode = ''
let fileCount = 0
const sessionFolderList: string[] = await listFilesInFolder(sessionFolder)
sessionFolderList.forEach(async (fileName) => {
if (fileName.includes('req_file')) {
fileCount++
uploadCode += `\n_WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
uploadCode += `\n_WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
}
})
uploadCode += `\n_WEBIN_FILE_COUNT = ${fileCount}`
return uploadCode
}

View File

@@ -1,5 +1,9 @@
import Joi from 'joi'
import { PermissionSetting, PrincipalType } from '../controllers/permission'
import {
PermissionType,
PermissionSettingForRoute,
PrincipalType
} from '../controllers/permission'
import { getAuthorizedRoutes } from './getAuthorizedRoutes'
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
@@ -89,12 +93,15 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>
Joi.object({
uri: Joi.string()
path: Joi.string()
.required()
.valid(...getAuthorizedRoutes()),
type: Joi.string()
.required()
.valid(...Object.values(PermissionType)),
setting: Joi.string()
.required()
.valid(...Object.values(PermissionSetting)),
.valid(...Object.values(PermissionSettingForRoute)),
principalType: Joi.string()
.required()
.valid(...Object.values(PrincipalType)),
@@ -105,7 +112,7 @@ export const updatePermissionValidation = (data: any): Joi.ValidationResult =>
Joi.object({
setting: Joi.string()
.required()
.valid(...Object.values(PermissionSetting))
.valid(...Object.values(PermissionSettingForRoute))
}).validate(data)
export const deployValidation = (data: any): Joi.ValidationResult =>
@@ -138,9 +145,23 @@ export const fileParamValidation = (data: any): Joi.ValidationResult =>
_filePath: filePathSchema
}).validate(data)
export const folderParamValidation = (data: any): Joi.ValidationResult =>
export const folderParamValidation = (
data: any,
folderPathRequired?: boolean
): Joi.ValidationResult =>
Joi.object({
_folderPath: Joi.string()
_folderPath: folderPathRequired ? Joi.string().required() : Joi.string()
}).validate(data)
export const folderBodyValidation = (data: any): Joi.ValidationResult =>
Joi.object({
folderPath: Joi.string().required()
}).validate(data)
export const renameBodyValidation = (data: any): Joi.ValidationResult =>
Joi.object({
oldPath: Joi.string().required(),
newPath: Joi.string().required()
}).validate(data)
export const runCodeValidation = (data: any): Joi.ValidationResult =>

View File

@@ -28,7 +28,8 @@ export enum LOG_FORMAT_MORGANType {
export enum RunTimeType {
SAS = 'sas',
JS = 'js'
JS = 'js',
PY = 'py'
}
export enum ReturnCode {
@@ -125,8 +126,27 @@ const verifyCORS = (): string[] => {
if (CORS) {
const corsTypes = Object.values(CorsType)
if (!corsTypes.includes(CORS as CorsType))
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
if (CORS === CorsType.ENABLED) {
const { WHITELIST } = process.env
const urls = WHITELIST?.trim()
.split(' ')
.filter((url) => !!url)
if (urls?.length) {
urls.forEach((url) => {
if (!url.startsWith('http://') && !url.startsWith('https://'))
errors.push(
`- CORS '${CORS}'\n - provided WHITELIST ${url} is not valid`
)
})
} else {
errors.push(`- CORS '${CORS}'\n - provide at least one WHITELIST URL`)
}
}
} else {
const { MODE } = process.env
process.env.CORS =
@@ -209,7 +229,7 @@ const verifyRUN_TIMES = (): string[] => {
const verifyExecutablePaths = () => {
const errors: string[] = []
const { RUN_TIMES, SAS_PATH, NODE_PATH, MODE } = process.env
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, MODE } = process.env
if (MODE === ModeType.Server) {
const runTimes = RUN_TIMES?.split(',')
@@ -221,6 +241,10 @@ const verifyExecutablePaths = () => {
if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) {
errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`)
}
if (runTimes?.includes(RunTimeType.PY) && !PYTHON_PATH) {
errors.push(`- PYTHON_PATH is required for ${RunTimeType.PY} run time`)
}
}
return errors

View File

@@ -12,28 +12,16 @@
},
"tags": [
{
"name": "Info",
"description": "Get Server Information"
},
{
"name": "Session",
"description": "Get Session information"
},
{
"name": "User",
"description": "Operations with users"
},
{
"name": "Permission",
"description": "Operations about permissions"
"name": "Auth",
"description": "Operations about auth"
},
{
"name": "Client",
"description": "Operations about clients"
},
{
"name": "Auth",
"description": "Operations about auth"
"name": "CODE",
"description": "Execution of code (various runtimes are supported)"
},
{
"name": "Drive",
@@ -43,13 +31,25 @@
"name": "Group",
"description": "Operations on groups and group memberships"
},
{
"name": "Info",
"description": "Get Server Information"
},
{
"name": "Permission",
"description": "Operations about permissions"
},
{
"name": "Session",
"description": "Get Session information"
},
{
"name": "STP",
"description": "Execution of Stored Programs"
},
{
"name": "CODE",
"description": "Execution of code (various runtimes are supported)"
"name": "User",
"description": "Operations with users"
},
{
"name": "Web",

572
web/package-lock.json generated
View File

@@ -10,7 +10,7 @@
"dependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^5.0.3",
"@mui/icons-material": "^5.8.4",
"@mui/lab": "^5.0.0-alpha.50",
"@mui/material": "^5.0.3",
"@mui/styles": "^5.0.1",
@@ -27,7 +27,7 @@
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2",
"react-monaco-editor": "^0.48.0",
"react-router-dom": "^5.3.0",
"react-router-dom": "^6.3.0",
"react-toastify": "^9.0.1"
},
"devDependencies": {
@@ -1836,9 +1836,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
"integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
@@ -2284,6 +2284,58 @@
"node": ">=8"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@mui/core": {
"version": "5.0.0-alpha.54",
"resolved": "https://registry.npmjs.org/@mui/core/-/core-5.0.0-alpha.54.tgz",
@@ -2312,19 +2364,23 @@
}
},
"node_modules/@mui/icons-material": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz",
"integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==",
"version": "5.8.4",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
"integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
"dependencies": {
"@babel/runtime": "^7.16.0"
"@babel/runtime": "^7.17.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
},
"peerDependencies": {
"@mui/material": "^5.0.0",
"@types/react": "^16.8.6 || ^17.0.0",
"react": "^17.0.2"
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -3933,11 +3989,9 @@
}
},
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
"peer": true,
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
"bin": {
"acorn": "bin/acorn"
},
@@ -6518,18 +6572,6 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/espree/node_modules/acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/espree/node_modules/eslint-visitor-keys": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
@@ -7128,16 +7170,11 @@
}
},
"node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
"@babel/runtime": "^7.7.6"
}
},
"node_modules/hoist-non-react-statics": {
@@ -7198,20 +7235,6 @@
"node": ">=12"
}
},
"node_modules/html-minifier-terser/node_modules/acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/html-minifier-terser/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@@ -7221,46 +7244,6 @@
"node": ">= 12"
}
},
"node_modules/html-minifier-terser/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/html-minifier-terser/node_modules/terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"dev": true,
"dependencies": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"acorn": "^8.5.0"
},
"peerDependenciesMeta": {
"acorn": {
"optional": true
}
}
},
"node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/html-webpack-plugin": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
@@ -7829,11 +7812,6 @@
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -8392,19 +8370,6 @@
"node": ">=4"
}
},
"node_modules/mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"dependencies": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
},
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -8967,14 +8932,6 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -9362,47 +9319,29 @@
"react": "^17.x"
}
},
"node_modules/react-router": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/react-router-dom": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"history": "^5.2.0",
"react-router": "6.3.0"
},
"peerDependencies": {
"react": ">=15"
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-router/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
"node_modules/react-router-dom/node_modules/react-router": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"dependencies": {
"history": "^5.2.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-toastify": {
"version": "9.0.1",
@@ -9679,11 +9618,6 @@
"node": ">=4"
}
},
"node_modules/resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@@ -10337,6 +10271,28 @@
"node": ">=6"
}
},
"node_modules/terser": {
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"dependencies": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -10349,11 +10305,6 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
"node_modules/tiny-invariant": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@@ -10733,11 +10684,6 @@
"node": ">= 0.10"
}
},
"node_modules/value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -11184,17 +11130,6 @@
"node": ">=10.0.0"
}
},
"node_modules/webpack/node_modules/acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/webpack/node_modules/acorn-import-assertions": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
@@ -11203,11 +11138,6 @@
"acorn": "^8"
}
},
"node_modules/webpack/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/webpack/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -11276,30 +11206,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/webpack/node_modules/terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"dependencies": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"acorn": "^8.5.0"
},
"peerDependenciesMeta": {
"acorn": {
"optional": true
}
}
},
"node_modules/webpack/node_modules/terser-webpack-plugin": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz",
@@ -11333,14 +11239,6 @@
}
}
},
"node_modules/webpack/node_modules/terser/node_modules/source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"engines": {
"node": ">= 8"
}
},
"node_modules/webpack/node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
@@ -12642,9 +12540,9 @@
}
},
"@babel/runtime": {
"version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz",
"integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
@@ -12974,6 +12872,49 @@
}
}
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
},
"@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
},
"@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
},
"@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@mui/core": {
"version": "5.0.0-alpha.54",
"resolved": "https://registry.npmjs.org/@mui/core/-/core-5.0.0-alpha.54.tgz",
@@ -12989,11 +12930,11 @@
}
},
"@mui/icons-material": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz",
"integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==",
"version": "5.8.4",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz",
"integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==",
"requires": {
"@babel/runtime": "^7.16.0"
"@babel/runtime": "^7.17.2"
}
},
"@mui/lab": {
@@ -14170,11 +14111,9 @@
}
},
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
"peer": true
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w=="
},
"acorn-jsx": {
"version": "5.3.2",
@@ -16121,12 +16060,6 @@
"eslint-visitor-keys": "^3.3.0"
},
"dependencies": {
"acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true
},
"eslint-visitor-keys": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
@@ -16587,16 +16520,11 @@
"dev": true
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz",
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
"@babel/runtime": "^7.7.6"
}
},
"hoist-non-react-statics": {
@@ -16650,44 +16578,11 @@
"terser": "^5.10.0"
},
"dependencies": {
"acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"optional": true,
"peer": true
},
"commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
}
}
}
}
},
@@ -17084,11 +16979,6 @@
"is-docker": "^2.0.0"
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -17530,15 +17420,6 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
},
"mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"requires": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
}
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -17961,14 +17842,6 @@
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
}
},
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -18260,44 +18133,25 @@
"prop-types": "^15.8.1"
}
},
"react-router": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"react-router-dom": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
"history": "^5.2.0",
"react-router": "6.3.0"
},
"dependencies": {
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
"react-router": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
"requires": {
"history": "^5.2.0"
}
}
}
},
"react-router-dom": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-toastify": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
@@ -18520,11 +18374,6 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
},
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@@ -19014,6 +18863,24 @@
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="
},
"terser": {
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
}
}
},
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -19026,11 +18893,6 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
"tiny-invariant": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@@ -19320,11 +19182,6 @@
"homedir-polyfill": "^1.0.1"
}
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -19380,22 +19237,12 @@
"webpack-sources": "^3.2.2"
},
"dependencies": {
"acorn": {
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
},
"acorn-import-assertions": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
"requires": {}
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -19442,23 +19289,6 @@
"has-flag": "^4.0.0"
}
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"requires": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
}
}
},
"terser-webpack-plugin": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz",

View File

@@ -9,7 +9,7 @@
"dependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^5.0.3",
"@mui/icons-material": "^5.8.4",
"@mui/lab": "^5.0.0-alpha.50",
"@mui/material": "^5.0.3",
"@mui/styles": "^5.0.1",
@@ -26,7 +26,7 @@
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2",
"react-monaco-editor": "^0.48.0",
"react-router-dom": "^5.3.0",
"react-router-dom": "^6.3.0",
"react-toastify": "^9.0.1"
},
"devDependencies": {

View File

@@ -1,12 +1,11 @@
import React, { useContext } from 'react'
import { Route, HashRouter, Switch } from 'react-router-dom'
import { Route, HashRouter, Routes } from 'react-router-dom'
import { ThemeProvider } from '@mui/material/styles'
import { theme } from './theme'
import Login from './components/login'
import Header from './components/header'
import Home from './components/home'
import Drive from './containers/Drive'
import Studio from './containers/Studio'
import Settings from './containers/Settings'
@@ -22,11 +21,9 @@ function App() {
<ThemeProvider theme={theme}>
<HashRouter>
<Header />
<Switch>
<Route path="/">
<Login />
</Route>
</Switch>
<Routes>
<Route path="*" element={<Login />} />
</Routes>
</HashRouter>
</ThemeProvider>
)
@@ -36,23 +33,12 @@ function App() {
<ThemeProvider theme={theme}>
<HashRouter>
<Header />
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route exact path="/SASjsDrive">
<Drive />
</Route>
<Route exact path="/SASjsStudio">
<Studio />
</Route>
<Route exact path="/SASjsSettings">
<Settings />
</Route>
<Route exact path="/SASjsLogon">
<AuthCode />
</Route>
</Switch>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/SASjsStudio" element={<Studio />} />
<Route path="/SASjsSettings" element={<Settings />} />
<Route path="/SASjsLogon" element={<AuthCode />} />
</Routes>
<ToastContainer />
</HashRouter>
</ThemeProvider>

View File

@@ -18,22 +18,27 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({
}
}))
type DeleteModalProps = {
type DeleteConfirmationModalProps = {
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
deletePermission: () => void
message: string
_delete: () => void
}
const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => {
const DeleteConfirmationModal = ({
open,
setOpen,
message,
_delete
}: DeleteConfirmationModalProps) => {
return (
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
<DialogContent dividers>
<Typography gutterBottom>
Are you sure you want to delete this permission?
</Typography>
<Typography gutterBottom>{message}</Typography>
</DialogContent>
<DialogActions>
<Button color="error" onClick={() => deletePermission()}>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button color="error" onClick={() => _delete()}>
Delete
</Button>
</DialogActions>
@@ -41,4 +46,4 @@ const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => {
)
}
export default DeleteModal
export default DeleteConfirmationModal

View File

@@ -0,0 +1,83 @@
import React, { useState } from 'react'
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
import { BootstrapDialogTitle } from './dialogTitle'
import { BootstrapDialog } from './modal'
type FilePathInputModalProps = {
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
saveFile: (filePath: string) => void
}
const FilePathInputModal = ({
open,
setOpen,
saveFile
}: FilePathInputModalProps) => {
const [filePath, setFilePath] = useState('')
const [hasError, setHasError] = useState(false)
const [errorText, setErrorText] = useState('')
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
const specialChars = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>?~]/
const fileExtension = /\.(exe|sh|htaccess)$/i
if (specialChars.test(value)) {
setHasError(true)
setErrorText('can not have special characters')
} else if (fileExtension.test(value)) {
setHasError(true)
setErrorText('can not save file with extensions [exe, sh, htaccess]')
} else {
setHasError(false)
setErrorText('')
}
setFilePath(value)
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (hasError || !filePath) return
saveFile(filePath)
}
return (
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
Save File
</BootstrapDialogTitle>
<DialogContent dividers>
<form onSubmit={handleSubmit}>
<TextField
fullWidth
autoFocus
variant="outlined"
label="File Path"
value={filePath}
onChange={handleChange}
error={hasError}
helperText={errorText}
/>
</form>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="contained"
onClick={() => saveFile(filePath)}
disabled={hasError || !filePath}
>
Save
</Button>
</DialogActions>
</BootstrapDialog>
)
}
export default FilePathInputModal

View File

@@ -1,17 +1,19 @@
import React, { useState, useEffect, useContext } from 'react'
import { Link, useHistory, useLocation } from 'react-router-dom'
import { Link, useNavigate, useLocation } from 'react-router-dom'
import {
Box,
AppBar,
Toolbar,
Tabs,
Tab,
Button,
Menu,
MenuItem
MenuItem,
IconButton,
Typography
} from '@mui/material'
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import SettingsIcon from '@mui/icons-material/Settings'
import { OpenInNew, Settings, Menu as MenuIcon } from '@mui/icons-material'
import Username from './username'
import { AppContext } from '../context/appContext'
@@ -24,37 +26,44 @@ const baseUrl =
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
const Header = (props: any) => {
const history = useHistory()
const navigate = useNavigate()
const { pathname } = useLocation()
const appContext = useContext(AppContext)
const [tabValue, setTabValue] = useState(
validTabs.includes(pathname) ? pathname : '/'
)
const [anchorEl, setAnchorEl] = useState<
(EventTarget & HTMLButtonElement) | null
>(null)
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(null)
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
null
)
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget)
}
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget)
}
const handleCloseNavMenu = () => {
setAnchorElNav(null)
}
const handleCloseUserMenu = () => {
setAnchorElUser(null)
}
useEffect(() => {
setTabValue(validTabs.includes(pathname) ? pathname : '/')
}, [pathname])
const handleMenu = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
setTabValue(value)
}
const handleLogout = () => {
if (appContext.logout) {
handleClose()
handleCloseUserMenu()
appContext.logout()
}
}
@@ -64,60 +73,129 @@ const Header = (props: any) => {
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
>
<Toolbar variant="dense">
<img
src="logo.png"
alt="logo"
style={{
width: '35px',
cursor: 'pointer',
marginRight: '25px'
}}
onClick={() => {
setTabValue('/')
history.push('/')
}}
/>
<Tabs
indicatorColor="secondary"
value={tabValue}
onChange={handleTabChange}
>
<Tab label="Home" value="/" to="/" component={Link} />
<Tab
label="Drive"
value="/SASjsDrive"
to="/SASjsDrive"
component={Link}
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
<img
src="logo.png"
alt="logo"
style={{
width: '35px',
height: '35px',
marginTop: '9px',
cursor: 'pointer',
marginRight: '25px'
}}
onClick={() => {
setTabValue('/')
navigate('/')
}}
/>
<Tab
label="Studio"
value="/SASjsStudio"
to="/SASjsStudio"
component={Link}
<Tabs
indicatorColor="secondary"
value={tabValue}
onChange={handleTabChange}
>
<Tab label="Home" value="/" to="/" component={Link} />
<Tab
label="Studio"
value="/SASjsStudio"
to="/SASjsStudio"
component={Link}
/>
</Tabs>
<Button
href={`${baseUrl}/AppStream`}
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
size="large"
endIcon={<OpenInNew />}
>
Apps
</Button>
</Box>
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}>
<IconButton size="large" onClick={handleOpenNavMenu} color="inherit">
<MenuIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorElNav}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left'
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'left'
}}
open={!!anchorElNav}
onClose={handleCloseNavMenu}
sx={{
display: { xs: 'block', md: 'none' }
}}
>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
component={Link}
to="/"
onClick={handleCloseNavMenu}
variant="contained"
color="primary"
>
Home
</Button>
</MenuItem>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
component={Link}
to="/SASjsStudio"
onClick={handleCloseNavMenu}
variant="contained"
color="primary"
>
Studio
</Button>
</MenuItem>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
href={`${baseUrl}/AppStream`}
target="_blank"
rel="noreferrer"
onClick={handleCloseNavMenu}
variant="contained"
color="primary"
endIcon={<OpenInNew />}
>
Apps
</Button>
</MenuItem>
</Menu>
</Box>
<Box sx={{ display: { xs: 'flex', md: 'none' } }}>
<img
src="logo.png"
alt="logo"
style={{
width: '35px',
height: '35px',
marginTop: '2px',
cursor: 'pointer',
marginRight: '25px'
}}
onClick={() => {
setTabValue('/')
navigate('/')
}}
/>
</Tabs>
<Button
href={`${baseUrl}/SASjsApi`}
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
size="large"
endIcon={<OpenInNewIcon />}
>
API Docs
</Button>
<Button
href={`${baseUrl}/AppStream`}
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
size="large"
endIcon={<OpenInNewIcon />}
>
App Stream
</Button>
</Box>
<div
style={{
display: 'flex',
@@ -127,11 +205,11 @@ const Header = (props: any) => {
>
<Username
username={appContext.displayName || appContext.username}
onClickHandler={handleMenu}
onClickHandler={handleOpenUserMenu}
/>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
@@ -141,38 +219,70 @@ const Header = (props: any) => {
vertical: 'top',
horizontal: 'center'
}}
open={!!anchorEl}
onClose={handleClose}
open={!!anchorElUser}
onClose={handleCloseUserMenu}
>
{appContext.loggedIn && (
<MenuItem
sx={{ justifyContent: 'center', display: { md: 'none' } }}
>
<Typography
variant="h5"
sx={{ border: '1px solid black', padding: '5px' }}
>
{appContext.displayName || appContext.username}
</Typography>
</MenuItem>
)}
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
component={Link}
to="/SASjsSettings"
onClick={handleCloseUserMenu}
variant="contained"
color="primary"
startIcon={<Settings />}
>
Settings
</Button>
</MenuItem>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
href={'https://server.sasjs.io'}
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
size="large"
color="primary"
endIcon={<OpenInNew />}
>
Documentation
Docs
</Button>
</MenuItem>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
component={Link}
to="/SASjsSettings"
onClick={handleClose}
href={`${baseUrl}/SASjsApi`}
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
startIcon={<SettingsIcon />}
size="large"
endIcon={<OpenInNew />}
>
Settings
</Button>
</MenuItem>
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
<Button variant="contained" color="primary">
Logout
API
</Button>
</MenuItem>
{appContext.loggedIn && (
<MenuItem
onClick={handleLogout}
sx={{ justifyContent: 'center' }}
>
<Button variant="contained" color="primary">
Logout
</Button>
</MenuItem>
)}
</Menu>
</div>
</Toolbar>

View File

@@ -5,7 +5,7 @@ import { styled } from '@mui/material/styles'
import { BootstrapDialogTitle } from './dialogTitle'
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
export const BootstrapDialog = styled(Dialog)(({ theme }) => ({
'& .MuiDialogContent-root': {
padding: theme.spacing(2)
},
@@ -14,7 +14,7 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({
}
}))
export interface ModalProps {
type ModalProps = {
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
title: string

View File

@@ -0,0 +1,109 @@
import React, { useState, useEffect } from 'react'
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
import { BootstrapDialogTitle } from './dialogTitle'
import { BootstrapDialog } from './modal'
type NameInputModalProps = {
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
title: string
isFolder: boolean
actionLabel: string
action: (name: string) => void
defaultName?: string
}
const NameInputModal = ({
open,
setOpen,
title,
isFolder,
actionLabel,
action,
defaultName
}: NameInputModalProps) => {
const [name, setName] = useState('')
const [hasError, setHasError] = useState(false)
const [errorText, setErrorText] = useState('')
useEffect(() => {
if (defaultName) setName(defaultName)
}, [defaultName])
const handleFocus = (
event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement, Element>
) => {
if (defaultName) {
event.target.select()
}
}
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
const fileNameRegex = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>/?~]/
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
const specialChars = isFolder ? folderNameRegex : fileNameRegex
if (specialChars.test(value)) {
setHasError(true)
setErrorText('can not have special characters')
} else if (!isFolder && fileNameExtensionRegex.test(value)) {
setHasError(true)
setErrorText('can not add file with extensions [exe, sh, htaccess]')
} else {
setHasError(false)
setErrorText('')
}
setName(value)
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (hasError || !name) return
action(name)
}
return (
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
{title}
</BootstrapDialogTitle>
<DialogContent dividers>
<form onSubmit={handleSubmit}>
<TextField
id="input-box"
fullWidth
autoFocus
onFocus={handleFocus}
variant="outlined"
label={isFolder ? 'Folder Name' : 'File Name'}
value={name}
onChange={handleChange}
error={hasError}
helperText={errorText}
/>
</form>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="contained"
onClick={() => action(name)}
disabled={hasError || !name}
>
{actionLabel}
</Button>
</DialogActions>
</BootstrapDialog>
)
}
export default NameInputModal

247
web/src/components/tree.tsx Normal file
View File

@@ -0,0 +1,247 @@
import React, { useEffect, useState } from 'react'
import { Menu, MenuItem } from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
import DeleteConfirmationModal from './deleteConfirmationModal'
import NameInputModal from './nameInputModal'
import { TreeNode } from '../utils/types'
type Props = {
node: TreeNode
selectedFilePath: string
handleSelect: (filePath: string) => void
deleteNode: (path: string, isFolder: boolean) => void
addFile: (path: string) => void
addFolder: (path: string) => void
rename: (oldPath: string, newPath: string) => void
defaultExpanded?: string[]
}
const TreeView = ({
node,
selectedFilePath,
handleSelect,
deleteNode,
addFile,
addFolder,
rename,
defaultExpanded
}: Props) => {
return (
<ul
style={{
listStyle: 'none',
padding: '0.25rem 0.85rem',
width: 'max-content'
}}
>
<TreeViewNode
node={node}
selectedFilePath={selectedFilePath}
handleSelect={handleSelect}
deleteNode={deleteNode}
addFile={addFile}
addFolder={addFolder}
rename={rename}
defaultExpanded={defaultExpanded}
/>
</ul>
)
}
export default TreeView
const TreeViewNode = ({
node,
selectedFilePath,
handleSelect,
deleteNode,
addFile,
addFolder,
rename,
defaultExpanded
}: Props) => {
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
useState(false)
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
useState('')
const [defaultInputModalName, setDefaultInputModalName] = useState('')
const [nameInputModalOpen, setNameInputModalOpen] = useState(false)
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
const [childVisible, setChildVisibility] = useState(false)
const [contextMenu, setContextMenu] = useState<{
mouseX: number
mouseY: number
} | null>(null)
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault()
event.stopPropagation()
setContextMenu(
contextMenu === null
? {
mouseX: event.clientX + 2,
mouseY: event.clientY - 6
}
: null
)
}
const hasChild = node.children.length ? true : false
const handleItemClick = () => {
if (node.children.length) {
setChildVisibility((v) => !v)
return
}
handleSelect(node.relativePath)
}
useEffect(() => {
if (defaultExpanded && defaultExpanded[0] === node.relativePath) {
setChildVisibility(true)
defaultExpanded.shift()
}
}, [defaultExpanded, node.relativePath])
const handleDeleteItemClick = () => {
setContextMenu(null)
setDeleteConfirmationModalOpen(true)
setDeleteConfirmationModalMessage(
`Are you sure you want to delete ${node.isFolder ? 'folder' : 'file'} "${
node.relativePath
}"?`
)
}
const deleteConfirm = () => {
setDeleteConfirmationModalOpen(false)
deleteNode(node.relativePath, node.isFolder)
}
const handleNewFolderItemClick = () => {
setContextMenu(null)
setNameInputModalOpen(true)
setNameInputModalTitle('Add Folder')
setNameInputModalActionLabel('Add')
setNameInputModalForFolder(true)
setDefaultInputModalName('')
}
const handleNewFileItemClick = () => {
setContextMenu(null)
setNameInputModalOpen(true)
setNameInputModalTitle('Add File')
setNameInputModalActionLabel('Add')
setNameInputModalForFolder(false)
setDefaultInputModalName('')
}
const addFileFolder = (name: string) => {
setNameInputModalOpen(false)
const path = node.relativePath + '/' + name
if (nameInputModalForFolder) addFolder(path)
else addFile(path)
}
const handleRenameItemClick = () => {
setContextMenu(null)
setNameInputModalOpen(true)
setNameInputModalTitle('Rename')
setNameInputModalActionLabel('Rename')
setNameInputModalForFolder(node.isFolder)
setDefaultInputModalName(node.relativePath.split('/').pop() ?? '')
}
const renameFileFolder = (name: string) => {
setNameInputModalOpen(false)
const oldPath = node.relativePath
const splittedPath = node.relativePath.split('/')
splittedPath.splice(-1, 1, name)
const newPath = splittedPath.join('/')
rename(oldPath, newPath)
}
return (
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
<li style={{ display: 'list-item' }}>
<div
className={`tree-item-label ${
selectedFilePath === node.relativePath ? 'selected' : ''
}`}
onClick={() => handleItemClick()}
>
{hasChild &&
(childVisible ? <ExpandMoreIcon /> : <ChevronRightIcon />)}
<div>{node.name}</div>
</div>
{hasChild &&
childVisible &&
node.children.map((child, index) => (
<TreeView
key={node.relativePath + '-' + index}
node={child}
selectedFilePath={selectedFilePath}
handleSelect={handleSelect}
deleteNode={deleteNode}
addFile={addFile}
addFolder={addFolder}
rename={rename}
defaultExpanded={defaultExpanded}
/>
))}
</li>
<DeleteConfirmationModal
open={deleteConfirmationModalOpen}
setOpen={setDeleteConfirmationModalOpen}
message={deleteConfirmationModalMessage}
_delete={deleteConfirm}
/>
<NameInputModal
open={nameInputModalOpen}
setOpen={setNameInputModalOpen}
title={nameInputModalTitle}
isFolder={nameInputModalForFolder}
actionLabel={nameInputModalActionLabel}
action={
nameInputModalActionLabel === 'Add' ? addFileFolder : renameFileFolder
}
defaultName={defaultInputModalName}
/>
<Menu
open={contextMenu !== null}
onClose={() => setContextMenu(null)}
anchorReference="anchorPosition"
anchorPosition={
contextMenu !== null
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
: undefined
}
>
{node.isFolder && (
<div>
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
<MenuItem
disabled={!node.relativePath}
onClick={handleNewFileItemClick}
>
Add File
</MenuItem>
</div>
)}
<MenuItem disabled={!node.relativePath} onClick={handleRenameItemClick}>
Rename
</MenuItem>
<MenuItem disabled={!node.relativePath} onClick={handleDeleteItemClick}>
Delete
</MenuItem>
</Menu>
</div>
)
}

View File

@@ -20,7 +20,14 @@ const Username = (props: any) => {
) : (
<AccountCircle></AccountCircle>
)}
<Typography variant="h6" sx={{ color: 'white', padding: '0 8px' }}>
<Typography
variant="h6"
sx={{
color: 'white',
padding: '0 8px',
display: { xs: 'none', md: 'flex' }
}}
>
{props.username}
</Typography>
</IconButton>

View File

@@ -1,106 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useLocation } from 'react-router-dom'
import axios from 'axios'
import CssBaseline from '@mui/material/CssBaseline'
import Box from '@mui/material/Box'
import SideBar from './sideBar'
import Main from './main'
export interface TreeNode {
name: string
relativePath: string
absolutePath: string
children: Array<TreeNode>
}
const Drive = () => {
const location = useLocation()
const baseUrl = window.location.origin
const [selectedFilePath, setSelectedFilePath] = useState('')
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
const setFilePathOnMount = useCallback(() => {
const queryParams = new URLSearchParams(location.search)
setSelectedFilePath(queryParams.get('filePath') ?? '')
}, [location.search])
useEffect(() => {
axios
.get(`/SASjsApi/drive/fileTree`)
.then((res: any) => {
if (res.data && res.data?.status === 'success') {
setDirectoryData(res.data.tree)
}
})
.catch((err) => {
console.log(err)
})
setFilePathOnMount()
}, [setFilePathOnMount])
const handleSelect = (node: TreeNode) => {
if (node.children.length) return
if (!node.name.includes('.')) return
window.history.pushState(
'',
'',
`${baseUrl}/#/SASjsDrive?filePath=${node.relativePath}`
)
setSelectedFilePath(node.relativePath)
}
const removeFileFromTree = (path: string) => {
if (directoryData) {
const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode
findAndRemoveNode(newTree, newTree, path)
setDirectoryData(newTree)
}
}
const findAndRemoveNode = (
node: TreeNode,
parentNode: TreeNode,
path: string
) => {
if (node.relativePath === path) {
removeNodeFromParent(parentNode, path)
return true
}
if (Array.isArray(node.children)) {
for (let i = 0; i < node.children.length; i++) {
if (findAndRemoveNode(node.children[i], node, path)) return
}
}
}
const removeNodeFromParent = (parent: TreeNode, path: string) => {
const index = parent.children.findIndex(
(node) => node.relativePath === path
)
if (index !== -1) {
parent.children.splice(index, 1)
}
}
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
<SideBar
selectedFilePath={selectedFilePath}
directoryData={directoryData}
handleSelect={handleSelect}
/>
<Main
selectedFilePath={selectedFilePath}
removeFileFromTree={removeFileFromTree}
/>
</Box>
)
}
export default Drive

View File

@@ -1,173 +0,0 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import Editor from 'react-monaco-editor'
import Box from '@mui/material/Box'
import Paper from '@mui/material/Paper'
import Stack from '@mui/material/Stack'
import Button from '@mui/material/Button'
import Toolbar from '@mui/material/Toolbar'
import CircularProgress from '@mui/material/CircularProgress'
type Props = {
selectedFilePath: string
removeFileFromTree: (path: string) => void
}
const Main = (props: Props) => {
const baseUrl = window.location.origin
const [isLoading, setIsLoading] = useState(false)
const [fileContentBeforeEdit, setFileContentBeforeEdit] = useState('')
const [fileContent, setFileContent] = useState('')
const [editMode, setEditMode] = useState(false)
useEffect(() => {
if (props.selectedFilePath) {
setIsLoading(true)
axios
.get(`/SASjsApi/drive/file?_filePath=${props.selectedFilePath}`)
.then((res: any) => {
setFileContent(res.data)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
setIsLoading(false)
})
}
}, [props.selectedFilePath])
const handleDeleteBtnClick = () => {
setIsLoading(true)
const filePath = props.selectedFilePath
axios
.delete(`/SASjsApi/drive/file?_filePath=${filePath}`)
.then((res) => {
setFileContent('')
props.removeFileFromTree(filePath)
window.history.pushState('', '', `${baseUrl}/#/SASjsDrive`)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
setIsLoading(false)
})
}
const handleEditSaveBtnClick = () => {
if (!editMode) {
setFileContentBeforeEdit(fileContent)
setEditMode(true)
} else {
setIsLoading(true)
const formData = new FormData()
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
formData.append('file', stringBlob, 'filename.sas')
formData.append('filePath', props.selectedFilePath)
axios
.patch(`/SASjsApi/drive/file`, formData)
.then((res) => {
setEditMode(false)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
setIsLoading(false)
})
}
}
const handleCancelExecuteBtnClick = () => {
if (editMode) {
setFileContent(fileContentBeforeEdit)
setEditMode(false)
} else {
window.open(
`${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath}`
)
}
}
return (
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
<Paper
sx={{
height: '75vh',
padding: '10px',
overflow: 'auto',
position: 'relative'
}}
elevation={3}
>
{isLoading && (
<CircularProgress
style={{ position: 'absolute', left: '50%', top: '50%' }}
/>
)}
{!isLoading && props?.selectedFilePath && !editMode && (
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
)}
{!isLoading && props?.selectedFilePath && editMode && (
<Editor
height="95%"
language="sas"
value={fileContent}
onChange={(val) => {
if (val) setFileContent(val)
}}
/>
)}
</Paper>
<Stack
spacing={3}
direction="row"
sx={{ justifyContent: 'center', marginTop: '20px' }}
>
<Button
variant="contained"
onClick={handleDeleteBtnClick}
disabled={isLoading || !props?.selectedFilePath}
>
Delete
</Button>
<Button
variant="contained"
onClick={handleEditSaveBtnClick}
disabled={isLoading || !props?.selectedFilePath}
>
{!editMode ? 'Edit' : 'Save'}
</Button>
<Button
variant="contained"
onClick={handleCancelExecuteBtnClick}
disabled={isLoading || !props?.selectedFilePath}
>
{editMode ? 'Cancel' : 'Execute'}
</Button>
{props?.selectedFilePath && (
<Button
variant="contained"
component={Link}
to={`/SASjsStudio?_program=${props.selectedFilePath}`}
>
Open in Studio
</Button>
)}
</Stack>
</Box>
)
}
export default Main

View File

@@ -1,100 +0,0 @@
import React, { useMemo } from 'react'
import { makeStyles } from '@mui/styles'
import Box from '@mui/material/Box'
import Drawer from '@mui/material/Drawer'
import Toolbar from '@mui/material/Toolbar'
import ListItem from '@mui/material/ListItem'
import ListItemText from '@mui/material/ListItemText'
import TreeView from '@mui/lab/TreeView'
import TreeItem from '@mui/lab/TreeItem'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
import { TreeNode } from '.'
const useStyles = makeStyles(() => ({
root: {
'& .MuiTreeItem-content': {
width: 'auto'
}
},
listItem: {
padding: 0
}
}))
const drawerWidth = 240
type Props = {
selectedFilePath: string
directoryData: TreeNode | null
handleSelect: (node: TreeNode) => void
}
const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => {
const classes = useStyles()
const defaultExpanded = useMemo(() => {
const splittedPath = selectedFilePath.split('/')
const arr = ['']
let nodeId = ''
splittedPath.forEach((path) => {
if (path !== '') {
nodeId += '/' + path
arr.push(nodeId)
}
})
return arr
}, [selectedFilePath])
const renderTree = (nodes: TreeNode) => (
<TreeItem
classes={{ root: classes.root }}
key={nodes.relativePath}
nodeId={nodes.relativePath}
label={
<ListItem
className={classes.listItem}
onClick={() => handleSelect(nodes)}
>
<ListItemText primary={nodes.name} />
</ListItem>
}
>
{Array.isArray(nodes.children)
? nodes.children.map((node) => renderTree(node))
: null}
</TreeItem>
)
return (
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
}}
>
<Toolbar />
<Box sx={{ overflow: 'auto' }}>
{directoryData && (
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
defaultExpanded={defaultExpanded}
selected={defaultExpanded.slice(-1)}
>
{renderTree(directoryData)}
</TreeView>
)}
</Box>
</Drawer>
)
}
export default SideBar

View File

@@ -32,7 +32,13 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({
type AddPermissionModalProps = {
open: boolean
handleOpen: Dispatch<SetStateAction<boolean>>
addPermission: (addPermissionPayload: RegisterPermissionPayload) => void
addPermission: (
permissions: RegisterPermissionPayload[],
permissionType: string,
principalType: string,
principal: string,
permissionSetting: string
) => void
}
const AddPermissionModal = ({
@@ -40,10 +46,11 @@ const AddPermissionModal = ({
handleOpen,
addPermission
}: AddPermissionModalProps) => {
const [URIs, setURIs] = useState<string[]>([])
const [loadingURIs, setLoadingURIs] = useState(false)
const [uri, setUri] = useState<string>()
const [principalType, setPrincipalType] = useState('user')
const [paths, setPaths] = useState<string[]>([])
const [loadingPaths, setLoadingPaths] = useState(false)
const [selectedPaths, setSelectedPaths] = useState<string[]>([])
const [permissionType, setPermissionType] = useState('Route')
const [principalType, setPrincipalType] = useState('Group')
const [userPrincipal, setUserPrincipal] = useState<UserResponse>()
const [groupPrincipal, setGroupPrincipal] = useState<GroupResponse>()
const [permissionSetting, setPermissionSetting] = useState('Grant')
@@ -52,29 +59,29 @@ const AddPermissionModal = ({
const [groupPrincipals, setGroupPrincipals] = useState<GroupResponse[]>([])
useEffect(() => {
setLoadingURIs(true)
setLoadingPaths(true)
axios
.get('/SASjsApi/info/authorizedRoutes')
.then((res: any) => {
if (res.data) {
setURIs(res.data.URIs)
setPaths(res.data.paths)
}
})
.catch((err) => {
console.log(err)
})
.finally(() => {
setLoadingURIs(false)
setLoadingPaths(false)
})
}, [])
useEffect(() => {
setLoadingPrincipals(true)
axios
.get(`/SASjsApi/${principalType}`)
.get(`/SASjsApi/${principalType.toLowerCase()}`)
.then((res: any) => {
if (res.data) {
if (principalType === 'user') {
if (principalType.toLowerCase() === 'user') {
const users: UserResponse[] = res.data
const nonAdminUsers = users.filter((user) => !user.isAdmin)
setUserPrincipals(nonAdminUsers)
@@ -92,21 +99,40 @@ const AddPermissionModal = ({
}, [principalType])
const handleAddPermission = () => {
const addPermissionPayload: any = {
uri,
setting: permissionSetting,
principalType
}
if (principalType === 'user' && userPrincipal) {
addPermissionPayload.principalId = userPrincipal.id
} else if (principalType === 'group' && groupPrincipal) {
addPermissionPayload.principalId = groupPrincipal.groupId
}
addPermission(addPermissionPayload)
const permissions: RegisterPermissionPayload[] = []
selectedPaths.forEach((path) => {
const addPermissionPayload: any = {
path,
type: permissionType,
setting: permissionSetting,
principalType: principalType.toLowerCase(),
principalId:
principalType.toLowerCase() === 'user'
? userPrincipal?.id
: groupPrincipal?.groupId
}
permissions.push(addPermissionPayload)
})
const principal =
principalType.toLowerCase() === 'user'
? userPrincipal?.username
: groupPrincipal?.name
addPermission(
permissions,
permissionType,
principalType,
principal!,
permissionSetting
)
}
const addButtonDisabled =
!uri || (principalType === 'user' ? !userPrincipal : !groupPrincipal)
!selectedPaths.length ||
(principalType.toLowerCase() === 'user' ? !userPrincipal : !groupPrincipal)
return (
<BootstrapDialog onClose={() => handleOpen(false)} open={open}>
@@ -120,22 +146,37 @@ const AddPermissionModal = ({
<Grid container spacing={2}>
<Grid item xs={12}>
<Autocomplete
options={URIs}
multiple
disableClearable
value={uri}
onChange={(event: any, newValue: string) => setUri(newValue)}
options={paths}
filterSelectedOptions
value={selectedPaths}
onChange={(event: any, newValue: string[]) => {
setSelectedPaths(newValue)
}}
renderInput={(params) => <TextField {...params} label="Paths" />}
/>
</Grid>
<Grid item xs={12}>
<Autocomplete
options={['Route']}
disableClearable
value={permissionType}
onChange={(event: any, newValue: string) =>
setPermissionType(newValue)
}
renderInput={(params) =>
loadingURIs ? (
loadingPaths ? (
<CircularProgress />
) : (
<TextField {...params} label="Principal" />
<TextField {...params} label="Permission Type" />
)
}
/>
</Grid>
<Grid item xs={12}>
<Autocomplete
options={['user', 'group']}
options={['Group', 'User']}
disableClearable
value={principalType}
onChange={(event: any, newValue: string) =>
@@ -147,7 +188,7 @@ const AddPermissionModal = ({
/>
</Grid>
<Grid item xs={12}>
{principalType === 'user' ? (
{principalType.toLowerCase() === 'user' ? (
<Autocomplete
options={userPrincipals}
getOptionLabel={(option) => option.displayName}

View File

@@ -0,0 +1,120 @@
import React from 'react'
import { Typography, DialogContent } from '@mui/material'
import { BootstrapDialog } from '../../components/modal'
import { BootstrapDialogTitle } from '../../components/dialogTitle'
import { PermissionResponse } from '../../utils/types'
export interface PermissionResponsePayload {
permissionType: string
principalType: string
principal: string
permissionSetting: string
existingPermissions: PermissionResponse[]
newAddedPermissions: PermissionResponse[]
updatedPermissions: PermissionResponse[]
errorPaths: string[]
}
type Props = {
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
payload: PermissionResponsePayload
}
const PermissionResponseModal = ({ open, setOpen, payload }: Props) => {
const newAddedPermissionsLength = payload.newAddedPermissions.length
const updatedPermissionsLength = payload.updatedPermissions.length
const existingPermissionsLength = payload.existingPermissions.length
const appliedPermissionsLength =
newAddedPermissionsLength + updatedPermissionsLength
return (
<div>
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
<BootstrapDialogTitle
id="permission-response-modal"
handleOpen={setOpen}
>
Permission Response
</BootstrapDialogTitle>
<DialogContent dividers>
<Typography sx={{ fontWeight: 'bold', marginBottom: '15px' }}>
{`${appliedPermissionsLength} "${payload.permissionSetting}", "${
payload.permissionType
}", "${payload.principalType}", "${payload.principal}" ${
appliedPermissionsLength > 1 ? 'Rules' : 'Rule'
}`}{' '}
Applied:
</Typography>
{newAddedPermissionsLength > 0 && (
<>
<Typography>
{`${newAddedPermissionsLength} ${
newAddedPermissionsLength > 1 ? 'Rules' : 'Rule'
}`}{' '}
Added:
</Typography>
<ul>
{payload.newAddedPermissions.map((permission, index) => (
<li key={index}>{permission.path}</li>
))}
</ul>
</>
)}
{updatedPermissionsLength > 0 && (
<>
<Typography>
{` ${updatedPermissionsLength} ${
updatedPermissionsLength > 1 ? 'Rules' : 'Rule'
}`}{' '}
Updated:
</Typography>
<ul>
{payload.updatedPermissions.map((permission, index) => (
<li key={index}>{permission.path}</li>
))}
</ul>
</>
)}
{existingPermissionsLength > 0 && (
<>
<Typography>
{`${existingPermissionsLength} ${
existingPermissionsLength > 1 ? 'Rules' : 'Rule'
}`}{' '}
Unchanged:
</Typography>
<ul>
{payload.existingPermissions.map((permission, index) => (
<li key={index}>{permission.path}</li>
))}
</ul>
</>
)}
{payload.errorPaths.length > 0 && (
<>
<Typography style={{ color: 'red', marginTop: '10px' }}>
Errors occurred for following paths:
</Typography>
<ul>
{payload.errorPaths.map((path, index) => (
<li key={index}>
<Typography>{path}</Typography>
</li>
))}
</ul>
</>
)}
</DialogContent>
</BootstrapDialog>
</div>
)
}
export default PermissionResponseModal

View File

@@ -31,11 +31,20 @@ const Settings = () => {
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
marginTop: '65px'
}}
>
<TabContext value={value}>
<Box component={Paper} sx={{ margin: '0 5px', height: '92vh' }}>
<Box
component={Paper}
sx={{
margin: '0 5px',
height: { md: '92vh' },
display: 'flex',
justifyContent: 'center'
}}
>
<TabList
TabIndicatorProps={{
style: {
@@ -47,7 +56,7 @@ const Settings = () => {
>
<StyledTab label="Profile" value="profile" />
{appContext.mode === ModeType.Server && (
<StyledTab label="Uri Access" value="permission" />
<StyledTab label="Permissions" value="permission" />
)}
</TabList>
</Box>

View File

@@ -27,8 +27,11 @@ import { styled } from '@mui/material/styles'
import Modal from '../../components/modal'
import PermissionFilterModal from './permissionFilterModal'
import AddPermissionModal from './addPermissionModal'
import PermissionResponseModal, {
PermissionResponsePayload
} from './addPermissionResponseModal'
import UpdatePermissionModal from './updatePermissionModal'
import DeleteModal from './deletePermissionModal'
import DeleteConfirmationModal from '../../components/deleteConfirmationModal'
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
import {
@@ -36,12 +39,23 @@ import {
PermissionResponse,
RegisterPermissionPayload
} from '../../utils/types'
import {
findExistingPermission,
findUpdatingPermission
} from '../../utils/helper'
import { AppContext } from '../../context/appContext'
const BootstrapTableCell = styled(TableCell)({
textAlign: 'left'
})
const BootstrapGridItem = styled(Grid)({
'&.MuiGrid-item': {
maxWidth: '100%'
}
})
export enum PrincipalType {
User = 'User',
Group = 'Group'
@@ -59,13 +73,30 @@ const Permission = () => {
AlertSeverityType.Success
)
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
useState(false)
const [permissionResponsePayload, setPermissionResponsePayload] =
useState<PermissionResponsePayload>({
permissionType: '',
principalType: '',
principal: '',
permissionSetting: '',
existingPermissions: [],
newAddedPermissions: [],
updatedPermissions: [],
errorPaths: []
})
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
useState(false)
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
useState('')
const [selectedPermission, setSelectedPermission] =
useState<PermissionResponse>()
const [filterModalOpen, setFilterModalOpen] = useState(false)
const [uriFilter, setUriFilter] = useState<string[]>([])
const [pathFilter, setPathFilter] = useState<string[]>([])
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
PrincipalType[]
@@ -108,8 +139,10 @@ const Permission = () => {
setFilterModalOpen(false)
const uriFilteredPermissions =
uriFilter.length > 0
? permissions.filter((permission) => uriFilter.includes(permission.uri))
pathFilter.length > 0
? permissions.filter((permission) =>
pathFilter.includes(permission.path)
)
: permissions
const principalFilteredPermissions =
@@ -169,36 +202,84 @@ const Permission = () => {
const resetFilter = () => {
setFilterModalOpen(false)
setUriFilter([])
setPathFilter([])
setPrincipalFilter([])
setSettingFilter([])
setFilteredPermissions([])
setFilterApplied(false)
}
const addPermission = (addPermissionPayload: RegisterPermissionPayload) => {
const addPermission = async (
permissionsToAdd: RegisterPermissionPayload[],
permissionType: string,
principalType: string,
principal: string,
permissionSetting: string
) => {
setAddPermissionModalOpen(false)
setIsLoading(true)
axios
.post('/SASjsApi/permission', addPermissionPayload)
.then((res: any) => {
fetchPermissions()
setSnackbarMessage('Permission added!')
setSnackbarSeverity(AlertSeverityType.Success)
setOpenSnackbar(true)
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => {
setIsLoading(false)
})
const newAddedPermissions: PermissionResponse[] = []
const updatedPermissions: PermissionResponse[] = []
const errorPaths: string[] = []
const existingPermissions: PermissionResponse[] = []
const updatingPermissions: PermissionResponse[] = []
const newPermissions: RegisterPermissionPayload[] = []
permissionsToAdd.forEach((permission) => {
const existingPermission = findExistingPermission(permissions, permission)
if (existingPermission) {
existingPermissions.push(existingPermission)
return
}
const updatingPermission = findUpdatingPermission(permissions, permission)
if (updatingPermission) {
updatingPermissions.push(updatingPermission)
return
}
newPermissions.push(permission)
})
for (const permission of newPermissions) {
await axios
.post('/SASjsApi/permission', permission)
.then((res) => {
newAddedPermissions.push(res.data)
})
.catch((error) => {
errorPaths.push(permission.path)
})
}
for (const permission of updatingPermissions) {
await axios
.patch(`/SASjsApi/permission/${permission.permissionId}`, {
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
})
.then((res) => {
updatedPermissions.push(res.data)
})
.catch((error) => {
errorPaths.push(permission.path)
})
}
fetchPermissions()
setIsLoading(false)
setPermissionResponsePayload({
permissionType,
principalType,
principal,
permissionSetting,
existingPermissions,
updatedPermissions,
newAddedPermissions,
errorPaths
})
setOpenPermissionResponseModal(true)
}
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
@@ -236,11 +317,14 @@ const Permission = () => {
const handleDeletePermissionClick = (permission: PermissionResponse) => {
setSelectedPermission(permission)
setDeleteModalOpen(true)
setDeleteConfirmationModalOpen(true)
setDeleteConfirmationModalMessage(
'Are you sure you want to delete this permission?'
)
}
const deletePermission = () => {
setDeleteModalOpen(false)
setDeleteConfirmationModalOpen(false)
setIsLoading(true)
axios
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
@@ -272,11 +356,11 @@ const Permission = () => {
) : (
<Box className="permissions-page">
<Grid container direction="column" spacing={1}>
<Grid item xs={12}>
<BootstrapGridItem item xs={12}>
<Paper elevation={3} sx={{ display: 'flex' }}>
<Tooltip title="Filter Permissions">
<IconButton>
<FilterListIcon onClick={() => setFilterModalOpen(true)} />
<IconButton onClick={() => setFilterModalOpen(true)}>
<FilterListIcon />
</IconButton>
</Tooltip>
{appContext.isAdmin && (
@@ -291,14 +375,14 @@ const Permission = () => {
</Tooltip>
)}
</Paper>
</Grid>
<Grid item xs={12}>
</BootstrapGridItem>
<BootstrapGridItem item xs={12}>
<PermissionTable
permissions={filterApplied ? filteredPermissions : permissions}
handleUpdatePermissionClick={handleUpdatePermissionClick}
handleDeletePermissionClick={handleDeletePermissionClick}
/>
</Grid>
</BootstrapGridItem>
</Grid>
<BootstrapSnackbar
open={openSnackbar}
@@ -316,8 +400,8 @@ const Permission = () => {
open={filterModalOpen}
handleOpen={setFilterModalOpen}
permissions={permissions}
uriFilter={uriFilter}
setUriFilter={setUriFilter}
pathFilter={pathFilter}
setPathFilter={setPathFilter}
principalFilter={principalFilter}
setPrincipalFilter={setPrincipalFilter}
principalTypeFilter={principalTypeFilter}
@@ -332,16 +416,22 @@ const Permission = () => {
handleOpen={setAddPermissionModalOpen}
addPermission={addPermission}
/>
<PermissionResponseModal
open={openPermissionResponseModal}
setOpen={setOpenPermissionResponseModal}
payload={permissionResponsePayload}
/>
<UpdatePermissionModal
open={updatePermissionModalOpen}
handleOpen={setUpdatePermissionModalOpen}
permission={selectedPermission}
updatePermission={updatePermission}
/>
<DeleteModal
open={deleteModalOpen}
setOpen={setDeleteModalOpen}
deletePermission={deletePermission}
<DeleteConfirmationModal
open={deleteConfirmationModalOpen}
setOpen={setDeleteConfirmationModalOpen}
message={deleteConfirmationModalMessage}
_delete={deletePermission}
/>
</Box>
)
@@ -367,9 +457,10 @@ const PermissionTable = ({
<Table sx={{ minWidth: 650 }}>
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
<TableRow>
<BootstrapTableCell>Uri</BootstrapTableCell>
<BootstrapTableCell>Path</BootstrapTableCell>
<BootstrapTableCell>Permission Type</BootstrapTableCell>
<BootstrapTableCell>Principal</BootstrapTableCell>
<BootstrapTableCell>Type</BootstrapTableCell>
<BootstrapTableCell>Principal Type</BootstrapTableCell>
<BootstrapTableCell>Setting</BootstrapTableCell>
{appContext.isAdmin && (
<BootstrapTableCell>Action</BootstrapTableCell>
@@ -379,7 +470,8 @@ const PermissionTable = ({
<TableBody>
{permissions.map((permission) => (
<TableRow key={permission.permissionId}>
<BootstrapTableCell>{permission.uri}</BootstrapTableCell>
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
<BootstrapTableCell>
{displayPrincipal(permission)}
</BootstrapTableCell>
@@ -467,8 +559,8 @@ const DisplayGroup = ({ group }: DisplayGroupProps) => {
<Typography sx={{ p: 1 }} variant="h6" component="div">
Group Members
</Typography>
{group.users.map((user) => (
<Typography sx={{ p: 1 }} component="li">
{group.users.map((user, index) => (
<Typography key={index} sx={{ p: 1 }} component="li">
{user.username}
</Typography>
))}

View File

@@ -27,8 +27,8 @@ type FilterModalProps = {
open: boolean
handleOpen: Dispatch<SetStateAction<boolean>>
permissions: PermissionResponse[]
uriFilter: string[]
setUriFilter: Dispatch<SetStateAction<string[]>>
pathFilter: string[]
setPathFilter: Dispatch<SetStateAction<string[]>>
principalFilter: string[]
setPrincipalFilter: Dispatch<SetStateAction<string[]>>
principalTypeFilter: PrincipalType[]
@@ -43,8 +43,8 @@ const PermissionFilterModal = ({
open,
handleOpen,
permissions,
uriFilter,
setUriFilter,
pathFilter,
setPathFilter,
principalFilter,
setPrincipalFilter,
principalTypeFilter,
@@ -54,8 +54,8 @@ const PermissionFilterModal = ({
applyFilter,
resetFilter
}: FilterModalProps) => {
const URIs = permissions
.map((permission) => permission.uri)
const paths = permissions
.map((permission) => permission.path)
.filter((uri, index, array) => array.indexOf(uri) === index)
// fetch all the principals from permissions array
@@ -86,13 +86,13 @@ const PermissionFilterModal = ({
<Grid item xs={12}>
<Autocomplete
multiple
options={URIs}
options={paths}
filterSelectedOptions
value={uriFilter}
value={pathFilter}
onChange={(event: any, newValue: string[]) => {
setUriFilter(newValue)
setPathFilter(newValue)
}}
renderInput={(params) => <TextField {...params} label="URIs" />}
renderInput={(params) => <TextField {...params} label="Paths" />}
/>
</Grid>
<Grid item xs={12}>

View File

@@ -0,0 +1,713 @@
import React, {
Dispatch,
SetStateAction,
useEffect,
useRef,
useState,
useContext,
useCallback
} from 'react'
import axios from 'axios'
import {
Backdrop,
Box,
Button,
CircularProgress,
FormControl,
IconButton,
Menu,
MenuItem,
Paper,
Select,
SelectChangeEvent,
Tab,
Tooltip,
Typography
} from '@mui/material'
import { styled } from '@mui/material/styles'
import {
RocketLaunch,
MoreVert,
Save,
SaveAs,
Difference,
Edit
} from '@mui/icons-material'
import Editor, {
MonacoDiffEditor,
DiffEditorDidMount,
EditorDidMount,
monaco
} from 'react-monaco-editor'
import { TabContext, TabList, TabPanel } from '@mui/lab'
import { AppContext, RunTimeType } from '../../context/appContext'
import FilePathInputModal from '../../components/filePathInputModal'
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
import Modal from '../../components/modal'
import { usePrompt, useStateWithCallback } from '../../utils/hooks'
const StyledTabPanel = styled(TabPanel)(() => ({
padding: '10px'
}))
const StyledTab = styled(Tab)(() => ({
fontSize: '1rem',
color: 'gray',
'&.Mui-selected': {
color: 'black'
}
}))
type SASjsEditorProps = {
selectedFilePath: string
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
tab: string
setTab: Dispatch<SetStateAction<string>>
}
const baseUrl = window.location.origin
const SASJS_LOGS_SEPARATOR =
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
const SASjsEditor = ({
selectedFilePath,
setSelectedFilePath,
tab,
setTab
}: SASjsEditorProps) => {
const appContext = useContext(AppContext)
const [isLoading, setIsLoading] = useState(false)
const [openModal, setOpenModal] = useState(false)
const [modalTitle, setModalTitle] = useState('')
const [modalPayload, setModalPayload] = useState('')
const [openSnackbar, setOpenSnackbar] = useState(false)
const [snackbarMessage, setSnackbarMessage] = useState('')
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
AlertSeverityType.Success
)
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('')
const [ctrlPressed, setCtrlPressed] = useState(false)
const [webout, setWebout] = useState('')
const [runTimes, setRunTimes] = useState<string[]>([])
const [selectedRunTime, setSelectedRunTime] = useState('')
const [selectedFileExtension, setSelectedFileExtension] = useState('')
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
const [showDiff, setShowDiff] = useState(false)
const editorRef = useRef(null as any)
const handleEditorDidMount: EditorDidMount = (editor) => {
editorRef.current = editor
editor.focus()
editor.addAction({
// An unique identifier of the contributed action.
id: 'show-difference',
// A label of the action that will be presented to the user.
label: 'Show Differences',
// An optional array of keybindings for the action.
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1,
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: function (ed) {
setShowDiff(true)
}
})
}
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
diffEditor.focus()
diffEditor.addCommand(monaco.KeyCode.Escape, function () {
setShowDiff(false)
})
}
usePrompt(
'Changes you made may not be saved.',
prevFileContent !== fileContent && !!selectedFilePath
)
const saveFile = useCallback(
(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)
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)
})
},
[
fileContent,
prevFileContent,
selectedFilePath,
setPrevFileContent,
setSelectedFilePath
]
)
useEffect(() => {
editorRef.current.addAction({
// An unique identifier of the contributed action.
id: 'save-file',
// A label of the action that will be presented to the user.
label: 'Save',
// An optional array of keybindings for the action.
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: () => {
if (!selectedFilePath) return setOpenFilePathInputModal(true)
if (prevFileContent !== fileContent) return saveFile()
}
})
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
useEffect(() => {
setRunTimes(Object.values(appContext.runTimes))
}, [appContext.runTimes])
useEffect(() => {
if (runTimes.length) setSelectedRunTime(runTimes[0])
}, [runTimes])
useEffect(() => {
if (selectedFilePath) {
setIsLoading(true)
setSelectedFileExtension(selectedFilePath.split('.').pop() ?? '')
axios
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
.then((res: any) => {
setPrevFileContent(res.data)
setFileContent(res.data)
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => setIsLoading(false))
} else {
const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content)
}
setLog('')
setWebout('')
setTab('code')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedFilePath])
useEffect(() => {
if (fileContent.length && !selectedFilePath) {
localStorage.setItem('fileContent', fileContent)
}
}, [fileContent, selectedFilePath])
useEffect(() => {
if (runTimes.includes(selectedFileExtension))
setSelectedRunTime(selectedFileExtension)
}, [selectedFileExtension, runTimes])
const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue)
}
const getSelection = () => {
const editor = editorRef.current as any
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
return selection ?? ''
}
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
const runCode = (code: string) => {
setIsLoading(true)
axios
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
.then((res: any) => {
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
setTab('log')
// Scroll to bottom of log
const logElement = document.getElementById('log')
if (logElement) logElement.scrollTop = logElement.scrollHeight
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => setIsLoading(false))
}
const handleKeyDown = (event: any) => {
if (event.ctrlKey) {
if (event.key === 'v') {
setCtrlPressed(false)
}
if (event.key === 'Enter') runCode(getSelection() || fileContent)
if (!ctrlPressed) setCtrlPressed(true)
}
}
const handleKeyUp = (event: any) => {
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
}
const handleChangeRunTime = (event: SelectChangeEvent) => {
setSelectedRunTime(event.target.value as RunTimeType)
}
const handleFilePathInput = (filePath: string) => {
setOpenFilePathInputModal(false)
saveFile(filePath)
}
return (
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={isLoading}
>
<CircularProgress color="inherit" />
</Backdrop>
{selectedFilePath && !runTimes.includes(selectedFileExtension) ? (
<Box sx={{ marginTop: '10px' }}>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<FileMenu
showDiff={showDiff}
setShowDiff={setShowDiff}
prevFileContent={prevFileContent}
currentFileContent={fileContent}
selectedFilePath={selectedFilePath}
setOpenFilePathInputModal={setOpenFilePathInputModal}
saveFile={saveFile}
/>
</Box>
<Paper
sx={{
height: 'calc(100vh - 140px)',
padding: '10px',
margin: '0 24px',
overflow: 'auto',
position: 'relative'
}}
elevation={3}
>
{showDiff ? (
<MonacoDiffEditor
height="98%"
language={getLanguage(selectedFileExtension)}
original={prevFileContent}
value={fileContent}
editorDidMount={handleDiffEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => setFileContent(val)}
/>
) : (
<Editor
height="98%"
language={getLanguage(selectedFileExtension)}
value={fileContent}
editorDidMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => setFileContent(val)}
/>
)}
</Paper>
</Box>
) : (
<TabContext value={tab}>
<Box
sx={{
borderBottom: 1,
borderColor: 'divider',
background: 'white'
}}
>
<TabList onChange={handleTabChange} centered>
<StyledTab label="Code" value="code" />
<StyledTab label="Log" value="log" />
<StyledTab
label={
<Tooltip title="Displays content from the _webout fileref">
<Typography>Webout</Typography>
</Tooltip>
}
value="webout"
/>
</TabList>
</Box>
<StyledTabPanel sx={{ paddingBottom: 0 }} value="code">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<RunMenu
fileContent={fileContent}
prevFileContent={prevFileContent}
selectedFilePath={selectedFilePath}
selectedRunTime={selectedRunTime}
runTimes={runTimes}
handleChangeRunTime={handleChangeRunTime}
handleRunBtnClick={handleRunBtnClick}
/>
<FileMenu
showDiff={showDiff}
setShowDiff={setShowDiff}
prevFileContent={prevFileContent}
currentFileContent={fileContent}
selectedFilePath={selectedFilePath}
setOpenFilePathInputModal={setOpenFilePathInputModal}
saveFile={saveFile}
/>
</Box>
<Paper
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
sx={{
height: 'calc(100vh - 170px)',
padding: '10px',
overflow: 'auto',
position: 'relative'
}}
elevation={3}
>
{showDiff ? (
<MonacoDiffEditor
height="98%"
language={getLanguage(selectedFileExtension)}
original={prevFileContent}
value={fileContent}
editorDidMount={handleDiffEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => setFileContent(val)}
/>
) : (
<Editor
height="98%"
language={getLanguage(selectedFileExtension)}
value={fileContent}
editorDidMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => setFileContent(val)}
/>
)}
<p
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: -10,
textAlign: 'center',
fontSize: '13px'
}}
>
Press CTRL + ENTER to run code
</p>
</Paper>
</StyledTabPanel>
<StyledTabPanel value="log">
<div>
<h2>Log</h2>
<pre id="log" style={{ overflow: 'auto', height: '75vh' }}>
{log}
</pre>
</div>
</StyledTabPanel>
<StyledTabPanel value="webout">
<div>
<pre>{webout}</pre>
</div>
</StyledTabPanel>
</TabContext>
)}
<Modal
open={openModal}
setOpen={setOpenModal}
title={modalTitle}
payload={modalPayload}
/>
<BootstrapSnackbar
open={openSnackbar}
setOpen={setOpenSnackbar}
message={snackbarMessage}
severity={snackbarSeverity}
/>
<FilePathInputModal
open={openFilePathInputModal}
setOpen={setOpenFilePathInputModal}
saveFile={handleFilePathInput}
/>
</Box>
)
}
export default SASjsEditor
type RunMenuProps = {
selectedFilePath: string
fileContent: string
prevFileContent: string
selectedRunTime: string
runTimes: string[]
handleChangeRunTime: (event: SelectChangeEvent) => void
handleRunBtnClick: () => void
}
const RunMenu = ({
selectedFilePath,
fileContent,
prevFileContent,
selectedRunTime,
runTimes,
handleChangeRunTime,
handleRunBtnClick
}: RunMenuProps) => {
const launchProgram = () => {
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
}
return (
<>
<Tooltip title="CTRL+ENTER will also run code">
<Button
onClick={handleRunBtnClick}
sx={{
display: 'flex',
alignItems: 'center',
padding: '5px 5px',
minWidth: 'unset'
}}
>
<img
alt=""
draggable="false"
style={{ width: '25px' }}
src="/running-sas.png"
></img>
<span style={{ fontSize: '12px' }}>RUN</span>
</Button>
</Tooltip>
{selectedFilePath ? (
<Box sx={{ marginLeft: '10px' }}>
<Tooltip
title={
fileContent !== prevFileContent
? 'Save file before launching program'
: 'Launch program in new window'
}
>
<span>
<IconButton
disabled={fileContent !== prevFileContent}
onClick={launchProgram}
>
<RocketLaunch />
</IconButton>
</span>
</Tooltip>
</Box>
) : (
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
<FormControl variant="standard">
<Select
labelId="run-time-select-label"
id="run-time-select"
value={selectedRunTime}
onChange={handleChangeRunTime}
>
{runTimes.map((runTime) => (
<MenuItem key={runTime} value={runTime}>
{runTime}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
)}
</>
)
}
type FileMenuProps = {
showDiff: boolean
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>
prevFileContent: string
currentFileContent: string
selectedFilePath: string
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
saveFile: () => void
}
const FileMenu = ({
showDiff,
setShowDiff,
prevFileContent,
currentFileContent,
selectedFilePath,
setOpenFilePathInputModal,
saveFile
}: FileMenuProps) => {
const [anchorEl, setAnchorEl] = useState<
(EventTarget & HTMLButtonElement) | null
>(null)
const handleMenu = (
event?: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
if (event) setAnchorEl(event.currentTarget)
else setAnchorEl(null)
}
const handleDiffBtnClick = () => {
setAnchorEl(null)
setShowDiff(!showDiff)
}
const handleSaveAsBtnClick = () => {
setAnchorEl(null)
setOpenFilePathInputModal(true)
}
const handleSaveBtnClick = () => {
setAnchorEl(null)
saveFile()
}
return (
<>
<Tooltip title="Save File Menu">
<IconButton onClick={handleMenu}>
<MoreVert />
</IconButton>
</Tooltip>
<Menu
id="save-file-menu"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
open={!!anchorEl}
onClose={() => handleMenu()}
>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
onClick={handleDiffBtnClick}
variant="contained"
color="primary"
startIcon={showDiff ? <Edit /> : <Difference />}
>
{showDiff ? 'Edit' : 'Diff'}
</Button>
</MenuItem>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
onClick={handleSaveBtnClick}
variant="contained"
color="primary"
startIcon={<Save />}
disabled={
!selectedFilePath || prevFileContent === currentFileContent
}
>
Save
</Button>
</MenuItem>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
onClick={handleSaveAsBtnClick}
variant="contained"
color="primary"
startIcon={<SaveAs />}
>
Save As
</Button>
</MenuItem>
</Menu>
</>
)
}
const getLanguage = (extension: string) => {
if (extension === 'js') return 'javascript'
if (extension === 'ts') return 'typescript'
if (extension === 'md' || extension === 'mdx') return 'markdown'
return extension
}

View File

@@ -1,253 +1,104 @@
import React, { useEffect, useRef, useState, useContext } from 'react'
import React, { useState, useEffect, useCallback } from 'react'
import { useSearchParams } from 'react-router-dom'
import axios from 'axios'
import {
Backdrop,
Box,
Button,
CircularProgress,
FormControl,
MenuItem,
Paper,
Select,
SelectChangeEvent,
Tab,
Tooltip
} from '@mui/material'
import { makeStyles } from '@mui/styles'
import Editor, { EditorDidMount } from 'react-monaco-editor'
import { useLocation } from 'react-router-dom'
import { TabContext, TabList, TabPanel } from '@mui/lab'
import CssBaseline from '@mui/material/CssBaseline'
import Box from '@mui/material/Box'
import { AppContext, RunTimeType } from '../../context/appContext'
import { TreeNode } from '../../utils/types'
const useStyles = makeStyles(() => ({
root: {
fontSize: '1rem',
color: 'gray',
'&.Mui-selected': {
color: 'black'
}
},
subMenu: {
marginTop: '25px',
display: 'flex',
justifyContent: 'center'
},
runButton: {
display: 'flex',
alignItems: 'center',
padding: '5px 5px',
minWidth: 'unset'
}
}))
import SideBar from './sideBar'
import SASjsEditor from './editor'
const Studio = () => {
const appContext = useContext(AppContext)
const location = useLocation()
const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('')
const [ctrlPressed, setCtrlPressed] = useState(false)
const [webout, setWebout] = useState('')
const [tab, setTab] = useState('1')
const [runTimes, setRunTimes] = useState<string[]>([])
const [selectedRunTime, setSelectedRunTime] = useState('')
const [isRunning, setIsRunning] = useState(false)
const [searchParams, setSearchParams] = useSearchParams()
const [selectedFilePath, setSelectedFilePath] = useState('')
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
const [tab, setTab] = useState('code')
useEffect(() => {
setRunTimes(Object.values(appContext.runTimes))
}, [appContext.runTimes])
setSelectedFilePath(searchParams.get('filePath') ?? '')
}, [searchParams])
useEffect(() => {
if (runTimes.length) setSelectedRunTime(runTimes[0])
}, [runTimes])
const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue)
}
const editorRef = useRef(null as any)
const handleEditorDidMount: EditorDidMount = (editor) => {
editor.focus()
editorRef.current = editor
}
const getSelection = () => {
const editor = editorRef.current as any
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
return selection ?? ''
}
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
const runCode = (code: string) => {
setIsRunning(true)
const fetchDirectoryData = useCallback(() => {
axios
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
.get(`/SASjsApi/drive/fileTree`)
.then((res: any) => {
const parsedLog = res?.data?.log
.map((logLine: any) => logLine.line)
.join('\n')
setLog(parsedLog)
setWebout(`${res.data?._webout}`)
setTab('2')
// Scroll to bottom of log
window.scrollTo(0, document.body.scrollHeight)
if (res.data && res.data?.status === 'success') {
setDirectoryData(res.data.tree)
}
})
.catch((err) => {
console.log(err)
})
.catch((err) => console.log(err))
.finally(() => setIsRunning(false))
}
const handleKeyDown = (event: any) => {
if (event.ctrlKey) {
if (event.key === 'v') {
setCtrlPressed(false)
}
if (event.key === 'Enter') runCode(getSelection() || fileContent)
if (!ctrlPressed) setCtrlPressed(true)
}
}
const handleKeyUp = (event: any) => {
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
}
const handleChangeRunTime = (event: SelectChangeEvent) => {
setSelectedRunTime(event.target.value as RunTimeType)
}
useEffect(() => {
const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content)
}, [])
useEffect(() => {
if (fileContent.length) {
localStorage.setItem('fileContent', fileContent)
fetchDirectoryData()
}, [fetchDirectoryData])
const handleSelect = (filePath: string, refreshSideBar?: boolean) => {
setSearchParams({ filePath })
if (refreshSideBar) fetchDirectoryData()
}
const removeFileFromTree = (path: string) => {
if (directoryData) {
const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode
findAndRemoveNode(newTree, newTree, path)
setDirectoryData(newTree)
}
}, [fileContent])
}
useEffect(() => {
const params = new URLSearchParams(location.search)
const programPath = params.get('_program')
const findAndRemoveNode = (
node: TreeNode,
parentNode: TreeNode,
path: string
) => {
if (node.relativePath === path) {
removeNodeFromParent(parentNode, path)
// reset selected file path and file path query param
if (
node.relativePath === selectedFilePath ||
selectedFilePath.startsWith(node.relativePath)
)
setSearchParams({})
return true
}
if (Array.isArray(node.children)) {
for (let i = 0; i < node.children.length; i++) {
if (findAndRemoveNode(node.children[i], node, path)) return
}
}
}
if (programPath?.length)
axios
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
.then((res: any) => setFileContent(res.data.fileContent))
.catch((err) => console.log(err))
}, [location.search])
const classes = useStyles()
const removeNodeFromParent = (parent: TreeNode, path: string) => {
const index = parent.children.findIndex(
(node) => node.relativePath === path
)
if (index !== -1) {
parent.children.splice(index, 1)
}
}
return (
<Box
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
>
<TabContext value={tab}>
<Box
sx={{
borderBottom: 1,
borderColor: 'divider'
}}
style={{ position: 'fixed', background: 'white', width: '100%' }}
>
<TabList onChange={handleTabChange} centered>
<Tab className={classes.root} label="Code" value="1" />
<Tab className={classes.root} label="Log" value="2" />
<Tooltip title="Displays content from the _webout fileref">
<Tab className={classes.root} label="Webout" value="3" />
</Tooltip>
</TabList>
</Box>
<TabPanel sx={{ paddingBottom: 0 }} value="1">
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={isRunning}
>
<CircularProgress color="inherit" />
</Backdrop>
<div className={classes.subMenu}>
<Tooltip title="CTRL+ENTER will also run SAS code">
<Button onClick={handleRunBtnClick} className={classes.runButton}>
<img
alt=""
draggable="false"
style={{ width: '25px' }}
src="/running-sas.png"
></img>
<span style={{ fontSize: '12px' }}>RUN</span>
</Button>
</Tooltip>
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
<FormControl variant="standard">
<Select
labelId="run-time-select-label"
id="run-time-select"
value={selectedRunTime}
onChange={handleChangeRunTime}
>
{runTimes.map((runTime) => (
<MenuItem key={runTime} value={runTime}>
{runTime}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
</div>
<Paper
sx={{
height: 'calc(100vh - 170px)',
padding: '10px',
overflow: 'auto',
position: 'relative'
}}
elevation={3}
>
<Editor
height="98%"
language="sas"
value={fileContent}
editorDidMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => {
if (val) setFileContent(val)
}}
/>
<p
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: -10,
textAlign: 'center',
fontSize: '13px'
}}
>
Press CTRL + ENTER to run SAS code
</p>
</Paper>
</TabPanel>
<TabPanel value="2">
<div style={{ marginTop: '50px' }}>
<h2>SAS Log</h2>
<pre>{log}</pre>
</div>
</TabPanel>
<TabPanel value="3">
<div style={{ marginTop: '50px' }}>
<pre>{webout}</pre>
</div>
</TabPanel>
</TabContext>
<Box sx={{ display: 'flex' }}>
<CssBaseline />
{tab === 'code' && (
<SideBar
selectedFilePath={selectedFilePath}
directoryData={directoryData}
handleSelect={handleSelect}
removeFileFromTree={removeFileFromTree}
refreshSideBar={fetchDirectoryData}
/>
)}
<SASjsEditor
selectedFilePath={selectedFilePath}
setSelectedFilePath={handleSelect}
tab={tab}
setTab={setTab}
/>
</Box>
)
}

View File

@@ -0,0 +1,267 @@
import React, { useState, useMemo } from 'react'
import axios from 'axios'
import {
Backdrop,
Box,
Paper,
CircularProgress,
Drawer,
Toolbar,
IconButton
} from '@mui/material'
import { FolderOpen } from '@mui/icons-material'
import TreeView from '../../components/tree'
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
import Modal from '../../components/modal'
import { TreeNode } from '../../utils/types'
const drawerWidth = '15%'
type Props = {
selectedFilePath: string
directoryData: TreeNode | null
handleSelect: (filePath: string) => void
removeFileFromTree: (filePath: string) => void
refreshSideBar: () => void
}
const SideBar = ({
selectedFilePath,
directoryData,
handleSelect,
removeFileFromTree,
refreshSideBar
}: Props) => {
const [isLoading, setIsLoading] = useState(false)
const [openModal, setOpenModal] = useState(false)
const [modalTitle, setModalTitle] = useState('')
const [modalPayload, setModalPayload] = useState('')
const [openSnackbar, setOpenSnackbar] = useState(false)
const [snackbarMessage, setSnackbarMessage] = useState('')
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
AlertSeverityType.Success
)
const [mobileOpen, setMobileOpen] = React.useState(false)
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen)
}
const handleFileSelect = (filePath: string) => {
setMobileOpen(false)
handleSelect(filePath)
}
const defaultExpanded = useMemo(() => {
const splittedPath = selectedFilePath.split('/')
const arr = ['']
let nodeId = ''
splittedPath.forEach((path) => {
if (path !== '') {
nodeId += '/' + path
arr.push(nodeId)
}
})
return arr
}, [selectedFilePath])
const deleteNode = (path: string, isFolder: boolean) => {
setIsLoading(true)
const axiosPromise = axios.delete(
`/SASjsApi/drive/${
isFolder ? `folder?_folderPath=${path}` : `file?_filePath=${path}`
}`
)
axiosPromise
.then(() => {
removeFileFromTree(path)
setSnackbarMessage('Deleted!')
setSnackbarSeverity(AlertSeverityType.Success)
setOpenSnackbar(true)
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => setIsLoading(false))
}
const addFile = (filePath: string) => {
const formData = new FormData()
const stringBlob = new Blob([''], { type: 'text/plain' })
formData.append('file', stringBlob)
formData.append('filePath', filePath)
setIsLoading(true)
axios
.post('/SASjsApi/drive/file', formData)
.then(() => {
setSnackbarMessage('File added!')
setSnackbarSeverity(AlertSeverityType.Success)
setOpenSnackbar(true)
refreshSideBar()
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => setIsLoading(false))
}
const addFolder = (folderPath: string) => {
setIsLoading(true)
axios
.post('/SASjsApi/drive/folder', { folderPath })
.then(() => {
setSnackbarMessage('Folder added!')
setSnackbarSeverity(AlertSeverityType.Success)
setOpenSnackbar(true)
refreshSideBar()
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => setIsLoading(false))
}
const rename = (oldPath: string, newPath: string) => {
setIsLoading(true)
axios
.post('/SASjsApi/drive/rename', { oldPath, newPath })
.then(() => {
setSnackbarMessage('Successfully Renamed')
setSnackbarSeverity(AlertSeverityType.Success)
setOpenSnackbar(true)
if (oldPath === selectedFilePath) handleSelect(newPath)
else if (selectedFilePath.startsWith(oldPath))
handleSelect(selectedFilePath.replace(oldPath, newPath))
refreshSideBar()
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => setIsLoading(false))
}
const drawer = (
<div>
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={isLoading}
>
<CircularProgress color="inherit" />
</Backdrop>
<Toolbar />
<Box sx={{ overflow: 'auto' }}>
{directoryData && (
<TreeView
node={directoryData}
selectedFilePath={selectedFilePath}
handleSelect={handleFileSelect}
deleteNode={deleteNode}
addFile={addFile}
addFolder={addFolder}
rename={rename}
defaultExpanded={defaultExpanded}
/>
)}
</Box>
<BootstrapSnackbar
open={openSnackbar}
setOpen={setOpenSnackbar}
message={snackbarMessage}
severity={snackbarSeverity}
/>
<Modal
open={openModal}
setOpen={setOpenModal}
title={modalTitle}
payload={modalPayload}
/>
</div>
)
return (
<>
<Box
component={Paper}
sx={{
margin: '5px',
height: '97vh',
paddingTop: '45px',
display: 'flex',
alignItems: 'flex-start'
}}
>
<IconButton
color="inherit"
size="large"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ left: '5px', display: { md: 'none' } }}
>
<FolderOpen />
</IconButton>
</Box>
<Drawer
variant="temporary"
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true // Better open performance on mobile.
}}
sx={{
display: { xs: 'block', md: 'none' },
flexShrink: 0,
[`& .MuiDrawer-paper`]: {
width: 240,
boxSizing: 'border-box'
}
}}
>
{drawer}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', md: 'block' },
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: {
width: drawerWidth,
boxSizing: 'border-box'
}
}}
>
{drawer}
</Drawer>
</>
)
}
export default SideBar

View File

@@ -80,7 +80,18 @@ const AppContextProvider = (props: { children: ReactNode }) => {
})
.catch(() => {
setLoggedIn(false)
axios.get('/') // get CSRF TOKEN
// get CSRF TOKEN and set cookie
axios
.get('/')
.then((res) => res.data)
.then((data: string) => {
const result =
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
data
)?.[1]
if (result) document.cookie = result
})
})
axios

View File

@@ -13,7 +13,7 @@ code {
}
.main {
margin-top: 50px;
margin: 50px 10px 0 10px;
display: flex;
flex-direction: column;
align-items: center;
@@ -25,3 +25,15 @@ code {
padding: '5px 10px';
margin-top: '10px';
}
.tree-item-label {
display: flex;
}
.tree-item-label.selected {
background: lightgoldenrodyellow;
}
.tree-item-label:hover {
background: lightgray;
}

59
web/src/utils/helper.ts Normal file
View File

@@ -0,0 +1,59 @@
import { PermissionResponse, RegisterPermissionPayload } from './types'
export const findExistingPermission = (
existingPermissions: PermissionResponse[],
newPermission: RegisterPermissionPayload
) => {
for (const permission of existingPermissions) {
if (
permission.user?.id === newPermission.principalId &&
hasSameCombination(permission, newPermission)
)
return permission
if (
permission.group?.groupId === newPermission.principalId &&
hasSameCombination(permission, newPermission)
)
return permission
}
return null
}
export const findUpdatingPermission = (
existingPermissions: PermissionResponse[],
newPermission: RegisterPermissionPayload
) => {
for (const permission of existingPermissions) {
if (
permission.user?.id === newPermission.principalId &&
hasDifferentSetting(permission, newPermission)
)
return permission
if (
permission.group?.groupId === newPermission.principalId &&
hasDifferentSetting(permission, newPermission)
)
return permission
}
return null
}
const hasSameCombination = (
existingPermission: PermissionResponse,
newPermission: RegisterPermissionPayload
) =>
existingPermission.path === newPermission.path &&
existingPermission.type === newPermission.type &&
existingPermission.setting === newPermission.setting
const hasDifferentSetting = (
existingPermission: PermissionResponse,
newPermission: RegisterPermissionPayload
) =>
existingPermission.path === newPermission.path &&
existingPermission.type === newPermission.type &&
existingPermission.setting !== newPermission.setting

View File

@@ -0,0 +1,2 @@
export * from './usePrompt'
export * from './useStateWithCallback'

View File

@@ -0,0 +1,36 @@
import { useEffect, useCallback, useContext } from 'react'
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom'
import { History, Blocker, Transition } from 'history'
const useBlocker = (blocker: Blocker, when = true) => {
const navigator = useContext(NavigationContext).navigator as History
useEffect(() => {
if (!when) return
const unblock = navigator.block((tx: Transition) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock()
tx.retry()
}
}
blocker(autoUnblockingTx)
})
return unblock
}, [navigator, blocker, when])
}
export const usePrompt = (message: string, when = true) => {
const blocker = useCallback(
(tx) => {
if (window.confirm(message)) tx.retry()
},
[message]
)
useBlocker(blocker, when)
}

View File

@@ -0,0 +1,27 @@
import { useState, useEffect, useRef } from 'react'
export const useStateWithCallback = <T>(
initialValue: T
): [T, (newValue: T, callback?: () => void) => void] => {
const callbackRef = useRef<any>(null)
const [value, setValue] = useState(initialValue)
useEffect(() => {
if (typeof callbackRef.current === 'function') {
callbackRef.current()
callbackRef.current = null
}
}, [value])
const setValueWithCallback = (newValue: T, callback?: () => void) => {
callbackRef.current = callback
setValue(newValue)
}
return [value, setValueWithCallback]
}
export default useStateWithCallback

View File

@@ -18,15 +18,24 @@ export interface GroupDetailsResponse extends GroupResponse {
export interface PermissionResponse {
permissionId: number
uri: string
path: string
type: string
setting: string
user?: UserResponse
group?: GroupDetailsResponse
}
export interface RegisterPermissionPayload {
uri: string
path: string
type: string
setting: string
principalType: string
principalId: number
}
export interface TreeNode {
name: string
relativePath: string
isFolder: boolean
children: Array<TreeNode>
}