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

Compare commits

...

46 Commits

Author SHA1 Message Date
semantic-release-bot
a61adbcac2 chore(release): 0.4.2 [skip ci]
## [0.4.2](https://github.com/sasjs/server/compare/v0.4.1...v0.4.2) (2022-06-15)

### Bug Fixes

* appStream redesign ([73792fb](73792fb574))
2022-06-15 15:04:11 +00:00
Allan Bowe
12000f4fc7 Merge pull request #195 from sasjs/appStream-design
fix: appStream redesign
2022-06-15 16:59:58 +02:00
73792fb574 fix: appStream redesign 2022-06-15 15:51:42 +02:00
semantic-release-bot
c08cfcbc38 chore(release): 0.4.1 [skip ci]
## [0.4.1](https://github.com/sasjs/server/compare/v0.4.0...v0.4.1) (2022-06-15)

### Bug Fixes

* add/remove group to User when adding/removing user from group and return group membership on getting user ([e08bbcc](e08bbcc543))
2022-06-15 10:38:22 +00:00
Saad Jutt
8d38d5ac64 Merge pull request #193 from sasjs/issue-192
fix: add/remove group to User when adding/removing user from group
2022-06-15 03:32:32 -07:00
e08bbcc543 fix: add/remove group to User when adding/removing user from group and return group membership on getting user 2022-06-15 15:18:42 +05:00
semantic-release-bot
eef3cb270d chore(release): 0.4.0 [skip ci]
# [0.4.0](https://github.com/sasjs/server/compare/v0.3.10...v0.4.0) (2022-06-14)

### Features

