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

Compare commits

..

44 Commits

Author SHA1 Message Date
semantic-release-bot
806ea4cb5c chore(release): 0.3.10 [skip ci]
## [0.3.10](https://github.com/sasjs/server/compare/v0.3.9...v0.3.10) (2022-06-14)

### Bug Fixes

* correct syntax for encoding option ([32d372b](32d372b42f))
2022-06-14 09:53:53 +00:00
Allan Bowe
7205072358 Merge pull request #191 from sasjs/encodingfix
fix: correct syntax for encoding option
2022-06-14 11:49:38 +02:00
Allan Bowe
32d372b42f fix: correct syntax for encoding option 2022-06-14 09:49:05 +00:00
semantic-release-bot
e059bee7dc chore(release): 0.3.9 [skip ci]
## [0.3.9](https://github.com/sasjs/server/compare/v0.3.8...v0.3.9) (2022-06-14)

### Bug Fixes

* forcing utf 8 encoding. Closes [#76](https://github.com/sasjs/server/issues/76) ([8734489](8734489cf0))
2022-06-14 09:20:37 +00:00
Allan Bowe
6f56aafab1 Merge pull request #190 from sasjs/allanbowe/enforce-utf-76
fix: forcing utf 8 encoding. Closes #76
2022-06-14 11:14:35 +02:00
Allan Bowe
8734489cf0 fix: forcing utf 8 encoding. Closes #76 2022-06-14 09:12:41 +00:00
semantic-release-bot
7e6635f40f chore(release): 0.3.8 [skip ci]
## [0.3.8](https://github.com/sasjs/server/compare/v0.3.7...v0.3.8) (2022-06-13)

### Bug Fixes

* execution controller better error handling ([8a617a7](8a617a73ae))
* execution controller error details ([3fa2a7e](3fa2a7e2e3))
2022-06-13 12:32:32 +00:00
Allan Bowe
c0022a22f4 Merge pull request #189 from sasjs/issue-187
Execution controller more details in error message
2022-06-13 14:27:12 +02:00
Mihajlo Medjedovic
3fa2a7e2e3 fix: execution controller error details 2022-06-13 12:25:06 +00:00
8a617a73ae fix: execution controller better error handling 2022-06-13 14:01:12 +02:00
semantic-release-bot
e7babb9f55 chore(release): 0.3.7 [skip ci]
## [0.3.7](https://github.com/sasjs/server/compare/v0.3.6...v0.3.7) (2022-06-08)

### Bug Fixes

* **appstream:** redirect to relative + nested resource should be accessed ([5ab35b0](5ab35b02c4))
2022-06-08 20:21:22 +00:00
Saad Jutt
5ab35b02c4 fix(appstream): redirect to relative + nested resource should be accessed 2022-06-09 01:16:25 +05:00
semantic-release-bot
ad82ee7106 chore(release): 0.3.6 [skip ci]
## [0.3.6](https://github.com/sasjs/server/compare/v0.3.5...v0.3.6) (2022-06-02)

### Bug Fixes

* **appstream:** should serve only new files for same app stream name with new deployment ([e6d1989](e6d1989847))
2022-06-02 08:30:39 +00:00
Allan Bowe
d2e9456d81 Merge pull request #185 from sasjs/issue183
fix(appstream): should serve only new files for same app stream name …
2022-06-02 11:25:27 +03:00
Saad Jutt
e6d1989847 fix(appstream): should serve only new files for same app stream name with new deployment 2022-06-02 04:17:12 +05:00
semantic-release-bot
7a932383b4 chore(release): 0.3.5 [skip ci]
## [0.3.5](https://github.com/sasjs/server/compare/v0.3.4...v0.3.5) (2022-05-30)

### Bug Fixes

* bumping sasjs/core library ([61815f8](61815f8ae1))
2022-05-30 18:07:50 +00:00
Allan Bowe
576e18347e Merge pull request #182 from sasjs/bumpcore
fix: bumping sasjs/core library
2022-05-30 21:02:59 +03:00
Allan Bowe
61815f8ae1 fix: bumping sasjs/core library 2022-05-30 18:02:30 +00:00
semantic-release-bot
afff27fd21 chore(release): 0.3.4 [skip ci]
## [0.3.4](https://github.com/sasjs/server/compare/v0.3.3...v0.3.4) (2022-05-30)

### Bug Fixes

* **web:** system username for DESKTOP mode ([a8ba378](a8ba378fd1))
2022-05-30 16:12:22 +00:00
Saad Jutt
a8ba378fd1 fix(web): system username for DESKTOP mode 2022-05-30 21:08:17 +05:00
semantic-release-bot
73c81a45dc chore(release): 0.3.3 [skip ci]
## [0.3.3](https://github.com/sasjs/server/compare/v0.3.2...v0.3.3) (2022-05-30)

### Bug Fixes

* usage of autoexec API in DESKTOP mode ([12d424a](12d424acce))
2022-05-30 12:18:45 +00:00
Saad Jutt
12d424acce fix: usage of autoexec API in DESKTOP mode 2022-05-30 17:12:17 +05:00
Saad Jutt
414fb19de3 chore: code changes 2022-05-30 00:32:05 +05:00
semantic-release-bot
cfddf1fb0c chore(release): 0.3.2 [skip ci]
## [0.3.2](https://github.com/sasjs/server/compare/v0.3.1...v0.3.2) (2022-05-27)

### Bug Fixes

* **web:** ability to use get/patch User API in desktop mode. ([2c259fe](2c259fe1de))
2022-05-27 19:43:00 +00:00
Muhammad Saad
1f483b1afc Merge pull request #180 from sasjs/desktop-autoexec
fix(web): ability to use get/patch User API in desktop mode.
2022-05-27 12:39:17 -07:00
Saad Jutt
0470239ef1 chore: quick fix 2022-05-27 17:35:58 +05:00
Saad Jutt
2c259fe1de fix(web): ability to use get/patch User API in desktop mode. 2022-05-27 17:01:14 +05:00
semantic-release-bot
b066734398 chore(release): 0.3.1 [skip ci]
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)

### Bug Fixes

* **api:** username should be lowercase ([5ad6ee5](5ad6ee5e0f))
* **web:** reduced width for autoexec input ([7d11cc7](7d11cc7916))
2022-05-26 15:30:45 +00:00
Allan Bowe
3b698fce5f Merge pull request #179 from sasjs/web-profile-fixes
Web profile fixes
2022-05-26 18:26:30 +03:00
Saad Jutt
5ad6ee5e0f fix(api): username should be lowercase 2022-05-26 20:20:02 +05:00
Saad Jutt
7d11cc7916 fix(web): reduced width for autoexec input 2022-05-26 19:48:59 +05:00
semantic-release-bot
ff1def6436 chore(release): 0.3.0 [skip ci]
# [0.3.0](https://github.com/sasjs/server/compare/v0.2.0...v0.3.0) (2022-05-25)

### Features

* **web:** added profile + edit + autoexec changes ([c275db1](c275db184e))
2022-05-25 23:29:22 +00:00
Saad Jutt
c275db184e feat(web): added profile + edit + autoexec changes 2022-05-26 04:25:15 +05:00
semantic-release-bot
e4239fbcc3 chore(release): 0.2.0 [skip ci]
# [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)

### Bug Fixes

* **autoexec:** usage in case of desktop from file ([79dc2db](79dc2dba23))

### Features

* **api:** added autoexec + major type setting changes ([2a7223a](2a7223ad7d))
2022-05-25 05:52:30 +00:00
Muhammad Saad
c6fd8fdd70 Merge pull request #178 from sasjs/issue117
feat(api): added autoexec + major type setting changes
2022-05-24 22:48:29 -07:00
Saad Jutt
79dc2dba23 fix(autoexec): usage in case of desktop from file 2022-05-25 10:44:57 +05:00
Saad Jutt
2a7223ad7d feat(api): added autoexec + major type setting changes 2022-05-24 21:12:32 +05:00
semantic-release-bot
1fed5ea6ac chore(release): 0.1.0 [skip ci]
# [0.1.0](https://github.com/sasjs/server/compare/v0.0.77...v0.1.0) (2022-05-23)

### Bug Fixes

* issue174 + issue175 + issue146 ([80b33c7](80b33c7a18))
* **web:** click to copy + notification ([f37f8e9](f37f8e95d1))

### Features

* **env:** added new env variable LOG_FORMAT_MORGAN ([53bf68a](53bf68a6af))
2022-05-23 21:22:02 +00:00
Muhammad Saad
97f689f292 Merge pull request #177 from sasjs/issue174
fix: issue174 + issue175 + issue146
2022-05-23 14:17:25 -07:00
Saad Jutt
53bf68a6af feat(env): added new env variable LOG_FORMAT_MORGAN 2022-05-23 21:14:37 +05:00
Saad Jutt
f37f8e95d1 fix(web): click to copy + notification 2022-05-23 20:29:29 +05:00
Saad Jutt
80b33c7a18 fix: issue174 + issue175 + issue146 2022-05-23 19:24:56 +05:00
Muhammad Saad
b1803fe385 Merge pull request #170 from sasjs/dummy-release-command
chore: added dummy release command
2022-05-16 09:42:23 -07:00
Saad Jutt
7dd08c3b5b chore: added dummy release command 2022-05-16 21:36:00 +05:00
63 changed files with 1285 additions and 263 deletions

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ node_modules/
.DS_Store
.env*
sas/
sasjs_root/
tmp/
build/
sasjsbuild/

View File

