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

Compare commits

...

82 Commits

Author SHA1 Message Date
semantic-release-bot
4ffc1ec6a9 chore(release): 0.28.0 [skip ci]
# [0.28.0](https://github.com/sasjs/server/compare/v0.27.0...v0.28.0) (2022-11-28)

### Bug Fixes

* update the response header of request to stp/execute routes ([112431a](112431a1b7))

### Features

* **api:** add the api endpoint for updating user password ([4581f32](4581f32534))
* ask for updated password on first login ([1d48f88](1d48f8856b))
* **web:** add the UI for updating user password ([8b8c43c](8b8c43c21b))
2022-11-28 17:43:05 +00:00
Allan Bowe
5a1d168e83 Merge pull request #324 from sasjs/issue-322
fix: update the response header of request to stp/execute routes
2022-11-28 17:38:05 +00:00
Allan Bowe
515c976685 Merge pull request #323 from sasjs/issue-222
feat: force user to change password on first login
2022-11-28 17:37:17 +00:00
112431a1b7 fix: update the response header of request to stp/execute routes 2022-11-27 21:57:26 +05:00
c26485afec chore: fix specs 2022-11-22 20:15:26 +05:00
1d48f8856b feat: ask for updated password on first login 2022-11-22 19:58:17 +05:00
68758aa616 chore: new password should be different to current password 2022-11-22 15:26:22 +05:00
8b8c43c21b feat(web): add the UI for updating user password 2022-11-22 00:03:25 +05:00
4581f32534 feat(api): add the api endpoint for updating user password 2022-11-22 00:02:59 +05:00
b47e74a7e1 chore: styles fix 2022-11-22 00:01:58 +05:00
b27d684145 chore: use process.logger instead of condole.log 2022-11-17 23:03:33 +05:00
semantic-release-bot
6b666d5554 chore(release): 0.27.0 [skip ci]
# [0.27.0](https://github.com/sasjs/server/compare/v0.26.2...v0.27.0) (2022-11-17)

### Features

* on startup add webout.sas file in sasautos folder ([200f6c5](200f6c596a))
2022-11-17 13:21:44 +00:00
Allan Bowe
b5f0911858 Merge pull request #321 from sasjs/issue-318
feat: on startup add webout.sas file in sasautos folder
2022-11-17 13:17:35 +00:00
b86ba5b8a3 chore: lint fix 2022-11-17 17:49:00 +05:00
200f6c596a feat: on startup add webout.sas file in sasautos folder 2022-11-17 17:03:23 +05:00
semantic-release-bot
1b7ccda6e9 chore(release): 0.26.2 [skip ci]
## [0.26.2](https://github.com/sasjs/server/compare/v0.26.1...v0.26.2) (2022-11-15)

### Bug Fixes

* comments ([7ae862c](7ae862c5ce))
2022-11-15 13:06:36 +00:00
Allan Bowe
532035d835 Merge pull request #317 from sasjs/docfix
fix: comments
2022-11-15 13:01:45 +00:00
Allan Bowe
7ae862c5ce fix: comments 2022-11-15 13:01:13 +00:00
semantic-release-bot
ab5858b8af chore(release): 0.26.1 [skip ci]
## [0.26.1](https://github.com/sasjs/server/compare/v0.26.0...v0.26.1) (2022-11-15)

### Bug Fixes

* change the expiration of access/refresh tokens from days to seconds ([bb05493](bb054938c5))
2022-11-15 12:31:03 +00:00
Allan Bowe
a39f5dd9f1 Merge pull request #316 from sasjs/access-token-expiration
fix: change the expiration of access/refresh tokens from days to seconds
2022-11-15 12:25:41 +00:00
Allan Bowe
3ea444756c Update Client.ts 2022-11-15 11:00:42 +00:00
Allan Bowe
96399ecbbe Update swagger.yaml 2022-11-15 10:54:52 +00:00
bb054938c5 fix: change the expiration of access/refresh tokens from days to seconds 2022-11-15 15:48:03 +05:00
semantic-release-bot
fb6a556630 chore(release): 0.26.0 [skip ci]
# [0.26.0](https://github.com/sasjs/server/compare/v0.25.1...v0.26.0) (2022-11-13)

### Bug Fixes

* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](acc25cbd68))

### Features

* make access token duration configurable when creating client/secret ([2413c05](2413c05fea))
* make refresh token duration configurable ([abd5c64](abd5c64b4a))
2022-11-13 14:04:03 +00:00
Allan Bowe
9dbd8e16bd Merge pull request #315 from sasjs/issue-307
feat: make access token duration configurable when creating client
2022-11-13 14:00:03 +00:00
fe07c41f5f chore: update header 2022-11-11 15:35:24 +05:00
acc25cbd68 fix(web): dispose monaco editor actions in return of useEffect 2022-11-11 15:27:12 +05:00
4ca61feda6 chore: npm audit fix 2022-11-10 21:05:41 +05:00
abd5c64b4a feat: make refresh token duration configurable 2022-11-10 21:02:20 +05:00
2413c05fea feat: make access token duration configurable when creating client/secret 2022-11-10 19:43:06 +05:00
semantic-release-bot
4c874c2c39 chore(release): 0.25.1 [skip ci]
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)

### Bug Fixes

* **web:** use mui treeView instead of custom implementation ([c51b504](c51b50428f))
2022-11-07 15:50:02 +00:00
Allan Bowe
d819d79bc9 Merge pull request #313 from sasjs/tree-view
fix(web): use mui treeView instead of custom implementation
2022-11-07 15:46:14 +00:00
c51b50428f fix(web): use mui treeView instead of custom implementation 2022-11-06 01:14:58 +05:00
semantic-release-bot
e10a0554f0 chore(release): 0.25.0 [skip ci]
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)

### Features

* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](1c9d167f86))
2022-11-02 15:24:25 +00:00
Allan Bowe
337e2eb2a0 Merge pull request #311 from sasjs/issue-310
feat: Enable DRIVE_LOCATION setting for deploying multiple instances
2022-11-02 15:19:54 +00:00
66f8e7840b chore: update readme 2022-11-02 20:18:28 +05:00
1c9d167f86 feat: Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server 2022-11-02 20:05:12 +05:00
semantic-release-bot
7e684b54a6 chore(release): 0.24.0 [skip ci]
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)

### Features

* cli mock testing ([6434123](6434123401))
* mocking sas9 responses with JS STP ([36be3a7](36be3a7d5e))
2022-10-28 10:05:48 +00:00
Sabir Hassan
aafda2922b Merge pull request #306 from sasjs/sas9-tests-mock-dynamic
feat: cli mock testing
2022-10-28 15:01:00 +05:00
418bf41e38 style: lint 2022-10-28 11:53:42 +02:00
81f0b03b09 chore: comments address 2022-10-28 11:53:25 +02:00
fe5ae44aab chore: typo 2022-10-17 18:32:58 +02:00
36be3a7d5e feat: mocking sas9 responses with JS STP 2022-10-17 18:31:08 +02:00
6434123401 feat: cli mock testing 2022-10-11 18:37:20 +02:00
semantic-release-bot
0a6b972c65 chore(release): 0.23.4 [skip ci]
## [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](2412622367))
2022-10-11 15:26:38 +00:00
Allan Bowe
be11707042 Merge pull request #303 from sasjs/issue-301
fix: add action to editor ref for running code
2022-10-11 16:08:57 +01:00
2412622367 fix: add action to editor ref for running code 2022-10-10 16:51:46 +05:00
semantic-release-bot
de3a190a8d chore(release): 0.23.3 [skip ci]
## [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](94072c3d24))
2022-10-09 20:32:07 +00:00
Allan Bowe
d5daafc6ed Merge pull request #302 from sasjs/cookies-with-domain
fix: added domain for session cookies
2022-10-09 21:26:40 +01:00
Saad Jutt
b1a2677b8c chore: specified domain for cookie for csrf as well 2022-10-10 00:48:13 +05:00
Saad Jutt
94072c3d24 fix: added domain for session cookies 2022-10-09 22:08:01 +05:00
semantic-release-bot
b64c0c12da chore(release): 0.23.2 [skip ci]
## [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](14731e8824))
* bumping sasjs/score ([258cc35](258cc35f14))
* reverting commit ([fda0e0b](fda0e0b57d))
2022-10-06 12:41:15 +00:00
Allan Bowe
79bc7b0e28 Merge pull request #300 from sasjs/corebump
fix: bumping sasjs/score
2022-10-06 13:36:20 +01:00
Allan Bowe
fda0e0b57d fix: reverting commit 2022-10-06 12:35:59 +00:00
Allan Bowe
14731e8824 fix: bump in correct place 2022-10-06 12:34:48 +00:00
Allan Bowe
258cc35f14 fix: bumping sasjs/score 2022-10-06 12:32:13 +00:00
semantic-release-bot
2295a518f0 chore(release): 0.23.1 [skip ci]
## [0.23.1](https://github.com/sasjs/server/compare/v0.23.0...v0.23.1) (2022-10-04)

### Bug Fixes

* ldap issues ([4d64420](4d64420c45))
2022-10-04 16:54:37 +00:00
Allan Bowe
1e5d621817 Merge pull request #298 from sasjs/fix-ldap
fix: ldap issues
2022-10-03 18:49:53 +01:00
4d64420c45 fix: ldap issues
logic fixed for updating user created by external auth provider
remove internal from AuthProviderType
replace AUTH_MECHANISM with AUTH_PROVIDERS
2022-10-03 21:24:10 +05:00
semantic-release-bot
799339de30 chore(release): 0.23.0 [skip ci]
# [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](424f0fc1fa))
2022-10-03 15:13:11 +00:00
Allan Bowe
042ed41189 Merge pull request #297 from sasjs/issue-292
feat: Enable SAS_PACKAGES in SASjs Server
2022-10-03 16:08:30 +01:00
424f0fc1fa feat: Enable SAS_PACKAGES in SASjs Server 2022-10-03 19:43:02 +05:00
semantic-release-bot
deafebde05 chore(release): 0.22.1 [skip ci]
## [0.22.1](https://github.com/sasjs/server/compare/v0.22.0...v0.22.1) (2022-10-03)