* new APIs added for GET|PATCH|DELETE of user by username ([aef411a](aef411a0ea))
2022-06-14 17:28:50 +00:00
Saad Jutt
9cfbca23f8 Merge pull request #194 from sasjs/issue188
feat: new APIs added for GET|PATCH|DELETE of user by username
2022-06-14 10:24:42 -07:00
Saad Jutt
aef411a0ea feat: new APIs added for GET|PATCH|DELETE of user by username 2022-06-14 22:08:56 +05:00
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
50 changed files with 1527 additions and 202 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,3 +1,115 @@
## [0.4.2](https://github.com/sasjs/server/compare/v0.4.1...v0.4.2) (2022-06-15)
### Bug Fixes
* appStream redesign ([73792fb](https://github.com/sasjs/server/commit/73792fb574c90bd280c4324e0b41c6fee7d572b6))
## [0.4.1](https://github.com/sasjs/server/compare/v0.4.0...v0.4.1) (2022-06-15)
### Bug Fixes
* add/remove group to User when adding/removing user from group and return group membership on getting user ([e08bbcc](https://github.com/sasjs/server/commit/e08bbcc5435cbabaee40a41a7fb667d4a1f078e6))
# [0.4.0](https://github.com/sasjs/server/compare/v0.3.10...v0.4.0) (2022-06-14)
### Features
* new APIs added for GET|PATCH|DELETE of user by username ([aef411a](https://github.com/sasjs/server/commit/aef411a0eac625c33274dfe3e88b6f75115c44d8))
## [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) # [0.1.0](https://github.com/sasjs/server/compare/v0.0.77...v0.1.0) (2022-05-23)

82
api/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "api", "name": "api",
"version": "0.0.2", "version": "0.0.2",
"dependencies": { "dependencies": {
"@sasjs/core": "^4.23.1", "@sasjs/core": "^4.27.3",
"@sasjs/utils": "2.42.1", "@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",
@@ -24,7 +24,8 @@
"mongoose-sequence": "^5.3.1", "mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.3", "multer": "^1.4.3",
"swagger-ui-express": "4.3.0" "swagger-ui-express": "4.3.0",
"url": "^0.10.3"
}, },
"bin": { "bin": {
"api": "build/src/server.js" "api": "build/src/server.js"
@@ -1385,9 +1386,9 @@
} }
}, },
"node_modules/@sasjs/core": { "node_modules/@sasjs/core": {
"version": "4.23.1", "version": "4.27.3",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg==" "integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
}, },
"node_modules/@sasjs/utils": { "node_modules/@sasjs/utils": {
"version": "2.42.1", "version": "2.42.1",
@@ -2883,7 +2884,7 @@
"node_modules/busboy": { "node_modules/busboy": {
"version": "0.2.14", "version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"dependencies": { "dependencies": {
"dicer": "0.2.5", "dicer": "0.2.5",
"readable-stream": "1.1.x" "readable-stream": "1.1.x"
@@ -3677,7 +3678,7 @@
"node_modules/dicer": { "node_modules/dicer": {
"version": "0.2.5", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"dependencies": { "dependencies": {
"readable-stream": "1.1.x", "readable-stream": "1.1.x",
"streamsearch": "0.1.2" "streamsearch": "0.1.2"
@@ -7590,9 +7591,10 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"node_modules/multer": { "node_modules/multer": {
"version": "1.4.3", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", "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": { "dependencies": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
"busboy": "^0.2.11", "busboy": "^0.2.11",
@@ -8552,6 +8554,15 @@
"node": ">=0.6" "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": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "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" "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": { "node_modules/url-parse-lax": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
@@ -9954,6 +9974,11 @@
"node": ">=4" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -11364,9 +11389,9 @@
} }
}, },
"@sasjs/core": { "@sasjs/core": {
"version": "4.23.1", "version": "4.27.3",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg==" "integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
}, },
"@sasjs/utils": { "@sasjs/utils": {
"version": "2.42.1", "version": "2.42.1",
@@ -12629,7 +12654,7 @@
"busboy": { "busboy": {
"version": "0.2.14", "version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", "integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"requires": { "requires": {
"dicer": "0.2.5", "dicer": "0.2.5",
"readable-stream": "1.1.x" "readable-stream": "1.1.x"
@@ -13267,7 +13292,7 @@
"dicer": { "dicer": {
"version": "0.2.5", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", "integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"requires": { "requires": {
"readable-stream": "1.1.x", "readable-stream": "1.1.x",
"streamsearch": "0.1.2" "streamsearch": "0.1.2"
@@ -16220,9 +16245,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"multer": { "multer": {
"version": "1.4.3", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", "integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
"requires": { "requires": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
"busboy": "^0.2.11", "busboy": "^0.2.11",
@@ -16933,6 +16958,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" "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": { "queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -17963,6 +17993,22 @@
"xdg-basedir": "^4.0.0" "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": { "url-parse-lax": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",

View File

@@ -47,7 +47,7 @@
}, },
"author": "4GL Ltd", "author": "4GL Ltd",
"dependencies": { "dependencies": {
"@sasjs/core": "^4.23.1", "@sasjs/core": "^4.27.3",
"@sasjs/utils": "2.42.1", "@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",
@@ -63,7 +63,8 @@
"mongoose-sequence": "^5.3.1", "mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.3", "multer": "^1.4.3",
"swagger-ui-express": "4.3.0" "swagger-ui-express": "4.3.0",
"url": "^0.10.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
@@ -96,7 +97,7 @@
}, },
"nodemonConfig": { "nodemonConfig": {
"ignore": [ "ignore": [
"tmp/**/*" "sasjs_root/**/*"
] ]
} }
} }

View File

@@ -310,6 +310,21 @@ components:
- displayName - displayName
type: object type: object
additionalProperties: false additionalProperties: false
GroupResponse:
properties:
groupId:
type: number
format: double
name:
type: string
description:
type: string
required:
- groupId
- name
- description
type: object
additionalProperties: false
UserDetailsResponse: UserDetailsResponse:
properties: properties:
id: id:
@@ -323,6 +338,12 @@ components:
type: boolean type: boolean
isAdmin: isAdmin:
type: boolean type: boolean
autoExec:
type: string
groups:
items:
$ref: '#/components/schemas/GroupResponse'
type: array
required: required:
- id - id
- displayName - displayName
@@ -352,27 +373,16 @@ components:
type: boolean type: boolean
description: 'Account should be active or not, defaults to true' description: 'Account should be active or not, defaults to true'
example: 'true' example: 'true'
autoExec:
type: string
description: 'User-specific auto-exec code'
example: ""
required: required:
- displayName - displayName
- username - username
- password - password
type: object type: object
additionalProperties: false additionalProperties: false
GroupResponse:
properties:
groupId:
type: number
format: double
name:
type: string
description:
type: string
required:
- groupId
- name
- description
type: object
additionalProperties: false
GroupDetailsResponse: GroupDetailsResponse:
properties: properties:
groupId: groupId:
@@ -537,7 +547,7 @@ paths:
application/json: application/json:
schema: schema:
properties: 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} loggedIn: {type: boolean}
required: required:
- user - user
@@ -979,6 +989,94 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/UserPayload' $ref: '#/components/schemas/UserPayload'
'/SASjsApi/user/by/username/{username}':
get:
operationId: GetUserByUsername
responses:
'200':
description: Ok
content:
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
security:
-
bearerAuth: []
parameters:
-
description: 'The User''s username'
in: path
name: username
required: true
schema:
type: string
example: johnSnow01
patch:
operationId: UpdateUserByUsername
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/UserDetailsResponse'
examples:
'Example 1':
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
tags:
- User
security:
-
bearerAuth: []
parameters:
-
description: 'The User''s username'
in: path
name: username
required: true
schema:
type: string
example: johnSnow01
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserPayload'
delete:
operationId: DeleteUserByUsername
responses:
'204':
description: 'No content'
summary: 'Delete a user. Can be performed either by admins, or the user in question.'
tags:
- User
security:
-
bearerAuth: []
parameters:
-
description: 'The User''s username'
in: path
name: username
required: true
schema:
type: string
example: johnSnow01
requestBody:
required: true
content:
application/json:
schema:
properties:
password:
type: string
type: object
'/SASjsApi/user/{userId}': '/SASjsApi/user/{userId}':
get: get:
operationId: GetUser operationId: GetUser
@@ -989,6 +1087,7 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/UserDetailsResponse' $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.' summary: 'Get user properties - such as group memberships, userName, displayName.'
tags: tags:
- User - User

View File

@@ -30,9 +30,7 @@ dotenv.config()
instantiateLogger() instantiateLogger()
if (verifyEnvVariables()) { if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
process.exit(ReturnCode.InvalidEnv)
}
const app = express() const app = express()
@@ -85,7 +83,7 @@ app.use(
/*********************************** /***********************************
* Enabling CORS * * Enabling CORS *
***********************************/ ***********************************/
if (MODE === ModeType.Server || CORS === CorsType.ENABLED) { if (CORS === CorsType.ENABLED) {
const whiteList: string[] = [] const whiteList: string[] = []
WHITELIST?.split(' ') WHITELIST?.split(' ')
?.filter((url) => !!url) ?.filter((url) => !!url)
@@ -125,6 +123,7 @@ if (MODE === ModeType.Server) {
}) })
) )
} }
app.use(express.json({ limit: '100mb' })) app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public'))) app.use(express.static(path.join(__dirname, '../public')))

View File

@@ -1,9 +1,13 @@
import express from 'express' import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa' import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal' import { ExecuteReturnJson, ExecutionController } from './internal'
import { PreProgramVars } from '../types'
import { ExecuteReturnJsonResponse } from '.' import { ExecuteReturnJsonResponse } from '.'
import { getPreProgramVariables, parseLogToArray } from '../utils' import {
getPreProgramVariables,
getUserAutoExec,
ModeType,
parseLogToArray
} from '../utils'
interface ExecuteSASCodePayload { 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 { try {
const { webout, log, httpHeaders } = const { webout, log, httpHeaders } =
(await new ExecutionController().executeProgram( (await new ExecutionController().executeProgram(
code, code,
getPreProgramVariables(req), getPreProgramVariables(req),
{ ...req.query, _debug: 131 }, { ...req.query, _debug: 131 },
undefined, { userAutoExec },
true true
)) as ExecuteReturnJson )) as ExecuteReturnJson

View File

@@ -14,7 +14,7 @@ import Group, { GroupPayload } from '../model/Group'
import User from '../model/User' import User from '../model/User'
import { UserResponse } from './user' import { UserResponse } from './user'
interface GroupResponse { export interface GroupResponse {
groupId: number groupId: number
name: string name: string
description: string description: string
@@ -210,6 +210,9 @@ const updateUsersListInGroup = async (
if (!updatedGroup) throw new Error('Unable to update group') if (!updatedGroup) throw new Error('Unable to update group')
if (action === 'addUser') user.addGroup(group._id)
else user.removeGroup(group._id)
return { return {
groupId: updatedGroup.groupId, groupId: updatedGroup.groupId,
name: updatedGroup.name, name: updatedGroup.name,

View File

@@ -43,7 +43,7 @@ export class ExecutionController {
session?: Session session?: Session
) { ) {
if (!(await fileExists(programPath))) 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) const program = await readFile(programPath)
@@ -119,6 +119,10 @@ filename _webout "${weboutPath}" mod;
/* dynamic user-provided vars */ /* dynamic user-provided vars */
${preProgramVarStatments} ${preProgramVarStatments}
/* user autoexec starts */
${otherArgs?.userAutoExec ?? ''}
/* user autoexec ends */
/* actual job code */ /* actual job code */
${program}` ${program}`

View File

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

View File

@@ -93,6 +93,8 @@ ${autoExecContent}`
session.path, session.path,
'-AUTOEXEC', '-AUTOEXEC',
autoExecPath, autoExecPath,
'-ENCODING',
'UTF-8',
process.platform === 'win32' ? '-nosplash' : '' process.platform === 'win32' ? '-nosplash' : ''
]) ])
.then(() => { .then(() => {

View File

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

View File

@@ -26,6 +26,7 @@ import {
makeFilesNamesMap, makeFilesNamesMap,
parseLogToArray parseLogToArray
} from '../utils' } from '../utils'
import { MulterFile } from '../types/Upload'
interface ExecuteReturnJsonPayload { interface ExecuteReturnJsonPayload {
/** /**
@@ -167,7 +168,7 @@ const executeReturnRaw = async (
} }
const executeReturnJson = async ( const executeReturnJson = async (
req: any, req: express.Request,
_program: string _program: string
): Promise<ExecuteReturnJsonResponse> => { ): Promise<ExecuteReturnJsonResponse> => {
const sasCodePath = const sasCodePath =
@@ -175,7 +176,9 @@ const executeReturnJson = async (
.join(getFilesFolder(), _program) .join(getFilesFolder(), _program)
.replace(new RegExp('/', 'g'), path.sep) + '.sas' .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 { try {
const { webout, log, httpHeaders } = const { webout, log, httpHeaders } =

View File

@@ -1,3 +1,4 @@
import express from 'express'
import { import {
Security, Security,
Route, Route,
@@ -10,10 +11,14 @@ import {
Patch, Patch,
Delete, Delete,
Body, Body,
Hidden Hidden,
Request
} from 'tsoa' } from 'tsoa'
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User' import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
import { GroupResponse } from './group'
export interface UserResponse { export interface UserResponse {
id: number id: number
@@ -27,6 +32,8 @@ interface UserDetailsResponse {
username: string username: string
isActive: boolean isActive: boolean
isAdmin: boolean isAdmin: boolean
autoExec?: string
groups?: GroupResponse[]
} }
@Security('bearerAuth') @Security('bearerAuth')
@@ -73,13 +80,68 @@ export class UserController {
} }
/** /**
* Only Admin or user itself will get user autoExec code.
* @summary Get user properties - such as group memberships, userName, displayName.
* @param username The User's username
* @example username "johnSnow01"
*/
@Get('by/username/{username}')
public async getUserByUsername(
@Request() req: express.Request,
@Path() username: string
): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
const { user } = req
const getAutoExec = user!.isAdmin || user!.username == username
return getUser({ username }, getAutoExec)
}
/**
* Only Admin or user itself will get user autoExec code.
* @summary Get user properties - such as group memberships, userName, displayName. * @summary Get user properties - such as group memberships, userName, displayName.
* @param userId The user's identifier * @param userId The user's identifier
* @example userId 1234 * @example userId 1234
*/ */
@Get('{userId}') @Get('{userId}')
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> { public async getUser(
return getUser(userId) @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({ id: userId }, getAutoExec)
}
/**
* @summary Update user properties - such as displayName. Can be performed either by admins, or the user in question.
* @param username The User's username
* @example username "johnSnow01"
*/
@Example<UserDetailsResponse>({
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
isAdmin: false,
isActive: true
})
@Patch('by/username/{username}')
public async updateUserByUsername(
@Path() username: string,
@Body() body: UserPayload
): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser({ username }, body)
} }
/** /**
@@ -99,7 +161,26 @@ export class UserController {
@Path() userId: number, @Path() userId: number,
@Body() body: UserPayload @Body() body: UserPayload
): Promise<UserDetailsResponse> { ): Promise<UserDetailsResponse> {
return updateUser(userId, body) const { MODE } = process.env
if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser({ id: userId }, body)
}
/**
* @summary Delete a user. Can be performed either by admins, or the user in question.
* @param username The User's username
* @example username "johnSnow01"
*/
@Delete('by/username/{username}')
public async deleteUserByUsername(
@Path() username: string,
@Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false
) {
return deleteUser({ username }, isAdmin, body)
} }
/** /**
@@ -113,7 +194,7 @@ export class UserController {
@Body() body: { password?: string }, @Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false @Query() @Hidden() isAdmin: boolean = false
) { ) {
return deleteUser(userId, isAdmin, body) return deleteUser({ id: userId }, isAdmin, body)
} }
} }
@@ -123,7 +204,7 @@ const getAllUsers = async (): Promise<UserResponse[]> =>
.exec() .exec()
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => { 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 // Checking if user is already in the database
const usernameExist = await User.findOne({ username }) const usernameExist = await User.findOne({ username })
@@ -138,7 +219,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
username, username,
password: hashPassword, password: hashPassword,
isAdmin, isAdmin,
isActive isActive,
autoExec
}) })
const savedUser = await user.save() const savedUser = await user.save()
@@ -148,38 +230,67 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
displayName: savedUser.displayName, displayName: savedUser.displayName,
username: savedUser.username, username: savedUser.username,
isActive: savedUser.isActive, isActive: savedUser.isActive,
isAdmin: savedUser.isAdmin isAdmin: savedUser.isAdmin,
autoExec: savedUser.autoExec
} }
} }
const getUser = async (id: number): Promise<UserDetailsResponse> => { interface GetUserBy {
const user = await User.findOne({ id }) id?: number
.select({ username?: string
_id: 0, }
id: 1,
username: 1, const getUser = async (
displayName: 1, findBy: GetUserBy,
isAdmin: 1, getAutoExec: boolean
isActive: 1 ): Promise<UserDetailsResponse> => {
}) const user = (await User.findOne(
.exec() findBy,
`id displayName username isActive isAdmin autoExec -_id`
).populate(
'groups',
'groupId name description -_id'
)) as unknown as UserDetailsResponse
if (!user) throw new Error('User is not found.') 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,
groups: user.groups
}
}
const getDesktopAutoExec = async () => {
return {
...desktopUser,
id: desktopUser.userId,
autoExec: await getUserAutoExec()
}
} }
const updateUser = async ( const updateUser = async (
id: number, findBy: GetUserBy,
data: UserPayload data: Partial<UserPayload>
): Promise<UserDetailsResponse> => { ): 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) { if (username) {
// Checking if user is already in the database // Checking if user is already in the database
const usernameExist = await User.findOne({ username }) const usernameExist = await User.findOne({ username })
if (usernameExist?.id != id) throw new Error('Username already exists.') if (usernameExist) {
if (
(findBy.id && usernameExist.id != findBy.id) ||
(findBy.username && usernameExist.username != findBy.username)
)
throw new Error('Username already exists.')
}
params.username = username params.username = username
} }
@@ -188,27 +299,36 @@ const updateUser = async (
params.password = User.hashPassword(password) params.password = User.hashPassword(password)
} }
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true }) const updatedUser = await User.findOneAndUpdate(findBy, 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 ${findBy.id || findBy.username}`)
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 ( const deleteUser = async (
id: number, findBy: GetUserBy,
isAdmin: boolean, isAdmin: boolean,
{ password }: { password?: string } { password }: { password?: string }
) => { ) => {
const user = await User.findOne({ id }) const user = await User.findOne(findBy)
if (!user) throw new Error('User is not found.') if (!user) throw new Error('User is not found.')
if (!isAdmin) { if (!isAdmin) {
@@ -216,5 +336,5 @@ const deleteUser = async (
if (!validPass) throw new Error('Invalid password.') if (!validPass) throw new Error('Invalid password.')
} }
await User.deleteOne({ id }) await User.deleteOne(findBy)
} }

View File

@@ -90,12 +90,14 @@ const login = async (
username: user.username, username: user.username,
displayName: user.displayName, displayName: user.displayName,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
isActive: user.isActive isActive: user.isActive,
autoExec: user.autoExec
} }
return { return {
loggedIn: true, loggedIn: true,
user: { user: {
id: user.id,
username: user.username, username: user.username,
displayName: user.displayName displayName: user.displayName
} }

View File

@@ -1,14 +1,34 @@
import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app' 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 // 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) { 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( 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( authenticateToken(
req, req,
res, res,
@@ -31,16 +51,16 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => {
} }
const authenticateToken = ( const authenticateToken = (
req: any, req: Request,
res: any, res: Response,
next: any, next: NextFunction,
key: string, key: string,
tokenType: 'accessToken' | 'refreshToken' tokenType: 'accessToken' | 'refreshToken'
) => { ) => {
const { MODE } = process.env const { MODE } = process.env
if (MODE?.trim() !== 'server') { if (MODE?.trim() !== 'server') {
req.user = { req.user = {
userId: '1234', userId: 1234,
clientId: 'desktopModeClientId', clientId: 'desktopModeClientId',
username: 'desktopModeUsername', username: 'desktopModeUsername',
displayName: 'desktopModeDisplayName', 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 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() 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,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 const { MODE } = process.env
if (MODE?.trim() !== 'server') return next() if (MODE?.trim() !== 'server') return next()

View File

@@ -1,9 +1,22 @@
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => { import { RequestHandler } from 'express'
const { user } = req
const userId = parseInt(req.params.userId)
if (!user.isAdmin && user.userId !== userId) { // This middleware checks if a non-admin user trying to
return res.status(401).send('Admin account required') // access information of other user
export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
const { user } = req
if (!user?.isAdmin) {
let adminAccountRequired: boolean = true
if (req.params.userId) {
adminAccountRequired = user?.userId !== parseInt(req.params.userId)
} else if (req.params.username) {
adminAccountRequired = user?.username !== req.params.username
}
if (adminAccountRequired)
return res.status(401).send('Admin account required')
} }
next() next()
} }

View File

@@ -27,18 +27,26 @@ export interface UserPayload {
* @example "true" * @example "true"
*/ */
isActive?: boolean isActive?: boolean
/**
* User-specific auto-exec code
* @example ""
*/
autoExec?: string
} }
interface IUserDocument extends UserPayload, Document { interface IUserDocument extends UserPayload, Document {
id: number id: number
isAdmin: boolean isAdmin: boolean
isActive: boolean isActive: boolean
autoExec: string
groups: Schema.Types.ObjectId[] groups: Schema.Types.ObjectId[]
tokens: [{ [key: string]: string }] tokens: [{ [key: string]: string }]
} }
interface IUser extends IUserDocument { interface IUser extends IUserDocument {
comparePassword(password: string): boolean comparePassword(password: string): boolean
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
} }
interface IUserModel extends Model<IUser> { interface IUserModel extends Model<IUser> {
hashPassword(password: string): string hashPassword(password: string): string
@@ -66,6 +74,9 @@ const userSchema = new Schema<IUserDocument>({
type: Boolean, type: Boolean,
default: true default: true
}, },
autoExec: {
type: String
},
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }], groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
tokens: [ tokens: [
{ {
@@ -97,6 +108,28 @@ userSchema.method('comparePassword', function (password: string): boolean {
if (bcrypt.compareSync(password, this.password)) return true if (bcrypt.compareSync(password, this.password)) return true
return false return false
}) })
userSchema.method(
'addGroup',
async function (groupObjectId: Schema.Types.ObjectId) {
const groupIdIndex = this.groups.indexOf(groupObjectId)
if (groupIdIndex === -1) {
this.groups.push(groupObjectId)
}
this.markModified('groups')
return this.save()
}
)
userSchema.method(
'removeGroup',
async function (groupObjectId: Schema.Types.ObjectId) {
const groupIdIndex = this.groups.indexOf(groupObjectId)
if (groupIdIndex > -1) {
this.groups.splice(groupIdIndex, 1)
}
this.markModified('groups')
return this.save()
}
)
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema) export const User: IUserModel = model<IUser, IUserModel>('User', userSchema)

View File

@@ -26,8 +26,11 @@ authRouter.post('/token', async (req, res) => {
} }
}) })
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => { authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
const userInfo: InfoJWT = req.user const userInfo: InfoJWT = {
userId: req.user!.userId!,
clientId: req.user!.clientId!
}
try { try {
const response = await controller.refresh(userInfo) 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) => { authRouter.delete('/logout', authenticateAccessToken, async (req, res) => {
const userInfo: InfoJWT = req.user const userInfo: InfoJWT = {
userId: req.user!.userId!,
clientId: req.user!.clientId!
}
try { try {
await controller.logout(userInfo) 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 { groupId } = req.params
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.getGroup(groupId) const response = await controller.getGroup(parseInt(groupId))
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
@@ -49,12 +49,15 @@ groupRouter.post(
'/:groupId/:userId', '/:groupId/:userId',
authenticateAccessToken, authenticateAccessToken,
verifyAdmin, verifyAdmin,
async (req: any, res) => { async (req, res) => {
const { groupId, userId } = req.params const { groupId, userId } = req.params
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.addUserToGroup(groupId, userId) const response = await controller.addUserToGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
@@ -66,12 +69,15 @@ groupRouter.delete(
'/:groupId/:userId', '/:groupId/:userId',
authenticateAccessToken, authenticateAccessToken,
verifyAdmin, verifyAdmin,
async (req: any, res) => { async (req, res) => {
const { groupId, userId } = req.params const { groupId, userId } = req.params
const controller = new GroupController() const controller = new GroupController()
try { try {
const response = await controller.removeUserFromGroup(groupId, userId) const response = await controller.removeUserFromGroup(
parseInt(groupId),
parseInt(userId)
)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
@@ -83,12 +89,12 @@ groupRouter.delete(
'/:groupId', '/:groupId',
authenticateAccessToken, authenticateAccessToken,
verifyAdmin, verifyAdmin,
async (req: any, res) => { async (req, res) => {
const { groupId } = req.params const { groupId } = req.params
const controller = new GroupController() const controller = new GroupController()
try { try {
await controller.deleteGroup(groupId) await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!') res.status(200).send('Group Deleted!')
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())

View File

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

View File

@@ -3,23 +3,24 @@ import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server' import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest' import request from 'supertest'
import appPromise from '../../../app' import appPromise from '../../../app'
import { UserController } from '../../../controllers/' import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils' import { generateAccessToken, saveTokensInDB } from '../../../utils'
const clientId = 'someclientID' const clientId = 'someclientID'
const adminUser = { const adminUser = {
displayName: 'Test Admin', displayName: 'Test Admin',
username: 'testAdminUsername', username: 'testadminusername',
password: '12345678', password: '12345678',
isAdmin: true, isAdmin: true,
isActive: true isActive: true
} }
const user = { const user = {
displayName: 'Test User', displayName: 'Test User',
username: 'testUsername', username: 'testusername',
password: '87654321', password: '87654321',
isAdmin: false, isAdmin: false,
isActive: true isActive: true,
autoExec: 'some sas code for auto exec;'
} }
const controller = new UserController() const controller = new UserController()
@@ -64,6 +65,21 @@ describe('user', () => {
expect(res.body.displayName).toEqual(user.displayName) expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin) expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive) 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 () => { 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 dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({ const dbUser2 = await controller.createUser({
...user, ...user,
username: 'randomUser' username: 'randomuser'
}) })
const res = await request(app) const res = await request(app)
@@ -254,6 +270,102 @@ describe('user', () => {
expect(res.text).toEqual('Error: Username already exists.') expect(res.text).toEqual('Error: Username already exists.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
describe('by username', () => {
it('should respond with updated user when admin user requests', async () => {
const dbUser = await controller.createUser(user)
const newDisplayName = 'My new display Name'
const res = await request(app)
.patch(`/SASjsApi/user/by/username/${user.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName })
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(newDisplayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
})
it('should respond with updated user when user himself requests', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const newDisplayName = 'My new display Name'
const res = await request(app)
.patch(`/SASjsApi/user/by/username/${user.username}`)
.auth(accessToken, { type: 'bearer' })
.send({
displayName: newDisplayName,
username: user.username,
password: user.password
})
.expect(200)
expect(res.body.username).toEqual(user.username)
expect(res.body.displayName).toEqual(newDisplayName)
expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive)
})
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const newDisplayName = 'My new display Name'
await request(app)
.patch(`/SASjsApi/user/by/username/${user.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ ...user, displayName: newDisplayName })
.expect(400)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/user/by/username/1234')
.send(user)
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
username: 'randomUser'
})
const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app)
.patch(`/SASjsApi/user/${dbUser1.id}`)
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is already present', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
username: 'randomuser'
})
const res = await request(app)
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send({ username: dbUser2.username })
.expect(403)
expect(res.text).toEqual('Error: Username already exists.')
expect(res.body).toEqual({})
})
})
}) })
describe('delete', () => { describe('delete', () => {
@@ -347,6 +459,89 @@ describe('user', () => {
expect(res.text).toEqual('Error: Invalid password.') expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
describe('by username', () => {
it('should respond with OK when admin user requests', async () => {
const dbUser = await controller.createUser(user)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body).toEqual({})
})
it('should respond with OK when user himself requests', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: user.password })
.expect(200)
expect(res.body).toEqual({})
})
it('should respond with Bad Request when user himself requests and password is missing', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(400)
expect(res.text).toEqual(`"password" is required`)
expect(res.body).toEqual({})
})
it('should respond with Unauthorized when access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/user/by/username/RandomUsername')
.send(user)
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
username: 'randomUser'
})
const accessToken = await generateAndSaveToken(dbUser2.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser1.username}`)
.auth(accessToken, { type: 'bearer' })
.send(user)
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
const dbUser = await controller.createUser(user)
const accessToken = await generateAndSaveToken(dbUser.id)
const res = await request(app)
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(accessToken, { type: 'bearer' })
.send({ password: 'incorrectpassword' })
.expect(403)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({})
})
})
}) })
describe('get', () => { describe('get', () => {
@@ -360,7 +555,26 @@ describe('user', () => {
await deleteAllUsers() 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)
expect(res.body.groups).toEqual([])
})
it('should respond with user autoExec when admin user requests', async () => {
const dbUser = await controller.createUser(user) const dbUser = await controller.createUser(user)
const userId = dbUser.id const userId = dbUser.id
@@ -374,6 +588,8 @@ describe('user', () => {
expect(res.body.displayName).toEqual(user.displayName) expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin) expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive) expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toEqual(user.autoExec)
expect(res.body.groups).toEqual([])
}) })
it('should respond with user when access token is not of an admin account', async () => { it('should respond with user when access token is not of an admin account', async () => {
@@ -395,6 +611,35 @@ describe('user', () => {
expect(res.body.displayName).toEqual(user.displayName) expect(res.body.displayName).toEqual(user.displayName)
expect(res.body.isAdmin).toEqual(user.isAdmin) expect(res.body.isAdmin).toEqual(user.isAdmin)
expect(res.body.isActive).toEqual(user.isActive) expect(res.body.isActive).toEqual(user.isActive)
expect(res.body.autoExec).toBeUndefined()
expect(res.body.groups).toEqual([])
})
it('should respond with user along with associated groups', async () => {
const dbUser = await controller.createUser(user)
const userId = dbUser.id
const accessToken = await generateAndSaveToken(userId)
const group = {
name: 'DCGroup1',
description: 'DC group for testing purposes.'
}
const groupController = new GroupController()
const dbGroup = await groupController.createGroup(group)
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
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)
expect(res.body.groups.length).toBeGreaterThan(0)
}) })
it('should respond with Unauthorized if access token is not present', async () => { it('should respond with Unauthorized if access token is not present', async () => {
@@ -419,6 +664,86 @@ describe('user', () => {
expect(res.text).toEqual('Error: User is not found.') expect(res.text).toEqual('Error: User is not found.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
describe('by username', () => {
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/by/username/${dbUser.username}`)
.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 res = await request(app)
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
.auth(adminAccessToken, { 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 when access token is not of an admin account', async () => {
const accessToken = await generateSaveTokenAndCreateUser({
...user,
username: 'randomUser'
})
const dbUser = await controller.createUser(user)
const res = await request(app)
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
.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).toBeUndefined()
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.get('/SASjsApi/user/by/username/randomUsername')
.send()
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden 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(res.text).toEqual('Error: User is not found.')
expect(res.body).toEqual({})
})
})
}) })
describe('getAll', () => { describe('getAll', () => {

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import {
} from '../../middlewares' } from '../../middlewares'
import { import {
deleteUserValidation, deleteUserValidation,
getUserValidation,
registerUserValidation, registerUserValidation,
updateUserValidation updateUserValidation
} from '../../utils' } from '../../utils'
@@ -36,12 +37,31 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
} }
}) })
userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => { userRouter.get(
'/by/username/:username',
authenticateAccessToken,
async (req, res) => {
const { error, value: params } = getUserValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
const { username } = params
const controller = new UserController()
try {
const response = await controller.getUserByUsername(req, username)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
const { userId } = req.params const { userId } = req.params
const controller = new UserController() const controller = new UserController()
try { try {
const response = await controller.getUser(userId) const response = await controller.getUser(req, parseInt(userId))
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
@@ -49,20 +69,26 @@ userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
}) })
userRouter.patch( userRouter.patch(
'/:userId', '/by/username/:username',
authenticateAccessToken, authenticateAccessToken,
verifyAdminIfNeeded, verifyAdminIfNeeded,
async (req: any, res) => { async (req, res) => {
const { user } = req const { user } = req
const { userId } = req.params const { error: errorUsername, value: params } = getUserValidation(
req.params
)
if (errorUsername)
return res.status(400).send(errorUsername.details[0].message)
const { username } = params
// only an admin can update `isActive` and `isAdmin` fields // 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) if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController() const controller = new UserController()
try { try {
const response = await controller.updateUser(userId, body) const response = await controller.updateUserByUsername(username, body)
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
@@ -70,21 +96,71 @@ userRouter.patch(
} }
) )
userRouter.patch(
'/:userId',
authenticateAccessToken,
verifyAdminIfNeeded,
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)
if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController()
try {
const response = await controller.updateUser(parseInt(userId), body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
userRouter.delete(
'/by/username/:username',
authenticateAccessToken,
verifyAdminIfNeeded,
async (req, res) => {
const { user } = req
const { error: errorUsername, value: params } = getUserValidation(
req.params
)
if (errorUsername)
return res.status(400).send(errorUsername.details[0].message)
const { username } = params
// only an admin can delete user without providing password
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.deleteUserByUsername(username, data, user!.isAdmin)
res.status(200).send('Account Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
userRouter.delete( userRouter.delete(
'/:userId', '/:userId',
authenticateAccessToken, authenticateAccessToken,
verifyAdminIfNeeded, verifyAdminIfNeeded,
async (req: any, res) => { async (req, res) => {
const { user } = req const { user } = req
const { userId } = req.params const { userId } = req.params
// only an admin can delete user without providing password // 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) if (error) return res.status(400).send(error.details[0].message)
const controller = new UserController() const controller = new UserController()
try { try {
await controller.deleteUser(userId, data, user.isAdmin) await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
res.status(200).send('Account Deleted!') res.status(200).send('Account Deleted!')
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())

View File

@@ -23,13 +23,21 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
${style} ${style}
</head> </head>
<body> <body>
<h1>App Stream</h1> <header>
<a href="/"><img src="/logo.png" alt="logo" class="logo"></a>
<h1>App Stream</h1>
</header>
<div class="app-container"> <div class="app-container">
${Object.entries(appStreamConfig) ${Object.entries(appStreamConfig)
.map(([streamServiceName, entry]) => .map(([streamServiceName, entry]) =>
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo) singleAppStreamHtml(
) streamServiceName,
.join('')} entry.appLoc,
entry.streamLogo
)
)
.join('')}
<a class="app" title="Upload build.json"> <a class="app" title="Upload build.json">
<input id="fileId" type="file" hidden /> <input id="fileId" type="file" hidden />
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer"> <button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">

View File

@@ -1,10 +1,12 @@
import path from 'path' import path from 'path'
import express from 'express' import express, { Request } from 'express'
import { folderExists } from '@sasjs/utils' import { folderExists } from '@sasjs/utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils' import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
import { appStreamHtml } from './appStreamHtml' import { appStreamHtml } from './appStreamHtml'
const appStreams: { [key: string]: string } = {}
const router = express.Router() const router = express.Router()
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
@@ -44,7 +46,7 @@ export const publishAppStream = async (
streamServiceName = `AppStreamName${appCount + 1}` streamServiceName = `AppStreamName${appCount + 1}`
} }
router.use(`/${streamServiceName}`, express.static(pathToDeployment)) appStreams[streamServiceName] = pathToDeployment
addEntryToAppStreamConfig( addEntryToAppStreamConfig(
streamServiceName, streamServiceName,
@@ -64,4 +66,26 @@ export const publishAppStream = async (
return {} 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 export default router

View File

@@ -5,18 +5,71 @@ export const style = `<style>
.app-container { .app-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: baseline; align-items: center;
justify-content: center; justify-content: center;
padding-top: 50px;
} }
.app-container .app { .app-container .app {
width: 150px; width: 150px;
height: 180px;
margin: 10px; margin: 10px;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
text-decoration: none;
color: black;
background: #efefef;
padding: 10px;
border-radius: 7px;
border: 1px solid #d7d7d7;
} }
.app-container .app img{ .app-container .app img{
width: 100%; width: 100%;
margin-bottom: 10px; margin-bottom: 10px;
border-radius: 10px; border-radius: 10px;
} }
#uploadButton {
border: 0
}
#uploadButton:focus {
outline: 0
}
#uploadMessage {
position: relative;
bottom: -5px;
}
header {
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
box-shadow: rgb(0 0 0 / 20%) 0px 2px 4px -1px, rgb(0 0 0 / 14%) 0px 4px 5px 0px, rgb(0 0 0 / 12%) 0px 1px 10px 0px;
display: flex;
width: 100%;
box-sizing: border-box;
flex-shrink: 0;
position: fixed;
top: 0px;
left: auto;
right: 0px;
background-color: rgb(0, 0, 0);
color: rgb(255, 255, 255);
z-index: 1201;
}
header h1 {
margin: 13px;
font-size: 20px;
}
header a {
align-self: center;
}
header .logo {
width: 35px;
margin-left: 10px;
align-self: center;
}
</style>` </style>`

View File

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

View File

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

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

@@ -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 path from 'path'
import { homedir } from 'os'
export const apiRoot = path.join(__dirname, '..', '..') export const apiRoot = path.join(__dirname, '..', '..')
export const codebaseRoot = path.join(apiRoot, '..') export const codebaseRoot = path.join(apiRoot, '..')
@@ -13,6 +14,11 @@ export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build') export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
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.driveLoc
export const getAppStreamConfigPath = () => export const getAppStreamConfigPath = () =>

View File

@@ -1,6 +1,7 @@
import { Request } from 'express'
import { PreProgramVars } from '../types' import { PreProgramVars } from '../types'
export const getPreProgramVariables = (req: any): PreProgramVars => { export const getPreProgramVariables = (req: Request): PreProgramVars => {
const host = req.get('host') const host = req.get('host')
const protocol = req.protocol + '://' const protocol = req.protocol + '://'
const { user, accessToken } = req const { user, accessToken } = req
@@ -20,9 +21,9 @@ export const getPreProgramVariables = (req: any): PreProgramVars => {
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`) if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
return { return {
username: user.username, username: user!.username,
userId: user.userId, userId: user!.userId,
displayName: user.displayName, displayName: user!.displayName,
serverUrl: protocol + host, serverUrl: protocol + host,
httpHeaders 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 './appStreamConfig'
export * from './connectDB' export * from './connectDB'
export * from './copySASjsCore' export * from './copySASjsCore'
export * from './desktopAutoExec'
export * from './extractHeaders' export * from './extractHeaders'
export * from './file' export * from './file'
export * from './generateAccessToken' export * from './generateAccessToken'
@@ -9,6 +10,7 @@ export * from './generateRefreshToken'
export * from './getCertificates' export * from './getCertificates'
export * from './getDesktopFields' export * from './getDesktopFields'
export * from './getPreProgramVariables' export * from './getPreProgramVariables'
export * from './getServerUrl'
export * from './instantiateLogger' export * from './instantiateLogger'
export * from './isDebugOn' export * from './isDebugOn'
export * from './parseLogToArray' export * from './parseLogToArray'

View File

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

View File

@@ -1,10 +1,15 @@
import Joi from 'joi' 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) const passwordSchema = Joi.string().min(6).max(1024)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i export const blockFileRegex = /\.(exe|sh|htaccess)$/i
export const getUserValidation = (data: any): Joi.ValidationResult =>
Joi.object({
username: usernameSchema.required()
}).validate(data)
export const loginWebValidation = (data: any): Joi.ValidationResult => export const loginWebValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
username: usernameSchema.required(), username: usernameSchema.required(),
@@ -35,7 +40,8 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
username: usernameSchema.required(), username: usernameSchema.required(),
password: passwordSchema.required(), password: passwordSchema.required(),
isAdmin: Joi.boolean(), isAdmin: Joi.boolean(),
isActive: Joi.boolean() isActive: Joi.boolean(),
autoExec: Joi.string().allow('')
}).validate(data) }).validate(data)
export const deleteUserValidation = ( export const deleteUserValidation = (
@@ -57,7 +63,8 @@ export const updateUserValidation = (
const validationChecks: any = { const validationChecks: any = {
displayName: Joi.string().min(6), displayName: Joi.string().min(6),
username: usernameSchema, username: usernameSchema,
password: passwordSchema password: passwordSchema,
autoExec: Joi.string().allow('')
} }
if (isAdmin) { if (isAdmin) {
validationChecks.isAdmin = Joi.boolean() validationChecks.isAdmin = Joi.boolean()

View File

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

View File

@@ -8,9 +8,11 @@ import Header from './components/header'
import Home from './components/home' import Home from './components/home'
import Drive from './containers/Drive' import Drive from './containers/Drive'
import Studio from './containers/Studio' import Studio from './containers/Studio'
import Settings from './containers/Settings'
import { AppContext } from './context/appContext' import { AppContext } from './context/appContext'
import AuthCode from './containers/AuthCode' import AuthCode from './containers/AuthCode'
import { ToastContainer } from 'react-toastify'
function App() { function App() {
const appContext = useContext(AppContext) const appContext = useContext(AppContext)
@@ -44,10 +46,14 @@ function App() {
<Route exact path="/SASjsStudio"> <Route exact path="/SASjsStudio">
<Studio /> <Studio />
</Route> </Route>
<Route exact path="/SASjsSettings">
<Settings />
</Route>
<Route exact path="/SASjsLogon"> <Route exact path="/SASjsLogon">
<AuthCode /> <AuthCode />
</Route> </Route>
</Switch> </Switch>
<ToastContainer />
</HashRouter> </HashRouter>
</ThemeProvider> </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 { Link, useHistory, useLocation } from 'react-router-dom'
import { import {
@@ -11,6 +11,7 @@ import {
MenuItem MenuItem
} from '@mui/material' } from '@mui/material'
import OpenInNewIcon from '@mui/icons-material/OpenInNew' import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import SettingsIcon from '@mui/icons-material/Settings'
import Username from './username' import Username from './username'
import { AppContext } from '../context/appContext' import { AppContext } from '../context/appContext'
@@ -20,17 +21,23 @@ const PORT_API = process.env.PORT_API
const baseUrl = const baseUrl =
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : '' NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
const Header = (props: any) => { const Header = (props: any) => {
const history = useHistory() const history = useHistory()
const { pathname } = useLocation() const { pathname } = useLocation()
const appContext = useContext(AppContext) const appContext = useContext(AppContext)
const [tabValue, setTabValue] = useState( const [tabValue, setTabValue] = useState(
pathname === '/SASjsLogon' ? '/' : pathname validTabs.includes(pathname) ? pathname : '/'
) )
const [anchorEl, setAnchorEl] = useState< const [anchorEl, setAnchorEl] = useState<
(EventTarget & HTMLButtonElement) | null (EventTarget & HTMLButtonElement) | null
>(null) >(null)
useEffect(() => {
setTabValue(validTabs.includes(pathname) ? pathname : '/')
}, [pathname])
const handleMenu = ( const handleMenu = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent> event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => { ) => {
@@ -46,7 +53,10 @@ const Header = (props: any) => {
} }
const handleLogout = () => { const handleLogout = () => {
if (appContext.logout) appContext.logout() if (appContext.logout) {
handleClose()
appContext.logout()
}
} }
return ( return (
<AppBar <AppBar
@@ -134,6 +144,18 @@ const Header = (props: any) => {
open={!!anchorEl} open={!!anchorEl}
onClose={handleClose} 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' }}> <MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
<Button variant="contained" color="primary"> <Button variant="contained" color="primary">
Logout Logout

View File

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

View File

@@ -1,7 +1,7 @@
import axios from 'axios' import axios from 'axios'
import { CopyToClipboard } from 'react-copy-to-clipboard' import { CopyToClipboard } from 'react-copy-to-clipboard'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { ToastContainer, toast } from 'react-toastify' import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css' import 'react-toastify/dist/ReactToastify.css'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
@@ -71,8 +71,6 @@ const AuthCode = () => {
> >
<Button variant="contained">Copy to Clipboard</Button> <Button variant="contained">Copy to Clipboard</Button>
</CopyToClipboard> </CopyToClipboard>
<ToastContainer />
</Box> </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' } from 'react'
import axios from 'axios' import axios from 'axios'
export enum ModeType {
Server = 'server',
Desktop = 'desktop'
}
interface AppContextProps { interface AppContextProps {
checkingSession: boolean checkingSession: boolean
loggedIn: boolean loggedIn: boolean
setLoggedIn: Dispatch<SetStateAction<boolean>> | null setLoggedIn: Dispatch<SetStateAction<boolean>> | null
userId: number
setUserId: Dispatch<SetStateAction<number>> | null
username: string username: string
setUsername: Dispatch<SetStateAction<string>> | null setUsername: Dispatch<SetStateAction<string>> | null
displayName: string displayName: string
setDisplayName: Dispatch<SetStateAction<string>> | null setDisplayName: Dispatch<SetStateAction<string>> | null
mode: ModeType
logout: (() => void) | null logout: (() => void) | null
} }
@@ -24,10 +32,13 @@ export const AppContext = createContext<AppContextProps>({
checkingSession: false, checkingSession: false,
loggedIn: false, loggedIn: false,
setLoggedIn: null, setLoggedIn: null,
userId: 0,
setUserId: null,
username: '', username: '',
setUsername: null, setUsername: null,
displayName: '', displayName: '',
setDisplayName: null, setDisplayName: null,
mode: ModeType.Server,
logout: null logout: null
}) })
@@ -35,8 +46,10 @@ const AppContextProvider = (props: { children: ReactNode }) => {
const { children } = props const { children } = props
const [checkingSession, setCheckingSession] = useState(false) const [checkingSession, setCheckingSession] = useState(false)
const [loggedIn, setLoggedIn] = useState(false) const [loggedIn, setLoggedIn] = useState(false)
const [userId, setUserId] = useState(0)
const [username, setUsername] = useState('') const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('') const [displayName, setDisplayName] = useState('')
const [mode, setMode] = useState(ModeType.Server)
useEffect(() => { useEffect(() => {
setCheckingSession(true) setCheckingSession(true)
@@ -46,14 +59,23 @@ const AppContextProvider = (props: { children: ReactNode }) => {
.then((res) => res.data) .then((res) => res.data)
.then((data: any) => { .then((data: any) => {
setCheckingSession(false) setCheckingSession(false)
setLoggedIn(true) setUserId(data.id)
setUsername(data.username) setUsername(data.username)
setDisplayName(data.displayName) setDisplayName(data.displayName)
setLoggedIn(true)
}) })
.catch(() => { .catch(() => {
setLoggedIn(false) setLoggedIn(false)
axios.get('/') // get CSRF TOKEN axios.get('/') // get CSRF TOKEN
}) })
axios
.get('/SASjsApi/info')
.then((res) => res.data)
.then((data: any) => {
setMode(data.mode)
})
.catch(() => {})
}, []) }, [])
const logout = useCallback(() => { const logout = useCallback(() => {
@@ -70,10 +92,13 @@ const AppContextProvider = (props: { children: ReactNode }) => {
checkingSession, checkingSession,
loggedIn, loggedIn,
setLoggedIn, setLoggedIn,
userId,
setUserId,
username, username,
setUsername, setUsername,
displayName, displayName,
setDisplayName, setDisplayName,
mode,
logout logout
}} }}
> >