mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e684b54a6 | ||
|
|
aafda2922b | ||
| 418bf41e38 | |||
| 81f0b03b09 | |||
| fe5ae44aab | |||
| 36be3a7d5e | |||
| 6434123401 | |||
|
|
0a6b972c65 | ||
|
|
be11707042 | ||
| 2412622367 | |||
|
|
de3a190a8d | ||
|
|
d5daafc6ed | ||
|
|
b1a2677b8c | ||
|
|
94072c3d24 | ||
|
|
b64c0c12da | ||
|
|
79bc7b0e28 | ||
|
|
fda0e0b57d | ||
|
|
14731e8824 | ||
|
|
258cc35f14 | ||
|
|
2295a518f0 | ||
|
|
1e5d621817 | ||
| 4d64420c45 | |||
|
|
799339de30 | ||
|
|
042ed41189 | ||
| 424f0fc1fa | |||
|
|
deafebde05 | ||
|
|
b66dc86b01 | ||
|
|
3bb05974d2 | ||
|
|
d1c1a59e91 | ||
|
|
668aff83fd | ||
| 3fc06b80fc | |||
| bbd7786c6c | |||
| 68f0c5c588 | |||
|
|
69ddf313b8 | ||
|
|
65e404cdbd | ||
| a14266077d | |||
|
|
fda6ad6356 | ||
|
|
fe3e5088f8 | ||
| f915c51b07 | |||
|
|
375f924f45 | ||
|
|
72329e30ed | ||
| 40f95f9072 | |||
|
|
58e8a869ef | ||
|
|
b558a3d01d | ||
| 249604384e | |||
|
|
056a436e10 | ||
|
|
06d59c618c | ||
|
|
a0e7875ae6 | ||
|
|
24966e695a | ||
|
|
5c40d8a342 | ||
| 6f5566dabb | |||
| d93470d183 | |||
| 330c020933 | |||
|
|
a810f6c7cf | ||
|
|
5d6c6086b4 | ||
|
|
0edcbdcefc | ||
|
|
ea0222f218 | ||
| edc2e2a302 | |||
|
|
efd2e1450e | ||
|
|
1092a73c10 | ||
| 9977c9d161 | |||
|
|
5c0eff5197 | ||
|
|
3bda991a58 | ||
| 0327f7c6ec | |||
| 92549402eb | |||
|
|
b88c911527 | ||
|
|
8b12f31060 | ||
|
|
e65cba9af0 | ||
| 0749d65173 | |||
|
|
a9c9b734f5 | ||
|
|
39da41c9f1 | ||
| 662b2ca36a | |||
| 16b7aa6abb | |||
| 4560ef942f | |||
| 06d3b17154 | |||
| d6651bbdbe | |||
| b9d032f148 | |||
|
|
70655e74d3 | ||
|
|
cb82fea0d8 | ||
| b9a596616d | |||
|
|
72a5393be3 | ||
|
|
769a840e9f | ||
| 730c7c52ac | |||
| ee2db276bb | |||
|
|
d0a24aacb6 | ||
|
|
57dfdf89a4 | ||
|
|
393b5eaf99 | ||
|
|
7477326b22 | ||
|
|
76bf84316e | ||
|
|
e355276e44 | ||
|
|
a3a9e3bd9f | ||
|
|
9f06080348 | ||
|
|
4bbf9cfdb3 | ||
|
|
e8e71fcde9 | ||
|
|
e63271a67a | ||
| 7633608318 | |||
| 572fe22d50 | |||
| 091268bf58 | |||
| 71a4a48443 | |||
| 3b188cd724 | |||
| eeba2328c0 | |||
| 0a0ba2cca5 |
165
CHANGELOG.md
165
CHANGELOG.md
@@ -1,3 +1,168 @@
|
||||
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* cli mock testing ([6434123](https://github.com/sasjs/server/commit/643412340162e854f31fba2f162d83b7ab1751d8))
|
||||
* mocking sas9 responses with JS STP ([36be3a7](https://github.com/sasjs/server/commit/36be3a7d5e7df79f9a1f3f00c3661b925f462383))
|
||||
|
||||
## [0.23.4](https://github.com/sasjs/server/compare/v0.23.3...v0.23.4) (2022-10-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add action to editor ref for running code ([2412622](https://github.com/sasjs/server/commit/2412622367eb46c40f388e988ae4606a7ec239b2))
|
||||
|
||||
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added domain for session cookies ([94072c3](https://github.com/sasjs/server/commit/94072c3d24a4d0d4c97900dc31bfbf1c9d2559b7))
|
||||
|
||||
## [0.23.2](https://github.com/sasjs/server/compare/v0.23.1...v0.23.2) (2022-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump in correct place ([14731e8](https://github.com/sasjs/server/commit/14731e8824fa9f3d1daf89fd62f9916d5e3fcae4))
|
||||
* bumping sasjs/score ([258cc35](https://github.com/sasjs/server/commit/258cc35f14cf50f2160f607000c60de27593fd79))
|
||||
* reverting commit ([fda0e0b](https://github.com/sasjs/server/commit/fda0e0b57d56e3b5231e626a8d933343ac0c5cdc))
|
||||
|
||||
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ldap issues ([4d64420](https://github.com/sasjs/server/commit/4d64420c45424134b4d2014a2d5dd6e846ed03b3))
|
||||
|
||||
# [0.23.0](https://github.com/sasjs/server/compare/v0.22.1...v0.23.0) (2022-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Enable SAS_PACKAGES in SASjs Server ([424f0fc](https://github.com/sasjs/server/commit/424f0fc1faec765eb7a14619584e649454105b70))
|
||||
|
||||
## [0.22.1](https://github.com/sasjs/server/compare/v0.22.0...v0.22.1) (2022-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* spelling issues ([3bb0597](https://github.com/sasjs/server/commit/3bb05974d216d69368f4498eb9f309bce7d97fd8))
|
||||
|
||||
# [0.22.0](https://github.com/sasjs/server/compare/v0.21.7...v0.22.0) (2022-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not throw error on deleting group when it is created by an external auth provider ([68f0c5c](https://github.com/sasjs/server/commit/68f0c5c5884431e7e8f586dccf98132abebb193e))
|
||||
* no need to restrict api endpoints when ldap auth is applied ([a142660](https://github.com/sasjs/server/commit/a14266077d3541c7a33b7635efa4208335e73519))
|
||||
* remove authProvider attribute from user and group payload interface ([bbd7786](https://github.com/sasjs/server/commit/bbd7786c6ce13b374d896a45c23255b8fa3e8bd2))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* implemented LDAP authentication ([f915c51](https://github.com/sasjs/server/commit/f915c51b077a2b8c4099727355ed914ecd6364bd))
|
||||
|
||||
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* csrf package is changed to pillarjs-csrf ([fe3e508](https://github.com/sasjs/server/commit/fe3e5088f8dfff50042ec8e8aac9ba5ba1394deb))
|
||||
|
||||
## [0.21.6](https://github.com/sasjs/server/compare/v0.21.5...v0.21.6) (2022-09-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* in getTokensFromDB handle the scenario when tokens are expired ([40f95f9](https://github.com/sasjs/server/commit/40f95f9072c8685910138d88fd2410f8704fc975))
|
||||
|
||||
## [0.21.5](https://github.com/sasjs/server/compare/v0.21.4...v0.21.5) (2022-09-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* made files extensions case insensitive ([2496043](https://github.com/sasjs/server/commit/249604384e42be4c12c88c70a7dff90fc1917a8f))
|
||||
|
||||
## [0.21.4](https://github.com/sasjs/server/compare/v0.21.3...v0.21.4) (2022-09-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removing single quotes from _program value ([a0e7875](https://github.com/sasjs/server/commit/a0e7875ae61cbb6e7d3995d2e36e7300b0daec86))
|
||||
|
||||
## [0.21.3](https://github.com/sasjs/server/compare/v0.21.2...v0.21.3) (2022-09-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return same tokens if not expired ([330c020](https://github.com/sasjs/server/commit/330c020933f1080261b38f07d6b627f6d7c62446))
|
||||
|
||||
## [0.21.2](https://github.com/sasjs/server/compare/v0.21.1...v0.21.2) (2022-09-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default content-type for sas programs should be text/plain ([9977c9d](https://github.com/sasjs/server/commit/9977c9d161947b11d45ab2513f99a5320a3f5a06))
|
||||
* **studio:** inject program path to code before sending for execution ([edc2e2a](https://github.com/sasjs/server/commit/edc2e2a302ccea4985f3d6b83ef8c23620ab82b6))
|
||||
|
||||
## [0.21.1](https://github.com/sasjs/server/compare/v0.21.0...v0.21.1) (2022-09-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SASJS_WEBOUT_HEADERS path for windows ([0749d65](https://github.com/sasjs/server/commit/0749d65173e8cfe9a93464711b7be1e123c289ff))
|
||||
|
||||
# [0.21.0](https://github.com/sasjs/server/compare/v0.20.0...v0.21.0) (2022-09-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* sas9 mocker improved - public access denied scenario ([06d3b17](https://github.com/sasjs/server/commit/06d3b1715432ea245ee755ae1dfd0579d3eb30e9))
|
||||
|
||||
# [0.20.0](https://github.com/sasjs/server/compare/v0.19.0...v0.20.0) (2022-09-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for R stored programs ([d6651bb](https://github.com/sasjs/server/commit/d6651bbdbeee5067f53c36e69a0eefa973c523b6))
|
||||
|
||||
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added mocking endpoints ([0a0ba2c](https://github.com/sasjs/server/commit/0a0ba2cca5db867de46fb2486d856a84ec68d3b4))
|
||||
|
||||
# [0.18.0](https://github.com/sasjs/server/compare/v0.17.5...v0.18.0) (2022-09-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add option for program launch in context menu ([ee2db27](https://github.com/sasjs/server/commit/ee2db276bb0bbd522f758e0b66f7e7b2f4afd9d5))
|
||||
|
||||
## [0.17.5](https://github.com/sasjs/server/compare/v0.17.4...v0.17.5) (2022-09-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SASINITIALFOLDER split over 2 params, closes [#271](https://github.com/sasjs/server/issues/271) ([393b5ea](https://github.com/sasjs/server/commit/393b5eaf990049c39eecf2b9e8dd21a001b6e298))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
31
README.md
31
README.md
@@ -66,14 +66,14 @@ 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
|
||||
# Possible options at the moment are sas, js, py and r
|
||||
|
||||
# This string sets the priority of the available analytic runtimes
|
||||
# Valid runtimes are SAS (sas), JavaScript (js) and Python (py)
|
||||
# Valid runtimes are SAS (sas), JavaScript (js), Python (py) and R (r)
|
||||
# For each option provided, there should be a corresponding path,
|
||||
# eg SAS_PATH, NODE_PATH or PYTHON_PATH
|
||||
# eg SAS_PATH, NODE_PATH, PYTHON_PATH or RSCRIPT_PATH
|
||||
# Priority is given to runtimes earlier in the string
|
||||
# Example options: [sas,js,py | js,py | sas | sas,js]
|
||||
# Example options: [sas,js,py | js,py | sas | sas,js | r | sas,r]
|
||||
RUN_TIMES=
|
||||
|
||||
# Path to SAS executable (sas.exe / sas.sh)
|
||||
@@ -85,6 +85,9 @@ NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||
# Path to Python executable
|
||||
PYTHON_PATH=/usr/bin/python
|
||||
|
||||
# Path to R executable
|
||||
R_PATH=/usr/bin/Rscript
|
||||
|
||||
# Path to working directory
|
||||
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
||||
SASJS_ROOT=./sasjs_root
|
||||
@@ -96,6 +99,14 @@ PROTOCOL=
|
||||
# default: 5000
|
||||
PORT=
|
||||
|
||||
# options: [sas9|sasviya]
|
||||
# If not present, mocking function is disabled
|
||||
MOCK_SERVERTYPE=
|
||||
|
||||
# default: /api/mocks
|
||||
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
|
||||
# Server will automatically use subdirectory accordingly
|
||||
STATIC_MOCK_LOCATION=
|
||||
|
||||
#
|
||||
## Additional SAS Options
|
||||
@@ -119,9 +130,19 @@ PRIVATE_KEY=privkey.pem (required)
|
||||
CERT_CHAIN=certificate.pem (required)
|
||||
CA_ROOT=fullchain.pem (optional)
|
||||
|
||||
# ENV variables required for MODE: `server`
|
||||
## ENV variables required for MODE: `server`
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
# AUTH_PROVIDERS options: [ldap] default: ``
|
||||
AUTH_PROVIDERS=
|
||||
|
||||
## ENV variables required for AUTH_MECHANISM: `ldap`
|
||||
LDAP_URL= <LDAP_SERVER_URL>
|
||||
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
|
||||
LDAP_BIND_PASSWORD = <password>
|
||||
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
||||
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
||||
|
||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
||||
CORS=
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
MODE=[desktop|server] default considered as desktop
|
||||
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
|
||||
ALLOWED_DOMAIN=<just domain e.g. example.com >
|
||||
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
||||
|
||||
PROTOCOL=[http|https] default considered as http
|
||||
@@ -14,10 +15,19 @@ 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
|
||||
|
||||
AUTH_PROVIDERS=[ldap]
|
||||
|
||||
LDAP_URL= <LDAP_SERVER_URL>
|
||||
LDAP_BIND_DN= <cn=admin,ou=system,dc=cloudron>
|
||||
LDAP_BIND_PASSWORD = <password>
|
||||
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
||||
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
||||
|
||||
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
|
||||
R_PATH=/usr/bin/Rscript
|
||||
|
||||
SASJS_ROOT=./sasjs_root
|
||||
|
||||
|
||||
1
api/mocks/sas9/generic/logged-in
Normal file
1
api/mocks/sas9/generic/logged-in
Normal file
@@ -0,0 +1 @@
|
||||
You have signed in.
|
||||
1
api/mocks/sas9/generic/logged-out
Normal file
1
api/mocks/sas9/generic/logged-out
Normal file
@@ -0,0 +1 @@
|
||||
You have signed out.
|
||||
30
api/mocks/sas9/generic/login
Normal file
30
api/mocks/sas9/generic/login
Normal file
@@ -0,0 +1,30 @@
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" dir="ltr" class="bg">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1" />
|
||||
</head>
|
||||
|
||||
|
||||
<div class="content">
|
||||
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
|
||||
<!--form container-->
|
||||
<input type="hidden" name="lt" value="validtoken" aria-hidden="true" />
|
||||
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
|
||||
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
|
||||
|
||||
<span class="userid">
|
||||
|
||||
<input id="username" name="username" tabindex="3" aria-labelledby="username1 message1 message2 message3" name="username" placeholder="User ID" type="text" autofocus="true" value="" maxlength="500" autocomplete="off" />
|
||||
</span>
|
||||
<span class="password">
|
||||
|
||||
<input id="password" name="password" tabindex="4" name="password" placeholder="Password" type="password" value="" maxlength="500" autocomplete="off" />
|
||||
</span>
|
||||
|
||||
<button type="submit" class="btn-submit" title="Sign In" tabindex="5" onClick="this.disabled=true;setSubmitUrl(this.form);this.form.submit();return false;">Sign In</button>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</html>
|
||||
1
api/mocks/sas9/generic/public-access-denied
Normal file
1
api/mocks/sas9/generic/public-access-denied
Normal file
@@ -0,0 +1 @@
|
||||
Public access has been denied.
|
||||
1
api/mocks/sas9/generic/sas-stored-process
Normal file
1
api/mocks/sas9/generic/sas-stored-process
Normal file
@@ -0,0 +1 @@
|
||||
"title": "Log Off SAS Demo User"
|
||||
495
api/package-lock.json
generated
495
api/package-lock.json
generated
@@ -8,18 +8,18 @@
|
||||
"name": "api",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "^4.31.3",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"@sasjs/core": "^4.40.1",
|
||||
"@sasjs/utils": "2.48.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"csurf": "^1.11.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"helmet": "^5.0.2",
|
||||
"joi": "^17.4.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "2.3.3",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
@@ -37,11 +37,11 @@
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/csurf": "^1.11.2",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/ldapjs": "^2.2.4",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/multer": "^1.4.7",
|
||||
@@ -50,10 +50,13 @@
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@types/unzipper": "^0.10.5",
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "0.27.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"http-headers-validation": "^0.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "^8.0.0",
|
||||
"nodejs-file-downloader": "4.10.2",
|
||||
"nodemon": "^2.0.7",
|
||||
"pkg": "5.6.0",
|
||||
"prettier": "^2.3.1",
|
||||
@@ -1391,14 +1394,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "4.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.31.3.tgz",
|
||||
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA=="
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.40.1.tgz",
|
||||
"integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
|
||||
},
|
||||
"node_modules/@sasjs/utils": {
|
||||
"version": "2.42.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
|
||||
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
|
||||
"version": "2.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz",
|
||||
"integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "9.0.13",
|
||||
@@ -1833,15 +1836,6 @@
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/csurf": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
|
||||
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/express-serve-static-core": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
|
||||
@@ -2044,6 +2038,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ldapjs": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.4.tgz",
|
||||
"integrity": "sha512-+ZMVolW4N1zpnQ4SgH8nfpIFuiDOfbnXSbwQoBiLaq8mF0vo8FOKotQzKkfoWxbV0lWU1d4V+keZZ07klyOSng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
|
||||
@@ -2219,6 +2222,11 @@
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/abstract-logging": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
||||
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
@@ -2474,6 +2482,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
@@ -2485,6 +2501,14 @@
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
@@ -2517,6 +2541,30 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "27.0.6",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.0.6.tgz",
|
||||
@@ -2646,6 +2694,17 @@
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/backoff": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
|
||||
"integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==",
|
||||
"dependencies": {
|
||||
"precond": "0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -3336,6 +3395,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
|
||||
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"rndm": "1.2.0",
|
||||
"tsscmp": "1.0.6",
|
||||
@@ -3369,40 +3429,6 @@
|
||||
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/csurf": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
|
||||
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
|
||||
"dependencies": {
|
||||
"cookie": "0.4.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"csrf": "3.1.0",
|
||||
"http-errors": "~1.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csurf/node_modules/http-errors": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
|
||||
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
|
||||
"dependencies": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/csurf/node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/csv-stringify": {
|
||||
"version": "5.6.5",
|
||||
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
||||
@@ -4049,6 +4075,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/extsprintf": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
|
||||
"integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
]
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||
@@ -4177,6 +4211,26 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||
@@ -6761,6 +6815,35 @@
|
||||
"node": ">8"
|
||||
}
|
||||
},
|
||||
"node_modules/ldap-filter": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
|
||||
"integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ldapjs": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz",
|
||||
"integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==",
|
||||
"dependencies": {
|
||||
"abstract-logging": "^2.0.0",
|
||||
"asn1": "^0.2.4",
|
||||
"assert-plus": "^1.0.0",
|
||||
"backoff": "^2.5.0",
|
||||
"ldap-filter": "^0.3.3",
|
||||
"once": "^1.4.0",
|
||||
"vasync": "^2.2.0",
|
||||
"verror": "^1.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@@ -7482,6 +7565,18 @@
|
||||
"integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nodejs-file-downloader": {
|
||||
"version": "4.10.2",
|
||||
"resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.10.2.tgz",
|
||||
"integrity": "sha512-pTVlytER/4wxcIpEhLXoqhuJ7WH1+xSFNLbI0wPmbwH3pWlJRRebb1Kbu91mz1CyOJmO4sj6YLH1wkF1B6efrQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.1",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"sanitize-filename": "^1.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.19.tgz",
|
||||
@@ -8050,6 +8145,14 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/precond": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
|
||||
"integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
||||
@@ -8377,7 +8480,8 @@
|
||||
"node_modules/rndm": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
|
||||
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
|
||||
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/rotating-file-stream": {
|
||||
"version": "3.0.4",
|
||||
@@ -8423,6 +8527,15 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/sanitize-filename": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
|
||||
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"truncate-utf8-bytes": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/saslprep": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
|
||||
@@ -9277,6 +9390,15 @@
|
||||
"resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
|
||||
"integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE="
|
||||
},
|
||||
"node_modules/truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
"integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-jest": {
|
||||
"version": "27.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.3.tgz",
|
||||
@@ -9389,6 +9511,7 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
|
||||
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.6.x"
|
||||
}
|
||||
@@ -9579,6 +9702,12 @@
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
||||
},
|
||||
"node_modules/utf8-byte-length": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
|
||||
"integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -9646,6 +9775,43 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vasync": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz",
|
||||
"integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"dependencies": {
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vasync/node_modules/verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/verror": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
|
||||
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-hr-time": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
|
||||
@@ -10969,14 +11135,14 @@
|
||||
}
|
||||
},
|
||||
"@sasjs/core": {
|
||||
"version": "4.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.31.3.tgz",
|
||||
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA=="
|
||||
"version": "4.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.40.1.tgz",
|
||||
"integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
|
||||
},
|
||||
"@sasjs/utils": {
|
||||
"version": "2.42.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
|
||||
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
|
||||
"version": "2.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz",
|
||||
"integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==",
|
||||
"requires": {
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/prompts": "2.0.13",
|
||||
@@ -11361,15 +11527,6 @@
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/csurf": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
|
||||
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express-serve-static-core": "*"
|
||||
}
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
|
||||
@@ -11547,6 +11704,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/ldapjs": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.4.tgz",
|
||||
"integrity": "sha512-+ZMVolW4N1zpnQ4SgH8nfpIFuiDOfbnXSbwQoBiLaq8mF0vo8FOKotQzKkfoWxbV0lWU1d4V+keZZ07klyOSng==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
|
||||
@@ -11721,6 +11887,11 @@
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"abstract-logging": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
||||
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
@@ -11919,6 +12090,14 @@
|
||||
"is-string": "^1.0.7"
|
||||
}
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"requires": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"asn1.js": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
@@ -11930,6 +12109,11 @@
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
@@ -11959,6 +12143,29 @@
|
||||
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"babel-jest": {
|
||||
"version": "27.0.6",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.0.6.tgz",
|
||||
@@ -12057,6 +12264,14 @@
|
||||
"babel-preset-current-node-syntax": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"backoff": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
|
||||
"integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==",
|
||||
"requires": {
|
||||
"precond": "0.2"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -12584,6 +12799,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
|
||||
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"rndm": "1.2.0",
|
||||
"tsscmp": "1.0.6",
|
||||
@@ -12613,36 +12829,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"csurf": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
|
||||
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
|
||||
"requires": {
|
||||
"cookie": "0.4.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"csrf": "3.1.0",
|
||||
"http-errors": "~1.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"http-errors": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
|
||||
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
|
||||
"requires": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"csv-stringify": {
|
||||
"version": "5.6.5",
|
||||
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
||||
@@ -13136,6 +13322,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
|
||||
"integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||
@@ -13246,6 +13437,12 @@
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"dev": true
|
||||
},
|
||||
"form-data": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||
@@ -15176,6 +15373,29 @@
|
||||
"asn1.js": "^5.4.1"
|
||||
}
|
||||
},
|
||||
"ldap-filter": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
|
||||
"integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"ldapjs": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz",
|
||||
"integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==",
|
||||
"requires": {
|
||||
"abstract-logging": "^2.0.0",
|
||||
"asn1": "^0.2.4",
|
||||
"assert-plus": "^1.0.0",
|
||||
"backoff": "^2.5.0",
|
||||
"ldap-filter": "^0.3.3",
|
||||
"once": "^1.4.0",
|
||||
"vasync": "^2.2.0",
|
||||
"verror": "^1.8.1"
|
||||
}
|
||||
},
|
||||
"leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
@@ -15733,6 +15953,18 @@
|
||||
"integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==",
|
||||
"dev": true
|
||||
},
|
||||
"nodejs-file-downloader": {
|
||||
"version": "4.10.2",
|
||||
"resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.10.2.tgz",
|
||||
"integrity": "sha512-pTVlytER/4wxcIpEhLXoqhuJ7WH1+xSFNLbI0wPmbwH3pWlJRRebb1Kbu91mz1CyOJmO4sj6YLH1wkF1B6efrQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.1",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"sanitize-filename": "^1.6.3"
|
||||
}
|
||||
},
|
||||
"nodemon": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.19.tgz",
|
||||
@@ -16147,6 +16379,11 @@
|
||||
"tunnel-agent": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"precond": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
|
||||
"integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ=="
|
||||
},
|
||||
"prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
||||
@@ -16379,7 +16616,8 @@
|
||||
"rndm": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
|
||||
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
|
||||
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=",
|
||||
"dev": true
|
||||
},
|
||||
"rotating-file-stream": {
|
||||
"version": "3.0.4",
|
||||
@@ -16405,6 +16643,15 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sanitize-filename": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
|
||||
"integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"truncate-utf8-bytes": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"saslprep": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
|
||||
@@ -17068,6 +17315,15 @@
|
||||
"resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
|
||||
"integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE="
|
||||
},
|
||||
"truncate-utf8-bytes": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||
"integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"ts-jest": {
|
||||
"version": "27.0.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.3.tgz",
|
||||
@@ -17134,7 +17390,8 @@
|
||||
"tsscmp": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
|
||||
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="
|
||||
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
|
||||
"dev": true
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
@@ -17289,6 +17546,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"utf8-byte-length": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
|
||||
"integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==",
|
||||
"dev": true
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -17340,6 +17603,36 @@
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||
},
|
||||
"vasync": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz",
|
||||
"integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
|
||||
"requires": {
|
||||
"verror": "1.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
|
||||
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
|
||||
"requires": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"w3c-hr-time": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Api of SASjs server",
|
||||
"main": "./src/server.ts",
|
||||
"scripts": {
|
||||
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
||||
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore && npm run downloadMacros",
|
||||
"prestart": "npm run initial",
|
||||
"prebuild": "npm run initial",
|
||||
"start": "NODE_ENV=development nodemon ./src/server.ts",
|
||||
@@ -17,20 +17,21 @@
|
||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"exe": "npm run build && pkg .",
|
||||
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
||||
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sas:copy && npm run web:copy",
|
||||
"public:copy": "cp -r ./public/ ./build/public/",
|
||||
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
||||
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
|
||||
"sas:copy": "cp -r ./sas/ ./build/sas/",
|
||||
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
|
||||
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
|
||||
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
|
||||
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts",
|
||||
"downloadMacros": "ts-node ./scripts/downloadMacros.ts"
|
||||
},
|
||||
"bin": "./build/src/server.js",
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"./build/public/**/*",
|
||||
"./build/sasjsbuild/**/*",
|
||||
"./build/sasjscore/**/*",
|
||||
"./build/sas/**/*",
|
||||
"./web/build/**/*"
|
||||
],
|
||||
"targets": [
|
||||
@@ -47,18 +48,18 @@
|
||||
},
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "^4.31.3",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"@sasjs/core": "^4.40.1",
|
||||
"@sasjs/utils": "2.48.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"csurf": "^1.11.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"helmet": "^5.0.2",
|
||||
"joi": "^17.4.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "2.3.3",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
@@ -73,11 +74,11 @@
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/csurf": "^1.11.2",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/ldapjs": "^2.2.4",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/multer": "^1.4.7",
|
||||
@@ -86,10 +87,13 @@
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@types/unzipper": "^0.10.5",
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "0.27.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"http-headers-validation": "^0.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "^8.0.0",
|
||||
"nodejs-file-downloader": "4.10.2",
|
||||
"nodemon": "^2.0.7",
|
||||
"pkg": "5.6.0",
|
||||
"prettier": "^2.3.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
39
api/scripts/downloadMacros.ts
Normal file
39
api/scripts/downloadMacros.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import axios from 'axios'
|
||||
import Downloader from 'nodejs-file-downloader'
|
||||
import { createFile, listFilesInFolder } from '@sasjs/utils'
|
||||
|
||||
import { sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils/file'
|
||||
|
||||
export const downloadMacros = async () => {
|
||||
const url =
|
||||
'https://api.github.com/repos/yabwon/SAS_PACKAGES/contents/SPF/Macros'
|
||||
|
||||
console.info(`Downloading macros from ${url}`)
|
||||
|
||||
await axios
|
||||
.get(url)
|
||||
.then(async (res) => {
|
||||
await downloadFiles(res.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw new Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
const downloadFiles = async function (fileList: any) {
|
||||
for (const file of fileList) {
|
||||
const downloader = new Downloader({
|
||||
url: file.download_url,
|
||||
directory: sasJSCoreMacros,
|
||||
fileName: file.path.replace(/^SPF\/Macros/, ''),
|
||||
cloneFiles: false
|
||||
})
|
||||
await downloader.download()
|
||||
}
|
||||
|
||||
const fileNames = await listFilesInFolder(sasJSCoreMacros)
|
||||
|
||||
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
|
||||
}
|
||||
|
||||
downloadMacros()
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Express } from 'express'
|
||||
import { Express, CookieOptions } from 'express'
|
||||
import mongoose from 'mongoose'
|
||||
import session from 'express-session'
|
||||
import MongoStore from 'connect-mongo'
|
||||
|
||||
import { ModeType } from '../utils'
|
||||
import { cookieOptions } from '../app'
|
||||
import { ModeType, ProtocolType } from '../utils'
|
||||
|
||||
export const configureExpressSession = (app: Express) => {
|
||||
const { MODE } = process.env
|
||||
@@ -19,6 +18,15 @@ export const configureExpressSession = (app: Express) => {
|
||||
})
|
||||
}
|
||||
|
||||
const { PROTOCOL, ALLOWED_DOMAIN } = process.env
|
||||
const cookieOptions: CookieOptions = {
|
||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||
httpOnly: true,
|
||||
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
domain: ALLOWED_DOMAIN?.trim() || undefined
|
||||
}
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: process.secrets.SESSION_SECRET,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from 'path'
|
||||
import express, { ErrorRequestHandler } from 'express'
|
||||
import csrf, { CookieOptions } from 'csurf'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
getWebBuildFolder,
|
||||
instantiateLogger,
|
||||
loadAppStreamConfig,
|
||||
ProtocolType,
|
||||
ReturnCode,
|
||||
setProcessVariables,
|
||||
setupFolders,
|
||||
@@ -30,24 +28,7 @@ if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
||||
|
||||
const app = express()
|
||||
|
||||
const { PROTOCOL } = process.env
|
||||
|
||||
export const cookieOptions: CookieOptions = {
|
||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||
httpOnly: true,
|
||||
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
|
||||
/***********************************
|
||||
* CSRF Protection *
|
||||
***********************************/
|
||||
export const csrfProtection = csrf({ cookie: cookieOptions })
|
||||
|
||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
if (err.code === 'EBADCSRFTOKEN')
|
||||
return res.status(400).send('Invalid CSRF token!')
|
||||
|
||||
console.error(err.stack)
|
||||
res.status(500).send('Something broke!')
|
||||
}
|
||||
@@ -77,6 +58,10 @@ export default setProcessVariables().then(async () => {
|
||||
app.use(express.json({ limit: '100mb' }))
|
||||
app.use(express.static(path.join(__dirname, '../public')))
|
||||
|
||||
// Body parser is used for decoding the formdata on POST request.
|
||||
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
await setupFolders()
|
||||
await copySASjsCore()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { InfoJWT } from '../types'
|
||||
import {
|
||||
generateAccessToken,
|
||||
generateRefreshToken,
|
||||
getTokensFromDB,
|
||||
removeTokensInDB,
|
||||
saveTokensInDB
|
||||
} from '../utils'
|
||||
@@ -73,6 +74,15 @@ const token = async (data: any): Promise<TokenResponse> => {
|
||||
|
||||
AuthController.deleteCode(userInfo.userId, clientId)
|
||||
|
||||
// get tokens from DB
|
||||
const existingTokens = await getTokensFromDB(userInfo.userId, clientId)
|
||||
if (existingTokens) {
|
||||
return {
|
||||
accessToken: existingTokens.accessToken,
|
||||
refreshToken: existingTokens.refreshToken
|
||||
}
|
||||
}
|
||||
|
||||
const accessToken = generateAccessToken(userInfo)
|
||||
const refreshToken = generateRefreshToken(userInfo)
|
||||
|
||||
|
||||
185
api/src/controllers/authConfig.ts
Normal file
185
api/src/controllers/authConfig.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import express from 'express'
|
||||
import { Security, Route, Tags, Get, Post, Example } from 'tsoa'
|
||||
|
||||
import { LDAPClient, LDAPUser, LDAPGroup, AuthProviderType } from '../utils'
|
||||
import { randomBytes } from 'crypto'
|
||||
import User from '../model/User'
|
||||
import Group from '../model/Group'
|
||||
import Permission from '../model/Permission'
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/authConfig')
|
||||
@Tags('Auth_Config')
|
||||
export class AuthConfigController {
|
||||
/**
|
||||
* @summary Gives the detail of Auth Mechanism.
|
||||
*
|
||||
*/
|
||||
@Example({
|
||||
ldap: {
|
||||
LDAP_URL: 'ldaps://my.ldap.server:636',
|
||||
LDAP_BIND_DN: 'cn=admin,ou=system,dc=cloudron',
|
||||
LDAP_BIND_PASSWORD: 'secret',
|
||||
LDAP_USERS_BASE_DN: 'ou=users,dc=cloudron',
|
||||
LDAP_GROUPS_BASE_DN: 'ou=groups,dc=cloudron'
|
||||
}
|
||||
})
|
||||
@Get('/')
|
||||
public getDetail() {
|
||||
return getAuthConfigDetail()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.
|
||||
*
|
||||
*/
|
||||
@Example({
|
||||
users: 5,
|
||||
groups: 3
|
||||
})
|
||||
@Post('/synchroniseWithLDAP')
|
||||
public async synchroniseWithLDAP() {
|
||||
return synchroniseWithLDAP()
|
||||
}
|
||||
}
|
||||
|
||||
const synchroniseWithLDAP = async () => {
|
||||
process.logger.info('Syncing LDAP with internal DB')
|
||||
|
||||
const permissions = await Permission.get({})
|
||||
await Permission.deleteMany()
|
||||
await User.deleteMany({ authProvider: AuthProviderType.LDAP })
|
||||
await Group.deleteMany({ authProvider: AuthProviderType.LDAP })
|
||||
|
||||
const ldapClient = await LDAPClient.init()
|
||||
|
||||
process.logger.info('fetching LDAP users')
|
||||
const users = await ldapClient.getAllLDAPUsers()
|
||||
|
||||
process.logger.info('inserting LDAP users to DB')
|
||||
|
||||
const existingUsers: string[] = []
|
||||
const importedUsers: LDAPUser[] = []
|
||||
|
||||
for (const user of users) {
|
||||
const usernameExists = await User.findOne({ username: user.username })
|
||||
if (usernameExists) {
|
||||
existingUsers.push(user.username)
|
||||
continue
|
||||
}
|
||||
|
||||
const hashPassword = User.hashPassword(randomBytes(64).toString('hex'))
|
||||
|
||||
await User.create({
|
||||
displayName: user.displayName,
|
||||
username: user.username,
|
||||
password: hashPassword,
|
||||
authProvider: AuthProviderType.LDAP
|
||||
})
|
||||
|
||||
importedUsers.push(user)
|
||||
}
|
||||
|
||||
if (existingUsers.length > 0) {
|
||||
process.logger.info(
|
||||
'Failed to insert following users as they already exist in DB:'
|
||||
)
|
||||
existingUsers.forEach((user) => process.logger.log(`* ${user}`))
|
||||
}
|
||||
|
||||
process.logger.info('fetching LDAP groups')
|
||||
const groups = await ldapClient.getAllLDAPGroups()
|
||||
|
||||
process.logger.info('inserting LDAP groups to DB')
|
||||
|
||||
const existingGroups: string[] = []
|
||||
const importedGroups: LDAPGroup[] = []
|
||||
|
||||
for (const group of groups) {
|
||||
const groupExists = await Group.findOne({ name: group.name })
|
||||
if (groupExists) {
|
||||
existingGroups.push(group.name)
|
||||
continue
|
||||
}
|
||||
|
||||
await Group.create({
|
||||
name: group.name,
|
||||
authProvider: AuthProviderType.LDAP
|
||||
})
|
||||
|
||||
importedGroups.push(group)
|
||||
}
|
||||
|
||||
if (existingGroups.length > 0) {
|
||||
process.logger.info(
|
||||
'Failed to insert following groups as they already exist in DB:'
|
||||
)
|
||||
existingGroups.forEach((group) => process.logger.log(`* ${group}`))
|
||||
}
|
||||
|
||||
process.logger.info('associating users and groups')
|
||||
|
||||
for (const group of importedGroups) {
|
||||
const dbGroup = await Group.findOne({ name: group.name })
|
||||
if (dbGroup) {
|
||||
for (const member of group.members) {
|
||||
const user = importedUsers.find((user) => user.uid === member)
|
||||
if (user) {
|
||||
const dbUser = await User.findOne({ username: user.username })
|
||||
if (dbUser) await dbGroup.addUser(dbUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.logger.info('setting permissions')
|
||||
|
||||
for (const permission of permissions) {
|
||||
const newPermission = new Permission({
|
||||
path: permission.path,
|
||||
type: permission.type,
|
||||
setting: permission.setting
|
||||
})
|
||||
|
||||
if (permission.user) {
|
||||
const dbUser = await User.findOne({ username: permission.user.username })
|
||||
if (dbUser) newPermission.user = dbUser._id
|
||||
} else if (permission.group) {
|
||||
const dbGroup = await Group.findOne({ name: permission.group.name })
|
||||
if (dbGroup) newPermission.group = dbGroup._id
|
||||
}
|
||||
await newPermission.save()
|
||||
}
|
||||
|
||||
process.logger.info('LDAP synchronization completed!')
|
||||
|
||||
return {
|
||||
userCount: importedUsers.length,
|
||||
groupCount: importedGroups.length
|
||||
}
|
||||
}
|
||||
|
||||
const getAuthConfigDetail = () => {
|
||||
const { AUTH_PROVIDERS } = process.env
|
||||
|
||||
const returnObj: any = {}
|
||||
|
||||
if (AUTH_PROVIDERS === AuthProviderType.LDAP) {
|
||||
const {
|
||||
LDAP_URL,
|
||||
LDAP_BIND_DN,
|
||||
LDAP_BIND_PASSWORD,
|
||||
LDAP_USERS_BASE_DN,
|
||||
LDAP_GROUPS_BASE_DN
|
||||
} = process.env
|
||||
|
||||
returnObj.ldap = {
|
||||
LDAP_URL: LDAP_URL ?? '',
|
||||
LDAP_BIND_DN: LDAP_BIND_DN ?? '',
|
||||
LDAP_BIND_PASSWORD: LDAP_BIND_PASSWORD ?? '',
|
||||
LDAP_USERS_BASE_DN: LDAP_USERS_BASE_DN ?? '',
|
||||
LDAP_GROUPS_BASE_DN: LDAP_GROUPS_BASE_DN ?? ''
|
||||
}
|
||||
}
|
||||
return returnObj
|
||||
}
|
||||
@@ -24,11 +24,11 @@ interface ExecuteCodePayload {
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/code')
|
||||
@Tags('CODE')
|
||||
@Tags('Code')
|
||||
export class CodeController {
|
||||
/**
|
||||
* Execute SAS code.
|
||||
* @summary Run SAS Code and returns log
|
||||
* Execute Code on the Specified Runtime
|
||||
* @summary Run Code and Return Webout Content and Log
|
||||
*/
|
||||
@Post('/execute')
|
||||
public async executeCode(
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
|
||||
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
|
||||
import User from '../model/User'
|
||||
import { AuthProviderType } from '../utils'
|
||||
import { UserResponse } from './user'
|
||||
|
||||
export interface GroupResponse {
|
||||
@@ -147,12 +148,14 @@ export class GroupController {
|
||||
@Delete('{groupId}')
|
||||
public async deleteGroup(@Path() groupId: number) {
|
||||
const group = await Group.findOne({ groupId })
|
||||
if (group) return await group.remove()
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'Group not found.'
|
||||
}
|
||||
if (!group)
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'Group not found.'
|
||||
}
|
||||
|
||||
return await group.remove()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +251,13 @@ const updateUsersListInGroup = async (
|
||||
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||
}
|
||||
|
||||
if (group.authProvider)
|
||||
throw {
|
||||
code: 405,
|
||||
status: 'Method Not Allowed',
|
||||
message: `Can't add/remove user to group created by external auth provider.`
|
||||
}
|
||||
|
||||
const user = await User.findOne({ id: userId })
|
||||
if (!user)
|
||||
throw {
|
||||
@@ -256,6 +266,13 @@ const updateUsersListInGroup = async (
|
||||
message: 'User not found.'
|
||||
}
|
||||
|
||||
if (user.authProvider)
|
||||
throw {
|
||||
code: 405,
|
||||
status: 'Method Not Allowed',
|
||||
message: `Can't add/remove user to group created by external auth provider.`
|
||||
}
|
||||
|
||||
const updatedGroup =
|
||||
action === 'addUser'
|
||||
? await group.addUser(user)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './auth'
|
||||
export * from './authConfig'
|
||||
export * from './client'
|
||||
export * from './code'
|
||||
export * from './drive'
|
||||
|
||||
@@ -28,6 +28,7 @@ interface ExecuteFileParams {
|
||||
returnJson?: boolean
|
||||
session?: Session
|
||||
runTime: RunTimeType
|
||||
forceStringResult?: boolean
|
||||
}
|
||||
|
||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
||||
@@ -42,7 +43,8 @@ export class ExecutionController {
|
||||
otherArgs,
|
||||
returnJson,
|
||||
session,
|
||||
runTime
|
||||
runTime,
|
||||
forceStringResult
|
||||
}: ExecuteFileParams) {
|
||||
const program = await readFile(programPath)
|
||||
|
||||
@@ -53,7 +55,8 @@ export class ExecutionController {
|
||||
otherArgs,
|
||||
returnJson,
|
||||
session,
|
||||
runTime
|
||||
runTime,
|
||||
forceStringResult
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +66,8 @@ export class ExecutionController {
|
||||
vars,
|
||||
otherArgs,
|
||||
session: sessionByFileUpload,
|
||||
runTime
|
||||
runTime,
|
||||
forceStringResult
|
||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||
const sessionController = getSessionController(runTime)
|
||||
|
||||
@@ -104,7 +108,7 @@ export class ExecutionController {
|
||||
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
||||
|
||||
const webout = (await fileExists(weboutPath))
|
||||
? fileResponse
|
||||
? fileResponse && !forceStringResult
|
||||
? await readFileBinary(weboutPath)
|
||||
: await readFile(weboutPath)
|
||||
: ''
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Session } from '../../types'
|
||||
import { promisify } from 'util'
|
||||
import { execFile } from 'child_process'
|
||||
import {
|
||||
getPackagesFolder,
|
||||
getSessionsFolder,
|
||||
generateUniqueFileName,
|
||||
sysInitCompiledPath,
|
||||
@@ -19,13 +20,41 @@ import {
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
|
||||
abstract class SessionController {
|
||||
export class SessionController {
|
||||
protected sessions: Session[] = []
|
||||
|
||||
protected getReadySessions = (): Session[] =>
|
||||
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
||||
|
||||
protected abstract createSession(): Promise<Session>
|
||||
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
|
||||
}
|
||||
|
||||
public async getSession() {
|
||||
const readySessions = this.getReadySessions()
|
||||
@@ -64,6 +93,9 @@ export class SASSessionController extends SessionController {
|
||||
path: sessionFolder
|
||||
}
|
||||
|
||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||
await createFile(headersPath, 'Content-type: text/plain')
|
||||
|
||||
// we do not want to leave sessions running forever
|
||||
// we clean them up after a predefined period, if unused
|
||||
this.scheduleSessionDestroy(session)
|
||||
@@ -73,7 +105,8 @@ export class SASSessionController extends SessionController {
|
||||
|
||||
// the autoexec file is executed on SAS startup
|
||||
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
||||
const contentForAutoExec = `/* compiled systemInit */
|
||||
const contentForAutoExec = `filename packages "${getPackagesFolder()}";
|
||||
/* compiled systemInit */
|
||||
${compiledSystemInitContent}
|
||||
/* autoexec */
|
||||
${autoExecContent}`
|
||||
@@ -101,14 +134,14 @@ ${autoExecContent}`
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
autoExecPath,
|
||||
'-SASINITIALFOLDER',
|
||||
session.path,
|
||||
isWindows() ? '-nologo' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
||||
isWindows() ? '-nologo' : ''
|
||||
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
|
||||
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
|
||||
])
|
||||
.then(() => {
|
||||
session.completed = true
|
||||
@@ -142,7 +175,7 @@ ${autoExecContent}`
|
||||
session.ready = true
|
||||
}
|
||||
|
||||
public async deleteSession(session: Session) {
|
||||
private async deleteSession(session: Session) {
|
||||
// remove the temporary files, to avoid buildup
|
||||
await deleteFolder(session.path)
|
||||
|
||||
@@ -167,110 +200,17 @@ ${autoExecContent}`
|
||||
}
|
||||
}
|
||||
|
||||
export class JSSessionController 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export const getSessionController = (
|
||||
runTime: RunTimeType
|
||||
): SASSessionController | JSSessionController | PythonSessionController => {
|
||||
if (runTime === RunTimeType.SAS) {
|
||||
return getSASSessionController()
|
||||
}
|
||||
): SessionController => {
|
||||
if (process.sessionController) return process.sessionController
|
||||
|
||||
if (runTime === RunTimeType.JS) {
|
||||
return getJSSessionController()
|
||||
}
|
||||
process.sessionController =
|
||||
runTime === RunTimeType.SAS
|
||||
? new SASSessionController()
|
||||
: new SessionController()
|
||||
|
||||
if (runTime === RunTimeType.PY) {
|
||||
return getPythonSessionController()
|
||||
}
|
||||
|
||||
throw new Error('No Runtime is configured')
|
||||
}
|
||||
|
||||
const getSASSessionController = (): SASSessionController => {
|
||||
if (process.sasSessionController) return process.sasSessionController
|
||||
|
||||
process.sasSessionController = new SASSessionController()
|
||||
|
||||
return process.sasSessionController
|
||||
}
|
||||
|
||||
const getJSSessionController = (): JSSessionController => {
|
||||
if (process.jsSessionController) return process.jsSessionController
|
||||
|
||||
process.jsSessionController = new JSSessionController()
|
||||
|
||||
return process.jsSessionController
|
||||
}
|
||||
|
||||
const getPythonSessionController = (): PythonSessionController => {
|
||||
if (process.pythonSessionController) return process.pythonSessionController
|
||||
|
||||
process.pythonSessionController = new PythonSessionController()
|
||||
|
||||
return process.pythonSessionController
|
||||
return process.sessionController
|
||||
}
|
||||
|
||||
const autoExecContent = `
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isWindows } from '@sasjs/utils'
|
||||
import { escapeWinSlashes } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadJSCode } from '../../utils'
|
||||
import { ExecutionVars } from './'
|
||||
@@ -21,13 +21,9 @@ export const createJSProgram = async (
|
||||
|
||||
const preProgramVarStatments = `
|
||||
let _webout = '';
|
||||
const weboutPath = '${
|
||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
||||
}';
|
||||
const _SASJS_TOKENFILE = '${
|
||||
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||
}';
|
||||
const _SASJS_WEBOUT_HEADERS = '${headersPath}';
|
||||
const weboutPath = '${escapeWinSlashes(weboutPath)}';
|
||||
const _SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
|
||||
const _SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
|
||||
const _SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||
const _SASJS_USERID = '${preProgramVariables?.userId}';
|
||||
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isWindows } from '@sasjs/utils'
|
||||
import { escapeWinSlashes } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadPythonCode } from '../../utils'
|
||||
import { ExecutionVars } from './'
|
||||
@@ -19,14 +19,10 @@ export const createPythonProgram = async (
|
||||
)
|
||||
|
||||
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_SESSION_PATH = '${escapeWinSlashes(session.path)}';
|
||||
_WEBOUT = '${escapeWinSlashes(weboutPath)}';
|
||||
_SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
|
||||
_SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
|
||||
_SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||
_SASJS_USERID = '${preProgramVariables?.userId}';
|
||||
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||
|
||||
64
api/src/controllers/internal/createRProgram.ts
Normal file
64
api/src/controllers/internal/createRProgram.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { escapeWinSlashes } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadRCode } from '../../utils'
|
||||
import { ExecutionVars } from '.'
|
||||
|
||||
export const createRProgram = 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 <- '${escapeWinSlashes(session.path)}';
|
||||
._WEBOUT <- '${escapeWinSlashes(weboutPath)}';
|
||||
._SASJS_WEBOUT_HEADERS <- '${escapeWinSlashes(headersPath)}';
|
||||
._SASJS_TOKENFILE <- '${escapeWinSlashes(tokenFile)}';
|
||||
._SASJS_USERNAME <- '${preProgramVariables?.username}';
|
||||
._SASJS_USERID <- '${preProgramVariables?.userId}';
|
||||
._SASJS_DISPLAYNAME <- '${preProgramVariables?.displayName}';
|
||||
._METAPERSON <- ._SASJS_DISPLAYNAME;
|
||||
._METAUSER <- ._SASJS_USERNAME;
|
||||
SASJSPROCESSMODE <- 'Stored Program';
|
||||
`
|
||||
|
||||
const requiredModules = ``
|
||||
|
||||
program = `
|
||||
# runtime vars
|
||||
${varStatments}
|
||||
|
||||
# dynamic user-provided vars
|
||||
${preProgramVarStatments}
|
||||
|
||||
# change working directory to session folder
|
||||
setwd(._SASJS_SESSION_PATH)
|
||||
|
||||
# actual job code
|
||||
${program}
|
||||
|
||||
`
|
||||
// if no files are uploaded filesNamesMap will be undefined
|
||||
if (otherArgs?.filesNamesMap) {
|
||||
const uploadRCode = await generateFileUploadRCode(
|
||||
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 (uploadRCode.length > 0) {
|
||||
program = `${uploadRCode}\n` + program
|
||||
}
|
||||
}
|
||||
return requiredModules + program
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export const createSASProgram = async (
|
||||
vars: ExecutionVars,
|
||||
session: Session,
|
||||
weboutPath: string,
|
||||
headersPath: string,
|
||||
tokenFile: string,
|
||||
otherArgs?: any
|
||||
) => {
|
||||
@@ -23,7 +24,7 @@ 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 _sasjs_webout_headers=${headersPath};
|
||||
%let _metaperson=&_sasjs_displayname;
|
||||
%let _metauser=&_sasjs_username;
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ export * from './FileUploadController'
|
||||
export * from './createSASProgram'
|
||||
export * from './createJSProgram'
|
||||
export * from './createPythonProgram'
|
||||
export * from './createRProgram'
|
||||
export * from './processProgram'
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
ExecutionVars,
|
||||
createSASProgram,
|
||||
createJSProgram,
|
||||
createPythonProgram
|
||||
createPythonProgram,
|
||||
createRProgram
|
||||
} from './'
|
||||
|
||||
export const processProgram = async (
|
||||
@@ -24,87 +25,14 @@ export const processProgram = async (
|
||||
logPath: string,
|
||||
otherArgs?: any
|
||||
) => {
|
||||
if (runTime === RunTimeType.JS) {
|
||||
program = await createJSProgram(
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
session,
|
||||
weboutPath,
|
||||
headersPath,
|
||||
tokenFile,
|
||||
otherArgs
|
||||
)
|
||||
|
||||
const codePath = path.join(session.path, 'code.js')
|
||||
|
||||
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.nodeLoc!, [codePath], {
|
||||
stdio: ['ignore', writeStream, writeStream]
|
||||
})
|
||||
|
||||
// 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) {
|
||||
session.completed = true
|
||||
session.crashed = err.toString()
|
||||
console.log('session crashed', session.id, session.crashed)
|
||||
}
|
||||
} else {
|
||||
if (runTime === RunTimeType.SAS) {
|
||||
program = await createSASProgram(
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
session,
|
||||
weboutPath,
|
||||
headersPath,
|
||||
tokenFile,
|
||||
otherArgs
|
||||
)
|
||||
@@ -124,6 +52,78 @@ export const processProgram = async (
|
||||
while (!session.completed) {
|
||||
await delay(50)
|
||||
}
|
||||
} else {
|
||||
let codePath: string
|
||||
let executablePath: string
|
||||
switch (runTime) {
|
||||
case RunTimeType.JS:
|
||||
program = await createJSProgram(
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
session,
|
||||
weboutPath,
|
||||
headersPath,
|
||||
tokenFile,
|
||||
otherArgs
|
||||
)
|
||||
codePath = path.join(session.path, 'code.js')
|
||||
executablePath = process.nodeLoc!
|
||||
|
||||
break
|
||||
case RunTimeType.PY:
|
||||
program = await createPythonProgram(
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
session,
|
||||
weboutPath,
|
||||
headersPath,
|
||||
tokenFile,
|
||||
otherArgs
|
||||
)
|
||||
codePath = path.join(session.path, 'code.py')
|
||||
executablePath = process.pythonLoc!
|
||||
|
||||
break
|
||||
case RunTimeType.R:
|
||||
program = await createRProgram(
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
session,
|
||||
weboutPath,
|
||||
headersPath,
|
||||
tokenFile,
|
||||
otherArgs
|
||||
)
|
||||
codePath = path.join(session.path, 'code.r')
|
||||
executablePath = process.rLoc!
|
||||
|
||||
break
|
||||
default:
|
||||
throw new Error('Invalid runtime!')
|
||||
}
|
||||
|
||||
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(executablePath, [codePath], {
|
||||
stdio: ['ignore', writeStream, writeStream]
|
||||
})
|
||||
// copy the code file 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
283
api/src/controllers/mock-sas9.ts
Normal file
283
api/src/controllers/mock-sas9.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import { readFile } from '@sasjs/utils'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import { Request, Post, Get } from 'tsoa'
|
||||
import dotenv from 'dotenv'
|
||||
import { ExecutionController } from './internal'
|
||||
import {
|
||||
getPreProgramVariables,
|
||||
getRunTimeAndFilePath,
|
||||
makeFilesNamesMap
|
||||
} from '../utils'
|
||||
import { MulterFile } from '../types/Upload'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export interface Sas9Response {
|
||||
content: string
|
||||
redirect?: string
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
export interface MockFileRead {
|
||||
content: string
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
export class MockSas9Controller {
|
||||
private loggedIn: string | undefined
|
||||
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||
|
||||
@Get('/SASStoredProcess')
|
||||
public async sasStoredProcess(
|
||||
@Request() req: express.Request
|
||||
): Promise<Sas9Response> {
|
||||
const username = req.query._username?.toString() || undefined
|
||||
const password = req.query._password?.toString() || undefined
|
||||
|
||||
if (username && password) this.loggedIn = req.body.username
|
||||
|
||||
if (!this.loggedIn) {
|
||||
return {
|
||||
content: '',
|
||||
redirect: '/SASLogon/login'
|
||||
}
|
||||
}
|
||||
|
||||
let program = req.query._program?.toString() || undefined
|
||||
const filePath: string[] = program
|
||||
? program.replace('/', '').split('/')
|
||||
: ['generic', 'sas-stored-process']
|
||||
|
||||
if (program) {
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
this.mocksPath,
|
||||
'sas9',
|
||||
...filePath
|
||||
])
|
||||
}
|
||||
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'sas9',
|
||||
...filePath
|
||||
])
|
||||
}
|
||||
|
||||
@Get('/SASStoredProcess/do')
|
||||
public async sasStoredProcessDoGet(
|
||||
@Request() req: express.Request
|
||||
): Promise<Sas9Response> {
|
||||
const username = req.query._username?.toString() || undefined
|
||||
const password = req.query._password?.toString() || undefined
|
||||
|
||||
if (username && password) this.loggedIn = username
|
||||
|
||||
if (!this.loggedIn) {
|
||||
return {
|
||||
content: '',
|
||||
redirect: '/SASLogon/login'
|
||||
}
|
||||
}
|
||||
|
||||
const program = req.query._program ?? req.body?._program
|
||||
const filePath: string[] = ['generic', 'sas-stored-process']
|
||||
|
||||
if (program) {
|
||||
const vars = { ...req.query, ...req.body, _requestMethod: req.method }
|
||||
const otherArgs = {}
|
||||
|
||||
try {
|
||||
const { codePath, runTime } = await getRunTimeAndFilePath(
|
||||
program + '.js'
|
||||
)
|
||||
|
||||
const result = await new ExecutionController().executeFile({
|
||||
programPath: codePath,
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: vars,
|
||||
otherArgs: otherArgs,
|
||||
runTime,
|
||||
forceStringResult: true
|
||||
})
|
||||
|
||||
return {
|
||||
content: result.result as string
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err', err)
|
||||
}
|
||||
|
||||
return {
|
||||
content: 'No webout returned.'
|
||||
}
|
||||
}
|
||||
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'sas9',
|
||||
...filePath
|
||||
])
|
||||
}
|
||||
|
||||
@Post('/SASStoredProcess/do/')
|
||||
public async sasStoredProcessDoPost(
|
||||
@Request() req: express.Request
|
||||
): Promise<Sas9Response> {
|
||||
if (!this.loggedIn) {
|
||||
return {
|
||||
content: '',
|
||||
redirect: '/SASLogon/login'
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isPublicAccount()) {
|
||||
return {
|
||||
content: '',
|
||||
redirect: '/SASLogon/Login'
|
||||
}
|
||||
}
|
||||
|
||||
const program = req.query._program ?? req.body?._program
|
||||
const vars = {
|
||||
...req.query,
|
||||
...req.body,
|
||||
_requestMethod: req.method,
|
||||
_driveLoc: process.driveLoc
|
||||
}
|
||||
const filesNamesMap = req.files?.length
|
||||
? makeFilesNamesMap(req.files as MulterFile[])
|
||||
: null
|
||||
const otherArgs = { filesNamesMap: filesNamesMap }
|
||||
const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js')
|
||||
try {
|
||||
const result = await new ExecutionController().executeFile({
|
||||
programPath: codePath,
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: vars,
|
||||
otherArgs: otherArgs,
|
||||
runTime,
|
||||
session: req.sasjsSession,
|
||||
forceStringResult: true
|
||||
})
|
||||
|
||||
return {
|
||||
content: result.result as string
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err', err)
|
||||
}
|
||||
|
||||
return {
|
||||
content: 'No webout returned.'
|
||||
}
|
||||
}
|
||||
|
||||
@Get('/SASLogon/login')
|
||||
public async loginGet(): Promise<Sas9Response> {
|
||||
if (this.loggedIn) {
|
||||
if (this.isPublicAccount()) {
|
||||
return {
|
||||
content: '',
|
||||
redirect: '/SASStoredProcess/Logoff?publicDenied=true'
|
||||
}
|
||||
} else {
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'sas9',
|
||||
'generic',
|
||||
'logged-in'
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'sas9',
|
||||
'generic',
|
||||
'login'
|
||||
])
|
||||
}
|
||||
|
||||
@Post('/SASLogon/login')
|
||||
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
||||
if (req.body.lt && req.body.lt !== 'validtoken')
|
||||
return {
|
||||
content: '',
|
||||
redirect: '/SASLogon/login'
|
||||
}
|
||||
|
||||
this.loggedIn = req.body.username
|
||||
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'sas9',
|
||||
'generic',
|
||||
'logged-in'
|
||||
])
|
||||
}
|
||||
|
||||
@Get('/SASLogon/logout')
|
||||
public async logout(req: express.Request): Promise<Sas9Response> {
|
||||
this.loggedIn = undefined
|
||||
|
||||
if (req.query.publicDenied === 'true') {
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'sas9',
|
||||
'generic',
|
||||
'public-access-denied'
|
||||
])
|
||||
}
|
||||
|
||||
return await getMockResponseFromFile([
|
||||
process.cwd(),
|
||||
'mocks',
|
||||
'sas9',
|
||||
'generic',
|
||||
'logged-out'
|
||||
])
|
||||
}
|
||||
|
||||
@Get('/SASStoredProcess/Logoff') //publicDenied=true
|
||||
public async logoff(req: express.Request): Promise<Sas9Response> {
|
||||
const params = req.query.publicDenied
|
||||
? `?publicDenied=${req.query.publicDenied}`
|
||||
: ''
|
||||
|
||||
return {
|
||||
content: '',
|
||||
redirect: '/SASLogon/logout' + params
|
||||
}
|
||||
}
|
||||
|
||||
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
|
||||
}
|
||||
|
||||
const getMockResponseFromFile = async (
|
||||
filePath: string[]
|
||||
): Promise<MockFileRead> => {
|
||||
const filePathParsed = path.join(...filePath)
|
||||
let error: boolean = false
|
||||
|
||||
let file = await readFile(filePathParsed).catch((err: any) => {
|
||||
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
|
||||
console.error(errMsg)
|
||||
|
||||
error = true
|
||||
|
||||
return errMsg
|
||||
})
|
||||
|
||||
return {
|
||||
content: file,
|
||||
error: error
|
||||
}
|
||||
}
|
||||
@@ -23,14 +23,14 @@ interface ExecutePostRequestPayload {
|
||||
@Tags('STP')
|
||||
export class STPController {
|
||||
/**
|
||||
* Trigger a SAS or JS program using the _program URL parameter.
|
||||
* Trigger a Stored Program using the _program URL parameter.
|
||||
*
|
||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||
*
|
||||
* https://server.sasjs.io/storedprograms
|
||||
*
|
||||
* @summary Execute a Stored Program, returns raw _webout content.
|
||||
* @param _program Location of SAS or JS code
|
||||
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
||||
* @param _program Location of code in SASjs Drive
|
||||
* @example _program "/Projects/myApp/some/program"
|
||||
*/
|
||||
@Get('/execute')
|
||||
@@ -43,21 +43,15 @@ export class STPController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a SAS or JS program using the _program URL parameter.
|
||||
* Trigger a Stored Program using the _program URL parameter.
|
||||
*
|
||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||
*
|
||||
* https://server.sasjs.io/storedprograms
|
||||
*
|
||||
* The response will be a JSON object with the following root attributes:
|
||||
* log, webout, headers.
|
||||
*
|
||||
* The webout attribute will be nested JSON ONLY if the response-header
|
||||
* contains a content-type of application/json AND it is valid JSON.
|
||||
* Otherwise it will be a stringified version of the webout content.
|
||||
*
|
||||
* @summary Execute a Stored Program, return a JSON object
|
||||
* @param _program Location of SAS or JS code
|
||||
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
||||
* @param _program Location of code in SASjs Drive
|
||||
* @example _program "/Projects/myApp/some/program"
|
||||
*/
|
||||
@Post('/execute')
|
||||
|
||||
@@ -17,7 +17,12 @@ import {
|
||||
import { desktopUser } from '../middlewares'
|
||||
|
||||
import User, { UserPayload } from '../model/User'
|
||||
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
|
||||
import {
|
||||
getUserAutoExec,
|
||||
updateUserAutoExec,
|
||||
ModeType,
|
||||
AuthProviderType
|
||||
} from '../utils'
|
||||
import { GroupResponse } from './group'
|
||||
|
||||
export interface UserResponse {
|
||||
@@ -211,7 +216,11 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
|
||||
// Checking if user is already in the database
|
||||
const usernameExist = await User.findOne({ username })
|
||||
if (usernameExist) throw new Error('Username already exists.')
|
||||
if (usernameExist)
|
||||
throw {
|
||||
code: 409,
|
||||
message: 'Username already exists.'
|
||||
}
|
||||
|
||||
// Hash passwords
|
||||
const hashPassword = User.hashPassword(password)
|
||||
@@ -255,7 +264,11 @@ const getUser = async (
|
||||
'groupId name description -_id'
|
||||
)) as unknown as UserDetailsResponse
|
||||
|
||||
if (!user) throw new Error('User is not found.')
|
||||
if (!user)
|
||||
throw {
|
||||
code: 404,
|
||||
message: 'User is not found.'
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
@@ -284,6 +297,24 @@ const updateUser = async (
|
||||
|
||||
const params: any = { displayName, isAdmin, isActive, autoExec }
|
||||
|
||||
const user = await User.findOne(findBy)
|
||||
|
||||
if (username && username !== user?.username && user?.authProvider) {
|
||||
throw {
|
||||
code: 405,
|
||||
message:
|
||||
'Can not update username of user that is created by an external auth provider.'
|
||||
}
|
||||
}
|
||||
|
||||
if (displayName && displayName !== user?.displayName && user?.authProvider) {
|
||||
throw {
|
||||
code: 405,
|
||||
message:
|
||||
'Can not update display name of user that is created by an external auth provider.'
|
||||
}
|
||||
}
|
||||
|
||||
if (username) {
|
||||
// Checking if user is already in the database
|
||||
const usernameExist = await User.findOne({ username })
|
||||
@@ -292,7 +323,10 @@ const updateUser = async (
|
||||
(findBy.id && usernameExist.id != findBy.id) ||
|
||||
(findBy.username && usernameExist.username != findBy.username)
|
||||
)
|
||||
throw new Error('Username already exists.')
|
||||
throw {
|
||||
code: 409,
|
||||
message: 'Username already exists.'
|
||||
}
|
||||
}
|
||||
params.username = username
|
||||
}
|
||||
@@ -305,7 +339,10 @@ const updateUser = async (
|
||||
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
|
||||
|
||||
if (!updatedUser)
|
||||
throw new Error(`Unable to find user with ${findBy.id || findBy.username}`)
|
||||
throw {
|
||||
code: 404,
|
||||
message: `Unable to find user with ${findBy.id || findBy.username}`
|
||||
}
|
||||
|
||||
return {
|
||||
id: updatedUser.id,
|
||||
@@ -332,11 +369,19 @@ const deleteUser = async (
|
||||
{ password }: { password?: string }
|
||||
) => {
|
||||
const user = await User.findOne(findBy)
|
||||
if (!user) throw new Error('User is not found.')
|
||||
if (!user)
|
||||
throw {
|
||||
code: 404,
|
||||
message: 'User is not found.'
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
const validPass = user.comparePassword(password!)
|
||||
if (!validPass) throw new Error('Invalid password.')
|
||||
if (!validPass)
|
||||
throw {
|
||||
code: 401,
|
||||
message: 'Invalid password.'
|
||||
}
|
||||
}
|
||||
|
||||
await User.deleteOne(findBy)
|
||||
|
||||
@@ -5,7 +5,12 @@ import { readFile } from '@sasjs/utils'
|
||||
|
||||
import User from '../model/User'
|
||||
import Client from '../model/Client'
|
||||
import { getWebBuildFolder, generateAuthCode } from '../utils'
|
||||
import {
|
||||
getWebBuildFolder,
|
||||
generateAuthCode,
|
||||
AuthProviderType,
|
||||
LDAPClient
|
||||
} from '../utils'
|
||||
import { InfoJWT } from '../types'
|
||||
import { AuthController } from './auth'
|
||||
|
||||
@@ -80,8 +85,16 @@ const login = async (
|
||||
const user = await User.findOne({ username })
|
||||
if (!user) throw new Error('Username is not found.')
|
||||
|
||||
const validPass = user.comparePassword(password)
|
||||
if (!validPass) throw new Error('Invalid password.')
|
||||
if (
|
||||
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
|
||||
user.authProvider === AuthProviderType.LDAP
|
||||
) {
|
||||
const ldapClient = await LDAPClient.init()
|
||||
await ldapClient.verifyUser(username, password)
|
||||
} else {
|
||||
const validPass = user.comparePassword(password)
|
||||
if (!validPass) throw new Error('Invalid password.')
|
||||
}
|
||||
|
||||
req.session.loggedIn = true
|
||||
req.session.user = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { csrfProtection } from '../app'
|
||||
import { csrfProtection } from './'
|
||||
import {
|
||||
fetchLatestAutoExec,
|
||||
ModeType,
|
||||
|
||||
@@ -10,9 +10,7 @@ import { getPath, isPublicRoute } from '../utils'
|
||||
export const authorize: RequestHandler = async (req, res, next) => {
|
||||
const { user } = req
|
||||
|
||||
if (!user) {
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
if (!user) return res.sendStatus(401)
|
||||
|
||||
// no need to check for permissions when user is admin
|
||||
if (user.isAdmin) return next()
|
||||
|
||||
32
api/src/middlewares/csrfProtection.ts
Normal file
32
api/src/middlewares/csrfProtection.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { RequestHandler } from 'express'
|
||||
import csrf from 'csrf'
|
||||
|
||||
const csrfTokens = new csrf()
|
||||
const secret = csrfTokens.secretSync()
|
||||
|
||||
export const generateCSRFToken = () => csrfTokens.create(secret)
|
||||
|
||||
export const csrfProtection: RequestHandler = (req, res, next) => {
|
||||
if (req.method === 'GET') return next()
|
||||
|
||||
// Reads the token from the following locations, in order:
|
||||
// req.body.csrf_token - typically generated by the body-parser module.
|
||||
// req.query.csrf_token - a built-in from Express.js to read from the URL query string.
|
||||
// req.headers['csrf-token'] - the CSRF-Token HTTP request header.
|
||||
// req.headers['xsrf-token'] - the XSRF-Token HTTP request header.
|
||||
// req.headers['x-csrf-token'] - the X-CSRF-Token HTTP request header.
|
||||
// req.headers['x-xsrf-token'] - the X-XSRF-Token HTTP request header.
|
||||
|
||||
const token =
|
||||
req.body?.csrf_token ||
|
||||
req.query?.csrf_token ||
|
||||
req.headers['csrf-token'] ||
|
||||
req.headers['xsrf-token'] ||
|
||||
req.headers['x-csrf-token'] ||
|
||||
req.headers['x-xsrf-token']
|
||||
|
||||
if (!csrfTokens.verify(secret, token)) {
|
||||
return res.status(400).send('Invalid CSRF token!')
|
||||
}
|
||||
next()
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './authenticateToken'
|
||||
export * from './authorize'
|
||||
export * from './csrfProtection'
|
||||
export * from './desktop'
|
||||
export * from './verifyAdmin'
|
||||
export * from './verifyAdminIfNeeded'
|
||||
export * from './authorize'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
import { GroupDetailsResponse } from '../controllers'
|
||||
import User, { IUser } from './User'
|
||||
import { AuthProviderType } from '../utils'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
|
||||
export const PUBLIC_GROUP_NAME = 'Public'
|
||||
@@ -27,6 +28,7 @@ interface IGroupDocument extends GroupPayload, Document {
|
||||
groupId: number
|
||||
isActive: boolean
|
||||
users: Schema.Types.ObjectId[]
|
||||
authProvider?: AuthProviderType
|
||||
}
|
||||
|
||||
interface IGroup extends IGroupDocument {
|
||||
@@ -46,6 +48,10 @@ const groupSchema = new Schema<IGroupDocument>({
|
||||
type: String,
|
||||
default: 'Group description.'
|
||||
},
|
||||
authProvider: {
|
||||
type: String,
|
||||
enum: AuthProviderType
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { AuthProviderType } from '../utils'
|
||||
|
||||
export interface UserPayload {
|
||||
/**
|
||||
@@ -42,6 +43,7 @@ interface IUserDocument extends UserPayload, Document {
|
||||
autoExec: string
|
||||
groups: Schema.Types.ObjectId[]
|
||||
tokens: [{ [key: string]: string }]
|
||||
authProvider?: AuthProviderType
|
||||
}
|
||||
|
||||
export interface IUser extends IUserDocument {
|
||||
@@ -67,6 +69,10 @@ const userSchema = new Schema<IUserDocument>({
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
authProvider: {
|
||||
type: String,
|
||||
enum: AuthProviderType
|
||||
},
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
authenticateRefreshToken
|
||||
} from '../../middlewares'
|
||||
|
||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
||||
import { tokenValidation } from '../../utils'
|
||||
import { InfoJWT } from '../../types'
|
||||
|
||||
const authRouter = express.Router()
|
||||
|
||||
25
api/src/routes/api/authConfig.ts
Normal file
25
api/src/routes/api/authConfig.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import express from 'express'
|
||||
import { AuthConfigController } from '../../controllers'
|
||||
const authConfigRouter = express.Router()
|
||||
|
||||
authConfigRouter.get('/', async (req, res) => {
|
||||
const controller = new AuthConfigController()
|
||||
try {
|
||||
const response = controller.getDetail()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(500).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
authConfigRouter.post('/synchroniseWithLDAP', async (req, res) => {
|
||||
const controller = new AuthConfigController()
|
||||
try {
|
||||
const response = await controller.synchroniseWithLDAP()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(500).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default authConfigRouter
|
||||
@@ -18,11 +18,7 @@ groupRouter.post(
|
||||
const response = await controller.createGroup(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -33,11 +29,7 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
|
||||
const response = await controller.getAllGroups()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -49,11 +41,7 @@ groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
|
||||
const response = await controller.getGroup(parseInt(groupId))
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -71,11 +59,7 @@ groupRouter.get(
|
||||
const response = await controller.getGroupByGroupName(name)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -95,11 +79,7 @@ groupRouter.post(
|
||||
)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -119,11 +99,7 @@ groupRouter.delete(
|
||||
)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -140,11 +116,7 @@ groupRouter.delete(
|
||||
await controller.deleteGroup(parseInt(groupId))
|
||||
res.status(200).send('Group Deleted!')
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import clientRouter from './client'
|
||||
import authRouter from './auth'
|
||||
import sessionRouter from './session'
|
||||
import permissionRouter from './permission'
|
||||
import authConfigRouter from './authConfig'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@@ -43,6 +44,14 @@ router.use(
|
||||
permissionRouter
|
||||
)
|
||||
|
||||
router.use(
|
||||
'/authConfig',
|
||||
desktopRestrict,
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
authConfigRouter
|
||||
)
|
||||
|
||||
router.use(
|
||||
'/',
|
||||
swaggerUi.serve,
|
||||
|
||||
@@ -4,8 +4,13 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
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'
|
||||
import {
|
||||
generateAccessToken,
|
||||
saveTokensInDB,
|
||||
AuthProviderType
|
||||
} from '../../../utils'
|
||||
import Group, { PUBLIC_GROUP_NAME } from '../../../model/Group'
|
||||
import User from '../../../model/User'
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
@@ -560,6 +565,46 @@ describe('group', () => {
|
||||
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||
)
|
||||
})
|
||||
|
||||
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
|
||||
const dbGroup = await Group.create({
|
||||
...group,
|
||||
authProvider: AuthProviderType.LDAP
|
||||
})
|
||||
const dbUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'ldapGroupUser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(405)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`Can't add/remove user to group created by external auth provider.`
|
||||
)
|
||||
})
|
||||
|
||||
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const dbUser = await User.create({
|
||||
...user,
|
||||
username: 'ldapUser',
|
||||
authProvider: AuthProviderType.LDAP
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(405)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`Can't add/remove user to group created by external auth provider.`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('RemoveUser', () => {
|
||||
@@ -611,6 +656,46 @@ describe('group', () => {
|
||||
expect(res.body.groups).toEqual([])
|
||||
})
|
||||
|
||||
it('should respond with Method Not Allowed if group is created by an external authProvider', async () => {
|
||||
const dbGroup = await Group.create({
|
||||
...group,
|
||||
authProvider: AuthProviderType.LDAP
|
||||
})
|
||||
const dbUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'removeLdapGroupUser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(405)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`Can't add/remove user to group created by external auth provider.`
|
||||
)
|
||||
})
|
||||
|
||||
it('should respond with Method Not Allowed if user is created by an external authProvider', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const dbUser = await User.create({
|
||||
...user,
|
||||
username: 'removeLdapUser',
|
||||
authProvider: AuthProviderType.LDAP
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(405)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`Can't add/remove user to group created by external auth provider.`
|
||||
)
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.delete('/SASjsApi/group/123/123')
|
||||
|
||||
@@ -21,9 +21,8 @@ import {
|
||||
} from '../../../utils'
|
||||
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
||||
import {
|
||||
SASSessionController,
|
||||
JSSessionController,
|
||||
PythonSessionController
|
||||
SessionController,
|
||||
SASSessionController
|
||||
} from '../../../controllers/internal'
|
||||
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
||||
import { Session } from '../../../types'
|
||||
@@ -472,11 +471,7 @@ const setupMocks = async () => {
|
||||
.mockImplementation(mockedGetSession)
|
||||
|
||||
jest
|
||||
.spyOn(JSSessionController.prototype, 'getSession')
|
||||
.mockImplementation(mockedGetSession)
|
||||
|
||||
jest
|
||||
.spyOn(PythonSessionController.prototype, 'getSession')
|
||||
.spyOn(SASSessionController.prototype, 'getSession')
|
||||
.mockImplementation(mockedGetSession)
|
||||
|
||||
jest
|
||||
|
||||
@@ -4,7 +4,12 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController, GroupController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
import {
|
||||
generateAccessToken,
|
||||
saveTokensInDB,
|
||||
AuthProviderType
|
||||
} from '../../../utils'
|
||||
import User from '../../../model/User'
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
@@ -110,16 +115,16 @@ describe('user', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if username is already present', async () => {
|
||||
it('should respond with Conflict if username is already present', async () => {
|
||||
await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send(user)
|
||||
.expect(403)
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual('Error: Username already exists.')
|
||||
expect(res.text).toEqual('Username already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -226,6 +231,36 @@ describe('user', () => {
|
||||
.expect(400)
|
||||
})
|
||||
|
||||
it('should respond with Method Not Allowed, when updating username of user created by an external auth provider', async () => {
|
||||
const dbUser = await User.create({
|
||||
...user,
|
||||
authProvider: AuthProviderType.LDAP
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser!.id)
|
||||
const newUsername = 'newUsername'
|
||||
|
||||
await request(app)
|
||||
.patch(`/SASjsApi/user/${dbUser!.id}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ username: newUsername })
|
||||
.expect(405)
|
||||
})
|
||||
|
||||
it('should respond with Method Not Allowed, when updating displayName of user created by an external auth provider', async () => {
|
||||
const dbUser = await User.create({
|
||||
...user,
|
||||
authProvider: AuthProviderType.LDAP
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser!.id)
|
||||
const newDisplayName = 'My new display Name'
|
||||
|
||||
await request(app)
|
||||
.patch(`/SASjsApi/user/${dbUser!.id}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ displayName: newDisplayName })
|
||||
.expect(405)
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/user/1234')
|
||||
@@ -254,7 +289,7 @@ describe('user', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if username is already present', async () => {
|
||||
it('should respond with Conflict if username is already present', async () => {
|
||||
const dbUser1 = await controller.createUser(user)
|
||||
const dbUser2 = await controller.createUser({
|
||||
...user,
|
||||
@@ -265,9 +300,9 @@ describe('user', () => {
|
||||
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ username: dbUser2.username })
|
||||
.expect(403)
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual('Error: Username already exists.')
|
||||
expect(res.text).toEqual('Username already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -349,7 +384,7 @@ describe('user', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if username is already present', async () => {
|
||||
it('should respond with Conflict if username is already present', async () => {
|
||||
const dbUser1 = await controller.createUser(user)
|
||||
const dbUser2 = await controller.createUser({
|
||||
...user,
|
||||
@@ -360,9 +395,9 @@ describe('user', () => {
|
||||
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ username: dbUser2.username })
|
||||
.expect(403)
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual('Error: Username already exists.')
|
||||
expect(res.text).toEqual('Username already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -446,7 +481,7 @@ describe('user', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
|
||||
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
|
||||
@@ -454,9 +489,9 @@ describe('user', () => {
|
||||
.delete(`/SASjsApi/user/${dbUser.id}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ password: 'incorrectpassword' })
|
||||
.expect(403)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Error: Invalid password.')
|
||||
expect(res.text).toEqual('Invalid password.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -528,7 +563,7 @@ describe('user', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
|
||||
it('should respond with Unauthorized when user himself requests and password is incorrect', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
|
||||
@@ -536,9 +571,9 @@ describe('user', () => {
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ password: 'incorrectpassword' })
|
||||
.expect(403)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Error: Invalid password.')
|
||||
expect(res.text).toEqual('Invalid password.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -652,16 +687,16 @@ describe('user', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if userId is incorrect', async () => {
|
||||
it('should respond with Not Found if userId is incorrect', async () => {
|
||||
await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/user/1234')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('Error: User is not found.')
|
||||
expect(res.text).toEqual('User is not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -731,16 +766,16 @@ describe('user', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if username is incorrect', async () => {
|
||||
it('should respond with Not Found if username is incorrect', async () => {
|
||||
await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/user/by/username/randomUsername')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('Error: User is not found.')
|
||||
expect(res.text).toEqual('User is not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -49,10 +49,9 @@ describe('web', () => {
|
||||
|
||||
describe('SASLogon/login', () => {
|
||||
let csrfToken: string
|
||||
let cookies: string
|
||||
|
||||
beforeAll(async () => {
|
||||
;({ csrfToken, cookies } = await getCSRF(app))
|
||||
;({ csrfToken } = await getCSRF(app))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -66,7 +65,6 @@ describe('web', () => {
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/login')
|
||||
.set('Cookie', cookies)
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({
|
||||
username: user.username,
|
||||
@@ -82,15 +80,45 @@ describe('web', () => {
|
||||
isAdmin: user.isAdmin
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if CSRF Token is not present', async () => {
|
||||
await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/login')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Invalid CSRF token!')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if CSRF Token is invalid', async () => {
|
||||
await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/login')
|
||||
.set('x-xsrf-token', 'INVALID_CSRF_TOKEN')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Invalid CSRF token!')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SASLogon/authorize', () => {
|
||||
let csrfToken: string
|
||||
let cookies: string
|
||||
let authCookies: string
|
||||
|
||||
beforeAll(async () => {
|
||||
;({ csrfToken, cookies } = await getCSRF(app))
|
||||
;({ csrfToken } = await getCSRF(app))
|
||||
|
||||
await userController.createUser(user)
|
||||
|
||||
@@ -99,12 +127,7 @@ describe('web', () => {
|
||||
password: user.password
|
||||
}
|
||||
|
||||
;({ cookies: authCookies } = await performLogin(
|
||||
app,
|
||||
credentials,
|
||||
cookies,
|
||||
csrfToken
|
||||
))
|
||||
;({ authCookies } = await performLogin(app, credentials, csrfToken))
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -116,17 +139,28 @@ describe('web', () => {
|
||||
it('should respond with authorization code', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies, cookies].join('; '))
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({ clientId })
|
||||
|
||||
expect(res.body).toHaveProperty('code')
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if CSRF Token is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.send({ clientId })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Invalid CSRF token!')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if clientId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies, cookies].join('; '))
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({})
|
||||
.expect(400)
|
||||
@@ -138,7 +172,7 @@ describe('web', () => {
|
||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies, cookies].join('; '))
|
||||
.set('Cookie', [authCookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({
|
||||
clientId: 'WrongClientID'
|
||||
@@ -153,27 +187,22 @@ describe('web', () => {
|
||||
|
||||
const getCSRF = async (app: Express) => {
|
||||
// make request to get CSRF
|
||||
const { header, text } = await request(app).get('/')
|
||||
const cookies = header['set-cookie'].join()
|
||||
const { text } = await request(app).get('/')
|
||||
|
||||
const csrfToken = extractCSRF(text)
|
||||
return { csrfToken, cookies }
|
||||
return { csrfToken: extractCSRF(text) }
|
||||
}
|
||||
|
||||
const performLogin = async (
|
||||
app: Express,
|
||||
credentials: { username: string; password: string },
|
||||
cookies: string,
|
||||
csrfToken: string
|
||||
) => {
|
||||
const { header } = await request(app)
|
||||
.post('/SASLogon/login')
|
||||
.set('Cookie', cookies)
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send(credentials)
|
||||
|
||||
const newCookies: string = header['set-cookie'].join()
|
||||
return { cookies: newCookies }
|
||||
return { authCookies: header['set-cookie'].join() }
|
||||
}
|
||||
|
||||
const extractCSRF = (text: string) =>
|
||||
|
||||
@@ -23,7 +23,7 @@ userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
|
||||
const response = await controller.createUser(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -33,7 +33,7 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
|
||||
const response = await controller.getAllUsers()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -51,7 +51,7 @@ userRouter.get(
|
||||
const response = await controller.getUserByUsername(req, username)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -64,7 +64,7 @@ userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
|
||||
const response = await controller.getUser(req, parseInt(userId))
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -91,7 +91,7 @@ userRouter.patch(
|
||||
const response = await controller.updateUserByUsername(username, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -113,7 +113,7 @@ userRouter.patch(
|
||||
const response = await controller.updateUser(parseInt(userId), body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -141,7 +141,7 @@ userRouter.delete(
|
||||
await controller.deleteUserByUsername(username, data, user!.isAdmin)
|
||||
res.status(200).send('Account Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -163,7 +163,7 @@ userRouter.delete(
|
||||
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
|
||||
res.status(200).send('Account Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
res.status(err.code).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path'
|
||||
import express, { Request } from 'express'
|
||||
import { authenticateAccessToken } from '../../middlewares'
|
||||
import { authenticateAccessToken, generateCSRFToken } from '../../middlewares'
|
||||
import { folderExists } from '@sasjs/utils'
|
||||
|
||||
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
||||
@@ -13,7 +13,7 @@ const router = express.Router()
|
||||
router.get('/', authenticateAccessToken, async (req, res) => {
|
||||
const content = appStreamHtml(process.appStreamConfig)
|
||||
|
||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||
res.cookie('XSRF-TOKEN', generateCSRFToken())
|
||||
|
||||
return res.send(content)
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import webRouter from './web'
|
||||
import apiRouter from './api'
|
||||
import appStreamRouter from './appStream'
|
||||
|
||||
import { csrfProtection } from '../app'
|
||||
import { csrfProtection } from '../middlewares'
|
||||
|
||||
export const setupRoutes = (app: Express) => {
|
||||
app.use('/SASjsApi', apiRouter)
|
||||
@@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => {
|
||||
appStreamRouter(req, res, next)
|
||||
})
|
||||
|
||||
app.use('/', csrfProtection, webRouter)
|
||||
app.use('/', webRouter)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import express from 'express'
|
||||
import sas9WebRouter from './sas9-web'
|
||||
import sasViyaWebRouter from './sasviya-web'
|
||||
import webRouter from './web'
|
||||
import { MOCK_SERVERTYPEType } from '../../utils'
|
||||
import { csrfProtection } from '../../middlewares'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/', webRouter)
|
||||
const { MOCK_SERVERTYPE } = process.env
|
||||
|
||||
switch (MOCK_SERVERTYPE) {
|
||||
case MOCK_SERVERTYPEType.SAS9: {
|
||||
router.use('/', sas9WebRouter)
|
||||
break
|
||||
}
|
||||
case MOCK_SERVERTYPEType.SASVIYA: {
|
||||
router.use('/', sasViyaWebRouter)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
router.use('/', csrfProtection, webRouter)
|
||||
}
|
||||
}
|
||||
|
||||
export default router
|
||||
|
||||
152
api/src/routes/web/sas9-web.ts
Normal file
152
api/src/routes/web/sas9-web.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import express from 'express'
|
||||
import { generateCSRFToken } from '../../middlewares'
|
||||
import { WebController } from '../../controllers'
|
||||
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
||||
import multer from 'multer'
|
||||
import path from 'path'
|
||||
import dotenv from 'dotenv'
|
||||
import { FileUploadController } from '../../controllers/internal'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const sas9WebRouter = express.Router()
|
||||
const webController = new WebController()
|
||||
// Mock controller must be singleton because it keeps the states
|
||||
// for example `isLoggedIn` and potentially more in future mocks
|
||||
const controller = new MockSas9Controller()
|
||||
const fileUploadController = new FileUploadController()
|
||||
|
||||
const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||
|
||||
const upload = multer({
|
||||
dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received')
|
||||
})
|
||||
|
||||
sas9WebRouter.get('/', async (req, res) => {
|
||||
let response
|
||||
try {
|
||||
response = await webController.home()
|
||||
} catch (_) {
|
||||
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||
} finally {
|
||||
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||
const injectedContent = response?.replace(
|
||||
'</head>',
|
||||
`${codeToInject}</head>`
|
||||
)
|
||||
|
||||
return res.send(injectedContent)
|
||||
}
|
||||
})
|
||||
|
||||
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
||||
const response = await controller.sasStoredProcess(req)
|
||||
|
||||
if (response.redirect) {
|
||||
res.redirect(response.redirect)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
res.send(response.content)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
|
||||
const response = await controller.sasStoredProcessDoGet(req)
|
||||
|
||||
if (response.redirect) {
|
||||
res.redirect(response.redirect)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
res.send(response.content)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
sas9WebRouter.post(
|
||||
'/SASStoredProcess/do/',
|
||||
fileUploadController.preUploadMiddleware,
|
||||
fileUploadController.getMulterUploadObject().any(),
|
||||
async (req, res) => {
|
||||
const response = await controller.sasStoredProcessDoPost(req)
|
||||
|
||||
if (response.redirect) {
|
||||
res.redirect(response.redirect)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
res.send(response.content)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
|
||||
const response = await controller.loginGet()
|
||||
|
||||
if (response.redirect) {
|
||||
res.redirect(response.redirect)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
res.send(response.content)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
sas9WebRouter.post('/SASLogon/login', async (req, res) => {
|
||||
const response = await controller.loginPost(req)
|
||||
|
||||
if (response.redirect) {
|
||||
res.redirect(response.redirect)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
res.send(response.content)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
sas9WebRouter.get('/SASLogon/logout', async (req, res) => {
|
||||
const response = await controller.logout(req)
|
||||
|
||||
if (response.redirect) {
|
||||
res.redirect(response.redirect)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
res.send(response.content)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
sas9WebRouter.get('/SASStoredProcess/Logoff', async (req, res) => {
|
||||
const response = await controller.logoff(req)
|
||||
|
||||
if (response.redirect) {
|
||||
res.redirect(response.redirect)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
res.send(response.content)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default sas9WebRouter
|
||||
33
api/src/routes/web/sasviya-web.ts
Normal file
33
api/src/routes/web/sasviya-web.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import express from 'express'
|
||||
import { generateCSRFToken } from '../../middlewares'
|
||||
import { WebController } from '../../controllers/web'
|
||||
|
||||
const sasViyaWebRouter = express.Router()
|
||||
const controller = new WebController()
|
||||
|
||||
sasViyaWebRouter.get('/', async (req, res) => {
|
||||
let response
|
||||
try {
|
||||
response = await controller.home()
|
||||
} catch (_) {
|
||||
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||
} finally {
|
||||
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||
const injectedContent = response?.replace(
|
||||
'</head>',
|
||||
`${codeToInject}</head>`
|
||||
)
|
||||
|
||||
return res.send(injectedContent)
|
||||
}
|
||||
})
|
||||
|
||||
sasViyaWebRouter.post('/SASJobExecution/', async (req, res) => {
|
||||
try {
|
||||
res.send({ test: 'test' })
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default sasViyaWebRouter
|
||||
@@ -1,4 +1,5 @@
|
||||
import express from 'express'
|
||||
import { generateCSRFToken } from '../../middlewares'
|
||||
import { WebController } from '../../controllers/web'
|
||||
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
|
||||
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||
@@ -13,7 +14,10 @@ webRouter.get('/', async (req, res) => {
|
||||
} catch (_) {
|
||||
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||
} finally {
|
||||
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||
const { ALLOWED_DOMAIN } = process.env
|
||||
const allowedDomain = ALLOWED_DOMAIN?.trim()
|
||||
const domain = allowedDomain ? ` Domain=${allowedDomain};` : ''
|
||||
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()};${domain} Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||
const injectedContent = response?.replace(
|
||||
'</head>',
|
||||
`${codeToInject}</head>`
|
||||
|
||||
5
api/src/types/system/process.d.ts
vendored
5
api/src/types/system/process.d.ts
vendored
@@ -3,12 +3,11 @@ declare namespace NodeJS {
|
||||
sasLoc?: string
|
||||
nodeLoc?: string
|
||||
pythonLoc?: string
|
||||
rLoc?: string
|
||||
driveLoc: string
|
||||
logsLoc: string
|
||||
logsUUID: string
|
||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
||||
pythonSessionController?: import('../../controllers/internal').PythonSessionController
|
||||
sessionController?: import('../../controllers/internal').SessionController
|
||||
appStreamConfig: import('../').AppStreamConfig
|
||||
logger: import('@sasjs/utils/logger').Logger
|
||||
runTimes: import('../../utils').RunTimeType[]
|
||||
|
||||
@@ -10,7 +10,7 @@ export const sysInitCompiledPath = path.join(
|
||||
'systemInitCompiled.sas'
|
||||
)
|
||||
|
||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||
export const sasJSCoreMacros = path.join(apiRoot, 'sas', 'sasautos')
|
||||
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||
|
||||
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
|
||||
@@ -28,7 +28,10 @@ export const getAppStreamConfigPath = () =>
|
||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
||||
|
||||
export const getMacrosFolder = () =>
|
||||
path.join(getSasjsRootFolder(), 'sasjscore')
|
||||
path.join(getSasjsRootFolder(), 'sas', 'sasautos')
|
||||
|
||||
export const getPackagesFolder = () =>
|
||||
path.join(getSasjsRootFolder(), 'sas', 'sas_packages')
|
||||
|
||||
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
||||
|
||||
|
||||
@@ -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, PYTHON_PATH } = process.env
|
||||
const { SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH } = process.env
|
||||
|
||||
let sasLoc, nodeLoc, pythonLoc
|
||||
let sasLoc, nodeLoc, pythonLoc, rLoc
|
||||
|
||||
if (process.runTimes.includes(RunTimeType.SAS)) {
|
||||
sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||
@@ -20,7 +20,11 @@ export const getDesktopFields = async () => {
|
||||
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
|
||||
}
|
||||
|
||||
return { sasLoc, nodeLoc, pythonLoc }
|
||||
if (process.runTimes.includes(RunTimeType.R)) {
|
||||
rLoc = R_PATH ?? (await getRLocation())
|
||||
}
|
||||
|
||||
return { sasLoc, nodeLoc, pythonLoc, rLoc }
|
||||
}
|
||||
|
||||
const getDriveLocation = async (): Promise<string> => {
|
||||
@@ -117,3 +121,25 @@ const getPythonLocation = async (): Promise<string> => {
|
||||
|
||||
return targetName
|
||||
}
|
||||
|
||||
const getRLocation = async (): Promise<string> => {
|
||||
const validator = async (filePath: string) => {
|
||||
if (!filePath) return 'Path to R executable is required.'
|
||||
|
||||
if (!(await fileExists(filePath))) {
|
||||
return 'No file found at provided path.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const defaultLocation = isWindows() ? 'C:\\Rscript' : '/usr/bin/Rscript'
|
||||
|
||||
const targetName = await getString(
|
||||
'Please enter full path to a R executable: ',
|
||||
validator,
|
||||
defaultLocation
|
||||
)
|
||||
|
||||
return targetName
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
||||
const { user, accessToken } = req
|
||||
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']
|
||||
const sessionId = req.cookies['connect.sid']
|
||||
const { _csrf } = req.cookies
|
||||
|
||||
const httpHeaders: string[] = []
|
||||
|
||||
@@ -16,14 +15,15 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
||||
|
||||
const cookies: string[] = []
|
||||
if (sessionId) cookies.push(`connect.sid=${sessionId}`)
|
||||
if (_csrf) cookies.push(`_csrf=${_csrf}`)
|
||||
|
||||
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
||||
|
||||
//In desktop mode when mocking mode is enabled, user was undefined.
|
||||
//So this is workaround.
|
||||
return {
|
||||
username: user!.username,
|
||||
userId: user!.userId,
|
||||
displayName: user!.displayName,
|
||||
username: user ? user.username : 'demo',
|
||||
userId: user ? user.userId : 0,
|
||||
displayName: user ? user.displayName : 'demo',
|
||||
serverUrl: protocol + host,
|
||||
httpHeaders
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { getFilesFolder } from './file'
|
||||
import { RunTimeType } from '.'
|
||||
|
||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||
const ext = path.extname(programPath)
|
||||
// If programPath (_program) is provided with a ".sas", ".js" or ".py" extension
|
||||
const ext = path.extname(programPath).toLowerCase()
|
||||
// If programPath (_program) is provided with a ".sas", ".js", ".py" or ".r" 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)
|
||||
|
||||
55
api/src/utils/getTokensFromDB.ts
Normal file
55
api/src/utils/getTokensFromDB.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import User from '../model/User'
|
||||
|
||||
const isValidToken = async (
|
||||
token: string,
|
||||
key: string,
|
||||
userId: number,
|
||||
clientId: string
|
||||
) => {
|
||||
const promise = new Promise<boolean>((resolve, reject) =>
|
||||
jwt.verify(token, key, (err, decoded) => {
|
||||
if (err) return reject(false)
|
||||
|
||||
if (decoded?.userId === userId && decoded?.clientId === clientId) {
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
return reject(false)
|
||||
})
|
||||
)
|
||||
|
||||
return await promise.then(() => true).catch(() => false)
|
||||
}
|
||||
|
||||
export const getTokensFromDB = async (userId: number, clientId: string) => {
|
||||
const user = await User.findOne({ id: userId })
|
||||
if (!user) return
|
||||
|
||||
const currentTokenObj = user.tokens.find(
|
||||
(tokenObj: any) => tokenObj.clientId === clientId
|
||||
)
|
||||
|
||||
if (currentTokenObj) {
|
||||
const accessToken = currentTokenObj.accessToken
|
||||
const refreshToken = currentTokenObj.refreshToken
|
||||
|
||||
const isValidAccessToken = await isValidToken(
|
||||
accessToken,
|
||||
process.secrets.ACCESS_TOKEN_SECRET,
|
||||
userId,
|
||||
clientId
|
||||
)
|
||||
|
||||
const isValidRefreshToken = await isValidToken(
|
||||
refreshToken,
|
||||
process.secrets.REFRESH_TOKEN_SECRET,
|
||||
userId,
|
||||
clientId
|
||||
)
|
||||
|
||||
if (isValidAccessToken && isValidRefreshToken) {
|
||||
return { accessToken, refreshToken }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,11 @@ export * from './getDesktopFields'
|
||||
export * from './getPreProgramVariables'
|
||||
export * from './getRunTimeAndFilePath'
|
||||
export * from './getServerUrl'
|
||||
export * from './getTokensFromDB'
|
||||
export * from './instantiateLogger'
|
||||
export * from './isDebugOn'
|
||||
export * from './isPublicRoute'
|
||||
export * from './ldapClient'
|
||||
export * from './zipped'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
|
||||
163
api/src/utils/ldapClient.ts
Normal file
163
api/src/utils/ldapClient.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { createClient, Client } from 'ldapjs'
|
||||
import { ReturnCode } from './verifyEnvVariables'
|
||||
|
||||
export interface LDAPUser {
|
||||
uid: string
|
||||
username: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export interface LDAPGroup {
|
||||
name: string
|
||||
members: string[]
|
||||
}
|
||||
|
||||
export class LDAPClient {
|
||||
private ldapClient: Client
|
||||
private static classInstance: LDAPClient | null
|
||||
|
||||
private constructor() {
|
||||
process.logger.info('creating LDAP client')
|
||||
this.ldapClient = createClient({ url: process.env.LDAP_URL as string })
|
||||
|
||||
this.ldapClient.on('error', (error) => {
|
||||
process.logger.error(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
static async init() {
|
||||
if (!LDAPClient.classInstance) {
|
||||
LDAPClient.classInstance = new LDAPClient()
|
||||
|
||||
process.logger.info('binding LDAP client')
|
||||
await LDAPClient.classInstance.bind().catch((error) => {
|
||||
LDAPClient.classInstance = null
|
||||
throw error
|
||||
})
|
||||
}
|
||||
return LDAPClient.classInstance
|
||||
}
|
||||
|
||||
private async bind() {
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
const { LDAP_BIND_DN, LDAP_BIND_PASSWORD } = process.env
|
||||
this.ldapClient.bind(LDAP_BIND_DN!, LDAP_BIND_PASSWORD!, (error) => {
|
||||
if (error) reject(error)
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
await promise.catch((error) => {
|
||||
throw new Error(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
async getAllLDAPUsers() {
|
||||
const promise = new Promise<LDAPUser[]>((resolve, reject) => {
|
||||
const { LDAP_USERS_BASE_DN } = process.env
|
||||
const filter = `(objectClass=*)`
|
||||
|
||||
this.ldapClient.search(
|
||||
LDAP_USERS_BASE_DN!,
|
||||
{ filter },
|
||||
(error, result) => {
|
||||
if (error) reject(error)
|
||||
|
||||
const users: LDAPUser[] = []
|
||||
|
||||
result.on('searchEntry', (entry) => {
|
||||
users.push({
|
||||
uid: entry.object.uid as string,
|
||||
username: entry.object.username as string,
|
||||
displayName: entry.object.displayname as string
|
||||
})
|
||||
})
|
||||
|
||||
result.on('end', (result) => {
|
||||
resolve(users)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return await promise
|
||||
.then((res) => res)
|
||||
.catch((error) => {
|
||||
throw new Error(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
async getAllLDAPGroups() {
|
||||
const promise = new Promise<LDAPGroup[]>((resolve, reject) => {
|
||||
const { LDAP_GROUPS_BASE_DN } = process.env
|
||||
|
||||
this.ldapClient.search(LDAP_GROUPS_BASE_DN!, {}, (error, result) => {
|
||||
if (error) reject(error)
|
||||
|
||||
const groups: LDAPGroup[] = []
|
||||
|
||||
result.on('searchEntry', (entry) => {
|
||||
const members =
|
||||
typeof entry.object.memberuid === 'string'
|
||||
? [entry.object.memberuid]
|
||||
: entry.object.memberuid
|
||||
groups.push({
|
||||
name: entry.object.cn as string,
|
||||
members
|
||||
})
|
||||
})
|
||||
|
||||
result.on('end', (result) => {
|
||||
resolve(groups)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return await promise
|
||||
.then((res) => res)
|
||||
.catch((error) => {
|
||||
throw new Error(error.message)
|
||||
})
|
||||
}
|
||||
|
||||
async verifyUser(username: string, password: string) {
|
||||
const promise = new Promise<boolean>((resolve, reject) => {
|
||||
const { LDAP_USERS_BASE_DN } = process.env
|
||||
const filter = `(username=${username})`
|
||||
|
||||
this.ldapClient.search(
|
||||
LDAP_USERS_BASE_DN!,
|
||||
{ filter },
|
||||
(error, result) => {
|
||||
if (error) reject(error)
|
||||
|
||||
const items: any = []
|
||||
|
||||
result.on('searchEntry', (entry) => {
|
||||
items.push(entry.object)
|
||||
})
|
||||
|
||||
result.on('end', (result) => {
|
||||
if (result?.status !== 0 || items.length === 0) return reject()
|
||||
|
||||
// pick the first found
|
||||
const user = items[0]
|
||||
|
||||
this.ldapClient.bind(user.dn, password, (error) => {
|
||||
if (error) return reject(error)
|
||||
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return await promise
|
||||
.then(() => true)
|
||||
.catch(() => {
|
||||
throw new Error('Invalid password.')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -29,12 +29,13 @@ export const setProcessVariables = async () => {
|
||||
process.sasLoc = process.env.SAS_PATH
|
||||
process.nodeLoc = process.env.NODE_PATH
|
||||
process.pythonLoc = process.env.PYTHON_PATH
|
||||
process.rLoc = process.env.R_PATH
|
||||
} else {
|
||||
const { sasLoc, nodeLoc, pythonLoc } = await getDesktopFields()
|
||||
|
||||
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
|
||||
process.sasLoc = sasLoc
|
||||
process.nodeLoc = nodeLoc
|
||||
process.pythonLoc = pythonLoc
|
||||
process.rLoc = rLoc
|
||||
}
|
||||
|
||||
const { SASJS_ROOT } = process.env
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
||||
import { getDesktopUserAutoExecPath, getFilesFolder } from './file'
|
||||
import {
|
||||
getDesktopUserAutoExecPath,
|
||||
getFilesFolder,
|
||||
getPackagesFolder
|
||||
} from './file'
|
||||
import { ModeType } from './verifyEnvVariables'
|
||||
|
||||
export const setupFolders = async () => {
|
||||
const drivePath = getFilesFolder()
|
||||
await createFolder(drivePath)
|
||||
await createFolder(getPackagesFolder())
|
||||
|
||||
if (process.env.MODE === ModeType.Desktop) {
|
||||
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
||||
|
||||
@@ -157,3 +157,30 @@ export const generateFileUploadPythonCode = async (
|
||||
|
||||
return uploadCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the R 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 generateFileUploadRCode = 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
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
export enum MOCK_SERVERTYPEType {
|
||||
SAS9 = 'sas9',
|
||||
SASVIYA = 'sasviya'
|
||||
}
|
||||
|
||||
export enum ModeType {
|
||||
Server = 'server',
|
||||
Desktop = 'desktop'
|
||||
}
|
||||
|
||||
export enum AuthProviderType {
|
||||
LDAP = 'ldap'
|
||||
}
|
||||
|
||||
export enum ProtocolType {
|
||||
HTTP = 'http',
|
||||
HTTPS = 'https'
|
||||
@@ -29,7 +38,8 @@ export enum LOG_FORMAT_MORGANType {
|
||||
export enum RunTimeType {
|
||||
SAS = 'sas',
|
||||
JS = 'js',
|
||||
PY = 'py'
|
||||
PY = 'py',
|
||||
R = 'r'
|
||||
}
|
||||
|
||||
export enum ReturnCode {
|
||||
@@ -40,6 +50,8 @@ export enum ReturnCode {
|
||||
export const verifyEnvVariables = (): ReturnCode => {
|
||||
const errors: string[] = []
|
||||
|
||||
errors.push(...verifyMOCK_SERVERTYPE())
|
||||
|
||||
errors.push(...verifyMODE())
|
||||
|
||||
errors.push(...verifyPROTOCOL())
|
||||
@@ -56,6 +68,8 @@ export const verifyEnvVariables = (): ReturnCode => {
|
||||
|
||||
errors.push(...verifyExecutablePaths())
|
||||
|
||||
errors.push(...verifyLDAPVariables())
|
||||
|
||||
if (errors.length) {
|
||||
process.logger?.error(
|
||||
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
||||
@@ -66,6 +80,23 @@ export const verifyEnvVariables = (): ReturnCode => {
|
||||
return ReturnCode.Success
|
||||
}
|
||||
|
||||
const verifyMOCK_SERVERTYPE = (): string[] => {
|
||||
const errors: string[] = []
|
||||
const { MOCK_SERVERTYPE } = process.env
|
||||
|
||||
if (MOCK_SERVERTYPE) {
|
||||
const modeTypes = Object.values(MOCK_SERVERTYPEType)
|
||||
if (!modeTypes.includes(MOCK_SERVERTYPE as MOCK_SERVERTYPEType))
|
||||
errors.push(
|
||||
`- MOCK_SERVERTYPE '${MOCK_SERVERTYPE}'\n - valid options ${modeTypes}`
|
||||
)
|
||||
} else {
|
||||
process.env.MOCK_SERVERTYPE = undefined
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const verifyMODE = (): string[] => {
|
||||
const errors: string[] = []
|
||||
const { MODE } = process.env
|
||||
@@ -79,13 +110,22 @@ const verifyMODE = (): string[] => {
|
||||
}
|
||||
|
||||
if (process.env.MODE === ModeType.Server) {
|
||||
const { DB_CONNECT } = process.env
|
||||
const { DB_CONNECT, AUTH_PROVIDERS } = process.env
|
||||
|
||||
if (process.env.NODE_ENV !== 'test')
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
if (!DB_CONNECT)
|
||||
errors.push(
|
||||
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
|
||||
)
|
||||
|
||||
if (AUTH_PROVIDERS) {
|
||||
const authProvidersType = Object.values(AuthProviderType)
|
||||
if (!authProvidersType.includes(AUTH_PROVIDERS as AuthProviderType))
|
||||
errors.push(
|
||||
`- AUTH_PROVIDERS '${AUTH_PROVIDERS}'\n - valid options ${authProvidersType}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
@@ -227,9 +267,10 @@ const verifyRUN_TIMES = (): string[] => {
|
||||
return errors
|
||||
}
|
||||
|
||||
const verifyExecutablePaths = () => {
|
||||
const verifyExecutablePaths = (): string[] => {
|
||||
const errors: string[] = []
|
||||
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, MODE } = process.env
|
||||
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
|
||||
process.env
|
||||
|
||||
if (MODE === ModeType.Server) {
|
||||
const runTimes = RUN_TIMES?.split(',')
|
||||
@@ -245,6 +286,57 @@ const verifyExecutablePaths = () => {
|
||||
if (runTimes?.includes(RunTimeType.PY) && !PYTHON_PATH) {
|
||||
errors.push(`- PYTHON_PATH is required for ${RunTimeType.PY} run time`)
|
||||
}
|
||||
|
||||
if (runTimes?.includes(RunTimeType.R) && !R_PATH) {
|
||||
errors.push(`- R_PATH is required for ${RunTimeType.R} run time`)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const verifyLDAPVariables = () => {
|
||||
const errors: string[] = []
|
||||
const {
|
||||
LDAP_URL,
|
||||
LDAP_BIND_DN,
|
||||
LDAP_BIND_PASSWORD,
|
||||
LDAP_USERS_BASE_DN,
|
||||
LDAP_GROUPS_BASE_DN,
|
||||
MODE,
|
||||
AUTH_PROVIDERS
|
||||
} = process.env
|
||||
|
||||
if (MODE === ModeType.Server && AUTH_PROVIDERS === AuthProviderType.LDAP) {
|
||||
if (!LDAP_URL) {
|
||||
errors.push(
|
||||
`- LDAP_URL is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!LDAP_BIND_DN) {
|
||||
errors.push(
|
||||
`- LDAP_BIND_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!LDAP_BIND_PASSWORD) {
|
||||
errors.push(
|
||||
`- LDAP_BIND_PASSWORD is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!LDAP_USERS_BASE_DN) {
|
||||
errors.push(
|
||||
`- LDAP_USERS_BASE_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
|
||||
if (!LDAP_GROUPS_BASE_DN) {
|
||||
errors.push(
|
||||
`- LDAP_GROUPS_BASE_DN is required for AUTH_PROVIDER '${AuthProviderType.LDAP}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
"name": "Auth",
|
||||
"description": "Operations about auth"
|
||||
},
|
||||
{
|
||||
"name": "Auth_Config",
|
||||
"description": "Operations about external auth providers"
|
||||
},
|
||||
{
|
||||
"name": "Client",
|
||||
"description": "Operations about clients"
|
||||
},
|
||||
{
|
||||
"name": "CODE",
|
||||
"name": "Code",
|
||||
"description": "Execution of code (various runtimes are supported)"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -78,6 +78,18 @@ const TreeViewNode = ({
|
||||
mouseY: number
|
||||
} | null>(null)
|
||||
|
||||
const launchProgram = () => {
|
||||
const baseUrl = window.location.origin
|
||||
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
||||
}
|
||||
|
||||
const launchProgramWithDebug = () => {
|
||||
const baseUrl = window.location.origin
|
||||
window.open(
|
||||
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
||||
)
|
||||
}
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -224,8 +236,8 @@ const TreeViewNode = ({
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{node.isFolder && (
|
||||
<div>
|
||||
{node.isFolder ? (
|
||||
<>
|
||||
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!node.relativePath}
|
||||
@@ -233,14 +245,21 @@ const TreeViewNode = ({
|
||||
>
|
||||
Add File
|
||||
</MenuItem>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem onClick={launchProgram}>Launch</MenuItem>
|
||||
<MenuItem onClick={launchProgramWithDebug}>
|
||||
Launch and Debug
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{!!node.relativePath && (
|
||||
<>
|
||||
<MenuItem onClick={handleRenameItemClick}>Rename</MenuItem>
|
||||
<MenuItem onClick={handleDeleteItemClick}>Delete</MenuItem>
|
||||
</>
|
||||
)}
|
||||
<MenuItem disabled={!node.relativePath} onClick={handleRenameItemClick}>
|
||||
Rename
|
||||
</MenuItem>
|
||||
<MenuItem disabled={!node.relativePath} onClick={handleDeleteItemClick}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
|
||||
151
web/src/containers/Settings/authConfig.tsx
Normal file
151
web/src/containers/Settings/authConfig.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
CircularProgress,
|
||||
Card,
|
||||
CardHeader,
|
||||
Divider,
|
||||
CardContent,
|
||||
TextField,
|
||||
CardActions,
|
||||
Button,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
const AuthConfig = () => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [authDetail, setAuthDetail] = useState<any>({})
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.get(`/SASjsApi/authConfig`)
|
||||
.then((res: any) => {
|
||||
setAuthDetail(res.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Failed: ' + err.response?.data || err.text, {
|
||||
theme: 'dark',
|
||||
position: toast.POSITION.BOTTOM_RIGHT
|
||||
})
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}, [])
|
||||
|
||||
const synchroniseWithLDAP = () => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post(`/SASjsApi/authConfig/synchroniseWithLDAP`)
|
||||
.then((res: any) => {
|
||||
const { userCount, groupCount } = res.data
|
||||
toast.success(
|
||||
`Imported ${userCount} ${
|
||||
userCount > 1 ? 'users' : 'user'
|
||||
} and ${groupCount} ${groupCount > 1 ? 'groups' : 'group'}`,
|
||||
{
|
||||
theme: 'dark',
|
||||
position: toast.POSITION.BOTTOM_RIGHT
|
||||
}
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Failed: ' + err.response?.data || err.text, {
|
||||
theme: 'dark',
|
||||
position: toast.POSITION.BOTTOM_RIGHT
|
||||
})
|
||||
})
|
||||
.finally(() => setIsLoading(false))
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<CircularProgress
|
||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||
/>
|
||||
) : (
|
||||
<Box>
|
||||
{Object.entries(authDetail).length === 0 && (
|
||||
<Typography>No external Auth Provider is used</Typography>
|
||||
)}
|
||||
{authDetail.ldap && (
|
||||
<Card>
|
||||
<CardHeader title="LDAP Authentication" />
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Grid container spacing={4}>
|
||||
<Grid item md={6} xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="LDAP_URL"
|
||||
name="LDAP_URL"
|
||||
value={authDetail.ldap.LDAP_URL}
|
||||
variant="outlined"
|
||||
disabled
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={6} xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="LDAP_BIND_DN"
|
||||
name="LDAP_BIND_DN"
|
||||
value={authDetail.ldap.LDAP_BIND_DN}
|
||||
variant="outlined"
|
||||
disabled={true}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={6} xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="LDAP_BIND_PASSWORD"
|
||||
name="LDAP_BIND_PASSWORD"
|
||||
type="password"
|
||||
value={authDetail.ldap.LDAP_BIND_PASSWORD}
|
||||
variant="outlined"
|
||||
disabled
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={6} xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="LDAP_USERS_BASE_DN"
|
||||
name="LDAP_USERS_BASE_DN"
|
||||
value={authDetail.ldap.LDAP_USERS_BASE_DN}
|
||||
variant="outlined"
|
||||
disabled={true}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item md={6} xs={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="LDAP_GROUPS_BASE_DN"
|
||||
name="LDAP_GROUPS_BASE_DN"
|
||||
value={authDetail.ldap.LDAP_GROUPS_BASE_DN}
|
||||
variant="outlined"
|
||||
disabled={true}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
onClick={synchroniseWithLDAP}
|
||||
>
|
||||
Synchronise
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthConfig
|
||||
@@ -7,8 +7,10 @@ import TabPanel from '@mui/lab/TabPanel'
|
||||
|
||||
import Permission from './permission'
|
||||
import Profile from './profile'
|
||||
import AuthConfig from './authConfig'
|
||||
|
||||
import { AppContext, ModeType } from '../../context/appContext'
|
||||
import PermissionsContextProvider from '../../context/permissionsContext'
|
||||
|
||||
const StyledTab = styled(Tab)({
|
||||
background: 'black',
|
||||
@@ -58,13 +60,21 @@ const Settings = () => {
|
||||
{appContext.mode === ModeType.Server && (
|
||||
<StyledTab label="Permissions" value="permission" />
|
||||
)}
|
||||
{appContext.mode === ModeType.Server && appContext.isAdmin && (
|
||||
<StyledTab label="Auth Config" value="auth_config" />
|
||||
)}
|
||||
</TabList>
|
||||
</Box>
|
||||
<StyledTabpanel value="profile">
|
||||
<Profile />
|
||||
</StyledTabpanel>
|
||||
<StyledTabpanel value="permission">
|
||||
<Permission />
|
||||
<PermissionsContextProvider>
|
||||
<Permission />
|
||||
</PermissionsContextProvider>
|
||||
</StyledTabpanel>
|
||||
<StyledTabpanel value="auth_config">
|
||||
<AuthConfig />
|
||||
</StyledTabpanel>
|
||||
</TabContext>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import { IconButton, Tooltip } from '@mui/material'
|
||||
import { Add } from '@mui/icons-material'
|
||||
import { RegisterPermissionPayload } from '../../../../utils/types'
|
||||
import AddPermissionModal from './addPermissionModal'
|
||||
|
||||
type Props = {
|
||||
openModal: boolean
|
||||
setOpenModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||
addPermission: (
|
||||
permissionsToAdd: RegisterPermissionPayload[],
|
||||
permissionType: string,
|
||||
principalType: string,
|
||||
principal: string,
|
||||
permissionSetting: string
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
const AddPermission = ({ openModal, setOpenModal, addPermission }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
sx={{ marginLeft: 'auto' }}
|
||||
title="Add Permission"
|
||||
placement="bottom-end"
|
||||
>
|
||||
<IconButton onClick={() => setOpenModal(true)}>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<AddPermissionModal
|
||||
open={openModal}
|
||||
handleOpen={setOpenModal}
|
||||
addPermission={addPermission}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddPermission
|
||||
@@ -3,31 +3,21 @@ import axios from 'axios'
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
CircularProgress,
|
||||
Autocomplete
|
||||
} from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
||||
import { BootstrapDialog } from '../../../../components/modal'
|
||||
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||
|
||||
import {
|
||||
UserResponse,
|
||||
GroupResponse,
|
||||
RegisterPermissionPayload
|
||||
} from '../../utils/types'
|
||||
|
||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
padding: theme.spacing(1)
|
||||
}
|
||||
}))
|
||||
} from '../../../../utils/types'
|
||||
|
||||
type AddPermissionModalProps = {
|
||||
open: boolean
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useState } from 'react'
|
||||
import { Typography, Popover } from '@mui/material'
|
||||
import { GroupDetailsResponse } from '../../../../utils/types'
|
||||
|
||||
type DisplayGroupProps = {
|
||||
group: GroupDetailsResponse
|
||||
}
|
||||
|
||||
const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
|
||||
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography
|
||||
aria-owns={open ? 'mouse-over-popover' : undefined}
|
||||
aria-haspopup="true"
|
||||
onMouseEnter={handlePopoverOpen}
|
||||
onMouseLeave={handlePopoverClose}
|
||||
>
|
||||
{group.name}
|
||||
</Typography>
|
||||
<Popover
|
||||
id="mouse-over-popover"
|
||||
sx={{
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
onClose={handlePopoverClose}
|
||||
disableRestoreFocus
|
||||
>
|
||||
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
||||
Group Members
|
||||
</Typography>
|
||||
{group.users.map((user, index) => (
|
||||
<Typography key={index} sx={{ p: 1 }} component="li">
|
||||
{user.username}
|
||||
</Typography>
|
||||
))}
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DisplayGroup
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { Dispatch, SetStateAction, useState } from 'react'
|
||||
import { IconButton, Tooltip } from '@mui/material'
|
||||
import { FilterList } from '@mui/icons-material'
|
||||
import { PermissionResponse } from '../../../../utils/types'
|
||||
import PermissionFilterModal from './permissionFilterModal'
|
||||
import { PrincipalType } from '../hooks/usePermission'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||
permissions: PermissionResponse[]
|
||||
applyFilter: (
|
||||
pathFilter: string[],
|
||||
principalFilter: string[],
|
||||
principalTypeFilter: PrincipalType[],
|
||||
settingFilter: string[]
|
||||
) => void
|
||||
resetFilter: () => void
|
||||
}
|
||||
|
||||
const FilterPermissions = ({
|
||||
open,
|
||||
handleOpen,
|
||||
permissions,
|
||||
applyFilter,
|
||||
resetFilter
|
||||
}: Props) => {
|
||||
const [pathFilter, setPathFilter] = useState<string[]>([])
|
||||
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
||||
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
||||
PrincipalType[]
|
||||
>([])
|
||||
const [settingFilter, setSettingFilter] = useState<string[]>([])
|
||||
const handleApplyFilter = () => {
|
||||
applyFilter(pathFilter, principalFilter, principalTypeFilter, settingFilter)
|
||||
}
|
||||
|
||||
const handleResetFilter = () => {
|
||||
setPathFilter([])
|
||||
setPrincipalFilter([])
|
||||
setPrincipalFilter([])
|
||||
setSettingFilter([])
|
||||
resetFilter()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Filter Permissions">
|
||||
<IconButton onClick={() => handleOpen(true)}>
|
||||
<FilterList />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<PermissionFilterModal
|
||||
open={open}
|
||||
handleOpen={handleOpen}
|
||||
permissions={permissions}
|
||||
pathFilter={pathFilter}
|
||||
setPathFilter={setPathFilter}
|
||||
principalFilter={principalFilter}
|
||||
setPrincipalFilter={setPrincipalFilter}
|
||||
principalTypeFilter={principalTypeFilter}
|
||||
setPrincipalTypeFilter={setPrincipalTypeFilter}
|
||||
settingFilter={settingFilter}
|
||||
setSettingFilter={setSettingFilter}
|
||||
applyFilter={handleApplyFilter}
|
||||
resetFilter={handleResetFilter}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilterPermissions
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
import { styled } from '@mui/material/styles'
|
||||
import Autocomplete from '@mui/material/Autocomplete'
|
||||
|
||||
import { PermissionResponse } from '../../utils/types'
|
||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
||||
import { PrincipalType } from './permission'
|
||||
import { PermissionResponse } from '../../../../utils/types'
|
||||
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||
import { PrincipalType } from '../hooks/usePermission'
|
||||
|
||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
@@ -2,9 +2,9 @@ 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'
|
||||
import { BootstrapDialog } from '../../../../components/modal'
|
||||
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||
import { PermissionResponse } from '../../../../utils/types'
|
||||
|
||||
export interface PermissionResponsePayload {
|
||||
permissionType: string
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useContext } from 'react'
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
||||
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
import { PermissionResponse } from '../../../../utils/types'
|
||||
|
||||
import { AppContext } from '../../../../context/appContext'
|
||||
import { displayPrincipal, displayPrincipalType } from '../helper'
|
||||
|
||||
const BootstrapTableCell = styled(TableCell)({
|
||||
textAlign: 'left'
|
||||
})
|
||||
|
||||
export enum PrincipalType {
|
||||
User = 'User',
|
||||
Group = 'Group'
|
||||
}
|
||||
|
||||
type PermissionTableProps = {
|
||||
permissions: PermissionResponse[]
|
||||
handleUpdatePermissionClick: (permission: PermissionResponse) => void
|
||||
handleDeletePermissionClick: (permission: PermissionResponse) => void
|
||||
}
|
||||
|
||||
const PermissionTable = ({
|
||||
permissions,
|
||||
handleUpdatePermissionClick,
|
||||
handleDeletePermissionClick
|
||||
}: PermissionTableProps) => {
|
||||
const appContext = useContext(AppContext)
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }}>
|
||||
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
||||
<TableRow>
|
||||
<BootstrapTableCell>Path</BootstrapTableCell>
|
||||
<BootstrapTableCell>Permission Type</BootstrapTableCell>
|
||||
<BootstrapTableCell>Principal</BootstrapTableCell>
|
||||
<BootstrapTableCell>Principal Type</BootstrapTableCell>
|
||||
<BootstrapTableCell>Setting</BootstrapTableCell>
|
||||
{appContext.isAdmin && (
|
||||
<BootstrapTableCell>Action</BootstrapTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{permissions.map((permission) => (
|
||||
<TableRow key={permission.permissionId}>
|
||||
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
|
||||
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
|
||||
<BootstrapTableCell>
|
||||
{displayPrincipal(permission)}
|
||||
</BootstrapTableCell>
|
||||
<BootstrapTableCell>
|
||||
{displayPrincipalType(permission)}
|
||||
</BootstrapTableCell>
|
||||
<BootstrapTableCell>{permission.setting}</BootstrapTableCell>
|
||||
{appContext.isAdmin && (
|
||||
<BootstrapTableCell>
|
||||
<Tooltip title="Edit Permission">
|
||||
<IconButton
|
||||
onClick={() => handleUpdatePermissionClick(permission)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete Permission">
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() => handleDeletePermissionClick(permission)}
|
||||
>
|
||||
<DeleteForeverIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</BootstrapTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default PermissionTable
|
||||
@@ -2,26 +2,17 @@ import React, { useState, Dispatch, SetStateAction, useEffect } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField
|
||||
} from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
import Autocomplete from '@mui/material/Autocomplete'
|
||||
|
||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
||||
import { BootstrapDialog } from '../../../../components/modal'
|
||||
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||
|
||||
import { PermissionResponse } from '../../utils/types'
|
||||
|
||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
padding: theme.spacing(1)
|
||||
}
|
||||
}))
|
||||
import { PermissionResponse } from '../../../../utils/types'
|
||||
|
||||
type UpdatePermissionModalProps = {
|
||||
open: boolean
|
||||
13
web/src/containers/Settings/internal/helper.tsx
Normal file
13
web/src/containers/Settings/internal/helper.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { PermissionResponse } from '../../../utils/types'
|
||||
import { PrincipalType } from './hooks/usePermission'
|
||||
import DisplayGroup from './components/displayGroup'
|
||||
|
||||
export const displayPrincipal = (permission: PermissionResponse) => {
|
||||
if (permission.user) return permission.user.username
|
||||
if (permission.group) return <DisplayGroup group={permission.group} />
|
||||
}
|
||||
|
||||
export const displayPrincipalType = (permission: PermissionResponse) => {
|
||||
if (permission.user) return PrincipalType.User
|
||||
if (permission.group) return PrincipalType.Group
|
||||
}
|
||||
109
web/src/containers/Settings/internal/hooks/useAddPermission.tsx
Normal file
109
web/src/containers/Settings/internal/hooks/useAddPermission.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import axios from 'axios'
|
||||
import { useState, useContext } from 'react'
|
||||
import {
|
||||
PermissionResponse,
|
||||
RegisterPermissionPayload
|
||||
} from '../../../../utils/types'
|
||||
import AddPermission from '../components/addPermission'
|
||||
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||
import {
|
||||
findExistingPermission,
|
||||
findUpdatingPermission
|
||||
} from '../../../../utils/helper'
|
||||
|
||||
const useAddPermission = () => {
|
||||
const {
|
||||
permissions,
|
||||
fetchPermissions,
|
||||
setIsLoading,
|
||||
setPermissionResponsePayload,
|
||||
setOpenPermissionResponseModal
|
||||
} = useContext(PermissionsContext)
|
||||
|
||||
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
||||
|
||||
const addPermission = async (
|
||||
permissionsToAdd: RegisterPermissionPayload[],
|
||||
permissionType: string,
|
||||
principalType: string,
|
||||
principal: string,
|
||||
permissionSetting: string
|
||||
) => {
|
||||
setAddPermissionModalOpen(false)
|
||||
setIsLoading(true)
|
||||
|
||||
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 AddPermissionButton = () => (
|
||||
<AddPermission
|
||||
openModal={addPermissionModalOpen}
|
||||
setOpenModal={setAddPermissionModalOpen}
|
||||
addPermission={addPermission}
|
||||
/>
|
||||
)
|
||||
|
||||
return { AddPermissionButton, setAddPermissionModalOpen }
|
||||
}
|
||||
|
||||
export default useAddPermission
|
||||
@@ -0,0 +1,61 @@
|
||||
import axios from 'axios'
|
||||
import { useState, useContext } from 'react'
|
||||
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||
import DeleteConfirmationModal from '../../../../components/deleteConfirmationModal'
|
||||
|
||||
const useDeletePermissionModal = () => {
|
||||
const {
|
||||
selectedPermission,
|
||||
setSelectedPermission,
|
||||
fetchPermissions,
|
||||
setIsLoading,
|
||||
setSnackbarMessage,
|
||||
setSnackbarSeverity,
|
||||
setOpenSnackbar,
|
||||
setModalTitle,
|
||||
setModalPayload,
|
||||
setOpenModal
|
||||
} = useContext(PermissionsContext)
|
||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||
useState(false)
|
||||
|
||||
const deletePermission = () => {
|
||||
setDeleteConfirmationModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
||||
.then((res: any) => {
|
||||
fetchPermissions()
|
||||
setSnackbarMessage('Permission deleted!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setSelectedPermission(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
const DeletePermissionDialog = () => (
|
||||
<DeleteConfirmationModal
|
||||
open={deleteConfirmationModalOpen}
|
||||
setOpen={setDeleteConfirmationModalOpen}
|
||||
message="Are you sure you want to delete this permission?"
|
||||
_delete={deletePermission}
|
||||
/>
|
||||
)
|
||||
|
||||
return { DeletePermissionDialog, setDeleteConfirmationModalOpen }
|
||||
}
|
||||
|
||||
export default useDeletePermissionModal
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useState, useContext } from 'react'
|
||||
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||
import { PrincipalType } from './usePermission'
|
||||
import FilterPermissions from '../components/filterPermissions'
|
||||
|
||||
const useFilterPermissions = () => {
|
||||
const { permissions, setFilteredPermissions, setFilterApplied } =
|
||||
useContext(PermissionsContext)
|
||||
|
||||
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||
|
||||
/**
|
||||
* first find the permissions w.r.t each filter type
|
||||
* take intersection of resultant arrays
|
||||
*/
|
||||
const applyFilter = (
|
||||
pathFilter: string[],
|
||||
principalFilter: string[],
|
||||
principalTypeFilter: PrincipalType[],
|
||||
settingFilter: string[]
|
||||
) => {
|
||||
setFilterModalOpen(false)
|
||||
|
||||
const uriFilteredPermissions =
|
||||
pathFilter.length > 0
|
||||
? permissions.filter((permission) =>
|
||||
pathFilter.includes(permission.path)
|
||||
)
|
||||
: permissions
|
||||
|
||||
const principalFilteredPermissions =
|
||||
principalFilter.length > 0
|
||||
? permissions.filter((permission) => {
|
||||
if (permission.user) {
|
||||
return principalFilter.includes(permission.user.username)
|
||||
}
|
||||
if (permission.group) {
|
||||
return principalFilter.includes(permission.group.name)
|
||||
}
|
||||
return false
|
||||
})
|
||||
: permissions
|
||||
|
||||
const principalTypeFilteredPermissions =
|
||||
principalTypeFilter.length > 0
|
||||
? permissions.filter((permission) => {
|
||||
if (permission.user) {
|
||||
return principalTypeFilter.includes(PrincipalType.User)
|
||||
}
|
||||
if (permission.group) {
|
||||
return principalTypeFilter.includes(PrincipalType.Group)
|
||||
}
|
||||
return false
|
||||
})
|
||||
: permissions
|
||||
|
||||
const settingFilteredPermissions =
|
||||
settingFilter.length > 0
|
||||
? permissions.filter((permission) =>
|
||||
settingFilter.includes(permission.setting)
|
||||
)
|
||||
: permissions
|
||||
|
||||
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
||||
principalFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
)
|
||||
|
||||
filteredArray = filteredArray.filter((permission) =>
|
||||
principalTypeFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
)
|
||||
|
||||
filteredArray = filteredArray.filter((permission) =>
|
||||
settingFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
)
|
||||
|
||||
setFilteredPermissions(filteredArray)
|
||||
setFilterApplied(true)
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
setFilterModalOpen(false)
|
||||
setFilterApplied(false)
|
||||
setFilteredPermissions([])
|
||||
}
|
||||
|
||||
const FilterPermissionsButton = () => (
|
||||
<FilterPermissions
|
||||
open={filterModalOpen}
|
||||
handleOpen={setFilterModalOpen}
|
||||
permissions={permissions}
|
||||
applyFilter={applyFilter}
|
||||
resetFilter={resetFilter}
|
||||
/>
|
||||
)
|
||||
|
||||
return { FilterPermissionsButton }
|
||||
}
|
||||
|
||||
export default useFilterPermissions
|
||||
71
web/src/containers/Settings/internal/hooks/usePermission.ts
Normal file
71
web/src/containers/Settings/internal/hooks/usePermission.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useContext, useEffect } from 'react'
|
||||
import { AppContext } from '../../../../context/appContext'
|
||||
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||
import { PermissionResponse } from '../../../../utils/types'
|
||||
import useAddPermission from './useAddPermission'
|
||||
import useUpdatePermissionModal from './useUpdatePermissionModal'
|
||||
import useDeletePermissionModal from './useDeletePermissionModal'
|
||||
import useFilterPermissions from './useFilterPermissions'
|
||||
|
||||
export enum PrincipalType {
|
||||
User = 'User',
|
||||
Group = 'Group'
|
||||
}
|
||||
|
||||
const usePermission = () => {
|
||||
const { isAdmin } = useContext(AppContext)
|
||||
const {
|
||||
filterApplied,
|
||||
filteredPermissions,
|
||||
isLoading,
|
||||
permissions,
|
||||
Dialog,
|
||||
Snackbar,
|
||||
PermissionResponseDialog,
|
||||
fetchPermissions,
|
||||
setSelectedPermission
|
||||
} = useContext(PermissionsContext)
|
||||
|
||||
const { AddPermissionButton } = useAddPermission()
|
||||
|
||||
const { UpdatePermissionDialog, setUpdatePermissionModalOpen } =
|
||||
useUpdatePermissionModal()
|
||||
|
||||
const { DeletePermissionDialog, setDeleteConfirmationModalOpen } =
|
||||
useDeletePermissionModal()
|
||||
|
||||
const { FilterPermissionsButton } = useFilterPermissions()
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchPermissions) fetchPermissions()
|
||||
}, [fetchPermissions])
|
||||
|
||||
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
||||
setSelectedPermission(permission)
|
||||
setUpdatePermissionModalOpen(true)
|
||||
}
|
||||
|
||||
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
||||
setSelectedPermission(permission)
|
||||
setDeleteConfirmationModalOpen(true)
|
||||
}
|
||||
|
||||
return {
|
||||
filterApplied,
|
||||
filteredPermissions,
|
||||
isAdmin,
|
||||
isLoading,
|
||||
permissions,
|
||||
AddPermissionButton,
|
||||
UpdatePermissionDialog,
|
||||
DeletePermissionDialog,
|
||||
FilterPermissionsButton,
|
||||
handleDeletePermissionClick,
|
||||
handleUpdatePermissionClick,
|
||||
PermissionResponseDialog,
|
||||
Dialog,
|
||||
Snackbar
|
||||
}
|
||||
}
|
||||
|
||||
export default usePermission
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useState } from 'react'
|
||||
import PermissionResponseModal, {
|
||||
PermissionResponsePayload
|
||||
} from '../components/permissionResponseModal'
|
||||
|
||||
const usePermissionResponseModal = () => {
|
||||
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
|
||||
useState(false)
|
||||
const [permissionResponsePayload, setPermissionResponsePayload] =
|
||||
useState<PermissionResponsePayload>({
|
||||
permissionType: '',
|
||||
principalType: '',
|
||||
principal: '',
|
||||
permissionSetting: '',
|
||||
existingPermissions: [],
|
||||
newAddedPermissions: [],
|
||||
updatedPermissions: [],
|
||||
errorPaths: []
|
||||
})
|
||||
|
||||
const PermissionResponseDialog = () => (
|
||||
<PermissionResponseModal
|
||||
open={openPermissionResponseModal}
|
||||
setOpen={setOpenPermissionResponseModal}
|
||||
payload={permissionResponsePayload}
|
||||
/>
|
||||
)
|
||||
|
||||
return {
|
||||
PermissionResponseDialog,
|
||||
setOpenPermissionResponseModal,
|
||||
setPermissionResponsePayload
|
||||
}
|
||||
}
|
||||
|
||||
export default usePermissionResponseModal
|
||||
@@ -0,0 +1,63 @@
|
||||
import axios from 'axios'
|
||||
import { useState, useContext } from 'react'
|
||||
import UpdatePermissionModal from '../components/updatePermissionModal'
|
||||
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||
|
||||
const useUpdatePermissionModal = () => {
|
||||
const {
|
||||
selectedPermission,
|
||||
setSelectedPermission,
|
||||
fetchPermissions,
|
||||
setIsLoading,
|
||||
setSnackbarMessage,
|
||||
setSnackbarSeverity,
|
||||
setOpenSnackbar,
|
||||
setModalTitle,
|
||||
setModalPayload,
|
||||
setOpenModal
|
||||
} = useContext(PermissionsContext)
|
||||
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
||||
useState(false)
|
||||
|
||||
const updatePermission = (setting: string) => {
|
||||
setUpdatePermissionModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
|
||||
setting
|
||||
})
|
||||
.then((res: any) => {
|
||||
fetchPermissions()
|
||||
setSnackbarMessage('Permission updated!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setSelectedPermission(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
const UpdatePermissionDialog = () => (
|
||||
<UpdatePermissionModal
|
||||
open={updatePermissionModalOpen}
|
||||
handleOpen={setUpdatePermissionModalOpen}
|
||||
permission={selectedPermission}
|
||||
updatePermission={updatePermission}
|
||||
/>
|
||||
)
|
||||
|
||||
return { UpdatePermissionDialog, setUpdatePermissionModalOpen }
|
||||
}
|
||||
|
||||
export default useUpdatePermissionModal
|
||||
@@ -1,54 +1,7 @@
|
||||
import React, { useState, useEffect, useContext, useCallback } from 'react'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Grid,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Popover
|
||||
} from '@mui/material'
|
||||
|
||||
import FilterListIcon from '@mui/icons-material/FilterList'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import EditIcon from '@mui/icons-material/Edit'
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
||||
|
||||
import { Box, Paper, Grid, CircularProgress } from '@mui/material'
|
||||
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 DeleteConfirmationModal from '../../components/deleteConfirmationModal'
|
||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||
|
||||
import {
|
||||
GroupDetailsResponse,
|
||||
PermissionResponse,
|
||||
RegisterPermissionPayload
|
||||
} from '../../utils/types'
|
||||
import {
|
||||
findExistingPermission,
|
||||
findUpdatingPermission
|
||||
} from '../../utils/helper'
|
||||
|
||||
import { AppContext } from '../../context/appContext'
|
||||
|
||||
const BootstrapTableCell = styled(TableCell)({
|
||||
textAlign: 'left'
|
||||
})
|
||||
import PermissionTable from './internal/components/permissionTable'
|
||||
import usePermission from './internal/hooks/usePermission'
|
||||
|
||||
const BootstrapGridItem = styled(Grid)({
|
||||
'&.MuiGrid-item': {
|
||||
@@ -56,298 +9,23 @@ const BootstrapGridItem = styled(Grid)({
|
||||
}
|
||||
})
|
||||
|
||||
export enum PrincipalType {
|
||||
User = 'User',
|
||||
Group = 'Group'
|
||||
}
|
||||
|
||||
const Permission = () => {
|
||||
const appContext = useContext(AppContext)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [openModal, setOpenModal] = useState(false)
|
||||
const [modalTitle, setModalTitle] = useState('')
|
||||
const [modalPayload, setModalPayload] = useState('')
|
||||
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||
AlertSeverityType.Success
|
||||
)
|
||||
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
||||
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
|
||||
useState(false)
|
||||
const [permissionResponsePayload, setPermissionResponsePayload] =
|
||||
useState<PermissionResponsePayload>({
|
||||
permissionType: '',
|
||||
principalType: '',
|
||||
principal: '',
|
||||
permissionSetting: '',
|
||||
existingPermissions: [],
|
||||
newAddedPermissions: [],
|
||||
updatedPermissions: [],
|
||||
errorPaths: []
|
||||
})
|
||||
|
||||
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
||||
useState(false)
|
||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||
useState(false)
|
||||
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
||||
useState('')
|
||||
const [selectedPermission, setSelectedPermission] =
|
||||
useState<PermissionResponse>()
|
||||
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||
const [pathFilter, setPathFilter] = useState<string[]>([])
|
||||
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
||||
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
||||
PrincipalType[]
|
||||
>([])
|
||||
const [settingFilter, setSettingFilter] = useState<string[]>([])
|
||||
const [permissions, setPermissions] = useState<PermissionResponse[]>([])
|
||||
const [filteredPermissions, setFilteredPermissions] = useState<
|
||||
PermissionResponse[]
|
||||
>([])
|
||||
const [filterApplied, setFilterApplied] = useState(false)
|
||||
|
||||
const fetchPermissions = useCallback(() => {
|
||||
axios
|
||||
.get(`/SASjsApi/permission`)
|
||||
.then((res: any) => {
|
||||
if (res.data?.length > 0) {
|
||||
setPermissions(res.data)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchPermissions()
|
||||
}, [fetchPermissions])
|
||||
|
||||
/**
|
||||
* first find the permissions w.r.t each filter type
|
||||
* take intersection of resultant arrays
|
||||
*/
|
||||
const applyFilter = () => {
|
||||
setFilterModalOpen(false)
|
||||
|
||||
const uriFilteredPermissions =
|
||||
pathFilter.length > 0
|
||||
? permissions.filter((permission) =>
|
||||
pathFilter.includes(permission.path)
|
||||
)
|
||||
: permissions
|
||||
|
||||
const principalFilteredPermissions =
|
||||
principalFilter.length > 0
|
||||
? permissions.filter((permission) => {
|
||||
if (permission.user) {
|
||||
return principalFilter.includes(permission.user.username)
|
||||
}
|
||||
if (permission.group) {
|
||||
return principalFilter.includes(permission.group.name)
|
||||
}
|
||||
return false
|
||||
})
|
||||
: permissions
|
||||
|
||||
const principalTypeFilteredPermissions =
|
||||
principalTypeFilter.length > 0
|
||||
? permissions.filter((permission) => {
|
||||
if (permission.user) {
|
||||
return principalTypeFilter.includes(PrincipalType.User)
|
||||
}
|
||||
if (permission.group) {
|
||||
return principalTypeFilter.includes(PrincipalType.Group)
|
||||
}
|
||||
return false
|
||||
})
|
||||
: permissions
|
||||
|
||||
const settingFilteredPermissions =
|
||||
settingFilter.length > 0
|
||||
? permissions.filter((permission) =>
|
||||
settingFilter.includes(permission.setting)
|
||||
)
|
||||
: permissions
|
||||
|
||||
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
||||
principalFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
)
|
||||
|
||||
filteredArray = filteredArray.filter((permission) =>
|
||||
principalTypeFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
)
|
||||
|
||||
filteredArray = filteredArray.filter((permission) =>
|
||||
settingFilteredPermissions.some(
|
||||
(item) => item.permissionId === permission.permissionId
|
||||
)
|
||||
)
|
||||
|
||||
setFilteredPermissions(filteredArray)
|
||||
setFilterApplied(true)
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
setFilterModalOpen(false)
|
||||
setPathFilter([])
|
||||
setPrincipalFilter([])
|
||||
setSettingFilter([])
|
||||
setFilteredPermissions([])
|
||||
setFilterApplied(false)
|
||||
}
|
||||
|
||||
const addPermission = async (
|
||||
permissionsToAdd: RegisterPermissionPayload[],
|
||||
permissionType: string,
|
||||
principalType: string,
|
||||
principal: string,
|
||||
permissionSetting: string
|
||||
) => {
|
||||
setAddPermissionModalOpen(false)
|
||||
setIsLoading(true)
|
||||
|
||||
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) => {
|
||||
setSelectedPermission(permission)
|
||||
setUpdatePermissionModalOpen(true)
|
||||
}
|
||||
|
||||
const updatePermission = (setting: string) => {
|
||||
setUpdatePermissionModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
|
||||
setting
|
||||
})
|
||||
.then((res: any) => {
|
||||
fetchPermissions()
|
||||
setSnackbarMessage('Permission updated!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setSelectedPermission(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
||||
setSelectedPermission(permission)
|
||||
setDeleteConfirmationModalOpen(true)
|
||||
setDeleteConfirmationModalMessage(
|
||||
'Are you sure you want to delete this permission?'
|
||||
)
|
||||
}
|
||||
|
||||
const deletePermission = () => {
|
||||
setDeleteConfirmationModalOpen(false)
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
||||
.then((res: any) => {
|
||||
fetchPermissions()
|
||||
setSnackbarMessage('Permission deleted!')
|
||||
setSnackbarSeverity(AlertSeverityType.Success)
|
||||
setOpenSnackbar(true)
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
setSelectedPermission(undefined)
|
||||
})
|
||||
}
|
||||
const {
|
||||
filterApplied,
|
||||
filteredPermissions,
|
||||
isAdmin,
|
||||
isLoading,
|
||||
permissions,
|
||||
AddPermissionButton,
|
||||
UpdatePermissionDialog,
|
||||
DeletePermissionDialog,
|
||||
FilterPermissionsButton,
|
||||
handleDeletePermissionClick,
|
||||
handleUpdatePermissionClick,
|
||||
PermissionResponseDialog,
|
||||
Dialog,
|
||||
Snackbar
|
||||
} = usePermission()
|
||||
|
||||
return isLoading ? (
|
||||
<CircularProgress
|
||||
@@ -358,22 +36,8 @@ const Permission = () => {
|
||||
<Grid container direction="column" spacing={1}>
|
||||
<BootstrapGridItem item xs={12}>
|
||||
<Paper elevation={3} sx={{ display: 'flex' }}>
|
||||
<Tooltip title="Filter Permissions">
|
||||
<IconButton onClick={() => setFilterModalOpen(true)}>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{appContext.isAdmin && (
|
||||
<Tooltip
|
||||
sx={{ marginLeft: 'auto' }}
|
||||
title="Add Permission"
|
||||
placement="bottom-end"
|
||||
>
|
||||
<IconButton onClick={() => setAddPermissionModalOpen(true)}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<FilterPermissionsButton />
|
||||
{isAdmin && <AddPermissionButton />}
|
||||
</Paper>
|
||||
</BootstrapGridItem>
|
||||
<BootstrapGridItem item xs={12}>
|
||||
@@ -384,192 +48,13 @@ const Permission = () => {
|
||||
/>
|
||||
</BootstrapGridItem>
|
||||
</Grid>
|
||||
<BootstrapSnackbar
|
||||
open={openSnackbar}
|
||||
setOpen={setOpenSnackbar}
|
||||
message={snackbarMessage}
|
||||
severity={snackbarSeverity}
|
||||
/>
|
||||
<Modal
|
||||
open={openModal}
|
||||
setOpen={setOpenModal}
|
||||
title={modalTitle}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
<PermissionFilterModal
|
||||
open={filterModalOpen}
|
||||
handleOpen={setFilterModalOpen}
|
||||
permissions={permissions}
|
||||
pathFilter={pathFilter}
|
||||
setPathFilter={setPathFilter}
|
||||
principalFilter={principalFilter}
|
||||
setPrincipalFilter={setPrincipalFilter}
|
||||
principalTypeFilter={principalTypeFilter}
|
||||
setPrincipalTypeFilter={setPrincipalTypeFilter}
|
||||
settingFilter={settingFilter}
|
||||
setSettingFilter={setSettingFilter}
|
||||
applyFilter={applyFilter}
|
||||
resetFilter={resetFilter}
|
||||
/>
|
||||
<AddPermissionModal
|
||||
open={addPermissionModalOpen}
|
||||
handleOpen={setAddPermissionModalOpen}
|
||||
addPermission={addPermission}
|
||||
/>
|
||||
<PermissionResponseModal
|
||||
open={openPermissionResponseModal}
|
||||
setOpen={setOpenPermissionResponseModal}
|
||||
payload={permissionResponsePayload}
|
||||
/>
|
||||
<UpdatePermissionModal
|
||||
open={updatePermissionModalOpen}
|
||||
handleOpen={setUpdatePermissionModalOpen}
|
||||
permission={selectedPermission}
|
||||
updatePermission={updatePermission}
|
||||
/>
|
||||
<DeleteConfirmationModal
|
||||
open={deleteConfirmationModalOpen}
|
||||
setOpen={setDeleteConfirmationModalOpen}
|
||||
message={deleteConfirmationModalMessage}
|
||||
_delete={deletePermission}
|
||||
/>
|
||||
<PermissionResponseDialog />
|
||||
<UpdatePermissionDialog />
|
||||
<DeletePermissionDialog />
|
||||
<Dialog />
|
||||
<Snackbar />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Permission
|
||||
|
||||
type PermissionTableProps = {
|
||||
permissions: PermissionResponse[]
|
||||
handleUpdatePermissionClick: (permission: PermissionResponse) => void
|
||||
handleDeletePermissionClick: (permission: PermissionResponse) => void
|
||||
}
|
||||
|
||||
const PermissionTable = ({
|
||||
permissions,
|
||||
handleUpdatePermissionClick,
|
||||
handleDeletePermissionClick
|
||||
}: PermissionTableProps) => {
|
||||
const appContext = useContext(AppContext)
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }}>
|
||||
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
||||
<TableRow>
|
||||
<BootstrapTableCell>Path</BootstrapTableCell>
|
||||
<BootstrapTableCell>Permission Type</BootstrapTableCell>
|
||||
<BootstrapTableCell>Principal</BootstrapTableCell>
|
||||
<BootstrapTableCell>Principal Type</BootstrapTableCell>
|
||||
<BootstrapTableCell>Setting</BootstrapTableCell>
|
||||
{appContext.isAdmin && (
|
||||
<BootstrapTableCell>Action</BootstrapTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{permissions.map((permission) => (
|
||||
<TableRow key={permission.permissionId}>
|
||||
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
|
||||
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
|
||||
<BootstrapTableCell>
|
||||
{displayPrincipal(permission)}
|
||||
</BootstrapTableCell>
|
||||
<BootstrapTableCell>
|
||||
{displayPrincipalType(permission)}
|
||||
</BootstrapTableCell>
|
||||
<BootstrapTableCell>{permission.setting}</BootstrapTableCell>
|
||||
{appContext.isAdmin && (
|
||||
<BootstrapTableCell>
|
||||
<Tooltip title="Edit Permission">
|
||||
<IconButton
|
||||
onClick={() => handleUpdatePermissionClick(permission)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete Permission">
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() => handleDeletePermissionClick(permission)}
|
||||
>
|
||||
<DeleteForeverIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</BootstrapTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const displayPrincipal = (permission: PermissionResponse) => {
|
||||
if (permission.user) return permission.user.username
|
||||
if (permission.group) return <DisplayGroup group={permission.group} />
|
||||
}
|
||||
|
||||
type DisplayGroupProps = {
|
||||
group: GroupDetailsResponse
|
||||
}
|
||||
|
||||
const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
|
||||
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handlePopoverClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography
|
||||
aria-owns={open ? 'mouse-over-popover' : undefined}
|
||||
aria-haspopup="true"
|
||||
onMouseEnter={handlePopoverOpen}
|
||||
onMouseLeave={handlePopoverClose}
|
||||
>
|
||||
{group.name}
|
||||
</Typography>
|
||||
<Popover
|
||||
id="mouse-over-popover"
|
||||
sx={{
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
onClose={handlePopoverClose}
|
||||
disableRestoreFocus
|
||||
>
|
||||
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
||||
Group Members
|
||||
</Typography>
|
||||
{group.users.map((user, index) => (
|
||||
<Typography key={index} sx={{ p: 1 }} component="li">
|
||||
{user.username}
|
||||
</Typography>
|
||||
))}
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const displayPrincipalType = (permission: PermissionResponse) => {
|
||||
if (permission.user) return PrincipalType.User
|
||||
if (permission.group) return PrincipalType.Group
|
||||
}
|
||||
|
||||
@@ -1,55 +1,26 @@
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useContext,
|
||||
useCallback
|
||||
} from 'react'
|
||||
import axios from 'axios'
|
||||
import React, { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
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 Editor, { MonacoDiffEditor } 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 FileMenu from './internal/components/fileMenu'
|
||||
import RunMenu from './internal/components/runMenu'
|
||||
|
||||
import { usePrompt, useStateWithCallback } from '../../utils/hooks'
|
||||
import { usePrompt } from '../../utils/hooks'
|
||||
import { getLanguageFromExtension } from './internal/helper'
|
||||
import useEditor from './internal/hooks/useEditor'
|
||||
|
||||
const StyledTabPanel = styled(TabPanel)(() => ({
|
||||
padding: '10px'
|
||||
@@ -70,267 +41,72 @@ type SASjsEditorProps = {
|
||||
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)
|
||||
})
|
||||
}
|
||||
const {
|
||||
fileContent,
|
||||
isLoading,
|
||||
log,
|
||||
openFilePathInputModal,
|
||||
prevFileContent,
|
||||
runTimes,
|
||||
selectedFileExtension,
|
||||
selectedRunTime,
|
||||
showDiff,
|
||||
webout,
|
||||
Dialog,
|
||||
handleChangeRunTime,
|
||||
handleDiffEditorDidMount,
|
||||
handleEditorDidMount,
|
||||
handleFilePathInput,
|
||||
handleRunBtnClick,
|
||||
handleTabChange,
|
||||
saveFile,
|
||||
setShowDiff,
|
||||
setOpenFilePathInputModal,
|
||||
setFileContent,
|
||||
Snackbar
|
||||
} = useEditor({ selectedFilePath, setSelectedFilePath, setTab })
|
||||
|
||||
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
|
||||
]
|
||||
const fileMenu = (
|
||||
<FileMenu
|
||||
showDiff={showDiff}
|
||||
setShowDiff={setShowDiff}
|
||||
prevFileContent={prevFileContent}
|
||||
currentFileContent={fileContent}
|
||||
selectedFilePath={selectedFilePath}
|
||||
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||
saveFile={saveFile}
|
||||
/>
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
const monacoEditor = showDiff ? (
|
||||
<MonacoDiffEditor
|
||||
height="98%"
|
||||
language={getLanguageFromExtension(selectedFileExtension)}
|
||||
original={prevFileContent}
|
||||
value={fileContent}
|
||||
editorDidMount={handleDiffEditorDidMount}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
/>
|
||||
) : (
|
||||
<Editor
|
||||
height="98%"
|
||||
language={getLanguageFromExtension(selectedFileExtension)}
|
||||
value={fileContent}
|
||||
editorDidMount={handleEditorDidMount}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
||||
@@ -343,15 +119,7 @@ const SASjsEditor = ({
|
||||
{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}
|
||||
/>
|
||||
{fileMenu}
|
||||
</Box>
|
||||
<Paper
|
||||
sx={{
|
||||
@@ -363,26 +131,7 @@ const SASjsEditor = ({
|
||||
}}
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
{monacoEditor}
|
||||
</Paper>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -419,19 +168,9 @@ const SASjsEditor = ({
|
||||
handleChangeRunTime={handleChangeRunTime}
|
||||
handleRunBtnClick={handleRunBtnClick}
|
||||
/>
|
||||
<FileMenu
|
||||
showDiff={showDiff}
|
||||
setShowDiff={setShowDiff}
|
||||
prevFileContent={prevFileContent}
|
||||
currentFileContent={fileContent}
|
||||
selectedFilePath={selectedFilePath}
|
||||
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||
saveFile={saveFile}
|
||||
/>
|
||||
{fileMenu}
|
||||
</Box>
|
||||
<Paper
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
sx={{
|
||||
height: 'calc(100vh - 170px)',
|
||||
padding: '10px',
|
||||
@@ -440,26 +179,7 @@ const SASjsEditor = ({
|
||||
}}
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
{monacoEditor}
|
||||
<p
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -489,18 +209,8 @@ const SASjsEditor = ({
|
||||
</StyledTabPanel>
|
||||
</TabContext>
|
||||
)}
|
||||
<Modal
|
||||
open={openModal}
|
||||
setOpen={setOpenModal}
|
||||
title={modalTitle}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
<BootstrapSnackbar
|
||||
open={openSnackbar}
|
||||
setOpen={setOpenSnackbar}
|
||||
message={snackbarMessage}
|
||||
severity={snackbarSeverity}
|
||||
/>
|
||||
<Dialog />
|
||||
<Snackbar />
|
||||
<FilePathInputModal
|
||||
open={openFilePathInputModal}
|
||||
setOpen={setOpenFilePathInputModal}
|
||||
@@ -511,203 +221,3 @@ const SASjsEditor = ({
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
112
web/src/containers/Studio/internal/components/fileMenu.tsx
Normal file
112
web/src/containers/Studio/internal/components/fileMenu.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Button, IconButton, Menu, MenuItem, Tooltip } from '@mui/material'
|
||||
|
||||
import { MoreVert, Save, SaveAs, Difference, Edit } from '@mui/icons-material'
|
||||
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileMenu
|
||||
100
web/src/containers/Studio/internal/components/runMenu.tsx
Normal file
100
web/src/containers/Studio/internal/components/runMenu.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
|
||||
import { RocketLaunch } from '@mui/icons-material'
|
||||
|
||||
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 = () => {
|
||||
const baseUrl = window.location.origin
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RunMenu
|
||||
39
web/src/containers/Studio/internal/helper.ts
Normal file
39
web/src/containers/Studio/internal/helper.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { RunTimeType } from '../../../context/appContext'
|
||||
|
||||
export const getLanguageFromExtension = (extension: string) => {
|
||||
if (extension === 'js') return 'javascript'
|
||||
|
||||
if (extension === 'ts') return 'typescript'
|
||||
|
||||
if (extension === 'md' || extension === 'mdx') return 'markdown'
|
||||
|
||||
return extension
|
||||
}
|
||||
|
||||
export const getSelection = (editor: any) => {
|
||||
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||
return selection ?? ''
|
||||
}
|
||||
|
||||
export const programPathInjection = (
|
||||
code: string,
|
||||
path: string,
|
||||
runtime: RunTimeType
|
||||
) => {
|
||||
if (path) {
|
||||
if (runtime === RunTimeType.JS) {
|
||||
return `const _PROGRAM = '${path}';\n${code}`
|
||||
}
|
||||
if (runtime === RunTimeType.PY) {
|
||||
return `_PROGRAM = '${path}';\n${code}`
|
||||
}
|
||||
if (runtime === RunTimeType.R) {
|
||||
return `._PROGRAM = '${path}';\n${code}`
|
||||
}
|
||||
if (runtime === RunTimeType.SAS) {
|
||||
return `%let _program = ${path};\n${code}`
|
||||
}
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
319
web/src/containers/Studio/internal/hooks/useEditor.ts
Normal file
319
web/src/containers/Studio/internal/hooks/useEditor.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import { DiffEditorDidMount, EditorDidMount, monaco } from 'react-monaco-editor'
|
||||
import { SelectChangeEvent } from '@mui/material'
|
||||
import { getSelection, programPathInjection } from '../helper'
|
||||
import { AppContext, RunTimeType } from '../../../../context/appContext'
|
||||
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||
import {
|
||||
useModal,
|
||||
useSnackbar,
|
||||
useStateWithCallback
|
||||
} from '../../../../utils/hooks'
|
||||
|
||||
const SASJS_LOGS_SEPARATOR =
|
||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
|
||||
type UseEditorParams = {
|
||||
selectedFilePath: string
|
||||
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
|
||||
setTab: Dispatch<SetStateAction<string>>
|
||||
}
|
||||
|
||||
const useEditor = ({
|
||||
selectedFilePath,
|
||||
setSelectedFilePath,
|
||||
setTab
|
||||
}: UseEditorParams) => {
|
||||
const appContext = useContext(AppContext)
|
||||
const { Dialog, setOpenModal, setModalTitle, setModalPayload } = useModal()
|
||||
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||
useSnackbar()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState('')
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fileContent, prevFileContent, selectedFilePath]
|
||||
)
|
||||
|
||||
const handleTabChange = (_e: any, newValue: string) => {
|
||||
setTab(newValue)
|
||||
}
|
||||
|
||||
const handleRunBtnClick = () =>
|
||||
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||
|
||||
const runCode = useCallback(
|
||||
(code: string) => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, {
|
||||
code: programPathInjection(
|
||||
code,
|
||||
selectedFilePath,
|
||||
selectedRunTime as RunTimeType
|
||||
),
|
||||
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))
|
||||
},
|
||||
[
|
||||
selectedFilePath,
|
||||
selectedRunTime,
|
||||
setModalPayload,
|
||||
setModalTitle,
|
||||
setOpenModal,
|
||||
setTab
|
||||
]
|
||||
)
|
||||
|
||||
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
||||
setSelectedRunTime(event.target.value as RunTimeType)
|
||||
}
|
||||
|
||||
const handleFilePathInput = (filePath: string) => {
|
||||
setOpenFilePathInputModal(false)
|
||||
saveFile(filePath)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
editorRef.current.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'run-code',
|
||||
|
||||
// A label of the action that will be presented to the user.
|
||||
label: 'Run Code',
|
||||
|
||||
// An optional array of keybindings for the action.
|
||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
|
||||
|
||||
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 () {
|
||||
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||
}
|
||||
})
|
||||
}, [fileContent, prevFileContent, selectedFilePath, saveFile, runCode])
|
||||
|
||||
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()?.toLowerCase() ?? ''
|
||||
)
|
||||
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(() => {
|
||||
const fileExtension = selectedFileExtension.toLowerCase()
|
||||
if (runTimes.includes(fileExtension)) setSelectedRunTime(fileExtension)
|
||||
}, [selectedFileExtension, runTimes])
|
||||
|
||||
return {
|
||||
fileContent,
|
||||
isLoading,
|
||||
log,
|
||||
openFilePathInputModal,
|
||||
prevFileContent,
|
||||
runTimes,
|
||||
selectedFileExtension,
|
||||
selectedRunTime,
|
||||
showDiff,
|
||||
webout,
|
||||
Dialog,
|
||||
handleChangeRunTime,
|
||||
handleDiffEditorDidMount,
|
||||
handleEditorDidMount,
|
||||
handleFilePathInput,
|
||||
handleRunBtnClick,
|
||||
handleTabChange,
|
||||
saveFile,
|
||||
setShowDiff,
|
||||
setOpenFilePathInputModal,
|
||||
setFileContent,
|
||||
Snackbar
|
||||
}
|
||||
}
|
||||
|
||||
export default useEditor
|
||||
@@ -16,7 +16,9 @@ export enum ModeType {
|
||||
|
||||
export enum RunTimeType {
|
||||
SAS = 'sas',
|
||||
JS = 'js'
|
||||
JS = 'js',
|
||||
PY = 'py',
|
||||
R = 'r'
|
||||
}
|
||||
|
||||
interface AppContextProps {
|
||||
|
||||
120
web/src/context/permissionsContext.tsx
Normal file
120
web/src/context/permissionsContext.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, {
|
||||
createContext,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useState,
|
||||
useCallback,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
import axios from 'axios'
|
||||
import { PermissionResponse } from '../utils/types'
|
||||
import { useModal, useSnackbar } from '../utils/hooks'
|
||||
import { AlertSeverityType } from '../components/snackbar'
|
||||
import usePermissionResponseModal from '../containers/Settings/internal/hooks/usePermissionResponseModal'
|
||||
import { PermissionResponsePayload } from '../containers/Settings/internal/components/permissionResponseModal'
|
||||
|
||||
interface PermissionsContextProps {
|
||||
isLoading: boolean
|
||||
setIsLoading: Dispatch<SetStateAction<boolean>>
|
||||
permissions: PermissionResponse[]
|
||||
setPermissions: Dispatch<React.SetStateAction<PermissionResponse[]>>
|
||||
selectedPermission: PermissionResponse | undefined
|
||||
setSelectedPermission: Dispatch<
|
||||
React.SetStateAction<PermissionResponse | undefined>
|
||||
>
|
||||
filteredPermissions: PermissionResponse[]
|
||||
setFilteredPermissions: Dispatch<React.SetStateAction<PermissionResponse[]>>
|
||||
filterApplied: boolean
|
||||
setFilterApplied: Dispatch<SetStateAction<boolean>>
|
||||
fetchPermissions: () => void
|
||||
Dialog: () => JSX.Element
|
||||
setOpenModal: Dispatch<SetStateAction<boolean>>
|
||||
setModalTitle: Dispatch<SetStateAction<string>>
|
||||
setModalPayload: Dispatch<SetStateAction<string>>
|
||||
Snackbar: () => JSX.Element
|
||||
setOpenSnackbar: Dispatch<React.SetStateAction<boolean>>
|
||||
setSnackbarMessage: Dispatch<React.SetStateAction<string>>
|
||||
setSnackbarSeverity: Dispatch<React.SetStateAction<AlertSeverityType>>
|
||||
PermissionResponseDialog: () => JSX.Element
|
||||
setOpenPermissionResponseModal: Dispatch<React.SetStateAction<boolean>>
|
||||
setPermissionResponsePayload: Dispatch<
|
||||
React.SetStateAction<PermissionResponsePayload>
|
||||
>
|
||||
}
|
||||
|
||||
export const PermissionsContext = createContext<PermissionsContextProps>(
|
||||
undefined!
|
||||
)
|
||||
|
||||
const PermissionsContextProvider = (props: { children: ReactNode }) => {
|
||||
const { children } = props
|
||||
const { Dialog, setOpenModal, setModalTitle, setModalPayload } = useModal()
|
||||
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||
useSnackbar()
|
||||
const {
|
||||
PermissionResponseDialog,
|
||||
setOpenPermissionResponseModal,
|
||||
setPermissionResponsePayload
|
||||
} = usePermissionResponseModal()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [permissions, setPermissions] = useState<PermissionResponse[]>([])
|
||||
const [selectedPermission, setSelectedPermission] =
|
||||
useState<PermissionResponse>()
|
||||
const [filteredPermissions, setFilteredPermissions] = useState<
|
||||
PermissionResponse[]
|
||||
>([])
|
||||
const [filterApplied, setFilterApplied] = useState(false)
|
||||
|
||||
const fetchPermissions = useCallback(() => {
|
||||
axios
|
||||
.get(`/SASjsApi/permission`)
|
||||
.then((res: any) => {
|
||||
if (res.data?.length > 0) {
|
||||
setPermissions(res.data)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setModalTitle('Abort')
|
||||
setModalPayload(
|
||||
typeof err.response.data === 'object'
|
||||
? JSON.stringify(err.response.data)
|
||||
: err.response.data
|
||||
)
|
||||
setOpenModal(true)
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<PermissionsContext.Provider
|
||||
value={{
|
||||
isLoading,
|
||||
permissions,
|
||||
selectedPermission,
|
||||
filteredPermissions,
|
||||
filterApplied,
|
||||
Dialog,
|
||||
Snackbar,
|
||||
PermissionResponseDialog,
|
||||
fetchPermissions,
|
||||
setIsLoading,
|
||||
setPermissions,
|
||||
setSelectedPermission,
|
||||
setFilteredPermissions,
|
||||
setFilterApplied,
|
||||
setOpenModal,
|
||||
setModalTitle,
|
||||
setModalPayload,
|
||||
setOpenPermissionResponseModal,
|
||||
setPermissionResponsePayload,
|
||||
setOpenSnackbar,
|
||||
setSnackbarMessage,
|
||||
setSnackbarSeverity
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PermissionsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default PermissionsContextProvider
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './useModal'
|
||||
export * from './usePrompt'
|
||||
export * from './useStateWithCallback'
|
||||
export * from './useSnackbar'
|
||||
|
||||
19
web/src/utils/hooks/useModal.tsx
Normal file
19
web/src/utils/hooks/useModal.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useState } from 'react'
|
||||
import Modal from '../../components/modal'
|
||||
|
||||
export const useModal = () => {
|
||||
const [openModal, setOpenModal] = useState(false)
|
||||
const [modalTitle, setModalTitle] = useState('')
|
||||
const [modalPayload, setModalPayload] = useState('')
|
||||
|
||||
const Dialog = () => (
|
||||
<Modal
|
||||
open={openModal}
|
||||
setOpen={setOpenModal}
|
||||
title={modalTitle}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
)
|
||||
|
||||
return { Dialog, setOpenModal, setModalTitle, setModalPayload }
|
||||
}
|
||||
21
web/src/utils/hooks/useSnackbar.tsx
Normal file
21
web/src/utils/hooks/useSnackbar.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useState } from 'react'
|
||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||
|
||||
export const useSnackbar = () => {
|
||||
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||
AlertSeverityType.Success
|
||||
)
|
||||
|
||||
const Snackbar = () => (
|
||||
<BootstrapSnackbar
|
||||
open={openSnackbar}
|
||||
setOpen={setOpenSnackbar}
|
||||
message={snackbarMessage}
|
||||
severity={snackbarSeverity}
|
||||
/>
|
||||
)
|
||||
|
||||
return { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity }
|
||||
}
|
||||
Reference in New Issue
Block a user