@@ -36,7 +36,7 @@
[
"@semantic-release/exec",
{
"publishCmd": "npx standard-version"
"publishCmd": "echo 'publish command'"
}
]
]

View File

@@ -1,3 +1,107 @@
## [0.3.10](https://github.com/sasjs/server/compare/v0.3.9...v0.3.10) (2022-06-14)
### Bug Fixes
* correct syntax for encoding option ([32d372b](https://github.com/sasjs/server/commit/32d372b42fbf56b6c0779e8f704164eaae1c7548))
## [0.3.9](https://github.com/sasjs/server/compare/v0.3.8...v0.3.9) (2022-06-14)
### Bug Fixes
* forcing utf 8 encoding. Closes [#76](https://github.com/sasjs/server/issues/76) ([8734489](https://github.com/sasjs/server/commit/8734489cf014aedaca3f325e689493e4fe0b71ca))
## [0.3.8](https://github.com/sasjs/server/compare/v0.3.7...v0.3.8) (2022-06-13)
### Bug Fixes
* execution controller better error handling ([8a617a7](https://github.com/sasjs/server/commit/8a617a73ae63233332f5788c90f173d6cd5e1283))
* execution controller error details ([3fa2a7e](https://github.com/sasjs/server/commit/3fa2a7e2e32f90050f6b09e30ce3ef725eb0b15f))
## [0.3.7](https://github.com/sasjs/server/compare/v0.3.6...v0.3.7) (2022-06-08)
### Bug Fixes
* **appstream:** redirect to relative + nested resource should be accessed ([5ab35b0](https://github.com/sasjs/server/commit/5ab35b02c4417132dddb5a800982f31d0d50ef66))
## [0.3.6](https://github.com/sasjs/server/compare/v0.3.5...v0.3.6) (2022-06-02)
### Bug Fixes
* **appstream:** should serve only new files for same app stream name with new deployment ([e6d1989](https://github.com/sasjs/server/commit/e6d1989847761fbe562d7861ffa0ee542839b125))
## [0.3.5](https://github.com/sasjs/server/compare/v0.3.4...v0.3.5) (2022-05-30)
### Bug Fixes
* bumping sasjs/core library ([61815f8](https://github.com/sasjs/server/commit/61815f8ae18be132e17c199cd8e3afbcc2fa0b60))
## [0.3.4](https://github.com/sasjs/server/compare/v0.3.3...v0.3.4) (2022-05-30)
### Bug Fixes
* **web:** system username for DESKTOP mode ([a8ba378](https://github.com/sasjs/server/commit/a8ba378fd1ff374ba025a96fdfae5c6c36954465))
## [0.3.3](https://github.com/sasjs/server/compare/v0.3.2...v0.3.3) (2022-05-30)
### Bug Fixes
* usage of autoexec API in DESKTOP mode ([12d424a](https://github.com/sasjs/server/commit/12d424acce8108a6f53aefbac01fddcdc5efb48f))
## [0.3.2](https://github.com/sasjs/server/compare/v0.3.1...v0.3.2) (2022-05-27)
### Bug Fixes
* **web:** ability to use get/patch User API in desktop mode. ([2c259fe](https://github.com/sasjs/server/commit/2c259fe1de95d84e6929e311aaa6b895e66b42a3))
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)
### Bug Fixes
* **api:** username should be lowercase ([5ad6ee5](https://github.com/sasjs/server/commit/5ad6ee5e0f5d7d6faa45b72215f1d9d55cfc37db))
* **web:** reduced width for autoexec input ([7d11cc7](https://github.com/sasjs/server/commit/7d11cc79161e5a07f6c5392d742ef6b9d8658071))
# [0.3.0](https://github.com/sasjs/server/compare/v0.2.0...v0.3.0) (2022-05-25)
### Features
* **web:** added profile + edit + autoexec changes ([c275db1](https://github.com/sasjs/server/commit/c275db184e874f0ee3a4f08f2592cfacf1e90742))
# [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)
### Bug Fixes
* **autoexec:** usage in case of desktop from file ([79dc2db](https://github.com/sasjs/server/commit/79dc2dba23dc48ec218a973119392a45cb3856b5))
### Features
* **api:** added autoexec + major type setting changes ([2a7223a](https://github.com/sasjs/server/commit/2a7223ad7d6b8f3d4682447fd25d9426a7c79ac3))
# [0.1.0](https://github.com/sasjs/server/compare/v0.0.77...v0.1.0) (2022-05-23)
### Bug Fixes
* issue174 + issue175 + issue146 ([80b33c7](https://github.com/sasjs/server/commit/80b33c7a18c1b7727316ffeca71658346733e935))
* **web:** click to copy + notification ([f37f8e9](https://github.com/sasjs/server/commit/f37f8e95d1a85e00ceca2413dbb5e1f3f3f72255))
### Features
* **env:** added new env variable LOG_FORMAT_MORGAN ([53bf68a](https://github.com/sasjs/server/commit/53bf68a6aff44bb7b2f40d40d6554809253a01a8))
## [0.0.77](https://github.com/sasjs/server/compare/v0.0.76...v0.0.77) (2022-05-16)

View File

@@ -63,7 +63,7 @@ SAS_PATH=/path/to/sas/executable.exe
# Path to working directory
# This location is for SAS WORK, staged files, DRIVE, configuration etc
DRIVE_PATH=/tmp
SASJS_ROOT=./sasjs_root
# options: [http|https] default: http
PROTOCOL=
@@ -125,6 +125,10 @@ HELMET_COEP=
# }
HELMET_CSP_CONFIG_PATH=./csp.config.json
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN=
```
## Persisting the Session
@@ -147,7 +151,7 @@ Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install p
```bash
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
export PORT=5001
export DRIVE_PATH=./tmp
export SASJS_ROOT=./sasjs_root
pm2 start api-linux
```

View File

@@ -18,4 +18,6 @@ SESSION_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
DRIVE_PATH=./tmp
SASJS_ROOT=./sasjs_root
LOG_FORMAT_MORGAN=common

82
api/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "api",
"version": "0.0.2",
"dependencies": {
"@sasjs/core": "^4.23.1",
"@sasjs/core": "^4.27.3",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
@@ -24,7 +24,8 @@
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"swagger-ui-express": "4.3.0"
"swagger-ui-express": "4.3.0",
"url": "^0.10.3"
},
"bin": {
"api": "build/src/server.js"
@@ -1385,9 +1386,9 @@
}
},
"node_modules/@sasjs/core": {
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
"integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
},
"node_modules/@sasjs/utils": {
"version": "2.42.1",
@@ -2883,7 +2884,7 @@
"node_modules/busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"dependencies": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
@@ -3677,7 +3678,7 @@
"node_modules/dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"dependencies": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
@@ -7590,9 +7591,10 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/multer": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz",
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
"deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
@@ -8552,6 +8554,15 @@
"node": ">=0.6"
}
},
"node_modules/querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
"deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
"engines": {
"node": ">=0.4.x"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -9942,6 +9953,15 @@
"url": "https://github.com/yeoman/update-notifier?sponsor=1"
}
},
"node_modules/url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
"dependencies": {
"punycode": "1.3.2",
"querystring": "0.2.0"
}
},
"node_modules/url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
@@ -9954,6 +9974,11 @@
"node": ">=4"
}
},
"node_modules/url/node_modules/punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -11364,9 +11389,9 @@
}
},
"@sasjs/core": {
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
"integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
},
"@sasjs/utils": {
"version": "2.42.1",
@@ -12629,7 +12654,7 @@
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"requires": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
@@ -13267,7 +13292,7 @@
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"requires": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
@@ -16220,9 +16245,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multer": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz",
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
"requires": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
@@ -16933,6 +16958,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -17963,6 +17993,22 @@
"xdg-basedir": "^4.0.0"
}
},
"url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
},
"dependencies": {
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
}
}
},
"url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",

View File

@@ -47,7 +47,7 @@
},
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^4.23.1",
"@sasjs/core": "^4.27.3",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
@@ -63,7 +63,8 @@
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"swagger-ui-express": "4.3.0"
"swagger-ui-express": "4.3.0",
"url": "^0.10.3"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
@@ -94,12 +95,9 @@
"tsoa": "3.14.1",
"typescript": "^4.3.2"
},
"configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
},
"nodemonConfig": {
"ignore": [
"tmp/**/*"
"sasjs_root/**/*"
]
}
}

View File

@@ -323,6 +323,8 @@ components:
type: boolean
isAdmin:
type: boolean
autoExec:
type: string
required:
- id
- displayName
@@ -352,6 +354,10 @@ components:
type: boolean
description: 'Account should be active or not, defaults to true'
example: 'true'
autoExec:
type: string
description: 'User-specific auto-exec code'
example: ""
required:
- displayName
- username
@@ -537,7 +543,7 @@ paths:
application/json:
schema:
properties:
user: {properties: {displayName: {type: string}, username: {type: string}}, required: [displayName, username], type: object}
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
loggedIn: {type: boolean}
required:
- user
@@ -989,6 +995,7 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/UserDetailsResponse'
description: 'Only Admin or user itself will get user autoExec code.'
summary: 'Get user properties - such as group memberships, userName, displayName.'
tags:
- User

View File

@@ -12,25 +12,44 @@ import helmet from 'helmet'
import {
connectDB,
copySASjsCore,
getWebBuildFolderPath,
CorsType,
getWebBuildFolder,
HelmetCoepType,
instantiateLogger,
loadAppStreamConfig,
ModeType,
ProtocolType,
ReturnCode,
setProcessVariables,
setupFolders
setupFolders,
verifyEnvVariables
} from './utils'
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
dotenv.config()
instantiateLogger()
if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
const app = express()
app.use(cookieParser())
app.use(morgan('tiny'))
const { MODE, CORS, WHITELIST, PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } =
process.env
const {
MODE,
CORS,
WHITELIST,
PROTOCOL,
HELMET_CSP_CONFIG_PATH,
HELMET_COEP,
LOG_FORMAT_MORGAN
} = process.env
app.use(morgan(LOG_FORMAT_MORGAN as string))
export const cookieOptions = {
secure: PROTOCOL === 'https',
secure: PROTOCOL === ProtocolType.HTTPS,
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
@@ -38,9 +57,8 @@ export const cookieOptions = {
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
HELMET_CSP_CONFIG_PATH
)
const coepFlag =
HELMET_COEP === 'true' || HELMET_COEP === undefined ? true : false
if (PROTOCOL === 'http') cspConfigJson['upgrade-insecure-requests'] = null
if (PROTOCOL === ProtocolType.HTTP)
cspConfigJson['upgrade-insecure-requests'] = null
/***********************************
* CSRF Protection *
@@ -58,14 +76,14 @@ app.use(
...cspConfigJson
}
},
crossOriginEmbedderPolicy: coepFlag
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
})
)
/***********************************
* Enabling CORS *
***********************************/
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
if (CORS === CorsType.ENABLED) {
const whiteList: string[] = []
WHITELIST?.split(' ')
?.filter((url) => !!url)
@@ -84,7 +102,7 @@ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
* Express Sessions *
* With Mongo Store *
***********************************/
if (MODE?.trim() === 'server') {
if (MODE === ModeType.Server) {
let store: MongoStore | undefined
// NOTE: when exporting app.js as agent for supertest
@@ -105,6 +123,7 @@ if (MODE?.trim() === 'server') {
})
)
}
app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public')))
@@ -129,7 +148,7 @@ export default setProcessVariables().then(async () => {
// should be served after setting up web route
// index.html needs to be injected with some js script.
app.use(express.static(getWebBuildFolderPath()))
app.use(express.static(getWebBuildFolder()))
app.use(onError)

View File

@@ -1,9 +1,13 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal'
import { PreProgramVars } from '../types'
import { ExecuteReturnJsonResponse } from '.'
import { getPreProgramVariables, parseLogToArray } from '../utils'
import {
getPreProgramVariables,
getUserAutoExec,
ModeType,
parseLogToArray
} from '../utils'
interface ExecuteSASCodePayload {
/**
@@ -30,14 +34,23 @@ export class CodeController {
}
}
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
const executeSASCode = async (
req: express.Request,
{ code }: ExecuteSASCodePayload
) => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await getUserAutoExec()
try {
const { webout, log, httpHeaders } =
(await new ExecutionController().executeProgram(
code,
getPreProgramVariables(req),
{ ...req.query, _debug: 131 },
undefined,
{ userAutoExec },
true
)) as ExecuteReturnJson

View File

@@ -32,7 +32,7 @@ import {
import { createFileTree, ExecutionController, getTreeExample } from './internal'
import { TreeNode } from '../types'
import { getTmpFilesFolderPath } from '../utils'
import { getFilesFolder } from '../utils'
interface DeployPayload {
appLoc: string
@@ -214,12 +214,12 @@ const getFileTree = () => {
}
const deploy = async (data: DeployPayload) => {
const driveFilesPath = getTmpFilesFolderPath()
const driveFilesPath = getFilesFolder()
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
const appLocPath = path
.join(getTmpFilesFolderPath(), ...appLocParts)
.join(getFilesFolder(), ...appLocParts)
.replace(new RegExp('/', 'g'), path.sep)
if (!appLocPath.includes(driveFilesPath)) {
@@ -238,10 +238,10 @@ const deploy = async (data: DeployPayload) => {
}
const getFile = async (req: express.Request, filePath: string) => {
const driveFilesPath = getTmpFilesFolderPath()
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(getTmpFilesFolderPath(), filePath)
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
@@ -261,11 +261,11 @@ const getFile = async (req: express.Request, filePath: string) => {
}
const getFolder = async (folderPath?: string) => {
const driveFilesPath = getTmpFilesFolderPath()
const driveFilesPath = getFilesFolder()
if (folderPath) {
const folderPathFull = path
.join(getTmpFilesFolderPath(), folderPath)
.join(getFilesFolder(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(driveFilesPath)) {
@@ -291,10 +291,10 @@ const getFolder = async (folderPath?: string) => {
}
const deleteFile = async (filePath: string) => {
const driveFilesPath = getTmpFilesFolderPath()
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(getTmpFilesFolderPath(), filePath)
.join(getFilesFolder(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
@@ -314,7 +314,7 @@ const saveFile = async (
filePath: string,
multerFile: Express.Multer.File
): Promise<GetFileResponse> => {
const driveFilesPath = getTmpFilesFolderPath()
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(driveFilesPath, filePath)
@@ -339,7 +339,7 @@ const updateFile = async (
filePath: string,
multerFile: Express.Multer.File
): Promise<GetFileResponse> => {
const driveFilesPath = getTmpFilesFolderPath()
const driveFilesPath = getFilesFolder()
const filePathFull = path
.join(driveFilesPath, filePath)

View File

@@ -12,8 +12,8 @@ import { PreProgramVars, Session, TreeNode } from '../../types'
import {
extractHeaders,
generateFileUploadSasCode,
getTmpFilesFolderPath,
getTmpMacrosPath,
getFilesFolder,
getMacrosFolder,
HTTPHeaders,
isDebugOn
} from '../../utils'
@@ -43,7 +43,7 @@ export class ExecutionController {
session?: Session
) {
if (!(await fileExists(programPath)))
throw 'ExecutionController: SAS file does not exist.'
throw `The Stored Program at (${vars._program}) does not exist, or you do not have permission to view it.`
const program = await readFile(programPath)
@@ -110,7 +110,7 @@ export class ExecutionController {
`
program = `
options insert=(SASAUTOS="${getTmpMacrosPath()}");
options insert=(SASAUTOS="${getMacrosFolder()}");
/* runtime vars */
${varStatments}
@@ -119,6 +119,10 @@ filename _webout "${weboutPath}" mod;
/* dynamic user-provided vars */
${preProgramVarStatments}
/* user autoexec starts */
${otherArgs?.userAutoExec ?? ''}
/* user autoexec ends */
/* actual job code */
${program}`
@@ -191,7 +195,7 @@ ${program}`
const root: TreeNode = {
name: 'files',
relativePath: '',
absolutePath: getTmpFilesFolderPath(),
absolutePath: getFilesFolder(),
children: []
}

View File

@@ -1,14 +1,15 @@
import { Request, RequestHandler } from 'express'
import multer from 'multer'
import { uuidv4 } from '@sasjs/utils'
import { getSessionController } from '.'
export class FileUploadController {
private storage = multer.diskStorage({
destination: function (req: any, file: any, cb: any) {
destination: function (req: Request, file: any, cb: any) {
//Sending the intercepted files to the sessions subfolder
cb(null, req.sasSession.path)
cb(null, req.sasSession?.path)
},
filename: function (req: any, file: any, cb: any) {
filename: function (req: Request, file: any, cb: any) {
//req_file prefix + unique hash added to sas request files
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
}
@@ -18,7 +19,7 @@ export class FileUploadController {
//It will intercept request and generate unique uuid to be used as a subfolder name
//that will store the files uploaded
public preUploadMiddleware = async (req: any, res: any, next: any) => {
public preUploadMiddleware: RequestHandler = async (req, res, next) => {
let session
const sessionController = getSessionController()

View File

@@ -3,7 +3,7 @@ import { Session } from '../../types'
import { promisify } from 'util'
import { execFile } from 'child_process'
import {
getTmpSessionsFolderPath,
getSessionsFolder,
generateUniqueFileName,
sysInitCompiledPath
} from '../../utils'
@@ -37,7 +37,7 @@ export class SessionController {
private async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
const sessionFolder = path.join(getSessionsFolder(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
@@ -93,6 +93,8 @@ ${autoExecContent}`
session.path,
'-AUTOEXEC',
autoExecPath,
'-ENCODING',
'UTF-8',
process.platform === 'win32' ? '-nosplash' : ''
])
.then(() => {

View File

@@ -1,5 +1,5 @@
import path from 'path'
import { getTmpFilesFolderPath } from '../../utils/file'
import { getFilesFolder } from '../../utils/file'
import {
createFolder,
createFile,
@@ -17,7 +17,7 @@ export const createFileTree = async (
parentFolders: string[] = []
) => {
const destinationPath = path.join(
getTmpFilesFolderPath(),
getFilesFolder(),
path.join(...parentFolders)
)

View File

@@ -23,8 +23,8 @@ export class SessionController {
}
}
const session = (req: any) => ({
id: req.user.userId,
username: req.user.username,
displayName: req.user.displayName
const session = (req: express.Request) => ({
id: req.user!.userId,
username: req.user!.username,
displayName: req.user!.displayName
})

View File

@@ -19,13 +19,14 @@ import {
} from './internal'
import {
getPreProgramVariables,
getTmpFilesFolderPath,
getFilesFolder,
HTTPHeaders,
isDebugOn,
LogLine,
makeFilesNamesMap,
parseLogToArray
} from '../utils'
import { MulterFile } from '../types/Upload'
interface ExecuteReturnJsonPayload {
/**
@@ -132,7 +133,7 @@ const executeReturnRaw = async (
const query = req.query as ExecutionVars
const sasCodePath =
path
.join(getTmpFilesFolderPath(), _program)
.join(getFilesFolder(), _program)
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
try {
@@ -167,15 +168,17 @@ const executeReturnRaw = async (
}
const executeReturnJson = async (
req: any,
req: express.Request,
_program: string
): Promise<ExecuteReturnJsonResponse> => {
const sasCodePath =
path
.join(getTmpFilesFolderPath(), _program)
.join(getFilesFolder(), _program)
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[])
: null
try {
const { webout, log, httpHeaders } =

View File

@@ -1,3 +1,4 @@
import express from 'express'
import {
Security,
Route,
@@ -10,10 +11,13 @@ import {
Patch,
Delete,
Body,
Hidden
Hidden,
Request
} from 'tsoa'
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
export interface UserResponse {
id: number
@@ -27,6 +31,7 @@ interface UserDetailsResponse {
username: string
isActive: boolean
isAdmin: boolean
autoExec?: string
}
@Security('bearerAuth')
@@ -73,13 +78,23 @@ export class UserController {
}
/**
* Only Admin or user itself will get user autoExec code.
* @summary Get user properties - such as group memberships, userName, displayName.
* @param userId The user's identifier
* @example userId 1234
*/
@Get('{userId}')
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
return getUser(userId)
public async getUser(
@Request() req: express.Request,
@Path() userId: number
): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
const { user } = req
const getAutoExec = user!.isAdmin || user!.userId == userId
return getUser(userId, getAutoExec)
}
/**
@@ -99,6 +114,11 @@ export class UserController {
@Path() userId: number,
@Body() body: UserPayload
): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser(userId, body)
}
@@ -123,7 +143,7 @@ const getAllUsers = async (): Promise<UserResponse[]> =>
.exec()
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive } = data
const { displayName, username, password, isAdmin, isActive, autoExec } = data
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
@@ -138,7 +158,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
username,
password: hashPassword,
isAdmin,
isActive
isActive,
autoExec
})
const savedUser = await user.save()
@@ -148,38 +169,50 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
displayName: savedUser.displayName,
username: savedUser.username,
isActive: savedUser.isActive,
isAdmin: savedUser.isAdmin
isAdmin: savedUser.isAdmin,
autoExec: savedUser.autoExec
}
}
const getUser = async (id: number): Promise<UserDetailsResponse> => {
const getUser = async (
id: number,
getAutoExec: boolean
): Promise<UserDetailsResponse> => {
const user = await User.findOne({ id })
.select({
_id: 0,
id: 1,
username: 1,
displayName: 1,
isAdmin: 1,
isActive: 1
})
.exec()
if (!user) throw new Error('User is not found.')
return user
return {
id: user.id,
displayName: user.displayName,
username: user.username,
isActive: user.isActive,
isAdmin: user.isAdmin,
autoExec: getAutoExec ? user.autoExec ?? '' : undefined
}
}
const getDesktopAutoExec = async () => {
return {
...desktopUser,
id: desktopUser.userId,
autoExec: await getUserAutoExec()
}
}
const updateUser = async (
id: number,
data: UserPayload
data: Partial<UserPayload>
): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive } = data
const { displayName, username, password, isAdmin, isActive, autoExec } = data
const params: any = { displayName, isAdmin, isActive }
const params: any = { displayName, isAdmin, isActive, autoExec }
if (username) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist?.id != id) throw new Error('Username already exists.')
if (usernameExist && usernameExist.id != id)
throw new Error('Username already exists.')
params.username = username
}
@@ -189,18 +222,26 @@ const updateUser = async (
}
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true })
.select({
_id: 0,
id: 1,
username: 1,
displayName: 1,
isAdmin: 1,
isActive: 1
})
.exec()
if (!updatedUser) throw new Error('Unable to update user')
return updatedUser
if (!updatedUser) throw new Error(`Unable to find user with id: ${id}`)
return {
id: updatedUser.id,
username: updatedUser.username,
displayName: updatedUser.displayName,
isAdmin: updatedUser.isAdmin,
isActive: updatedUser.isActive,
autoExec: updatedUser.autoExec
}
}
const updateDesktopAutoExec = async (autoExec: string) => {
await updateUserAutoExec(autoExec)
return {
...desktopUser,
id: desktopUser.userId,
autoExec
}
}
const deleteUser = async (

View File

@@ -5,7 +5,7 @@ import { readFile } from '@sasjs/utils'
import User from '../model/User'
import Client from '../model/Client'
import { getWebBuildFolderPath, generateAuthCode } from '../utils'
import { getWebBuildFolder, generateAuthCode } from '../utils'
import { InfoJWT } from '../types'
import { AuthController } from './auth'
@@ -63,7 +63,7 @@ export class WebController {
}
const home = async () => {
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
const indexHtmlPath = path.join(getWebBuildFolder(), 'index.html')
// Attention! Cannot use fileExists here,
// due to limitation after building executable
@@ -90,12 +90,14 @@ const login = async (
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin,
isActive: user.isActive
isActive: user.isActive,
autoExec: user.autoExec
}
return {
loggedIn: true,
user: {
id: user.id,
username: user.username,
displayName: user.displayName
}

View File

@@ -1,14 +1,34 @@
import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app'
import { verifyTokenInDB } from '../utils'
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
import { desktopUser } from './desktop'
export const authenticateAccessToken: RequestHandler = async (
req,
res,
next
) => {
const { MODE } = process.env
if (MODE === ModeType.Desktop) {
req.user = desktopUser
return next()
}
export const authenticateAccessToken = (req: any, res: any, next: any) => {
// if request is coming from web and has valid session
// we can validate the request and check for CSRF Token
// it can be validated.
if (req.session?.loggedIn) {
req.user = req.session.user
if (req.session.user) {
const user = await fetchLatestAutoExec(req.session.user)
return csrfProtection(req, res, next)
if (user) {
if (user.isActive) {
req.user = user
return csrfProtection(req, res, next)
} else return res.sendStatus(401)
}
}
return res.sendStatus(401)
}
authenticateToken(
@@ -20,7 +40,7 @@ export const authenticateAccessToken = (req: any, res: any, next: any) => {
)
}
export const authenticateRefreshToken = (req: any, res: any, next: any) => {
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
authenticateToken(
req,
res,
@@ -31,16 +51,16 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => {
}
const authenticateToken = (
req: any,
res: any,
next: any,
req: Request,
res: Response,
next: NextFunction,
key: string,
tokenType: 'accessToken' | 'refreshToken'
) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
req.user = {
userId: '1234',
userId: 1234,
clientId: 'desktopModeClientId',
username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName',

View File

@@ -1,18 +1,37 @@
export const desktopRestrict = (req: any, res: any, next: any) => {
import { RequestHandler, Request } from 'express'
import { userInfo } from 'os'
import { RequestUser } from '../types'
import { ModeType } from '../utils'
const regexUser = /^\/SASjsApi\/user\/[0-9]*$/ // /SASjsApi/user/1
const allowedInDesktopMode: { [key: string]: RegExp[] } = {
GET: [regexUser],
PATCH: [regexUser]
}
const reqAllowedInDesktopMode = (request: Request): boolean => {
const { method, originalUrl: url } = request
return !!allowedInDesktopMode[method]?.find((urlRegex) => urlRegex.test(url))
}
export const desktopRestrict: RequestHandler = (req, res, next) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server')
return res.status(403).send('Not Allowed while in Desktop Mode.')
if (MODE === ModeType.Desktop) {
if (!reqAllowedInDesktopMode(req))
return res.status(403).send('Not Allowed while in Desktop Mode.')
}
next()
}
export const desktopUsername = (req: any, res: any, next: any) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server')
return res.status(200).send({
userId: 12345,
username: 'DESKTOPusername',
displayName: 'DESKTOP User'
})
next()
export const desktopUser: RequestUser = {
userId: 12345,
clientId: 'desktop_app',
username: userInfo().username,
displayName: userInfo().username,
isAdmin: true,
isActive: true
}

View File

@@ -1,13 +1,13 @@
import path from 'path'
import { Request } from 'express'
import multer, { FileFilterCallback, Options } from 'multer'
import { blockFileRegex, getTmpUploadsPath } from '../utils'
import { blockFileRegex, getUploadsFolder } from '../utils'
const fieldNameSize = 300
const fileSize = 104857600 // 100 MB
const storage = multer.diskStorage({
destination: getTmpUploadsPath(),
destination: getUploadsFolder(),
filename: function (
_req: Request,
file: Express.Multer.File,

View File

@@ -1,4 +1,6 @@
export const verifyAdmin = (req: any, res: any, next: any) => {
import { RequestHandler } from 'express'
export const verifyAdmin: RequestHandler = (req, res, next) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') return next()

View File

@@ -1,8 +1,10 @@
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => {
import { RequestHandler } from 'express'
export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
const { user } = req
const userId = parseInt(req.params.userId)
if (!user.isAdmin && user.userId !== userId) {
if (!user?.isAdmin && user?.userId !== userId) {
return res.status(401).send('Admin account required')
}
next()

View File

@@ -27,12 +27,18 @@ export interface UserPayload {
* @example "true"
*/
isActive?: boolean
/**
* User-specific auto-exec code
* @example ""
*/
autoExec?: string
}
interface IUserDocument extends UserPayload, Document {
id: number
isAdmin: boolean
isActive: boolean
autoExec: string
groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }]
}
@@ -66,6 +72,9 @@ const userSchema = new Schema<IUserDocument>({
type: Boolean,
default: true
},
autoExec: {
type: String
},
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
tokens: [
{

View File

@@ -26,8 +26,11 @@ authRouter.post('/token', async (req, res) => {
}
})
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
const userInfo: InfoJWT = req.user
authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
const userInfo: InfoJWT = {
userId: req.user!.userId!,
clientId: req.user!.clientId!
}
try {
const response = await controller.refresh(userInfo)
@@ -38,8 +41,11 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
}
})
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
const userInfo: InfoJWT = req.user
authRouter.delete('/logout', authenticateAccessToken, async (req, res) => {
const userInfo: InfoJWT = {
userId: req.user!.userId!,
clientId: req.user!.clientId!
}
try {
await controller.logout(userInfo)

View File

@@ -33,12 +33,12 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
}
})
groupRouter.get('/:groupId', authenticateAccessToken, async (req: any, res) => {
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const { groupId } = req.params
const controller = new GroupController()
try {
const response = await controller.getGroup(groupId)
const response = await controller.getGroup(parseInt(groupId))
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
@@ -49,12 +49,15 @@ groupRouter.post(
'/:groupId/:userId',
authenticateAccessToken,
verifyAdmin,
async (req: any, res) => {
async (req, res) => {
const { groupId, userId } = req.params
const controller = new GroupController()
try {
const response = await controller.addUserToGroup(groupId, userId)
const response = await controller.addUserToGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
@@ -66,12 +69,15 @@ groupRouter.delete(
'/:groupId/:userId',
authenticateAccessToken,
verifyAdmin,
async (req: any, res) => {
async (req, res) => {
const { groupId, userId } = req.params
const controller = new GroupController()
try {
const response = await controller.removeUserFromGroup(groupId, userId)
const response = await controller.removeUserFromGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
@@ -83,12 +89,12 @@ groupRouter.delete(
'/:groupId',
authenticateAccessToken,
verifyAdmin,
async (req: any, res) => {
async (req, res) => {
const { groupId } = req.params
const controller = new GroupController()
try {
await controller.deleteGroup(groupId)
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())

View File

@@ -5,7 +5,6 @@ import swaggerUi from 'swagger-ui-express'
import {
authenticateAccessToken,
desktopRestrict,
desktopUsername,
verifyAdmin
} from '../../middlewares'
@@ -22,7 +21,7 @@ import sessionRouter from './session'
const router = express.Router()
router.use('/info', infoRouter)
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
router.use('/session', authenticateAccessToken, sessionRouter)
router.use('/auth', desktopRestrict, authRouter)
router.use(
'/client',

View File

@@ -21,17 +21,17 @@ import * as fileUtilModules from '../../../utils/file'
const timestamp = generateTimestamp()
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
jest
.spyOn(fileUtilModules, 'getTmpFolderPath')
.spyOn(fileUtilModules, 'getSasjsRootFolder')
.mockImplementation(() => tmpFolder)
jest
.spyOn(fileUtilModules, 'getTmpUploadsPath')
.spyOn(fileUtilModules, 'getUploadsFolder')
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import { getTreeExample } from '../../../controllers/internal'
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
const { getTmpFilesFolderPath } = fileUtilModules
const { getFilesFolder } = fileUtilModules
const clientId = 'someclientID'
const user = {
@@ -157,10 +157,10 @@ describe('drive', () => {
expect(res.text).toEqual(
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
)
await expect(folderExists(getTmpFilesFolderPath())).resolves.toEqual(true)
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
const testJobFolder = path.join(
getTmpFilesFolderPath(),
getFilesFolder(),
'public',
'jobs',
'extract'
@@ -174,7 +174,7 @@ describe('drive', () => {
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
await deleteFolder(path.join(getTmpFilesFolderPath(), 'public'))
await deleteFolder(path.join(getFilesFolder(), 'public'))
})
})
@@ -192,7 +192,7 @@ describe('drive', () => {
})
it('should get a SAS folder on drive having _folderPath as query param', async () => {
const pathToDrive = fileUtilModules.getTmpFilesFolderPath()
const pathToDrive = fileUtilModules.getFilesFolder()
const dirLevel1 = 'level1'
const dirLevel2 = 'level2'
@@ -267,10 +267,7 @@ describe('drive', () => {
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
const filePath = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
filePath
)
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
await copy(fileToCopyPath, pathToCopy)
const res = await request(app)
@@ -333,7 +330,7 @@ describe('drive', () => {
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
fileUtilModules.getFilesFolder(),
pathToUpload
)
await copy(fileToAttachPath, pathToCopy)
@@ -445,7 +442,7 @@ describe('drive', () => {
const pathToUpload = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
fileUtilModules.getFilesFolder(),
pathToUpload
)
await copy(fileToAttachPath, pathToCopy)
@@ -467,7 +464,7 @@ describe('drive', () => {
const pathToUpload = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
fileUtilModules.getFilesFolder(),
pathToUpload
)
await copy(fileToAttachPath, pathToCopy)
@@ -603,10 +600,7 @@ describe('drive', () => {
const fileToCopyContent = await readFile(fileToCopyPath)
const filePath = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
filePath
)
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
await copy(fileToCopyPath, pathToCopy)
const res = await request(app)

View File

@@ -9,17 +9,18 @@ import { generateAccessToken, saveTokensInDB } from '../../../utils'
const clientId = 'someclientID'
const adminUser = {
displayName: 'Test Admin',
username: 'testAdminUsername',
username: 'testadminusername',
password: '12345678',
isAdmin: true,
isActive: true
}
const user = {
displayName: 'Test User',
username: 'testUsername',
username: 'testusername',
password: '87654321',
isAdmin: false,
isActive: true
isActive: true,
autoExec: 'some sas code for auto exec;'
}
const controller = new UserController()
@@ -64,6 +65,21 @@ describe('user', () => {
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
})
it('should respond with new user having username as lowercase', async () => {
const res = await request(app)
.post('/SASjsApi/user')
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...user, username: user.username.toUpperCase() })
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
})
it('should respond with Unauthorized if access token is not present', async () => {
@@ -242,7 +258,7 @@ describe('user', () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
username: 'randomUser'
username: 'randomuser'
})
const res = await request(app)
@@ -360,7 +376,25 @@ describe('user', () => {
await deleteAllUsers()
})
it('should respond with user', async () => {
it('should respond with user autoExec when same user requests', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId)
const res = await request(app)
.get(`/SASjsApi/user/${userId}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
})
it('should respond with user autoExec when admin user requests', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.id
@@ -374,6 +408,7 @@ describe('user', () => {
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
})
it('should respond with user when access token is not of an admin account', async () => {
@@ -395,6 +430,7 @@ describe('user', () => {
expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toBeUndefined()
})
it('should respond with Unauthorized if access token is not present', async () => {

View File

@@ -10,7 +10,7 @@ const clientSecret = 'someclientSecret'
const user = {
id: 1234,
displayName: 'Test User',
username: 'testUsername',
username: 'testusername',
password: '87654321',
isAdmin: false,
isActive: true
@@ -77,6 +77,7 @@ describe('web', () => {
expect(res.body.loggedIn).toBeTruthy()
expect(res.body.user).toEqual({
id: expect.any(Number),
username: user.username,
displayName: user.displayName
})
@@ -155,7 +156,6 @@ const getCSRF = async (app: Express) => {
const { header } = await request(app).get('/')
const cookies = header['set-cookie'].join()
console.log('cookies', cookies)
const csrfToken = extractCSRF(cookies)
return { csrfToken, cookies }
}

View File

@@ -34,7 +34,7 @@ stpRouter.post(
'/execute',
fileUploadController.preUploadMiddleware,
fileUploadController.getMulterUploadObject().any(),
async (req: any, res: any) => {
async (req, res: any) => {
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
const { error: errB, value: body } = executeProgramRawValidation(req.body)
@@ -47,10 +47,11 @@ stpRouter.post(
query?._program
)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
return res.end(response)
}
// TODO: investigate if this code is required
// if (response instanceof Buffer) {
// res.writeHead(200, (req as any).sasHeaders)
// return res.end(response)
// }
res.send(response)
} catch (err: any) {

View File

@@ -36,12 +36,12 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
}
})
userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
const { userId } = req.params
const controller = new UserController()
try {
const response = await controller.getUser(userId)
const response = await controller.getUser(req, parseInt(userId))
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
@@ -52,17 +52,17 @@ userRouter.patch(
'/:userId',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req: any, res) => {
async (req, res) => {
const { user } = req
const { userId } = req.params
// only an admin can update `isActive` and `isAdmin` fields
const { error, value: body } = updateUserValidation(req.body, user.isAdmin)
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
const response = await controller.updateUser(userId, body)
const response = await controller.updateUser(parseInt(userId), body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
@@ -74,17 +74,17 @@ userRouter.delete(
'/:userId',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req: any, res) => {
async (req, res) => {
const { user } = req
const { userId } = req.params
// only an admin can delete user without providing password
const { error, value: data } = deleteUserValidation(req.body, user.isAdmin)
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
await controller.deleteUser(userId, data, user.isAdmin)
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())

View File

@@ -1,10 +1,12 @@
import path from 'path'
import express from 'express'
import express, { Request } from 'express'
import { folderExists } from '@sasjs/utils'
import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
import { appStreamHtml } from './appStreamHtml'
const appStreams: { [key: string]: string } = {}
const router = express.Router()
router.get('/', async (req, res) => {
@@ -22,7 +24,7 @@ export const publishAppStream = async (
streamLogo?: string,
addEntryToFile: boolean = true
) => {
const driveFilesPath = getTmpFilesFolderPath()
const driveFilesPath = getFilesFolder()
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
const appLocPath = path.join(driveFilesPath, ...appLocParts)
@@ -44,7 +46,7 @@ export const publishAppStream = async (
streamServiceName = `AppStreamName${appCount + 1}`
}
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
appStreams[streamServiceName] = pathToDeployment
addEntryToAppStreamConfig(
streamServiceName,
@@ -64,4 +66,26 @@ export const publishAppStream = async (
return {}
}
router.get(`/*`, function (req: Request, res, next) {
const reqPath = req.path.replace(/^\//, '')
// Redirecting to url with trailing slash for appStream base URL only
if (reqPath.split('/').length === 1 && !reqPath.endsWith('/'))
// navigating to same url with slash at start
return res.redirect(301, `${reqPath}/`)
const appStream = reqPath.split('/')[0]
const appStreamFilesPath = appStreams[appStream]
if (appStreamFilesPath) {
// resourcePath is without appStream base path
const resourcePath = reqPath.split('/').slice(1).join('/') || 'index.html'
req.url = resourcePath
return express.static(appStreamFilesPath)(req, res, next)
}
return res.send("There's no App Stream available here.")
})
export default router

View File

@@ -1,6 +1,6 @@
import express from 'express'
import { WebController } from '../../controllers/web'
import { authenticateAccessToken } from '../../middlewares'
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
import { authorizeValidation, loginWebValidation } from '../../utils'
const webRouter = express.Router()
@@ -19,7 +19,7 @@ webRouter.get('/', async (req, res) => {
}
})
webRouter.post('/SASLogon/login', async (req, res) => {
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
const { error, value: body } = loginWebValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
@@ -33,6 +33,7 @@ webRouter.post('/SASLogon/login', async (req, res) => {
webRouter.post(
'/SASLogon/authorize',
desktopRestrict,
authenticateAccessToken,
async (req, res) => {
const { error, value: body } = authorizeValidation(req.body)
@@ -47,7 +48,7 @@ webRouter.post(
}
)
webRouter.get('/logout', async (req, res) => {
webRouter.get('/logout', desktopRestrict, async (req, res) => {
try {
await controller.logout(req)
res.status(200).send('OK!')

View File

@@ -0,0 +1,9 @@
export interface RequestUser {
userId: number
clientId: string
username: string
displayName: string
isAdmin: boolean
isActive: boolean
autoExec?: string
}

View File

@@ -5,3 +5,4 @@ export * from './InfoJWT'
export * from './PreProgramVars'
export * from './Session'
export * from './TreeNode'
export * from './RequestUser'

View File

@@ -2,13 +2,6 @@ import express from 'express'
declare module 'express-session' {
interface SessionData {
loggedIn: boolean
user: {
userId: number
clientId: string
username: string
displayName: string
isAdmin: boolean
isActive: boolean
}
user: import('../').RequestUser
}
}

7
api/src/types/system/express.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare namespace Express {
export interface Request {
accessToken?: string
user?: import('../').RequestUser
sasSession?: import('../').Session
}
}

View File

@@ -4,5 +4,6 @@ declare namespace NodeJS {
driveLoc: string
sessionController?: import('../../controllers/internal').SessionController
appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger
}
}

View File

@@ -2,12 +2,12 @@ import { createFile, fileExists, readFile } from '@sasjs/utils'
import { publishAppStream } from '../routes/appStream'
import { AppStreamConfig } from '../types'
import { getTmpAppStreamConfigPath } from './file'
import { getAppStreamConfigPath } from './file'
export const loadAppStreamConfig = async () => {
if (process.env.NODE_ENV === 'test') return
const appStreamConfigPath = getTmpAppStreamConfigPath()
const appStreamConfigPath = getAppStreamConfigPath()
const content = (await fileExists(appStreamConfigPath))
? await readFile(appStreamConfigPath)
@@ -63,7 +63,7 @@ export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
}
const saveAppStreamConfig = async () => {
const appStreamConfigPath = getTmpAppStreamConfigPath()
const appStreamConfigPath = getAppStreamConfigPath()
try {
await createFile(

View File

@@ -7,14 +7,14 @@ import {
readFile
} from '@sasjs/utils'
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
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).')
const macrosDrivePath = getTmpMacrosPath()
const macrosDrivePath = getMacrosFolder()
await deleteFolder(macrosDrivePath)
await createFolder(macrosDrivePath)

View File

@@ -0,0 +1,8 @@
import { createFile, readFile } from '@sasjs/utils'
import { getDesktopUserAutoExecPath } from './file'
export const getUserAutoExec = async (): Promise<string> =>
readFile(getDesktopUserAutoExecPath())
export const updateUserAutoExec = async (autoExecContent: string) =>
createFile(getDesktopUserAutoExecPath(), autoExecContent)

View File

@@ -1,4 +1,5 @@
import path from 'path'
import { homedir } from 'os'
export const apiRoot = path.join(__dirname, '..', '..')
export const codebaseRoot = path.join(apiRoot, '..')
@@ -11,28 +12,31 @@ export const sysInitCompiledPath = path.join(
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
export const getWebBuildFolderPath = () =>
path.join(codebaseRoot, 'web', 'build')
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
export const getTmpFolderPath = () => process.driveLoc
export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
export const getTmpAppStreamConfigPath = () =>
path.join(getTmpFolderPath(), 'appStreamConfig.json')
export const getDesktopUserAutoExecPath = () =>
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
export const getTmpMacrosPath = () => path.join(getTmpFolderPath(), 'sasjscore')
export const getSasjsRootFolder = () => process.driveLoc
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
export const getAppStreamConfigPath = () =>
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
export const getTmpFilesFolderPath = () =>
path.join(getTmpFolderPath(), 'files')
export const getMacrosFolder = () =>
path.join(getSasjsRootFolder(), 'sasjscore')
export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs')
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
export const getTmpWeboutFolderPath = () =>
path.join(getTmpFolderPath(), 'webouts')
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
export const getTmpSessionsFolderPath = () =>
path.join(getTmpFolderPath(), 'sessions')
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
export const getSessionsFolder = () =>
path.join(getSasjsRootFolder(), 'sessions')
export const generateUniqueFileName = (fileName: string, extension = '') =>
[

View File

@@ -5,12 +5,12 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
const isWindows = () => process.platform === 'win32'
export const getDesktopFields = async () => {
const { SAS_PATH, DRIVE_PATH } = process.env
const { SAS_PATH } = process.env
const sasLoc = SAS_PATH ?? (await getSASLocation())
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
return { sasLoc, driveLoc }
return { sasLoc }
}
const getDriveLocation = async (): Promise<string> => {

View File

@@ -1,6 +1,7 @@
import { Request } from 'express'
import { PreProgramVars } from '../types'
export const getPreProgramVariables = (req: any): PreProgramVars => {
export const getPreProgramVariables = (req: Request): PreProgramVars => {
const host = req.get('host')
const protocol = req.protocol + '://'
const { user, accessToken } = req
@@ -20,9 +21,9 @@ export const getPreProgramVariables = (req: any): PreProgramVars => {
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
return {
username: user.username,
userId: user.userId,
displayName: user.displayName,
username: user!.username,
userId: user!.userId,
displayName: user!.displayName,
serverUrl: protocol + host,
httpHeaders
}

View File

@@ -0,0 +1,15 @@
import express from 'express'
import url from 'url'
export const getFullUrl = (req: express.Request) =>
url.format({
protocol: req.protocol,
host: req.get('host'),
pathname: req.originalUrl
})
export const getServerUrl = (req: express.Request) =>
url.format({
protocol: req.protocol,
host: req.get('x-forwarded-host') || req.get('host')
})

View File

@@ -1,6 +1,7 @@
export * from './appStreamConfig'
export * from './connectDB'
export * from './copySASjsCore'
export * from './desktopAutoExec'
export * from './extractHeaders'
export * from './file'
export * from './generateAccessToken'
@@ -9,6 +10,8 @@ export * from './generateRefreshToken'
export * from './getCertificates'
export * from './getDesktopFields'
export * from './getPreProgramVariables'
export * from './getServerUrl'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './parseLogToArray'
export * from './removeTokensInDB'
@@ -18,4 +21,5 @@ export * from './setProcessVariables'
export * from './setupFolders'
export * from './upload'
export * from './validation'
export * from './verifyEnvVariables'
export * from './verifyTokenInDB'

View File

@@ -0,0 +1,7 @@
import { LogLevel, Logger } from '@sasjs/utils/logger'
export const instantiateLogger = () => {
const logLevel = (process.env.LOG_LEVEL || LogLevel.Info) as LogLevel
const logger = new Logger(logLevel)
process.logger = logger
}

View File

@@ -1,30 +1,29 @@
import path from 'path'
import { getAbsolutePath, getRealPath } from '@sasjs/utils'
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
import { configuration } from '../../package.json'
import { getDesktopFields } from '.'
import { getDesktopFields, ModeType } from '.'
export const setProcessVariables = async () => {
if (process.env.NODE_ENV === 'test') {
process.driveLoc = path.join(process.cwd(), 'tmp')
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
return
}
const { MODE } = process.env
if (MODE?.trim() === 'server') {
const { SAS_PATH, DRIVE_PATH } = process.env
process.sasLoc = SAS_PATH ?? configuration.sasPath
const absPath = getAbsolutePath(DRIVE_PATH ?? 'tmp', process.cwd())
process.driveLoc = getRealPath(absPath)
if (MODE === ModeType.Server) {
process.sasLoc = process.env.SAS_PATH as string
} else {
const { sasLoc, driveLoc } = await getDesktopFields()
const { sasLoc } = await getDesktopFields()
process.sasLoc = sasLoc
process.driveLoc = driveLoc
}
const { SASJS_ROOT } = process.env
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
await createFolder(absPath)
process.driveLoc = getRealPath(absPath)
console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc)
}

View File

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

View File

@@ -1,6 +1,6 @@
import Joi from 'joi'
const usernameSchema = Joi.string().alphanum().min(3).max(16)
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
const passwordSchema = Joi.string().min(6).max(1024)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
@@ -35,7 +35,8 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
username: usernameSchema.required(),
password: passwordSchema.required(),
isAdmin: Joi.boolean(),
isActive: Joi.boolean()
isActive: Joi.boolean(),
autoExec: Joi.string().allow('')
}).validate(data)
export const deleteUserValidation = (
@@ -57,7 +58,8 @@ export const updateUserValidation = (
const validationChecks: any = {
displayName: Joi.string().min(6),
username: usernameSchema,
password: passwordSchema
password: passwordSchema,
autoExec: Joi.string().allow('')
}
if (isAdmin) {
validationChecks.isAdmin = Joi.boolean()

View File

@@ -0,0 +1,211 @@
export enum ModeType {
Server = 'server',
Desktop = 'desktop'
}
export enum ProtocolType {
HTTP = 'http',
HTTPS = 'https'
}
export enum CorsType {
ENABLED = 'enable',
DISABLED = 'disable'
}
export enum HelmetCoepType {
TRUE = 'true',
FALSE = 'false'
}
export enum LOG_FORMAT_MORGANType {
Combined = 'combined',
Common = 'common',
Dev = 'dev',
Short = 'short',
tiny = 'tiny'
}
export enum ReturnCode {
Success,
InvalidEnv
}
export const verifyEnvVariables = (): ReturnCode => {
const errors: string[] = []
errors.push(...verifyMODE())
errors.push(...verifyPROTOCOL())
errors.push(...verifyPORT())
errors.push(...verifyCORS())
errors.push(...verifyHELMET_COEP())
errors.push(...verifyLOG_FORMAT_MORGAN())
if (errors.length) {
process.logger?.error(
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
)
return ReturnCode.InvalidEnv
}
return ReturnCode.Success
}
const verifyMODE = (): string[] => {
const errors: string[] = []
const { MODE } = process.env
if (MODE) {
const modeTypes = Object.values(ModeType)
if (!modeTypes.includes(MODE as ModeType))
errors.push(`- MODE '${MODE}'\n - valid options ${modeTypes}`)
} else {
process.env.MODE = DEFAULTS.MODE
}
if (process.env.MODE === ModeType.Server) {
const {
ACCESS_TOKEN_SECRET,
REFRESH_TOKEN_SECRET,
AUTH_CODE_SECRET,
SESSION_SECRET,
DB_CONNECT
} = process.env
if (!ACCESS_TOKEN_SECRET)
errors.push(
`- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
)
if (!REFRESH_TOKEN_SECRET)
errors.push(
`- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
)
if (!AUTH_CODE_SECRET)
errors.push(
`- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'`
)
if (!SESSION_SECRET)
errors.push(
`- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'`
)
if (process.env.NODE_ENV !== 'test')
if (!DB_CONNECT)
errors.push(
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
)
}
return errors
}
const verifyPROTOCOL = (): string[] => {
const errors: string[] = []
const { PROTOCOL } = process.env
if (PROTOCOL) {
const protocolTypes = Object.values(ProtocolType)
if (!protocolTypes.includes(PROTOCOL as ProtocolType))
errors.push(`- PROTOCOL '${PROTOCOL}'\n - valid options ${protocolTypes}`)
} else {
process.env.PROTOCOL = DEFAULTS.PROTOCOL
}
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
const { PRIVATE_KEY, FULL_CHAIN } = process.env
if (!PRIVATE_KEY)
errors.push(
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
)
if (!FULL_CHAIN)
errors.push(
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
)
}
return errors
}
const verifyCORS = (): string[] => {
const errors: string[] = []
const { CORS } = process.env
if (CORS) {
const corsTypes = Object.values(CorsType)
if (!corsTypes.includes(CORS as CorsType))
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
} else {
const { MODE } = process.env
process.env.CORS =
MODE === ModeType.Server ? CorsType.DISABLED : CorsType.ENABLED
}
return errors
}
const verifyPORT = (): string[] => {
const errors: string[] = []
const { PORT } = process.env
if (PORT) {
if (Number.isNaN(parseInt(PORT)))
errors.push(`- PORT '${PORT}'\n - should be a valid number`)
} else {
process.env.PORT = DEFAULTS.PORT
}
return errors
}
const verifyHELMET_COEP = (): string[] => {
const errors: string[] = []
const { HELMET_COEP } = process.env
if (HELMET_COEP) {
const helmetCoepTypes = Object.values(HelmetCoepType)
if (!helmetCoepTypes.includes(HELMET_COEP as HelmetCoepType))
errors.push(
`- HELMET_COEP '${HELMET_COEP}'\n - valid options ${helmetCoepTypes}`
)
HELMET_COEP
} else {
process.env.HELMET_COEP = DEFAULTS.HELMET_COEP
}
return errors
}
const verifyLOG_FORMAT_MORGAN = (): string[] => {
const errors: string[] = []
const { LOG_FORMAT_MORGAN } = process.env
if (LOG_FORMAT_MORGAN) {
const logFormatMorganTypes = Object.values(LOG_FORMAT_MORGANType)
if (
!logFormatMorganTypes.includes(LOG_FORMAT_MORGAN as LOG_FORMAT_MORGANType)
)
errors.push(
`- LOG_FORMAT_MORGAN '${LOG_FORMAT_MORGAN}'\n - valid options ${logFormatMorganTypes}`
)
LOG_FORMAT_MORGAN
} else {
process.env.LOG_FORMAT_MORGAN = DEFAULTS.LOG_FORMAT_MORGAN
}
return errors
}
const DEFAULTS = {
MODE: ModeType.Desktop,
PROTOCOL: ProtocolType.HTTP,
PORT: '5000',
HELMET_COEP: HelmetCoepType.TRUE,
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common
}

View File

@@ -1,11 +1,30 @@
import User from '../model/User'
import { RequestUser } from '../types'
export const fetchLatestAutoExec = async (
reqUser: RequestUser
): Promise<RequestUser | undefined> => {
const dbUser = await User.findOne({ id: reqUser.userId })
if (!dbUser) return undefined
return {
userId: reqUser.userId,
clientId: reqUser.clientId,
username: dbUser.username,
displayName: dbUser.displayName,
isAdmin: dbUser.isAdmin,
isActive: dbUser.isActive,
autoExec: dbUser.autoExec
}
}
export const verifyTokenInDB = async (
userId: number,
clientId: string,
token: string,
tokenType: 'accessToken' | 'refreshToken'
) => {
): Promise<RequestUser | undefined> => {
const dbUser = await User.findOne({ id: userId })
if (!dbUser) return undefined
@@ -21,7 +40,8 @@ export const verifyTokenInDB = async (
username: dbUser.username,
displayName: dbUser.displayName,
isAdmin: dbUser.isAdmin,
isActive: dbUser.isActive
isActive: dbUser.isActive,
autoExec: dbUser.autoExec
}
: undefined
}

90
web/package-lock.json generated
View File

@@ -24,9 +24,11 @@
"monaco-editor": "^0.33.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2",
"react-monaco-editor": "^0.48.0",
"react-router-dom": "^5.3.0"
"react-router-dom": "^5.3.0",
"react-toastify": "^9.0.1"
},
"devDependencies": {
"@babel/core": "^7.16.0",
@@ -38,6 +40,7 @@
"@types/dotenv-webpack": "^7.0.3",
"@types/prismjs": "^1.16.6",
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.1",
"babel-loader": "^8.2.3",
@@ -3296,6 +3299,15 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-copy-to-clipboard": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz",
"integrity": "sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-dom": {
"version": "17.0.11",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
@@ -4863,6 +4875,14 @@
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
"dev": true
},
"node_modules/copy-to-clipboard": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
"integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/copy-webpack-plugin": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz",
@@ -9299,6 +9319,18 @@
"node": ">=0.10.0"
}
},
"node_modules/react-copy-to-clipboard": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
"dependencies": {
"copy-to-clipboard": "^3.3.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": "^15.3.0 || 16 || 17 || 18"
}
},
"node_modules/react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
@@ -9372,6 +9404,18 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-toastify": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
"integrity": "sha512-c2zeZHkCX+WXuItS/JRqQ/8CH8Qm/je+M0rt09xe9fnu5YPJigtNOdD8zX4fwLA093V2am3abkGfOowwpkrwOQ==",
"dependencies": {
"clsx": "^1.1.1"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
@@ -10335,6 +10379,11 @@
"node": ">=8.0"
}
},
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
@@ -13641,6 +13690,15 @@
"csstype": "^3.0.2"
}
},
"@types/react-copy-to-clipboard": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz",
"integrity": "sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-dom": {
"version": "17.0.11",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
@@ -14848,6 +14906,14 @@
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
"dev": true
},
"copy-to-clipboard": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
"integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
"requires": {
"toggle-selection": "^1.0.6"
}
},
"copy-webpack-plugin": {
"version": "10.2.4",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz",
@@ -18162,6 +18228,15 @@
"object-assign": "^4.1.1"
}
},
"react-copy-to-clipboard": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
"requires": {
"copy-to-clipboard": "^3.3.1",
"prop-types": "^15.8.1"
}
},
"react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
@@ -18223,6 +18298,14 @@
"tiny-warning": "^1.0.0"
}
},
"react-toastify": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
"integrity": "sha512-c2zeZHkCX+WXuItS/JRqQ/8CH8Qm/je+M0rt09xe9fnu5YPJigtNOdD8zX4fwLA093V2am3abkGfOowwpkrwOQ==",
"requires": {
"clsx": "^1.1.1"
}
},
"react-transition-group": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
@@ -18967,6 +19050,11 @@
"is-number": "^7.0.0"
}
},
"toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",

View File

@@ -23,9 +23,11 @@
"monaco-editor": "^0.33.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2",
"react-monaco-editor": "^0.48.0",
"react-router-dom": "^5.3.0"
"react-router-dom": "^5.3.0",
"react-toastify": "^9.0.1"
},
"devDependencies": {
"@babel/core": "^7.16.0",
@@ -37,6 +39,7 @@
"@types/dotenv-webpack": "^7.0.3",
"@types/prismjs": "^1.16.6",
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.1",
"babel-loader": "^8.2.3",

View File

@@ -8,9 +8,11 @@ import Header from './components/header'
import Home from './components/home'
import Drive from './containers/Drive'
import Studio from './containers/Studio'
import Settings from './containers/Settings'
import { AppContext } from './context/appContext'
import AuthCode from './containers/AuthCode'
import { ToastContainer } from 'react-toastify'
function App() {
const appContext = useContext(AppContext)
@@ -44,10 +46,14 @@ function App() {
<Route exact path="/SASjsStudio">
<Studio />
</Route>
<Route exact path="/SASjsSettings">
<Settings />
</Route>
<Route exact path="/SASjsLogon">
<AuthCode />
</Route>
</Switch>
<ToastContainer />
</HashRouter>
</ThemeProvider>
)

View File

@@ -1,4 +1,4 @@
import React, { useState, useContext } from 'react'
import React, { useState, useEffect, useContext } from 'react'
import { Link, useHistory, useLocation } from 'react-router-dom'
import {
@@ -11,6 +11,7 @@ import {
MenuItem
} from '@mui/material'
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import SettingsIcon from '@mui/icons-material/Settings'
import Username from './username'
import { AppContext } from '../context/appContext'
@@ -20,15 +21,23 @@ const PORT_API = process.env.PORT_API
const baseUrl =
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
const Header = (props: any) => {
const history = useHistory()
const { pathname } = useLocation()
const appContext = useContext(AppContext)
const [tabValue, setTabValue] = useState(pathname)
const [tabValue, setTabValue] = useState(
validTabs.includes(pathname) ? pathname : '/'
)
const [anchorEl, setAnchorEl] = useState<
(EventTarget & HTMLButtonElement) | null
>(null)
useEffect(() => {
setTabValue(validTabs.includes(pathname) ? pathname : '/')
}, [pathname])
const handleMenu = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
@@ -44,7 +53,10 @@ const Header = (props: any) => {
}
const handleLogout = () => {
if (appContext.logout) appContext.logout()
if (appContext.logout) {
handleClose()
appContext.logout()
}
}
return (
<AppBar
@@ -132,6 +144,18 @@ const Header = (props: any) => {
open={!!anchorEl}
onClose={handleClose}
>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
component={Link}
to="/SASjsSettings"
onClick={handleClose}
variant="contained"
color="primary"
startIcon={<SettingsIcon />}
>
Settings
</Button>
</MenuItem>
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
<Button variant="contained" color="primary">
Logout

View File

@@ -27,9 +27,10 @@ const Login = () => {
})
if (loggedIn) {
appContext.setLoggedIn?.(loggedIn)
appContext.setUserId?.(user.id)
appContext.setUsername?.(user.username)
appContext.setDisplayName?.(user.displayName)
appContext.setLoggedIn?.(loggedIn)
}
}

View File

@@ -1,15 +1,18 @@
import axios from 'axios'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import React, { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import { useLocation } from 'react-router-dom'
import { CssBaseline, Box, Typography } from '@mui/material'
import { CssBaseline, Box, Typography, Button } from '@mui/material'
const getAuthCode = async (credentials: any) =>
axios.post('/SASLogon/authorize', credentials).then((res) => res.data)
const AuthCode = () => {
const location = useLocation()
const [displayCode, setDisplayCode] = useState(null)
const [displayCode, setDisplayCode] = useState('')
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
@@ -56,6 +59,18 @@ const AuthCode = () => {
{errorMessage && <Typography>{errorMessage}</Typography>}
<br />
<CopyToClipboard
text={displayCode}
onCopy={() =>
toast.info('Code copied to ClipBoard', {
theme: 'dark',
position: toast.POSITION.BOTTOM_RIGHT
})
}
>
<Button variant="contained">Copy to Clipboard</Button>
</CopyToClipboard>
</Box>
)
}

View File

@@ -0,0 +1,55 @@
import * as React from 'react'
import { Box, Paper, Tab, styled } from '@mui/material'
import TabContext from '@mui/lab/TabContext'
import TabList from '@mui/lab/TabList'
import TabPanel from '@mui/lab/TabPanel'
import Profile from './profile'
const StyledTab = styled(Tab)({
background: 'black',
margin: '0 5px 5px 0'
})
const StyledTabpanel = styled(TabPanel)({
flexGrow: 1
})
const Settings = () => {
const [value, setValue] = React.useState('profile')
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
setValue(newValue)
}
return (
<Box
sx={{
display: 'flex',
marginTop: '65px'
}}
>
<TabContext value={value}>
<Box component={Paper} sx={{ margin: '0 5px', height: '92vh' }}>
<TabList
TabIndicatorProps={{
style: {
display: 'none'
}
}}
orientation="vertical"
onChange={handleChange}
>
<StyledTab label="Profile" value="profile" />
</TabList>
</Box>
<StyledTabpanel value="profile">
<Profile />
</StyledTabpanel>
</TabContext>
</Box>
)
}
export default Settings

View File

@@ -0,0 +1,150 @@
import React, { useState, useEffect, useContext } from 'react'
import axios from 'axios'
import {
Grid,
CircularProgress,
Card,
CardHeader,
Divider,
CardContent,
TextField,
CardActions,
Button,
FormGroup,
FormControlLabel,
Checkbox
} from '@mui/material'
import { toast } from 'react-toastify'
import { AppContext, ModeType } from '../../context/appContext'
const Profile = () => {
const [isLoading, setIsLoading] = useState(false)
const appContext = useContext(AppContext)
const [user, setUser] = useState({} as any)
useEffect(() => {
setIsLoading(true)
axios
.get(`/SASjsApi/user/${appContext.userId}`)
.then((res: any) => {
setUser(res.data)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
setIsLoading(false)
})
}, [])
const handleChange = (event: any) => {
const { name, value } = event.target
setUser({ ...user, [name]: value })
}
const handleSubmit = () => {
setIsLoading(true)
axios
.patch(`/SASjsApi/user/${appContext.userId}`, {
username: user.username,
displayName: user.displayName,
autoExec: user.autoExec
})
.then((res: any) => {
toast.success('User information 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"
/>
<FormControlLabel
disabled
control={<Checkbox checked={user.isAdmin} />}
label="isAdmin"
/>
</FormGroup>
</Grid>
</Grid>
</CardContent>
<Divider />
<CardActions>
<Button type="submit" variant="contained" onClick={handleSubmit}>
Save Changes
</Button>
</CardActions>
</Card>
)
}
export default Profile

View File

@@ -9,14 +9,22 @@ import React, {
} from 'react'
import axios from 'axios'
export enum ModeType {
Server = 'server',
Desktop = 'desktop'
}
interface AppContextProps {
checkingSession: boolean
loggedIn: boolean
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
userId: number
setUserId: Dispatch<SetStateAction<number>> | null
username: string
setUsername: Dispatch<SetStateAction<string>> | null
displayName: string
setDisplayName: Dispatch<SetStateAction<string>> | null
mode: ModeType
logout: (() => void) | null
}
@@ -24,10 +32,13 @@ export const AppContext = createContext<AppContextProps>({
checkingSession: false,
loggedIn: false,
setLoggedIn: null,
userId: 0,
setUserId: null,
username: '',
setUsername: null,
displayName: '',
setDisplayName: null,
mode: ModeType.Server,
logout: null
})
@@ -35,8 +46,10 @@ const AppContextProvider = (props: { children: ReactNode }) => {
const { children } = props
const [checkingSession, setCheckingSession] = useState(false)
const [loggedIn, setLoggedIn] = useState(false)
const [userId, setUserId] = useState(0)
const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('')
const [mode, setMode] = useState(ModeType.Server)
useEffect(() => {
setCheckingSession(true)
@@ -46,14 +59,23 @@ const AppContextProvider = (props: { children: ReactNode }) => {
.then((res) => res.data)
.then((data: any) => {
setCheckingSession(false)
setLoggedIn(true)
setUserId(data.id)
setUsername(data.username)
setDisplayName(data.displayName)
setLoggedIn(true)
})
.catch(() => {
setLoggedIn(false)
axios.get('/') // get CSRF TOKEN
})
axios
.get('/SASjsApi/info')
.then((res) => res.data)
.then((data: any) => {
setMode(data.mode)
})
.catch(() => {})
}, [])
const logout = useCallback(() => {
@@ -70,10 +92,13 @@ const AppContextProvider = (props: { children: ReactNode }) => {
checkingSession,
loggedIn,
setLoggedIn,
userId,
setUserId,
username,
setUsername,
displayName,
setDisplayName,
mode,
logout
}}
>