### Bug Fixes

* spelling issues ([3bb0597](3bb05974d2))
2022-10-03 13:17:14 +00:00
Allan Bowe
b66dc86b01 Merge pull request #296 from sasjs/spellingz
fix: spelling issues
2022-10-03 14:11:55 +01:00
Allan Bowe
3bb05974d2 fix: spelling issues 2022-10-03 13:10:30 +00:00
semantic-release-bot
d1c1a59e91 chore(release): 0.22.0 [skip ci]
# [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](68f0c5c588))
* no need to restrict api endpoints when ldap auth is applied ([a142660](a14266077d))
* remove authProvider attribute from user and group payload interface ([bbd7786](bbd7786c6c))

### Features

* implemented LDAP authentication ([f915c51](f915c51b07))
2022-10-03 12:13:18 +00:00
Allan Bowe
668aff83fd Merge pull request #293 from sasjs/ldap
feat: integratedLDAP authentication
2022-10-03 13:09:07 +01:00
3fc06b80fc chore: add specs 2022-10-01 16:08:29 +05:00
bbd7786c6c fix: remove authProvider attribute from user and group payload interface 2022-10-01 15:06:55 +05:00
68f0c5c588 fix: do not throw error on deleting group when it is created by an external auth provider 2022-10-01 14:52:36 +05:00
semantic-release-bot
69ddf313b8 chore(release): 0.21.7 [skip ci]
## [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](fe3e5088f8))
2022-09-30 21:44:16 +00:00
Saad Jutt
65e404cdbd Merge pull request #294 from sasjs/csrf-package-migration
fix: csrf package is changed to pillarjs-csrf
2022-10-01 02:39:06 +05:00
a14266077d fix: no need to restrict api endpoints when ldap auth is applied 2022-09-30 14:41:09 +05:00
Saad Jutt
fda6ad6356 chore(csrf): removed _csrf completely 2022-09-30 03:07:21 +05:00
Saad Jutt
fe3e5088f8 fix: csrf package is changed to pillarjs-csrf 2022-09-29 20:33:30 +05:00
f915c51b07 feat: implemented LDAP authentication 2022-09-29 18:41:28 +05:00
semantic-release-bot
375f924f45 chore(release): 0.21.6 [skip ci]
## [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](40f95f9072))
2022-09-23 09:33:49 +00:00
Allan Bowe
72329e30ed Merge pull request #291 from sasjs/issue-290
fix: in getTokensFromDB handle the scenario when tokens are expired
2022-09-23 10:29:51 +01:00
40f95f9072 fix: in getTokensFromDB handle the scenario when tokens are expired 2022-09-23 09:35:30 +05:00
semantic-release-bot
58e8a869ef chore(release): 0.21.5 [skip ci]
## [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](249604384e))
2022-09-22 15:50:53 +00:00
Allan Bowe
b558a3d01d Merge pull request #289 from sasjs/issue-288
fix: made files extensions case insensitive
2022-09-22 16:47:00 +01:00
249604384e fix: made files extensions case insensitive 2022-09-22 20:37:16 +05:00
94 changed files with 2824 additions and 640 deletions

2
.gitignore vendored
View File

@@ -5,8 +5,6 @@ node_modules/
.env*
sas/
sasjs_root/
api/mocks/custom/*
!api/mocks/custom/.keep
tmp/
build/
sasjsbuild/

View File

@@ -1,3 +1,152 @@
# [0.28.0](https://github.com/sasjs/server/compare/v0.27.0...v0.28.0) (2022-11-28)
### Bug Fixes
* update the response header of request to stp/execute routes ([112431a](https://github.com/sasjs/server/commit/112431a1b7461989c04100418d67d975a2a8f354))
### Features
* **api:** add the api endpoint for updating user password ([4581f32](https://github.com/sasjs/server/commit/4581f325344eb68c5df5a28492f132312f15bb5c))
* ask for updated password on first login ([1d48f88](https://github.com/sasjs/server/commit/1d48f8856b1fbbf3ef868914558333190e04981f))
* **web:** add the UI for updating user password ([8b8c43c](https://github.com/sasjs/server/commit/8b8c43c21bde5379825c5ec44ecd81a92425f605))
# [0.27.0](https://github.com/sasjs/server/compare/v0.26.2...v0.27.0) (2022-11-17)
### Features
* on startup add webout.sas file in sasautos folder ([200f6c5](https://github.com/sasjs/server/commit/200f6c596a6e732d799ed408f1f0fd92f216ba58))
## [0.26.2](https://github.com/sasjs/server/compare/v0.26.1...v0.26.2) (2022-11-15)
### Bug Fixes
* comments ([7ae862c](https://github.com/sasjs/server/commit/7ae862c5ce720e9483d4728f4295dede4f849436))
## [0.26.1](https://github.com/sasjs/server/compare/v0.26.0...v0.26.1) (2022-11-15)
### Bug Fixes
* change the expiration of access/refresh tokens from days to seconds ([bb05493](https://github.com/sasjs/server/commit/bb054938c5bd0535ae6b9da93ba0b14f9b80ddcd))
# [0.26.0](https://github.com/sasjs/server/compare/v0.25.1...v0.26.0) (2022-11-13)
### Bug Fixes
* **web:** dispose monaco editor actions in return of useEffect ([acc25cb](https://github.com/sasjs/server/commit/acc25cbd686952d3f1c65e57aefcebe1cb859cc7))
### Features
* make access token duration configurable when creating client/secret ([2413c05](https://github.com/sasjs/server/commit/2413c05fea3960f7e5c3c8b7b2f85d61314f08db))
* make refresh token duration configurable ([abd5c64](https://github.com/sasjs/server/commit/abd5c64b4a726e3f17594a98111b6aa269b71fee))
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)
### Bug Fixes
* **web:** use mui treeView instead of custom implementation ([c51b504](https://github.com/sasjs/server/commit/c51b50428f32608bc46438e9d7964429b2d595da))
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)
### Features
* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](https://github.com/sasjs/server/commit/1c9d167f86bbbb108b96e9bc30efaf8de65d82ff))
# [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)

View File

@@ -93,6 +93,10 @@ R_PATH=/usr/bin/Rscript
SASJS_ROOT=./sasjs_root
# This location is for files, sasjs packages and appStreamConfig.json
DRIVE_LOCATION=./sasjs_root/drive
# options: [http|https] default: http
PROTOCOL=
@@ -103,6 +107,11 @@ PORT=
# 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
#
@@ -125,9 +134,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=

View File

@@ -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,6 +15,14 @@ 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
@@ -21,6 +30,7 @@ PYTHON_PATH=/usr/bin/python
R_PATH=/usr/bin/Rscript
SASJS_ROOT=./sasjs_root
DRIVE_LOCATION=./sasjs_root/drive
LOG_FORMAT_MORGAN=common
LOG_LOCATION=./sasjs_root/logs

View File

View File

@@ -9,7 +9,7 @@
<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="LT-8-WGkt9EXwICBihaVbxGc92opjufTK1D" aria-hidden="true" />
<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" />

493
api/package-lock.json generated
View File

@@ -8,18 +8,18 @@
"name": "api",
"version": "0.0.2",
"dependencies": {
"@sasjs/core": "^4.31.3",
"@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,9 +1394,9 @@
}
},
"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.48.1",
@@ -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",
@@ -7009,9 +7092,9 @@
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -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,9 +11135,9 @@
}
},
"@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.48.1",
@@ -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",
@@ -15372,9 +15592,9 @@
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -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",

View File

@@ -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/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",

View File

@@ -47,6 +47,21 @@ components:
- userId
type: object
additionalProperties: false
UpdatePasswordPayload:
properties:
currentPassword:
type: string
description: 'Current Password'
example: currentPasswordString
newPassword:
type: string
description: 'New Password'
example: newPassword
required:
- currentPassword
- newPassword
type: object
additionalProperties: false
ClientPayload:
properties:
clientId:
@@ -57,6 +72,16 @@ components:
type: string
description: 'Client Secret'
example: someRandomCryptoString
accessTokenExpiration:
type: number
format: double
description: 'Number of seconds after which access token will expire. Default is 86400 (1 day)'
example: 86400
refreshTokenExpiration:
type: number
format: double
description: 'Number of seconds after which access token will expire. Default is 2592000 (30 days)'
example: 2592000
required:
- clientId
- clientSecret
@@ -509,6 +534,27 @@ components:
- setting
type: object
additionalProperties: false
SessionResponse:
properties:
id:
type: number
format: double
username:
type: string
displayName:
type: string
isAdmin:
type: boolean
needsToUpdatePassword:
type: boolean
required:
- id
- username
- displayName
- isAdmin
- needsToUpdatePassword
type: object
additionalProperties: false
ExecutePostRequestPayload:
properties:
_program:
@@ -622,6 +668,70 @@ paths:
-
bearerAuth: []
parameters: []
/SASjsApi/auth/updatePassword:
patch:
operationId: UpdatePassword
responses:
'204':
description: 'No content'
summary: 'Update user''s password.'
tags:
- Auth
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdatePasswordPayload'
/SASjsApi/authConfig:
get:
operationId: GetDetail
responses:
'200':
description: Ok
content:
application/json:
schema: {}
examples:
'Example 1':
value: {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'}}
summary: 'Gives the detail of Auth Mechanism.'
tags:
- Auth_Config
security:
-
bearerAuth: []
parameters: []
/SASjsApi/authConfig/synchroniseWithLDAP:
post:
operationId: SynchroniseWithLDAP
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
groupCount: {type: number, format: double}
userCount: {type: number, format: double}
required:
- groupCount
- userCount
type: object
examples:
'Example 1':
value: {users: 5, groups: 3}
summary: 'Synchronises LDAP users and groups with internal DB and returns the count of imported users and groups.'
tags:
- Auth_Config
security:
-
bearerAuth: []
parameters: []
/SASjsApi/client:
post:
operationId: CreateClient
@@ -634,8 +744,8 @@ paths:
$ref: '#/components/schemas/ClientPayload'
examples:
'Example 1':
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString}
summary: 'Create client with the following attributes: ClientId, ClientSecret. Admin only task.'
value: {clientId: someFormattedClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}
summary: "Admin only task. Create client with the following attributes:\nClientId,\nClientSecret,\naccessTokenExpiration (optional),\nrefreshTokenExpiration (optional)"
tags:
- Client
security:
@@ -1635,7 +1745,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
$ref: '#/components/schemas/SessionResponse'
examples:
'Example 1':
value: {id: 123, username: johnusername, displayName: John, isAdmin: false}
@@ -1732,7 +1842,7 @@ paths:
application/json:
schema:
properties:
user: {properties: {isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [isAdmin, displayName, username, id], type: object}
user: {properties: {needsToUpdatePassword: {type: boolean}, isAdmin: {type: boolean}, displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [needsToUpdatePassword, isAdmin, displayName, username, id], type: object}
loggedIn: {type: boolean}
required:
- user
@@ -1794,6 +1904,9 @@ tags:
-
name: Auth
description: 'Operations about auth'
-
name: Auth_Config
description: 'Operations about external auth providers'
-
name: Client
description: 'Operations about clients'

View 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()

View File

@@ -15,7 +15,7 @@ export const configureCors = (app: Express) => {
whiteList.push(url.replace(/\/$/, ''))
})
console.log('All CORS Requests are enabled for:', whiteList)
process.logger.info('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
}
}

View File

@@ -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,

View File

@@ -23,7 +23,7 @@ export const configureLogger = (app: Express) => {
path: logsFolder
})
console.log('Writing Logs to :', path.join(logsFolder, filename))
process.logger.info('Writing Logs to :', path.join(logsFolder, filename))
options = { stream: accessLogStream }
}

View File

@@ -1,18 +1,21 @@
import path from 'path'
import express, { ErrorRequestHandler } from 'express'
import csrf, { CookieOptions } from 'csurf'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
import {
copySASjsCore,
createWeboutSasFile,
getFilesFolder,
getPackagesFolder,
getWebBuildFolder,
instantiateLogger,
loadAppStreamConfig,
ProtocolType,
ReturnCode,
setProcessVariables,
setupFolders,
setupFilesFolder,
setupPackagesFolder,
setupUserAutoExec,
verifyEnvVariables
} from './utils'
import {
@@ -21,6 +24,7 @@ import {
configureLogger,
configureSecurity
} from './app-modules'
import { folderExists } from '@sasjs/utils'
dotenv.config()
@@ -30,25 +34,8 @@ 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)
process.logger.error(err.stack)
res.status(500).send('Something broke!')
}
@@ -81,8 +68,21 @@ export default setProcessVariables().then(async () => {
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
app.use(express.urlencoded({ extended: true }))
await setupFolders()
await copySASjsCore()
await setupUserAutoExec()
if (!(await folderExists(getFilesFolder()))) await setupFilesFolder()
if (!(await folderExists(getPackagesFolder()))) await setupPackagesFolder()
const sasautosPath = path.join(process.driveLoc, 'sas', 'sasautos')
if (await folderExists(sasautosPath)) {
process.logger.warn(
`SASAUTOS was not refreshed. To force a refresh, delete the ${sasautosPath} folder`
)
} else {
await copySASjsCore()
await createWeboutSasFile()
}
// loading these modules after setting up variables due to
// multer's usage of process var process.driveLoc

View File

@@ -1,4 +1,16 @@
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
import express from 'express'
import {
Security,
Route,
Tags,
Example,
Post,
Patch,
Request,
Body,
Query,
Hidden
} from 'tsoa'
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import {
@@ -8,6 +20,8 @@ import {
removeTokensInDB,
saveTokensInDB
} from '../utils'
import Client from '../model/Client'
import User from '../model/User'
@Route('SASjsApi/auth')
@Tags('Auth')
@@ -61,6 +75,18 @@ export class AuthController {
public async logout(@Query() @Hidden() data?: InfoJWT) {
return logout(data!)
}
/**
* @summary Update user's password.
*/
@Security('bearerAuth')
@Patch('updatePassword')
public async updatePassword(
@Request() req: express.Request,
@Body() body: UpdatePasswordPayload
) {
return updatePassword(req, body)
}
}
const token = async (data: any): Promise<TokenResponse> => {
@@ -83,8 +109,17 @@ const token = async (data: any): Promise<TokenResponse> => {
}
}
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
const client = await Client.findOne({ clientId })
if (!client) throw new Error('Invalid clientId.')
const accessToken = generateAccessToken(
userInfo,
client.accessTokenExpiration
)
const refreshToken = generateRefreshToken(
userInfo,
client.refreshTokenExpiration
)
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
@@ -92,8 +127,17 @@ const token = async (data: any): Promise<TokenResponse> => {
}
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
const client = await Client.findOne({ clientId: userInfo.clientId })
if (!client) throw new Error('Invalid clientId.')
const accessToken = generateAccessToken(
userInfo,
client.accessTokenExpiration
)
const refreshToken = generateRefreshToken(
userInfo,
client.refreshTokenExpiration
)
await saveTokensInDB(
userInfo.userId,
@@ -109,6 +153,40 @@ const logout = async (userInfo: InfoJWT) => {
await removeTokensInDB(userInfo.userId, userInfo.clientId)
}
const updatePassword = async (
req: express.Request,
data: UpdatePasswordPayload
) => {
const { currentPassword, newPassword } = data
const userId = req.user?.userId
const dbUser = await User.findOne({ userId })
if (!dbUser)
throw {
code: 404,
message: `User not found!`
}
if (dbUser?.authProvider) {
throw {
code: 405,
message:
'Can not update password of user that is created by an external auth provider.'
}
}
const validPass = dbUser.comparePassword(currentPassword)
if (!validPass)
throw {
code: 403,
message: `Invalid current password!`
}
dbUser.password = User.hashPassword(newPassword)
dbUser.needsToUpdatePassword = false
await dbUser.save()
}
interface TokenPayload {
/**
* Client ID
@@ -135,6 +213,19 @@ interface TokenResponse {
refreshToken: string
}
interface UpdatePasswordPayload {
/**
* Current Password
* @example "currentPasswordString"
*/
currentPassword: string
/**
* New Password
* @example "newPassword"
*/
newPassword: string
}
const verifyAuthCode = async (
clientId: string,
code: string

View File

@@ -0,0 +1,186 @@
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,
needsToUpdatePassword: false
})
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
}

View File

@@ -1,18 +1,27 @@
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
import Client, { ClientPayload } from '../model/Client'
import Client, {
ClientPayload,
NUMBER_OF_SECONDS_IN_A_DAY
} from '../model/Client'
@Security('bearerAuth')
@Route('SASjsApi/client')
@Tags('Client')
export class ClientController {
/**
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
* @summary Admin only task. Create client with the following attributes:
* ClientId,
* ClientSecret,
* accessTokenExpiration (optional),
* refreshTokenExpiration (optional)
*
*/
@Example<ClientPayload>({
clientId: 'someFormattedClientID1234',
clientSecret: 'someRandomCryptoString'
clientSecret: 'someRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
})
@Post('/')
public async createClient(
@@ -22,8 +31,13 @@ export class ClientController {
}
}
const createClient = async (data: any): Promise<ClientPayload> => {
const { clientId, clientSecret } = data
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
const {
clientId,
clientSecret,
accessTokenExpiration,
refreshTokenExpiration
} = data
// Checking if client is already in the database
const clientExist = await Client.findOne({ clientId })
@@ -32,13 +46,17 @@ const createClient = async (data: any): Promise<ClientPayload> => {
// Create a new client
const client = new Client({
clientId,
clientSecret
clientSecret,
accessTokenExpiration,
refreshTokenExpiration
})
const savedClient = await client.save()
return {
clientId: savedClient.clientId,
clientSecret: savedClient.clientSecret
clientSecret: savedClient.clientSecret,
accessTokenExpiration: savedClient.accessTokenExpiration,
refreshTokenExpiration: savedClient.refreshTokenExpiration
}
}

View File

@@ -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)

View File

@@ -1,4 +1,5 @@
export * from './auth'
export * from './authConfig'
export * from './client'
export * from './code'
export * from './drive'

View File

@@ -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)
@@ -74,6 +78,11 @@ export class ExecutionController {
const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt')
if (isDebugOn(vars)) {
await createFile(headersPath, 'content-type: text/plain')
}
const weboutPath = path.join(session.path, 'webout.txt')
const tokenFile = path.join(session.path, 'reqHeaders.txt')
@@ -104,7 +113,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)
: ''

View File

@@ -3,6 +3,7 @@ import { Session } from '../../types'
import { promisify } from 'util'
import { execFile } from 'child_process'
import {
getPackagesFolder,
getSessionsFolder,
generateUniqueFileName,
sysInitCompiledPath,
@@ -49,7 +50,7 @@ export class SessionController {
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'Content-type: text/plain')
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
this.sessions.push(session)
return session
@@ -93,7 +94,7 @@ export class SASSessionController extends SessionController {
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'Content-type: text/plain')
await createFile(headersPath, 'content-type: text/html; charset=utf-8')
// we do not want to leave sessions running forever
// we clean them up after a predefined period, if unused
@@ -104,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}`
@@ -143,12 +145,12 @@ ${autoExecContent}`
])
.then(() => {
session.completed = true
console.log('session completed', session)
process.logger.info('session completed', session)
})
.catch((err) => {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
process.logger.error('session crashed', session.id, session.crashed)
})
// we have a triggered session - add to array
@@ -168,7 +170,10 @@ ${autoExecContent}`
while ((await fileExists(codeFilePath)) && !session.crashed) {}
if (session.crashed)
console.log('session crashed! while waiting to be ready', session.crashed)
process.logger.error(
'session crashed! while waiting to be ready',
session.crashed
)
session.ready = true
}

View File

@@ -110,23 +110,19 @@ export const processProgram = async (
// 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)
process.logger.info('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
process.logger.error('session crashed', session.id, session.crashed)
}
}
}

View File

@@ -2,6 +2,16 @@ 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
@@ -16,9 +26,17 @@ export interface MockFileRead {
export class MockSas9Controller {
private loggedIn: string | undefined
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
@Get('/SASStoredProcess')
public async sasStoredProcess(): Promise<Sas9Response> {
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: '',
@@ -26,17 +44,87 @@ export class MockSas9Controller {
}
}
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',
'generic',
'sas9',
'sas-stored-process'
...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) {
process.logger.error('err', err)
}
return {
content: 'No webout returned.'
}
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'sas9',
...filePath
])
}
@Post('/SASStoredProcess/do/')
public async sasStoredProcessDo(
public async sasStoredProcessDoPost(
@Request() req: express.Request
): Promise<Sas9Response> {
if (!this.loggedIn) {
@@ -53,23 +141,38 @@ export class MockSas9Controller {
}
}
let program = req.query._program?.toString() || ''
program = program.replace('/', '')
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
})
const content = await getMockResponseFromFile([
process.cwd(),
'mocks',
...program.split('/')
])
if (content.error) {
return content
return {
content: result.result as string
}
} catch (err) {
process.logger.error('err', err)
}
const parsedContent = parseJsonIfValid(content.content)
return {
content: parsedContent
content: 'No webout returned.'
}
}
@@ -85,8 +188,8 @@ export class MockSas9Controller {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'generic',
'sas9',
'generic',
'logged-in'
])
}
@@ -95,21 +198,27 @@ export class MockSas9Controller {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'generic',
'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',
'generic',
'sas9',
'generic',
'logged-in'
])
}
@@ -122,8 +231,8 @@ export class MockSas9Controller {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'generic',
'sas9',
'generic',
'public-access-denied'
])
}
@@ -131,8 +240,8 @@ export class MockSas9Controller {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'generic',
'sas9',
'generic',
'logged-out'
])
}
@@ -152,23 +261,6 @@ export class MockSas9Controller {
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
}
/**
* If JSON is valid it will be parsed otherwise will return text unaltered
* @param content string to be parsed
* @returns JSON or string
*/
const parseJsonIfValid = (content: string) => {
let fileContent = ''
try {
fileContent = JSON.parse(content)
} catch (err: any) {
fileContent = content
}
return fileContent
}
const getMockResponseFromFile = async (
filePath: string[]
): Promise<MockFileRead> => {
@@ -177,7 +269,7 @@ const getMockResponseFromFile = async (
let file = await readFile(filePathParsed).catch((err: any) => {
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
console.error(errMsg)
process.logger.error(errMsg)
error = true

View File

@@ -2,6 +2,10 @@ import express from 'express'
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
import { UserResponse } from './user'
interface SessionResponse extends UserResponse {
needsToUpdatePassword: boolean
}
@Security('bearerAuth')
@Route('SASjsApi/session')
@Tags('Session')
@@ -19,7 +23,7 @@ export class SessionController {
@Get('/')
public async session(
@Request() request: express.Request
): Promise<UserResponse> {
): Promise<SessionResponse> {
return session(request)
}
}
@@ -28,5 +32,6 @@ const session = (req: express.Request) => ({
id: req.user!.userId,
username: req.user!.username,
displayName: req.user!.displayName,
isAdmin: req.user!.isAdmin
isAdmin: req.user!.isAdmin,
needsToUpdatePassword: req.user!.needsToUpdatePassword
})

View File

@@ -91,6 +91,8 @@ const execute = async (
}
)
req.res?.header(httpHeaders)
if (result instanceof Buffer) {
;(req as any).sasHeaders = httpHeaders
}

View File

@@ -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)

View File

@@ -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 = {
@@ -91,7 +104,8 @@ const login = async (
displayName: user.displayName,
isAdmin: user.isAdmin,
isActive: user.isActive,
autoExec: user.autoExec
autoExec: user.autoExec,
needsToUpdatePassword: user.needsToUpdatePassword
}
return {
@@ -100,7 +114,8 @@ const login = async (
id: user.id,
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin
isAdmin: user.isAdmin,
needsToUpdatePassword: user.needsToUpdatePassword
}
}
}

View File

@@ -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,
@@ -81,7 +81,8 @@ const authenticateToken = async (
username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName',
isAdmin: true,
isActive: true
isActive: true,
needsToUpdatePassword: false
}
req.accessToken = 'desktopModeAccessToken'
return next()

View File

@@ -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()

View 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()
}

View File

@@ -33,5 +33,6 @@ export const desktopUser: RequestUser = {
username: userInfo().username,
displayName: userInfo().username,
isAdmin: true,
isActive: true
isActive: true,
needsToUpdatePassword: false
}

View File

@@ -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'

View File

@@ -1,5 +1,6 @@
import mongoose, { Schema } from 'mongoose'
export const NUMBER_OF_SECONDS_IN_A_DAY = 86400
export interface ClientPayload {
/**
* Client ID
@@ -11,6 +12,16 @@ export interface ClientPayload {
* @example "someRandomCryptoString"
*/
clientSecret: string
/**
* Number of seconds after which access token will expire. Default is 86400 (1 day)
* @example 86400
*/
accessTokenExpiration?: number
/**
* Number of seconds after which access token will expire. Default is 2592000 (30 days)
* @example 2592000
*/
refreshTokenExpiration?: number
}
const ClientSchema = new Schema<ClientPayload>({
@@ -21,6 +32,14 @@ const ClientSchema = new Schema<ClientPayload>({
clientSecret: {
type: String,
required: true
},
accessTokenExpiration: {
type: Number,
default: NUMBER_OF_SECONDS_IN_A_DAY
},
refreshTokenExpiration: {
type: Number,
default: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
})

View File

@@ -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

View File

@@ -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 {
/**
@@ -39,9 +40,11 @@ interface IUserDocument extends UserPayload, Document {
id: number
isAdmin: boolean
isActive: boolean
needsToUpdatePassword: boolean
autoExec: string
groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }]
authProvider?: AuthProviderType
}
export interface IUser extends IUserDocument {
@@ -67,6 +70,10 @@ const userSchema = new Schema<IUserDocument>({
type: String,
required: true
},
authProvider: {
type: String,
enum: AuthProviderType
},
isAdmin: {
type: Boolean,
default: false
@@ -75,6 +82,10 @@ const userSchema = new Schema<IUserDocument>({
type: Boolean,
default: true
},
needsToUpdatePassword: {
type: Boolean,
default: true
},
autoExec: {
type: String
},

View File

@@ -7,12 +7,28 @@ import {
authenticateRefreshToken
} from '../../middlewares'
import { tokenValidation } from '../../utils'
import { tokenValidation, updatePasswordValidation } from '../../utils'
import { InfoJWT } from '../../types'
const authRouter = express.Router()
const controller = new AuthController()
authRouter.patch(
'/updatePassword',
authenticateAccessToken,
async (req, res) => {
const { error, value: body } = updatePasswordValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
await controller.updatePassword(req, body)
res.sendStatus(204)
} catch (err: any) {
res.status(err.code).send(err.message)
}
}
)
authRouter.post('/token', async (req, res) => {
const { error, value: body } = tokenValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)

View 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

View File

@@ -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)
}
}
)

View File

@@ -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,

View File

@@ -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')

View File

@@ -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({})
})
})

View File

@@ -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,
@@ -79,18 +77,49 @@ describe('web', () => {
id: expect.any(Number),
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin
isAdmin: user.isAdmin,
needsToUpdatePassword: true
})
})
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 +128,7 @@ describe('web', () => {
password: user.password
}
;({ cookies: authCookies } = await performLogin(
app,
credentials,
cookies,
csrfToken
))
;({ authCookies } = await performLogin(app, credentials, csrfToken))
})
afterAll(async () => {
@@ -116,17 +140,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 +173,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 +188,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) =>

View File

@@ -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)
}
}
)

View File

@@ -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)
})
@@ -58,7 +58,7 @@ export const publishAppStream = async (
)
const sasJsPort = process.env.PORT || 5000
console.log(
process.logger.info(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
)

View File

@@ -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)
}

View File

@@ -3,6 +3,7 @@ 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()
@@ -18,7 +19,7 @@ switch (MOCK_SERVERTYPE) {
break
}
default: {
router.use('/', webRouter)
router.use('/', csrfProtection, webRouter)
}
}

View File

@@ -1,12 +1,26 @@
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
@@ -15,7 +29,7 @@ sas9WebRouter.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 codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
@@ -26,7 +40,7 @@ sas9WebRouter.get('/', async (req, res) => {
})
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
const response = await controller.sasStoredProcess()
const response = await controller.sasStoredProcess(req)
if (response.redirect) {
res.redirect(response.redirect)
@@ -40,8 +54,8 @@ sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
}
})
sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
const response = await controller.sasStoredProcessDo(req)
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
const response = await controller.sasStoredProcessDoGet(req)
if (response.redirect) {
res.redirect(response.redirect)
@@ -55,6 +69,26 @@ sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
}
})
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()

View File

@@ -1,4 +1,5 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web'
const sasViyaWebRouter = express.Router()
@@ -11,7 +12,7 @@ sasViyaWebRouter.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 codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`

View File

@@ -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>`

View File

@@ -7,11 +7,11 @@ appPromise.then(async (app) => {
const protocol = process.env.PROTOCOL || 'http'
const sasJsPort = process.env.PORT || 5000
console.log('PROTOCOL: ', protocol)
process.logger.info('PROTOCOL: ', protocol)
if (protocol !== 'https') {
app.listen(sasJsPort, () => {
console.log(
process.logger.info(
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
)
})
@@ -20,7 +20,7 @@ appPromise.then(async (app) => {
const httpsServer = createServer({ key, cert, ca }, app)
httpsServer.listen(sasJsPort, () => {
console.log(
process.logger.info(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
)
})

View File

@@ -5,5 +5,6 @@ export interface RequestUser {
displayName: string
isAdmin: boolean
isActive: boolean
needsToUpdatePassword: boolean
autoExec?: string
}

View File

@@ -5,6 +5,7 @@ declare namespace NodeJS {
pythonLoc?: string
rLoc?: string
driveLoc: string
sasjsRoot: string
logsLoc: string
logsUUID: string
sessionController?: import('../../controllers/internal').SessionController

View File

@@ -36,7 +36,7 @@ export const loadAppStreamConfig = async () => {
)
}
console.log('App Stream Config loaded!')
process.logger.info('App Stream Config loaded!')
}
export const addEntryToAppStreamConfig = (

View File

@@ -8,6 +8,6 @@ export const connectDB = async () => {
throw new Error('Unable to connect to DB!')
}
console.log('Connected to DB!')
process.logger.success('Connected to DB!')
return seedDB()
}

View File

@@ -12,7 +12,7 @@ import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
export const copySASjsCore = async () => {
if (process.env.NODE_ENV === 'test') return
console.log('Copying Macros from container to drive(tmp).')
process.logger.log('Copying Macros from container to drive.')
const macrosDrivePath = getMacrosFolder()
@@ -30,5 +30,5 @@ export const copySASjsCore = async () => {
await createFile(macroFileDestPath, macroContent)
})
console.log('Macros Drive Path:', macrosDrivePath)
process.logger.info('Macros Drive Path:', macrosDrivePath)
}

View File

@@ -0,0 +1,18 @@
import path from 'path'
import { createFile } from '@sasjs/utils'
import { getMacrosFolder } from './file'
const fileContent = `%macro webout(action,ds,dslabel=,fmt=,missing=NULL,showmeta=NO,maxobs=MAX);
%ms_webout(&action,ds=&ds,dslabel=&dslabel,fmt=&fmt
,missing=&missing
,showmeta=&showmeta
,maxobs=&maxobs
)
%mend;`
export const createWeboutSasFile = async () => {
const macrosDrivePath = getMacrosFolder()
process.logger.log(`Creating webout.sas at ${macrosDrivePath}`)
const filePath = path.join(macrosDrivePath, 'webout.sas')
await createFile(filePath, fileContent)
}

View File

@@ -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')
@@ -20,19 +20,24 @@ export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
export const getDesktopUserAutoExecPath = () =>
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
export const getSasjsRootFolder = () => process.driveLoc
export const getSasjsRootFolder = () => process.sasjsRoot
export const getSasjsDriveFolder = () => process.driveLoc
export const getLogFolder = () => process.logsLoc
export const getAppStreamConfigPath = () =>
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
path.join(getSasjsDriveFolder(), 'appStreamConfig.json')
export const getMacrosFolder = () =>
path.join(getSasjsRootFolder(), 'sasjscore')
path.join(getSasjsDriveFolder(), 'sas', 'sasautos')
export const getPackagesFolder = () =>
path.join(getSasjsDriveFolder(), 'sas', 'sas_packages')
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
export const getFilesFolder = () => path.join(getSasjsDriveFolder(), 'files')
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')

View File

@@ -1,7 +1,8 @@
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
export const generateAccessToken = (data: InfoJWT) =>
export const generateAccessToken = (data: InfoJWT, expiry?: number) =>
jwt.sign(data, process.secrets.ACCESS_TOKEN_SECRET, {
expiresIn: '1day'
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
})

View File

@@ -1,7 +1,8 @@
import jwt from 'jsonwebtoken'
import { InfoJWT } from '../types'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../model/Client'
export const generateRefreshToken = (data: InfoJWT) =>
export const generateRefreshToken = (data: InfoJWT, expiry?: number) =>
jwt.sign(data, process.secrets.REFRESH_TOKEN_SECRET, {
expiresIn: '30 days'
expiresIn: expiry ? expiry : NUMBER_OF_SECONDS_IN_A_DAY
})

View File

@@ -10,9 +10,9 @@ export const getCertificates = async () => {
const certPath = CERT_CHAIN ?? (await getFileInput('Certificate Chain (PEM)'))
const caPath = CA_ROOT
console.log('keyPath: ', keyPath)
console.log('certPath: ', certPath)
if (caPath) console.log('caPath: ', caPath)
process.logger.info('keyPath: ', keyPath)
process.logger.info('certPath: ', certPath)
if (caPath) process.logger.info('caPath: ', caPath)
const key = await readFile(keyPath)
const cert = await readFile(certPath)

View File

@@ -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
}

View File

@@ -4,7 +4,7 @@ import { getFilesFolder } from './file'
import { RunTimeType } from '.'
export const getRunTimeAndFilePath = async (programPath: string) => {
const ext = path.extname(programPath)
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)) {

View File

@@ -1,6 +1,27 @@
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
@@ -13,22 +34,22 @@ export const getTokensFromDB = async (userId: number, clientId: string) => {
const accessToken = currentTokenObj.accessToken
const refreshToken = currentTokenObj.refreshToken
const verifiedAccessToken: any = jwt.verify(
const isValidAccessToken = await isValidToken(
accessToken,
process.secrets.ACCESS_TOKEN_SECRET
process.secrets.ACCESS_TOKEN_SECRET,
userId,
clientId
)
const verifiedRefreshToken: any = jwt.verify(
const isValidRefreshToken = await isValidToken(
refreshToken,
process.secrets.REFRESH_TOKEN_SECRET
process.secrets.REFRESH_TOKEN_SECRET,
userId,
clientId
)
if (
verifiedAccessToken?.userId === userId &&
verifiedAccessToken?.clientId === clientId &&
verifiedRefreshToken?.userId === userId &&
verifiedRefreshToken?.clientId === clientId
)
if (isValidAccessToken && isValidRefreshToken) {
return { accessToken, refreshToken }
}
}
}

View File

@@ -1,6 +1,7 @@
export * from './appStreamConfig'
export * from './connectDB'
export * from './copySASjsCore'
export * from './createWeboutSasFile'
export * from './desktopAutoExec'
export * from './extractHeaders'
export * from './extractName'
@@ -18,6 +19,7 @@ export * from './getTokensFromDB'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './isPublicRoute'
export * from './ldapClient'
export * from './zipped'
export * from './parseLogToArray'
export * from './removeTokensInDB'
@@ -25,6 +27,7 @@ export * from './saveTokensInDB'
export * from './seedDB'
export * from './setProcessVariables'
export * from './setupFolders'
export * from './setupUserAutoExec'
export * from './upload'
export * from './validation'
export * from './verifyEnvVariables'

View File

@@ -27,5 +27,6 @@ export const publicUser: RequestUser = {
username: 'publicUser',
displayName: 'Public User',
isAdmin: false,
isActive: true
isActive: true,
needsToUpdatePassword: false
}

163
api/src/utils/ldapClient.ts Normal file
View 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.')
})
}
}

View File

@@ -22,12 +22,12 @@ export const getEnvCSPDirectives = (
try {
cspConfigJson = JSON.parse(file)
} catch (e) {
console.error(
process.logger.error(
'Parsing Content Security Policy JSON config failed. Make sure it is valid json'
)
}
} catch (e) {
console.error('Error reading HELMET CSP config file', e)
process.logger.error('Error reading HELMET CSP config file', e)
}
}

View File

@@ -19,7 +19,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
const client = new Client(CLIENT)
await client.save()
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
process.logger.success(`DB Seed - client created: ${CLIENT.clientId}`)
}
// Checking if 'AllUsers' Group is already in the database
@@ -28,7 +28,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
const group = new Group(GROUP)
groupExist = await group.save()
console.log(`DB Seed - Group created: ${GROUP.name}`)
process.logger.success(`DB Seed - Group created: ${GROUP.name}`)
}
// Checking if 'Public' Group is already in the database
@@ -37,7 +37,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
const group = new Group(PUBLIC_GROUP)
await group.save()
console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
process.logger.success(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
}
// Checking if user is already in the database
@@ -46,12 +46,14 @@ export const seedDB = async (): Promise<ConfigurationType> => {
const user = new User(ADMIN_USER)
usernameExist = await user.save()
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
process.logger.success(
`DB Seed - admin account created: ${ADMIN_USER.username}`
)
}
if (!groupExist.hasUser(usernameExist)) {
groupExist.addUser(usernameExist)
console.log(
process.logger.success(
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${GROUP.name}'`
)
}
@@ -62,7 +64,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
const configuration = new Configuration(SECRETS)
configExist = await configuration.save()
console.log('DB Seed - configuration added')
process.logger.success('DB Seed - configuration added')
}
return {

View File

@@ -19,7 +19,8 @@ export const setProcessVariables = async () => {
}
if (process.env.NODE_ENV === 'test') {
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
return
}
@@ -32,7 +33,6 @@ export const setProcessVariables = async () => {
process.rLoc = process.env.R_PATH
} else {
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
process.sasLoc = sasLoc
process.nodeLoc = nodeLoc
process.pythonLoc = pythonLoc
@@ -42,11 +42,19 @@ export const setProcessVariables = async () => {
const { SASJS_ROOT } = process.env
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
await createFolder(absPath)
process.driveLoc = getRealPath(absPath)
process.sasjsRoot = getRealPath(absPath)
const { DRIVE_LOCATION } = process.env
const absDrivePath = getAbsolutePath(
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
process.cwd()
)
await createFolder(absDrivePath)
process.driveLoc = getRealPath(absDrivePath)
const { LOG_LOCATION } = process.env
const absLogsPath = getAbsolutePath(
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
process.cwd()
)
await createFolder(absLogsPath)
@@ -54,8 +62,8 @@ export const setProcessVariables = async () => {
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc)
console.log('sasLogs: ', process.logsLoc)
console.log('runTimes: ', process.runTimes)
process.logger.info('sasLoc: ', process.sasLoc)
process.logger.info('sasDrive: ', process.driveLoc)
process.logger.info('sasLogs: ', process.logsLoc)
process.logger.info('runTimes: ', process.runTimes)
}

View File

@@ -1,14 +1,7 @@
import { createFile, createFolder, fileExists } from '@sasjs/utils'
import { getDesktopUserAutoExecPath, getFilesFolder } from './file'
import { ModeType } from './verifyEnvVariables'
import { createFolder } from '@sasjs/utils'
import { getFilesFolder, getPackagesFolder } from './file'
export const setupFolders = async () => {
const drivePath = getFilesFolder()
await createFolder(drivePath)
export const setupFilesFolder = async () => await createFolder(getFilesFolder())
if (process.env.MODE === ModeType.Desktop) {
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
await createFile(getDesktopUserAutoExecPath(), '')
}
}
}
export const setupPackagesFolder = async () =>
await createFolder(getPackagesFolder())

View File

@@ -0,0 +1,11 @@
import { createFile, fileExists } from '@sasjs/utils'
import { getDesktopUserAutoExecPath } from './file'
import { ModeType } from './verifyEnvVariables'
export const setupUserAutoExec = async () => {
if (process.env.MODE === ModeType.Desktop) {
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
await createFile(getDesktopUserAutoExecPath(), '')
}
}
}

View File

@@ -85,10 +85,18 @@ export const updateUserValidation = (
return Joi.object(validationChecks).validate(data)
}
export const updatePasswordValidation = (data: any): Joi.ValidationResult =>
Joi.object({
currentPassword: Joi.string().required(),
newPassword: passwordSchema.required()
}).validate(data)
export const registerClientValidation = (data: any): Joi.ValidationResult =>
Joi.object({
clientId: Joi.string().required(),
clientSecret: Joi.string().required()
clientSecret: Joi.string().required(),
accessTokenExpiration: Joi.number(),
refreshTokenExpiration: Joi.number()
}).validate(data)
export const registerPermissionValidation = (data: any): Joi.ValidationResult =>

View File

@@ -8,6 +8,10 @@ export enum ModeType {
Desktop = 'desktop'
}
export enum AuthProviderType {
LDAP = 'ldap'
}
export enum ProtocolType {
HTTP = 'http',
HTTPS = 'https'
@@ -64,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')}`
@@ -104,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
@@ -252,7 +267,7 @@ const verifyRUN_TIMES = (): string[] => {
return errors
}
const verifyExecutablePaths = () => {
const verifyExecutablePaths = (): string[] => {
const errors: string[] = []
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
process.env
@@ -280,6 +295,53 @@ const verifyExecutablePaths = () => {
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
}
const DEFAULTS = {
MODE: ModeType.Desktop,
PROTOCOL: ProtocolType.HTTP,

View File

@@ -15,6 +15,7 @@ export const fetchLatestAutoExec = async (
displayName: dbUser.displayName,
isAdmin: dbUser.isAdmin,
isActive: dbUser.isActive,
needsToUpdatePassword: dbUser.needsToUpdatePassword,
autoExec: dbUser.autoExec
}
}
@@ -41,6 +42,7 @@ export const verifyTokenInDB = async (
displayName: dbUser.displayName,
isAdmin: dbUser.isAdmin,
isActive: dbUser.isActive,
needsToUpdatePassword: dbUser.needsToUpdatePassword,
autoExec: dbUser.autoExec
}
: undefined

View File

@@ -15,6 +15,10 @@
"name": "Auth",
"description": "Operations about auth"
},
{
"name": "Auth_Config",
"description": "Operations about external auth providers"
},
{
"name": "Client",
"description": "Operations about clients"

View File

@@ -8,6 +8,7 @@ import Header from './components/header'
import Home from './components/home'
import Studio from './containers/Studio'
import Settings from './containers/Settings'
import UpdatePassword from './components/updatePassword'
import { AppContext } from './context/appContext'
import AuthCode from './containers/AuthCode'
@@ -29,6 +30,20 @@ function App() {
)
}
if (appContext.needsToUpdatePassword) {
return (
<ThemeProvider theme={theme}>
<HashRouter>
<Header />
<Routes>
<Route path="*" element={<UpdatePassword />} />
</Routes>
<ToastContainer />
</HashRouter>
</ThemeProvider>
)
}
return (
<ThemeProvider theme={theme}>
<HashRouter>

View File

@@ -31,14 +31,24 @@ const DeleteConfirmationModal = ({
message,
_delete
}: DeleteConfirmationModalProps) => {
const handleDeleteClick = (event: React.MouseEvent) => {
event.stopPropagation()
_delete()
}
const handleClose = (event: any) => {
event.stopPropagation()
setOpen(false)
}
return (
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
<BootstrapDialog onClose={handleClose} open={open}>
<DialogContent dividers>
<Typography gutterBottom>{message}</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button color="error" onClick={() => _delete()}>
<Button onClick={handleClose}>Cancel</Button>
<Button color="error" onClick={handleDeleteClick}>
Delete
</Button>
</DialogActions>

View File

@@ -32,6 +32,7 @@ const Login = () => {
appContext.setDisplayName?.(user.displayName)
appContext.setIsAdmin?.(user.isAdmin)
appContext.setLoggedIn?.(loggedIn)
appContext.setNeedsToUpdatePassword?.(user.needsToUpdatePassword)
}
}

View File

@@ -69,8 +69,18 @@ const NameInputModal = ({
action(name)
}
const handleActionClick = (event: React.MouseEvent) => {
event.stopPropagation()
action(name)
}
const handleClose = (event: any) => {
event.stopPropagation()
setOpen(false)
}
return (
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
<BootstrapDialog fullWidth onClose={handleClose} open={open}>
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
{title}
</BootstrapDialogTitle>
@@ -91,12 +101,12 @@ const NameInputModal = ({
</form>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={() => setOpen(false)}>
<Button variant="contained" onClick={handleClose}>
Cancel
</Button>
<Button
variant="contained"
onClick={() => action(name)}
onClick={handleActionClick}
disabled={hasError || !name}
>
{actionLabel}

View File

@@ -0,0 +1,145 @@
import React, { useEffect, useState } from 'react'
import {
Grid,
DialogContent,
DialogActions,
Button,
OutlinedInput,
InputAdornment,
IconButton,
FormControl,
InputLabel,
FormHelperText
} from '@mui/material'
import Visibility from '@mui/icons-material/Visibility'
import VisibilityOff from '@mui/icons-material/VisibilityOff'
import { BootstrapDialogTitle } from './dialogTitle'
import { BootstrapDialog } from './modal'
type Props = {
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
title: string
updatePassword: (currentPassword: string, newPassword: string) => void
}
const UpdatePasswordModal = (props: Props) => {
const { open, setOpen, title, updatePassword } = props
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [hasError, setHasError] = useState(false)
const [errorText, setErrorText] = useState('')
useEffect(() => {
if (
currentPassword.length > 0 &&
newPassword.length > 0 &&
newPassword === currentPassword
) {
setErrorText('New password should be different to current password.')
setHasError(true)
} else if (newPassword.length >= 6) {
setErrorText('')
setHasError(false)
}
}, [currentPassword, newPassword])
const handleBlur = () => {
if (newPassword.length < 6) {
setErrorText('Password length should be at least 6 characters.')
setHasError(true)
}
}
return (
<div>
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
{title}
</BootstrapDialogTitle>
<DialogContent dividers>
<Grid container spacing={2}>
<Grid item xs={12}>
<PasswordInput
label="Current Password"
password={currentPassword}
setPassword={setCurrentPassword}
/>
</Grid>
<Grid item xs={12}>
<PasswordInput
label="New Password"
password={newPassword}
setPassword={setNewPassword}
hasError={hasError}
errorText={errorText}
handleBlur={handleBlur}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="contained"
onClick={() => updatePassword(currentPassword, newPassword)}
disabled={hasError || !currentPassword || !newPassword}
>
Update
</Button>
</DialogActions>
</BootstrapDialog>
</div>
)
}
export default UpdatePasswordModal
type PasswordInputProps = {
label: string
password: string
setPassword: React.Dispatch<React.SetStateAction<string>>
hasError?: boolean
errorText?: string
handleBlur?: () => void
}
export const PasswordInput = ({
label,
password,
setPassword,
hasError,
errorText,
handleBlur
}: PasswordInputProps) => {
const [showPassword, setShowPassword] = useState(false)
return (
<FormControl sx={{ width: '100%' }} variant="outlined" error={hasError}>
<InputLabel htmlFor="outlined-adornment-password">{label}</InputLabel>
<OutlinedInput
id="outlined-adornment-password"
type={showPassword ? 'text' : 'password'}
label={label}
value={password}
onChange={(e) => setPassword(e.target.value)}
onBlur={handleBlur}
endAdornment={
<InputAdornment position="end">
<IconButton
onClick={() => setShowPassword((val) => !val)}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
}
/>
{errorText && <FormHelperText>{errorText}</FormHelperText>}
</FormControl>
)
}

View File

@@ -1,67 +1,79 @@
import React, { useEffect, useState } from 'react'
import { Menu, MenuItem } from '@mui/material'
import React, { useState } from 'react'
import { Menu, MenuItem, Typography } from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
import MuiTreeView from '@mui/lab/TreeView'
import MuiTreeItem from '@mui/lab/TreeItem'
import DeleteConfirmationModal from './deleteConfirmationModal'
import NameInputModal from './nameInputModal'
import { TreeNode } from '../utils/types'
type Props = {
interface Props {
node: TreeNode
selectedFilePath: string
handleSelect: (filePath: string) => void
deleteNode: (path: string, isFolder: boolean) => void
addFile: (path: string) => void
addFolder: (path: string) => void
rename: (oldPath: string, newPath: string) => void
}
interface TreeViewProps extends Props {
defaultExpanded?: string[]
}
const TreeView = ({
node,
selectedFilePath,
handleSelect,
deleteNode,
addFile,
addFolder,
rename,
defaultExpanded
}: Props) => {
return (
<ul
style={{
listStyle: 'none',
padding: '0.25rem 0.85rem',
width: 'max-content'
}}
}: TreeViewProps) => {
const renderTree = (nodes: TreeNode) => (
<MuiTreeItem
key={nodes.relativePath}
nodeId={nodes.relativePath}
label={
<TreeItemWithContextMenu
node={nodes}
handleSelect={handleSelect}
deleteNode={deleteNode}
addFile={addFile}
addFolder={addFolder}
rename={rename}
/>
}
>
<TreeViewNode
node={node}
selectedFilePath={selectedFilePath}
handleSelect={handleSelect}
deleteNode={deleteNode}
addFile={addFile}
addFolder={addFolder}
rename={rename}
defaultExpanded={defaultExpanded}
/>
</ul>
{Array.isArray(nodes.children)
? nodes.children.map((node) => renderTree(node))
: null}
</MuiTreeItem>
)
return (
<MuiTreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
defaultExpanded={defaultExpanded}
sx={{ flexGrow: 1, maxWidth: 400, overflowY: 'auto' }}
>
{renderTree(node)}
</MuiTreeView>
)
}
export default TreeView
const TreeViewNode = ({
const TreeItemWithContextMenu = ({
node,
selectedFilePath,
handleSelect,
deleteNode,
addFile,
addFolder,
rename,
defaultExpanded
rename
}: Props) => {
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
useState(false)
@@ -72,18 +84,19 @@ const TreeViewNode = ({
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
const [childVisible, setChildVisibility] = useState(false)
const [contextMenu, setContextMenu] = useState<{
mouseX: number
mouseY: number
} | null>(null)
const launchProgram = () => {
const launchProgram = (event: React.MouseEvent) => {
event.stopPropagation()
const baseUrl = window.location.origin
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
}
const launchProgramWithDebug = () => {
const launchProgramWithDebug = (event: React.MouseEvent) => {
event.stopPropagation()
const baseUrl = window.location.origin
window.open(
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
@@ -103,25 +116,18 @@ const TreeViewNode = ({
)
}
const hasChild = node.children.length ? true : false
const handleItemClick = () => {
if (node.children.length) {
setChildVisibility((v) => !v)
return
}
const handleClose = (event: any) => {
event.stopPropagation()
setContextMenu(null)
}
const handleItemClick = (event: React.MouseEvent) => {
if (node.children.length) return
handleSelect(node.relativePath)
}
useEffect(() => {
if (defaultExpanded && defaultExpanded[0] === node.relativePath) {
setChildVisibility(true)
defaultExpanded.shift()
}
}, [defaultExpanded, node.relativePath])
const handleDeleteItemClick = () => {
const handleDeleteItemClick = (event: React.MouseEvent) => {
event.stopPropagation()
setContextMenu(null)
setDeleteConfirmationModalOpen(true)
setDeleteConfirmationModalMessage(
@@ -136,7 +142,8 @@ const TreeViewNode = ({
deleteNode(node.relativePath, node.isFolder)
}
const handleNewFolderItemClick = () => {
const handleNewFolderItemClick = (event: React.MouseEvent) => {
event.stopPropagation()
setContextMenu(null)
setNameInputModalOpen(true)
setNameInputModalTitle('Add Folder')
@@ -145,7 +152,8 @@ const TreeViewNode = ({
setDefaultInputModalName('')
}
const handleNewFileItemClick = () => {
const handleNewFileItemClick = (event: React.MouseEvent) => {
event.stopPropagation()
setContextMenu(null)
setNameInputModalOpen(true)
setNameInputModalTitle('Add File')
@@ -161,7 +169,8 @@ const TreeViewNode = ({
else addFile(path)
}
const handleRenameItemClick = () => {
const handleRenameItemClick = (event: React.MouseEvent) => {
event.stopPropagation()
setContextMenu(null)
setNameInputModalOpen(true)
setNameInputModalTitle('Rename')
@@ -181,34 +190,7 @@ const TreeViewNode = ({
return (
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
<li style={{ display: 'list-item' }}>
<div
className={`tree-item-label ${
selectedFilePath === node.relativePath ? 'selected' : ''
}`}
onClick={() => handleItemClick()}
>
{hasChild &&
(childVisible ? <ExpandMoreIcon /> : <ChevronRightIcon />)}
<div>{node.name}</div>
</div>
{hasChild &&
childVisible &&
node.children.map((child, index) => (
<TreeView
key={node.relativePath + '-' + index}
node={child}
selectedFilePath={selectedFilePath}
handleSelect={handleSelect}
deleteNode={deleteNode}
addFile={addFile}
addFolder={addFolder}
rename={rename}
defaultExpanded={defaultExpanded}
/>
))}
</li>
<Typography onClick={handleItemClick}>{node.name}</Typography>
<DeleteConfirmationModal
open={deleteConfirmationModalOpen}
setOpen={setDeleteConfirmationModalOpen}
@@ -228,7 +210,7 @@ const TreeViewNode = ({
/>
<Menu
open={contextMenu !== null}
onClose={() => setContextMenu(null)}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
contextMenu !== null

View File

@@ -0,0 +1,109 @@
import React, { useState, useEffect, useContext } from 'react'
import axios from 'axios'
import { Box, CssBaseline, Button, CircularProgress } from '@mui/material'
import { toast } from 'react-toastify'
import { PasswordInput } from './passwordModal'
import { AppContext } from '../context/appContext'
const UpdatePassword = () => {
const appContext = useContext(AppContext)
const [isLoading, setIsLoading] = useState(false)
const [currentPassword, setCurrentPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [hasError, setHasError] = useState(false)
const [errorText, setErrorText] = useState('')
useEffect(() => {
if (
currentPassword.length > 0 &&
newPassword.length > 0 &&
newPassword === currentPassword
) {
setErrorText('New password should be different to current password.')
setHasError(true)
} else if (newPassword.length >= 6) {
setErrorText('')
setHasError(false)
}
}, [currentPassword, newPassword])
const handleBlur = () => {
if (newPassword.length < 6) {
setErrorText('Password length should be at least 6 characters.')
setHasError(true)
}
}
const handleSubmit = async (e: any) => {
e.preventDefault()
if (hasError || !currentPassword || !newPassword) return
setIsLoading(true)
axios
.patch(`/SASjsApi/auth/updatePassword`, {
currentPassword,
newPassword
})
.then((res: any) => {
appContext.setNeedsToUpdatePassword?.(false)
toast.success('Password updated', {
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
className="main"
component="form"
onSubmit={handleSubmit}
sx={{
'& > :not(style)': { m: 1, width: '25ch' }
}}
>
<CssBaseline />
<h2>Welcome to SASjs Server!</h2>
<p style={{ width: 'auto' }}>
This is your first time login to SASjs server. Therefore, you need to
update your password.
</p>
<PasswordInput
label="Current Password"
password={currentPassword}
setPassword={setCurrentPassword}
/>
<PasswordInput
label="New Password"
password={newPassword}
setPassword={setNewPassword}
hasError={hasError}
errorText={errorText}
handleBlur={handleBlur}
/>
<Button
type="submit"
variant="outlined"
disabled={hasError || !currentPassword || !newPassword}
>
Update
</Button>
</Box>
)
}
export default UpdatePassword

View 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

View File

@@ -7,6 +7,7 @@ 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'
@@ -59,6 +60,9 @@ 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">
@@ -69,6 +73,9 @@ const Settings = () => {
<Permission />
</PermissionsContextProvider>
</StyledTabpanel>
<StyledTabpanel value="auth_config">
<AuthConfig />
</StyledTabpanel>
</TabContext>
</Box>
)

View File

@@ -17,11 +17,13 @@ import {
import { toast } from 'react-toastify'
import { AppContext, ModeType } from '../../context/appContext'
import UpdatePasswordModal from '../../components/passwordModal'
const Profile = () => {
const [isLoading, setIsLoading] = useState(false)
const appContext = useContext(AppContext)
const [user, setUser] = useState({} as any)
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
useEffect(() => {
setIsLoading(true)
@@ -36,7 +38,7 @@ const Profile = () => {
.finally(() => {
setIsLoading(false)
})
}, [])
}, [appContext.userId])
const handleChange = (event: any) => {
const { name, value } = event.target
@@ -68,82 +70,124 @@ const Profile = () => {
})
}
const updatePassword = (currentPassword: string, newPassword: string) => {
setIsLoading(true)
setIsPasswordModalOpen(false)
axios
.patch(`/SASjsApi/auth/updatePassword`, {
currentPassword,
newPassword
})
.then((res: any) => {
toast.success('Password updated', {
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%' }}
/>
) : (
<Card>
<CardHeader title="Profile Information" />
<Divider />
<CardContent>
<Grid container spacing={4}>
<Grid item md={6} xs={12}>
<TextField
fullWidth
error={user.displayName?.length === 0}
helperText="Please specify display name"
label="Display Name"
name="displayName"
onChange={handleChange}
required
value={user.displayName}
variant="outlined"
disabled={appContext.mode === ModeType.Desktop}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
error={user.username?.length === 0}
helperText="Please specify username"
label="Username"
name="username"
onChange={handleChange}
required
value={user.username}
variant="outlined"
disabled={appContext.mode === ModeType.Desktop}
/>
</Grid>
<Grid item lg={6} md={8} sm={12} xs={12}>
<TextField
fullWidth
label="autoExec"
name="autoExec"
onChange={handleChange}
multiline
rows="10"
value={user.autoExec}
variant="outlined"
/>
</Grid>
<Grid item xs={6}>
<FormGroup row>
<FormControlLabel
disabled
control={<Checkbox checked={user.isActive} />}
label="isActive"
<>
<Card>
<CardHeader title="Profile Information" />
<Divider />
<CardContent>
<Grid container spacing={4}>
<Grid item md={6} xs={12}>
<TextField
fullWidth
error={user.displayName?.length === 0}
helperText="Please specify display name"
label="Display Name"
name="displayName"
onChange={handleChange}
required
value={user.displayName}
variant="outlined"
disabled={appContext.mode === ModeType.Desktop}
/>
<FormControlLabel
disabled
control={<Checkbox checked={user.isAdmin} />}
label="isAdmin"
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
error={user.username?.length === 0}
helperText="Please specify username"
label="Username"
name="username"
onChange={handleChange}
required
value={user.username}
variant="outlined"
disabled={appContext.mode === ModeType.Desktop}
/>
</FormGroup>
</Grid>
<Grid item lg={6} md={8} sm={12} xs={12}>
<TextField
fullWidth
label="autoExec"
name="autoExec"
onChange={handleChange}
multiline
rows="10"
value={user.autoExec}
variant="outlined"
/>
</Grid>
<Grid item xs={6}>
<FormGroup row>
<FormControlLabel
disabled
control={<Checkbox checked={user.isActive} />}
label="isActive"
/>
<FormControlLabel
disabled
control={<Checkbox checked={user.isAdmin} />}
label="isAdmin"
/>
</FormGroup>
</Grid>
<Grid item xs={12}>
<Button
variant="contained"
onClick={() => setIsPasswordModalOpen(true)}
>
Update Password
</Button>
</Grid>
</Grid>
</Grid>
</CardContent>
<Divider />
<CardActions>
<Button type="submit" variant="contained" onClick={handleSubmit}>
Save Changes
</Button>
</CardActions>
</Card>
</CardContent>
<Divider />
<CardActions>
<Button type="submit" variant="contained" onClick={handleSubmit}>
Save Changes
</Button>
</CardActions>
</Card>
<UpdatePasswordModal
open={isPasswordModalOpen}
setOpen={setIsPasswordModalOpen}
title="Update Password"
updatePassword={updatePassword}
/>
</>
)
}

View File

@@ -48,7 +48,6 @@ const SASjsEditor = ({
setTab
}: SASjsEditorProps) => {
const {
ctrlPressed,
fileContent,
isLoading,
log,
@@ -64,8 +63,6 @@ const SASjsEditor = ({
handleDiffEditorDidMount,
handleEditorDidMount,
handleFilePathInput,
handleKeyDown,
handleKeyUp,
handleRunBtnClick,
handleTabChange,
saveFile,
@@ -99,7 +96,6 @@ const SASjsEditor = ({
original={prevFileContent}
value={fileContent}
editorDidMount={handleDiffEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => setFileContent(val)}
/>
) : (
@@ -108,7 +104,6 @@ const SASjsEditor = ({
language={getLanguageFromExtension(selectedFileExtension)}
value={fileContent}
editorDidMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => setFileContent(val)}
/>
)
@@ -176,8 +171,6 @@ const SASjsEditor = ({
{fileMenu}
</Box>
<Paper
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
sx={{
height: 'calc(100vh - 170px)',
padding: '10px',
@@ -204,7 +197,10 @@ const SASjsEditor = ({
<StyledTabPanel value="log">
<div>
<h2>Log</h2>
<pre id="log" style={{ overflow: 'auto', height: '75vh' }}>
<pre
id="log"
style={{ overflow: 'auto', height: 'calc(100vh - 220px)' }}
>
{log}
</pre>
</div>

View File

@@ -42,7 +42,6 @@ const useEditor = ({
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('')
@@ -50,7 +49,7 @@ const useEditor = ({
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
const [showDiff, setShowDiff] = useState(false)
const editorRef = useRef(null as any)
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const handleEditorDidMount: EditorDidMount = (editor) => {
editorRef.current = editor
@@ -148,53 +147,47 @@ const useEditor = ({
const handleRunBtnClick = () =>
runCode(getSelection(editorRef.current as any) || fileContent)
const runCode = (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')
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))
}
const handleKeyDown = (event: any) => {
if (event.ctrlKey) {
if (event.key === 'v') {
setCtrlPressed(false)
}
if (event.key === 'Enter')
runCode(getSelection(editorRef.current as any) || fileContent)
if (!ctrlPressed) setCtrlPressed(true)
}
}
const handleKeyUp = (event: any) => {
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
}
// 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)
@@ -206,7 +199,7 @@ const useEditor = ({
}
useEffect(() => {
editorRef.current.addAction({
const saveFileAction = editorRef.current?.addAction({
// An unique identifier of the contributed action.
id: 'save-file',
@@ -216,6 +209,8 @@ const useEditor = ({
// An optional array of keybindings for the action.
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
contextMenuGroupId: '9_cutcopypaste',
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: () => {
@@ -223,7 +218,31 @@ const useEditor = ({
if (prevFileContent !== fileContent) return saveFile()
}
})
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
const runCodeAction = 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',
// 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)
}
})
return () => {
saveFileAction?.dispose()
runCodeAction?.dispose()
}
}, [fileContent, prevFileContent, selectedFilePath, saveFile, runCode])
useEffect(() => {
setRunTimes(Object.values(appContext.runTimes))
@@ -236,7 +255,9 @@ const useEditor = ({
useEffect(() => {
if (selectedFilePath) {
setIsLoading(true)
setSelectedFileExtension(selectedFilePath.split('.').pop() ?? '')
setSelectedFileExtension(
selectedFilePath.split('.').pop()?.toLowerCase() ?? ''
)
axios
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
.then((res: any) => {
@@ -270,12 +291,11 @@ const useEditor = ({
}, [fileContent, selectedFilePath])
useEffect(() => {
if (runTimes.includes(selectedFileExtension))
setSelectedRunTime(selectedFileExtension)
const fileExtension = selectedFileExtension.toLowerCase()
if (runTimes.includes(fileExtension)) setSelectedRunTime(fileExtension)
}, [selectedFileExtension, runTimes])
return {
ctrlPressed,
fileContent,
isLoading,
log,
@@ -291,8 +311,6 @@ const useEditor = ({
handleDiffEditorDidMount,
handleEditorDidMount,
handleFilePathInput,
handleKeyDown,
handleKeyUp,
handleRunBtnClick,
handleTabChange,
saveFile,

View File

@@ -180,7 +180,6 @@ const SideBar = ({
{directoryData && (
<TreeView
node={directoryData}
selectedFilePath={selectedFilePath}
handleSelect={handleFileSelect}
deleteNode={deleteNode}
addFile={addFile}

View File

@@ -25,6 +25,8 @@ interface AppContextProps {
checkingSession: boolean
loggedIn: boolean
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
needsToUpdatePassword: boolean
setNeedsToUpdatePassword: Dispatch<SetStateAction<boolean>> | null
userId: number
setUserId: Dispatch<SetStateAction<number>> | null
username: string
@@ -42,6 +44,8 @@ export const AppContext = createContext<AppContextProps>({
checkingSession: false,
loggedIn: false,
setLoggedIn: null,
needsToUpdatePassword: false,
setNeedsToUpdatePassword: null,
userId: 0,
setUserId: null,
username: '',
@@ -59,6 +63,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
const { children } = props
const [checkingSession, setCheckingSession] = useState(false)
const [loggedIn, setLoggedIn] = useState(false)
const [needsToUpdatePassword, setNeedsToUpdatePassword] = useState(false)
const [userId, setUserId] = useState(0)
const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('')
@@ -79,6 +84,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
setDisplayName(data.displayName)
setIsAdmin(data.isAdmin)
setLoggedIn(true)
setNeedsToUpdatePassword(data.needsToUpdatePassword)
})
.catch(() => {
setLoggedIn(false)
@@ -120,6 +126,8 @@ const AppContextProvider = (props: { children: ReactNode }) => {
checkingSession,
loggedIn,
setLoggedIn,
needsToUpdatePassword,
setNeedsToUpdatePassword,
userId,
setUserId,
username,

View File

@@ -25,15 +25,3 @@ code {
padding: '5px 10px';
margin-top: '10px';
}
.tree-item-label {
display: flex;
}
.tree-item-label.selected {
background: lightgoldenrodyellow;
}
.tree-item-label:hover {
background: lightgray;
}