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

Compare commits

...

47 Commits

Author SHA1 Message Date
semantic-release-bot
6d12b900ad chore(release): 0.5.0 [skip ci]
# [0.5.0](https://github.com/sasjs/server/compare/v0.4.2...v0.5.0) (2022-06-16)

### Bug Fixes

* npm audit fix to avoid warnings on npm i ([28a6a36](28a6a36bb7))

### Features

* **api:** deployment through zipped/compressed file ([b81d742](b81d742c6c))
2022-06-16 13:31:09 +00:00
Saad Jutt
ae5aa02733 Merge pull request #196 from sasjs/issue173
feat(api): deployment through zipped/compressed file
2022-06-16 06:26:32 -07:00
Allan Bowe
28a6a36bb7 fix: npm audit fix to avoid warnings on npm i 2022-06-16 13:07:59 +00:00
Saad Jutt
4e7579dc10 chore(specs): specs added for deploy upload file and zipped file 2022-06-16 17:58:56 +05:00
Saad Jutt
b81d742c6c feat(api): deployment through zipped/compressed file 2022-06-16 00:56:51 +05:00
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
41 changed files with 2098 additions and 184 deletions

View File

@@ -1,3 +1,115 @@
# [0.5.0](https://github.com/sasjs/server/compare/v0.4.2...v0.5.0) (2022-06-16)
### Bug Fixes
* npm audit fix to avoid warnings on npm i ([28a6a36](https://github.com/sasjs/server/commit/28a6a36bb708b93fb5c2b74d587e9b2e055582be))
### Features
* **api:** deployment through zipped/compressed file ([b81d742](https://github.com/sasjs/server/commit/b81d742c6c70d4cf1cab365b0e3efc087441db00))
## [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)

466
api/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "api",
"version": "0.0.2",
"dependencies": {
"@sasjs/core": "^4.23.1",
"@sasjs/core": "^4.27.3",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
@@ -24,12 +24,15 @@
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"swagger-ui-express": "4.3.0"
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
"url": "^0.10.3"
},
"bin": {
"api": "build/src/server.js"
},
"devDependencies": {
"@types/adm-zip": "^0.5.0",
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
@@ -44,6 +47,8 @@
"@types/node": "^15.12.2",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
@@ -1385,9 +1390,9 @@
}
},
"node_modules/@sasjs/core": {
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
"integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
},
"node_modules/@sasjs/utils": {
"version": "2.42.1",
@@ -1752,6 +1757,15 @@
"yarn": ">=1.9.4"
}
},
"node_modules/@types/adm-zip": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.0.tgz",
"integrity": "sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/babel__core": {
"version": "7.1.15",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.15.tgz",
@@ -2175,6 +2189,15 @@
"integrity": "sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==",
"dev": true
},
"node_modules/@types/unzipper": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz",
"integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/webidl-conversions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz",
@@ -2271,6 +2294,15 @@
"node": ">=0.4.0"
}
},
"node_modules/adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
"dev": true,
"engines": {
"node": ">=6.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -2683,6 +2715,26 @@
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
},
"node_modules/big-integer": {
"version": "1.6.51",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/binary": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
"integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
"dependencies": {
"buffers": "~0.1.1",
"chainsaw": "~0.1.0"
},
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -2709,6 +2761,11 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
},
"node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
@@ -2880,10 +2937,26 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
"node_modules/buffer-indexof-polyfill": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
"engines": {
"node": ">=0.2.0"
}
},
"node_modules/busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"dependencies": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
@@ -3010,6 +3083,17 @@
}
]
},
"node_modules/chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
"integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
"dependencies": {
"traverse": ">=0.3.0 <0.4"
},
"engines": {
"node": "*"
}
},
"node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
@@ -3677,7 +3761,7 @@
"node_modules/dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"dependencies": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
@@ -3779,6 +3863,36 @@
"node": ">=10"
}
},
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
"dependencies": {
"readable-stream": "^2.0.2"
}
},
"node_modules/duplexer2/node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/duplexer2/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -4449,6 +4563,42 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"dependencies": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
},
"engines": {
"node": ">=0.6"
}
},
"node_modules/fstream/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/fstream/node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -7063,6 +7213,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -7590,9 +7745,10 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/multer": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz",
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
"deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
@@ -8552,6 +8708,15 @@
"node": ">=0.6"
}
},
"node_modules/querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
"deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
"engines": {
"node": ">=0.4.x"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -8899,6 +9064,11 @@
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
@@ -9637,6 +9807,14 @@
"node": ">=8"
}
},
"node_modules/traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
"engines": {
"node": "*"
}
},
"node_modules/traverse-chain": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
@@ -9915,6 +10093,45 @@
"node": ">= 0.8"
}
},
"node_modules/unzipper": {
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
"dependencies": {
"big-integer": "^1.6.17",
"binary": "~0.3.0",
"bluebird": "~3.4.1",
"buffer-indexof-polyfill": "~1.0.0",
"duplexer2": "~0.1.4",
"fstream": "^1.0.12",
"graceful-fs": "^4.2.2",
"listenercount": "~1.0.1",
"readable-stream": "~2.3.6",
"setimmediate": "~1.0.4"
}
},
"node_modules/unzipper/node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/unzipper/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/update-notifier": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz",
@@ -9942,6 +10159,15 @@
"url": "https://github.com/yeoman/update-notifier?sponsor=1"
}
},
"node_modules/url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
"dependencies": {
"punycode": "1.3.2",
"querystring": "0.2.0"
}
},
"node_modules/url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
@@ -9954,6 +10180,11 @@
"node": ">=4"
}
},
"node_modules/url/node_modules/punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -11364,9 +11595,9 @@
}
},
"@sasjs/core": {
"version": "4.23.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
"integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
},
"@sasjs/utils": {
"version": "2.42.1",
@@ -11675,6 +11906,15 @@
"validator": "^13.6.0"
}
},
"@types/adm-zip": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.0.tgz",
"integrity": "sha512-FCJBJq9ODsQZUNURo5ILAQueuA8WJhRvuihS3ke2iI25mJlfV2LK8jG2Qj2z2AWg8U0FtWWqBHVRetceLskSaw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/babel__core": {
"version": "7.1.15",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.15.tgz",
@@ -12072,6 +12312,15 @@
"integrity": "sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==",
"dev": true
},
"@types/unzipper": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz",
"integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/webidl-conversions": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz",
@@ -12152,6 +12401,12 @@
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
"dev": true
},
"adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
"dev": true
},
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -12475,6 +12730,20 @@
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms="
},
"big-integer": {
"version": "1.6.51",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
},
"binary": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
"integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
"requires": {
"buffers": "~0.1.1",
"chainsaw": "~0.1.0"
}
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -12500,6 +12769,11 @@
}
}
},
"bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
},
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
@@ -12626,10 +12900,20 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
"buffer-indexof-polyfill": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="
},
"buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
"requires": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
@@ -12723,6 +13007,14 @@
"integrity": "sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==",
"dev": true
},
"chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
"integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
"requires": {
"traverse": ">=0.3.0 <0.4"
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
@@ -13267,7 +13559,7 @@
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
"requires": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
@@ -13349,6 +13641,38 @@
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"dev": true
},
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
"requires": {
"readable-stream": "^2.0.2"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -13871,6 +14195,35 @@
"dev": true,
"optional": true
},
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"requires": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
},
"dependencies": {
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": {
"glob": "^7.1.3"
}
}
}
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -15814,6 +16167,11 @@
"type-check": "~0.3.2"
}
},
"listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@@ -16220,9 +16578,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multer": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz",
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
"requires": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
@@ -16933,6 +17291,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -17189,6 +17552,11 @@
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
@@ -17755,6 +18123,11 @@
"punycode": "^2.1.1"
}
},
"traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="
},
"traverse-chain": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz",
@@ -17942,6 +18315,47 @@
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"unzipper": {
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
"requires": {
"big-integer": "^1.6.17",
"binary": "~0.3.0",
"bluebird": "~3.4.1",
"buffer-indexof-polyfill": "~1.0.0",
"duplexer2": "~0.1.4",
"fstream": "^1.0.12",
"graceful-fs": "^4.2.2",
"listenercount": "~1.0.1",
"readable-stream": "~2.3.6",
"setimmediate": "~1.0.4"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"update-notifier": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz",
@@ -17963,6 +18377,22 @@
"xdg-basedir": "^4.0.0"
}
},
"url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
},
"dependencies": {
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
}
}
},
"url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",

View File

@@ -47,7 +47,7 @@
},
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^4.23.1",
"@sasjs/core": "^4.27.3",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
@@ -63,9 +63,12 @@
"mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.3",
"swagger-ui-express": "4.3.0"
"swagger-ui-express": "4.3.0",
"unzipper": "^0.10.11",
"url": "^0.10.3"
},
"devDependencies": {
"@types/adm-zip": "^0.5.0",
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
@@ -80,6 +83,8 @@
"@types/node": "^15.12.2",
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
@@ -96,7 +101,7 @@
},
"nodemonConfig": {
"ignore": [
"tmp/**/*"
"sasjs_root/**/*"
]
}
}

View File

@@ -310,6 +310,21 @@ components:
- displayName
type: object
additionalProperties: false
GroupResponse:
properties:
groupId:
type: number
format: double
name:
type: string
description:
type: string
required:
- groupId
- name
- description
type: object
additionalProperties: false
UserDetailsResponse:
properties:
id:
@@ -325,6 +340,10 @@ components:
type: boolean
autoExec:
type: string
groups:
items:
$ref: '#/components/schemas/GroupResponse'
type: array
required:
- id
- displayName
@@ -357,28 +376,13 @@ components:
autoExec:
type: string
description: 'User-specific auto-exec code'
example: '<SAS code>'
example: ""
required:
- displayName
- username
- password
type: object
additionalProperties: false
GroupResponse:
properties:
groupId:
type: number
format: double
name:
type: string
description:
type: string
required:
- groupId
- name
- description
type: object
additionalProperties: false
GroupDetailsResponse:
properties:
groupId:
@@ -543,7 +547,7 @@ paths:
application/json:
schema:
properties:
user: {properties: {displayName: {type: string}, username: {type: string}}, required: [displayName, username], type: object}
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
loggedIn: {type: boolean}
required:
- user
@@ -723,7 +727,8 @@ paths:
examples:
'Example 1':
value: {status: failure, message: 'Deployment failed!'}
summary: 'Creates/updates files within SASjs Drive using uploaded JSON file.'
description: "Accepts JSON file and zipped compressed JSON file as well.\nCompressed file should only contain one JSON file and should have same name\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\nAny other file or JSON file in zipped will be ignored!"
summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.'
tags:
- Drive
security:
@@ -985,6 +990,94 @@ paths:
application/json:
schema:
$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}':
get:
operationId: GetUser

View File

@@ -3,12 +3,11 @@ import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal'
import { ExecuteReturnJsonResponse } from '.'
import {
getDesktopUserAutoExecPath,
getPreProgramVariables,
getUserAutoExec,
ModeType,
parseLogToArray
} from '../utils'
import { readFile } from '@sasjs/utils'
interface ExecuteSASCodePayload {
/**
@@ -43,7 +42,7 @@ const executeSASCode = async (
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await readFile(getDesktopUserAutoExecPath())
: await getUserAutoExec()
try {
const { webout, log, httpHeaders } =

View File

@@ -96,7 +96,12 @@ export class DriveController {
}
/**
* @summary Creates/updates files within SASjs Drive using uploaded JSON file.
* Accepts JSON file and zipped compressed JSON file as well.
* Compressed file should only contain one JSON file and should have same name
* as of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip
* Any other file or JSON file in zipped will be ignored!
*
* @summary Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.
*
*/
@Example<DeployResponse>(successDeployResponse)

View File

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

View File

@@ -43,7 +43,7 @@ export class ExecutionController {
session?: Session
) {
if (!(await fileExists(programPath)))
throw 'ExecutionController: SAS file does not exist.'
throw `The Stored Program at (${vars._program}) does not exist, or you do not have permission to view it.`
const program = await readFile(programPath)
@@ -119,9 +119,9 @@ filename _webout "${weboutPath}" mod;
/* dynamic user-provided vars */
${preProgramVarStatments}
/* user auto exec starts */
${otherArgs?.userAutoExec}
/* user auto exec ends */
/* user autoexec starts */
${otherArgs?.userAutoExec ?? ''}
/* user autoexec ends */
/* actual job code */
${program}`

View File

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

View File

@@ -14,8 +14,11 @@ import {
Hidden,
Request
} from 'tsoa'
import { desktopUser } from '../middlewares'
import User, { UserPayload } from '../model/User'
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
import { GroupResponse } from './group'
export interface UserResponse {
id: number
@@ -30,6 +33,7 @@ interface UserDetailsResponse {
isActive: boolean
isAdmin: boolean
autoExec?: string
groups?: GroupResponse[]
}
@Security('bearerAuth')
@@ -75,6 +79,26 @@ export class UserController {
return createUser(body)
}
/**
* 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.
@@ -86,9 +110,38 @@ export class UserController {
@Request() req: express.Request,
@Path() userId: number
): Promise<UserDetailsResponse> {
const { MODE } = process.env
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
const { user } = req
const getAutoExec = user!.isAdmin || user!.userId == userId
return getUser(userId, getAutoExec)
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)
}
/**
@@ -108,7 +161,26 @@ export class UserController {
@Path() userId: number,
@Body() body: UserPayload
): 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)
}
/**
@@ -122,7 +194,7 @@ export class UserController {
@Body() body: { password?: string },
@Query() @Hidden() isAdmin: boolean = false
) {
return deleteUser(userId, isAdmin, body)
return deleteUser({ id: userId }, isAdmin, body)
}
}
@@ -163,11 +235,22 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
}
}
interface GetUserBy {
id?: number
username?: string
}
const getUser = async (
id: number,
findBy: GetUserBy,
getAutoExec: boolean
): Promise<UserDetailsResponse> => {
const user = await User.findOne({ id })
const user = (await User.findOne(
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.')
@@ -177,12 +260,21 @@ const getUser = async (
username: user.username,
isActive: user.isActive,
isAdmin: user.isAdmin,
autoExec: getAutoExec ? user.autoExec : undefined
autoExec: getAutoExec ? user.autoExec ?? '' : undefined,
groups: user.groups
}
}
const getDesktopAutoExec = async () => {
return {
...desktopUser,
id: desktopUser.userId,
autoExec: await getUserAutoExec()
}
}
const updateUser = async (
id: number,
findBy: GetUserBy,
data: Partial<UserPayload>
): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive, autoExec } = data
@@ -192,8 +284,13 @@ const updateUser = async (
if (username) {
// Checking if user is already in the database
const usernameExist = await User.findOne({ username })
if (usernameExist && 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
}
@@ -202,9 +299,10 @@ const updateUser = async (
params.password = User.hashPassword(password)
}
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true })
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
if (!updatedUser) throw new Error(`Unable to find user with id: ${id}`)
if (!updatedUser)
throw new Error(`Unable to find user with ${findBy.id || findBy.username}`)
return {
id: updatedUser.id,
@@ -216,12 +314,21 @@ const updateUser = async (
}
}
const updateDesktopAutoExec = async (autoExec: string) => {
await updateUserAutoExec(autoExec)
return {
...desktopUser,
id: desktopUser.userId,
autoExec
}
}
const deleteUser = async (
id: number,
findBy: GetUserBy,
isAdmin: boolean,
{ 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 (!isAdmin) {
@@ -229,5 +336,5 @@ const deleteUser = async (
if (!validPass) throw new Error('Invalid password.')
}
await User.deleteOne({ id })
await User.deleteOne(findBy)
}

View File

@@ -97,6 +97,7 @@ const login = async (
return {
loggedIn: true,
user: {
id: user.id,
username: user.username,
displayName: user.displayName
}

View File

@@ -1,15 +1,22 @@
import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app'
import { fetchLatestAutoExec, 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()
}
// 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.user) {
const user = await fetchLatestAutoExec(req.session.user)

View File

@@ -1,20 +1,37 @@
import { RequestHandler } from 'express'
import { RequestHandler, Request } from 'express'
import { userInfo } from 'os'
import { RequestUser } from '../types'
import { ModeType } from '../utils'
const regexUser = /^\/SASjsApi\/user\/[0-9]*$/ // /SASjsApi/user/1
const allowedInDesktopMode: { [key: string]: RegExp[] } = {
GET: [regexUser],
PATCH: [regexUser]
}
const reqAllowedInDesktopMode = (request: Request): boolean => {
const { method, originalUrl: url } = request
return !!allowedInDesktopMode[method]?.find((urlRegex) => urlRegex.test(url))
}
export const desktopRestrict: RequestHandler = (req, res, next) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server')
return res.status(403).send('Not Allowed while in Desktop Mode.')
if (MODE === ModeType.Desktop) {
if (!reqAllowedInDesktopMode(req))
return res.status(403).send('Not Allowed while in Desktop Mode.')
}
next()
}
export const desktopUsername: RequestHandler = (req, res, next) => {
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,11 +1,22 @@
import { RequestHandler } from 'express'
// This middleware checks if a non-admin user trying to
// access information of other user
export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
const { user } = req
const userId = parseInt(req.params.userId)
if (!user?.isAdmin && user?.userId !== userId) {
return res.status(401).send('Admin account required')
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()
}

View File

@@ -29,7 +29,7 @@ export interface UserPayload {
isActive?: boolean
/**
* User-specific auto-exec code
* @example "<SAS code>"
* @example ""
*/
autoExec?: string
}
@@ -45,6 +45,8 @@ interface IUserDocument extends UserPayload, Document {
interface IUser extends IUserDocument {
comparePassword(password: string): boolean
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
}
interface IUserModel extends Model<IUser> {
hashPassword(password: string): string
@@ -106,6 +108,28 @@ userSchema.method('comparePassword', function (password: string): boolean {
if (bcrypt.compareSync(password, this.password)) return true
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)

View File

@@ -7,9 +7,12 @@ import { multerSingle } from '../../middlewares/multer'
import { DriveController } from '../../controllers/'
import {
deployValidation,
extractJSONFromZip,
extractName,
fileBodyValidation,
fileParamValidation,
folderParamValidation
folderParamValidation,
isZipFile
} from '../../utils'
const controller = new DriveController()
@@ -49,7 +52,24 @@ driveRouter.post(
async (req, res) => {
if (!req.file) return res.status(400).send('"file" is not present.')
const fileContent = await readFile(req.file.path)
let fileContent: string = ''
const { value: zipFile } = isZipFile(req.file)
if (zipFile) {
fileContent = await extractJSONFromZip(zipFile)
const fileInZip = extractName(zipFile.originalname)
if (!fileContent) {
deleteFile(req.file.path)
return res
.status(400)
.send(
`No content present in ${fileInZip} of compressed file ${zipFile.originalname}`
)
}
} else {
fileContent = await readFile(req.file.path)
}
let jsonContent
try {

View File

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

View File

@@ -3,6 +3,7 @@ import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import AdmZip from 'adm-zip'
import {
folderExists,
@@ -72,11 +73,52 @@ describe('drive', () => {
})
describe('deploy', () => {
const shouldFailAssertion = async (payload: any) => {
const res = await request(app)
.post('/SASjsApi/drive/deploy')
.auth(accessToken, { type: 'bearer' })
.send({ appLoc: '/Public', fileTree: payload })
const makeRequest = async (payload: any, type: string = 'payload') => {
const requestUrl =
type === 'payload'
? '/SASjsApi/drive/deploy'
: '/SASjsApi/drive/deploy/upload'
if (type === 'payload') {
return await request(app)
.post(requestUrl)
.auth(accessToken, { type: 'bearer' })
.send({ appLoc: '/Public', fileTree: payload })
}
if (type === 'file') {
const deployContents = JSON.stringify({
appLoc: '/Public',
fileTree: payload
})
return await request(app)
.post(requestUrl)
.auth(accessToken, { type: 'bearer' })
.attach('file', Buffer.from(deployContents), 'deploy.json')
} else {
const deployContents = JSON.stringify({
appLoc: '/Public',
fileTree: payload
})
const zip = new AdmZip()
// add file directly
zip.addFile(
'deploy.json',
Buffer.from(deployContents, 'utf8'),
'entry comment goes here'
)
return await request(app)
.post(requestUrl)
.auth(accessToken, { type: 'bearer' })
.attach('file', zip.toBuffer(), 'deploy.json.zip')
}
}
const shouldFailAssertion = async (
payload: any,
type: string = 'payload'
) => {
const res = await makeRequest(payload, type)
expect(res.statusCode).toEqual(400)
@@ -176,6 +218,240 @@ describe('drive', () => {
await deleteFolder(path.join(getFilesFolder(), 'public'))
})
describe('upload', () => {
it('should respond with payload example if valid JSON file was not provided', async () => {
await shouldFailAssertion(null, 'file')
await shouldFailAssertion(undefined, 'file')
await shouldFailAssertion('data', 'file')
await shouldFailAssertion({}, 'file')
await shouldFailAssertion(
{
userId: 1,
title: 'test is cool'
},
'file'
)
await shouldFailAssertion(
{
membersWRONG: []
},
'file'
)
await shouldFailAssertion(
{
members: {}
},
'file'
)
await shouldFailAssertion(
{
members: [
{
nameWRONG: 'jobs',
type: 'folder',
members: []
}
]
},
'file'
)
await shouldFailAssertion(
{
members: [
{
name: 'jobs',
type: 'WRONG',
members: []
}
]
},
'file'
)
await shouldFailAssertion(
{
members: [
{
name: 'jobs',
type: 'folder',
members: [
{
name: 'extract',
type: 'folder',
members: [
{
name: 'makedata1',
type: 'service',
codeWRONG: '%put Hello World!;'
}
]
}
]
}
]
},
'file'
)
})
it('should successfully deploy if valid JSON file was provided', async () => {
const deployContents = JSON.stringify({
appLoc: '/public',
fileTree: getTreeExample()
})
const res = await request(app)
.post('/SASjsApi/drive/deploy/upload')
.auth(accessToken, { type: 'bearer' })
.attach('file', Buffer.from(deployContents), 'deploy.json')
expect(res.statusCode).toEqual(200)
expect(res.text).toEqual(
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
)
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
const testJobFolder = path.join(
getFilesFolder(),
'public',
'jobs',
'extract'
)
await expect(folderExists(testJobFolder)).resolves.toEqual(true)
const exampleService = getExampleService()
const testJobFile =
path.join(testJobFolder, exampleService.name) + '.sas'
await expect(fileExists(testJobFile)).resolves.toEqual(true)
await expect(readFile(testJobFile)).resolves.toEqual(
exampleService.code
)
await deleteFolder(path.join(getFilesFolder(), 'public'))
})
})
describe('upload - zipped', () => {
it('should respond with payload example if valid Zipped file was not provided', async () => {
await shouldFailAssertion(null, 'zip')
await shouldFailAssertion(undefined, 'zip')
await shouldFailAssertion('data', 'zip')
await shouldFailAssertion({}, 'zip')
await shouldFailAssertion(
{
userId: 1,
title: 'test is cool'
},
'zip'
)
await shouldFailAssertion(
{
membersWRONG: []
},
'zip'
)
await shouldFailAssertion(
{
members: {}
},
'zip'
)
await shouldFailAssertion(
{
members: [
{
nameWRONG: 'jobs',
type: 'folder',
members: []
}
]
},
'zip'
)
await shouldFailAssertion(
{
members: [
{
name: 'jobs',
type: 'WRONG',
members: []
}
]
},
'zip'
)
await shouldFailAssertion(
{
members: [
{
name: 'jobs',
type: 'folder',
members: [
{
name: 'extract',
type: 'folder',
members: [
{
name: 'makedata1',
type: 'service',
codeWRONG: '%put Hello World!;'
}
]
}
]
}
]
},
'zip'
)
})
it('should successfully deploy if valid Zipped file was provided', async () => {
const deployContents = JSON.stringify({
appLoc: '/public',
fileTree: getTreeExample()
})
const zip = new AdmZip()
// add file directly
zip.addFile(
'deploy.json',
Buffer.from(deployContents, 'utf8'),
'entry comment goes here'
)
const res = await request(app)
.post('/SASjsApi/drive/deploy/upload')
.auth(accessToken, { type: 'bearer' })
.attach('file', zip.toBuffer(), 'deploy.json.zip')
expect(res.statusCode).toEqual(200)
expect(res.text).toEqual(
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
)
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
const testJobFolder = path.join(
getFilesFolder(),
'public',
'jobs',
'extract'
)
await expect(folderExists(testJobFolder)).resolves.toEqual(true)
const exampleService = getExampleService()
const testJobFile =
path.join(testJobFolder, exampleService.name) + '.sas'
await expect(fileExists(testJobFile)).resolves.toEqual(true)
await expect(readFile(testJobFile)).resolves.toEqual(
exampleService.code
)
await deleteFolder(path.join(getFilesFolder(), 'public'))
})
})
})
describe('folder', () => {

View File

@@ -3,20 +3,20 @@ import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
const clientId = 'someclientID'
const adminUser = {
displayName: 'Test Admin',
username: 'testAdminUsername',
username: 'testadminusername',
password: '12345678',
isAdmin: true,
isActive: true
}
const user = {
displayName: 'Test User',
username: 'testUsername',
username: 'testusername',
password: '87654321',
isAdmin: false,
isActive: true,
@@ -68,6 +68,20 @@ describe('user', () => {
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 () => {
const res = await request(app)
.post('/SASjsApi/user')
@@ -244,7 +258,7 @@ describe('user', () => {
const dbUser1 = await controller.createUser(user)
const dbUser2 = await controller.createUser({
...user,
username: 'randomUser'
username: 'randomuser'
})
const res = await request(app)
@@ -256,6 +270,102 @@ describe('user', () => {
expect(res.text).toEqual('Error: Username already exists.')
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', () => {
@@ -349,6 +459,89 @@ describe('user', () => {
expect(res.text).toEqual('Error: Invalid password.')
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', () => {
@@ -378,6 +571,7 @@ describe('user', () => {
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 () => {
@@ -395,6 +589,7 @@ describe('user', () => {
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 when access token is not of an admin account', async () => {
@@ -417,6 +612,34 @@ describe('user', () => {
expect(res.body.isAdmin).toEqual(user.isAdmin)
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 () => {
@@ -441,6 +664,86 @@ describe('user', () => {
expect(res.text).toEqual('Error: User is not found.')
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', () => {

View File

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

View File

@@ -47,10 +47,11 @@ stpRouter.post(
query?._program
)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
return res.end(response)
}
// TODO: investigate if this code is required
// if (response instanceof Buffer) {
// res.writeHead(200, (req as any).sasHeaders)
// return res.end(response)
// }
res.send(response)
} catch (err: any) {

View File

@@ -7,6 +7,7 @@ import {
} from '../../middlewares'
import {
deleteUserValidation,
getUserValidation,
registerUserValidation,
updateUserValidation
} from '../../utils'
@@ -36,6 +37,25 @@ userRouter.get('/', authenticateAccessToken, async (req, 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
@@ -48,6 +68,34 @@ userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
}
})
userRouter.patch(
'/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 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.updateUserByUsername(username, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
userRouter.patch(
'/:userId',
authenticateAccessToken,
@@ -70,6 +118,34 @@ userRouter.patch(
}
)
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(
'/:userId',
authenticateAccessToken,

View File

@@ -23,13 +23,21 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
${style}
</head>
<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">
${Object.entries(appStreamConfig)
.map(([streamServiceName, entry]) =>
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
)
.join('')}
${Object.entries(appStreamConfig)
.map(([streamServiceName, entry]) =>
singleAppStreamHtml(
streamServiceName,
entry.appLoc,
entry.streamLogo
)
)
.join('')}
<a class="app" title="Upload build.json">
<input id="fileId" type="file" hidden />
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">

View File

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

View File

@@ -5,18 +5,71 @@ export const style = `<style>
.app-container {
display: flex;
flex-wrap: wrap;
align-items: baseline;
align-items: center;
justify-content: center;
padding-top: 50px;
}
.app-container .app {
width: 150px;
height: 180px;
margin: 10px;
overflow: hidden;
text-align: center;
text-decoration: none;
color: black;
background: #efefef;
padding: 10px;
border-radius: 7px;
border: 1px solid #d7d7d7;
}
.app-container .app img{
width: 100%;
margin-bottom: 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>`

View File

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

View File

@@ -0,0 +1,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

@@ -0,0 +1,6 @@
import path from 'path'
export const extractName = (filePath: string) => {
const extension = path.extname(filePath)
return path.basename(filePath, extension)
}

View File

@@ -1,5 +1,6 @@
import path from 'path'
import { homedir } from 'os'
import fs from 'fs-extra'
export const apiRoot = path.join(__dirname, '..', '..')
export const codebaseRoot = path.join(apiRoot, '..')
@@ -47,3 +48,6 @@ export const generateUniqueFileName = (fileName: string, extension = '') =>
new Date().getTime(),
extension
].join('')
export const createReadStream = async (filePath: string) =>
fs.createReadStream(filePath)

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,7 +1,9 @@
export * from './appStreamConfig'
export * from './connectDB'
export * from './copySASjsCore'
export * from './desktopAutoExec'
export * from './extractHeaders'
export * from './extractName'
export * from './file'
export * from './generateAccessToken'
export * from './generateAuthCode'
@@ -9,8 +11,10 @@ export * from './generateRefreshToken'
export * from './getCertificates'
export * from './getDesktopFields'
export * from './getPreProgramVariables'
export * from './getServerUrl'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './zipped'
export * from './parseLogToArray'
export * from './removeTokensInDB'
export * from './saveTokensInDB'

View File

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

40
api/src/utils/zipped.ts Normal file
View File

@@ -0,0 +1,40 @@
import path from 'path'
import unZipper from 'unzipper'
import { extractName } from './extractName'
import { createReadStream } from './file'
export const isZipFile = (
file: Express.Multer.File
): { error?: string; value?: Express.Multer.File } => {
const fileExtension = path.extname(file.originalname)
if (fileExtension.toUpperCase() !== '.ZIP')
return { error: `"file" has invalid extension ${fileExtension}` }
const allowedMimetypes = ['application/zip', 'application/x-zip-compressed']
if (!allowedMimetypes.includes(file.mimetype))
return { error: `"file" has invalid type ${file.mimetype}` }
return { value: file }
}
export const extractJSONFromZip = async (zipFile: Express.Multer.File) => {
let fileContent: string = ''
const fileInZip = extractName(zipFile.originalname)
const zip = (await createReadStream(zipFile.path)).pipe(
unZipper.Parse({ forceStream: true })
)
for await (const entry of zip) {
const fileName = entry.path as string
if (fileName.toUpperCase().endsWith('.JSON') && fileName === fileInZip) {
fileContent = await entry.buffer()
break
} else {
entry.autodrain()
}
}
return fileContent
}

122
package-lock.json generated
View File

@@ -2770,9 +2770,9 @@
}
},
"node_modules/npm": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/npm/-/npm-8.10.0.tgz",
"integrity": "sha512-6oo65q9Quv9mRPGZJufmSH+C/UFdgelwzRXiglT/2mDB50zdy/lZK5dFY0TJ9fJ/8gHqnxcX1NM206KLjTBMlQ==",
"version": "8.12.2",
"resolved": "https://registry.npmjs.org/npm/-/npm-8.12.2.tgz",
"integrity": "sha512-TArexqro9wpl/6wz6t6YdYhOoiy/UArqiSsSsqI7fieEhQEswDQSJcgt/LuCDjl6mfCDi0So7S2UZ979qLYRPg==",
"bundleDependencies": [
"@isaacs/string-locale-compare",
"@npmcli/arborist",
@@ -2858,7 +2858,7 @@
"@npmcli/run-script": "^3.0.1",
"abbrev": "~1.1.1",
"archy": "~1.0.0",
"cacache": "^16.0.7",
"cacache": "^16.1.1",
"chalk": "^4.1.2",
"chownr": "^2.0.0",
"cli-columns": "^4.0.0",
@@ -2883,7 +2883,7 @@
"libnpmsearch": "^5.0.2",
"libnpmteam": "^4.0.2",
"libnpmversion": "^3.0.1",
"make-fetch-happen": "^10.1.3",
"make-fetch-happen": "^10.1.7",
"minipass": "^3.1.6",
"minipass-pipeline": "^1.2.4",
"mkdirp": "^1.0.4",
@@ -2900,7 +2900,7 @@
"npm-user-validate": "^1.0.1",
"npmlog": "^6.0.2",
"opener": "^1.5.2",
"pacote": "^13.3.0",
"pacote": "^13.6.0",
"parse-conflict-json": "^2.0.2",
"proc-log": "^2.0.1",
"qrcode-terminal": "^0.12.0",
@@ -2910,7 +2910,7 @@
"readdir-scoped-modules": "^1.1.0",
"rimraf": "^3.0.2",
"semver": "^7.3.7",
"ssri": "^9.0.0",
"ssri": "^9.0.1",
"tar": "^6.1.11",
"text-table": "~0.2.0",
"tiny-relative-date": "^1.3.0",
@@ -2965,7 +2965,7 @@
"peer": true
},
"node_modules/npm/node_modules/@npmcli/arborist": {
"version": "5.2.0",
"version": "5.2.1",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -3389,7 +3389,7 @@
}
},
"node_modules/npm/node_modules/cacache": {
"version": "16.0.7",
"version": "16.1.1",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -3759,7 +3759,7 @@
}
},
"node_modules/npm/node_modules/glob": {
"version": "8.0.1",
"version": "8.0.3",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -3769,8 +3769,7 @@
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
"once": "^1.3.0"
},
"engines": {
"node": ">=12"
@@ -4121,7 +4120,7 @@
}
},
"node_modules/npm/node_modules/libnpmexec": {
"version": "4.0.5",
"version": "4.0.6",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -4186,7 +4185,7 @@
}
},
"node_modules/npm/node_modules/libnpmpack": {
"version": "4.0.3",
"version": "4.1.0",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -4194,7 +4193,7 @@
"dependencies": {
"@npmcli/run-script": "^3.0.0",
"npm-package-arg": "^9.0.1",
"pacote": "^13.0.5"
"pacote": "^13.5.0"
},
"engines": {
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
@@ -4272,14 +4271,14 @@
}
},
"node_modules/npm/node_modules/make-fetch-happen": {
"version": "10.1.3",
"version": "10.1.7",
"dev": true,
"inBundle": true,
"license": "ISC",
"peer": true,
"dependencies": {
"agentkeepalive": "^4.2.1",
"cacache": "^16.0.2",
"cacache": "^16.1.0",
"http-cache-semantics": "^4.1.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
@@ -4292,7 +4291,7 @@
"minipass-pipeline": "^1.2.4",
"negotiator": "^0.6.3",
"promise-retry": "^2.0.1",
"socks-proxy-agent": "^6.1.1",
"socks-proxy-agent": "^7.0.0",
"ssri": "^9.0.0"
},
"engines": {
@@ -4300,7 +4299,7 @@
}
},
"node_modules/npm/node_modules/minimatch": {
"version": "5.0.1",
"version": "5.1.0",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -4509,7 +4508,7 @@
}
},
"node_modules/npm/node_modules/node-gyp/node_modules/glob": {
"version": "7.2.0",
"version": "7.2.3",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -4518,7 +4517,7 @@
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
@@ -4633,7 +4632,7 @@
}
},
"node_modules/npm/node_modules/npm-packlist": {
"version": "5.0.3",
"version": "5.1.0",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -4760,7 +4759,7 @@
}
},
"node_modules/npm/node_modules/pacote": {
"version": "13.3.0",
"version": "13.6.0",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -4777,7 +4776,7 @@
"minipass": "^3.1.6",
"mkdirp": "^1.0.4",
"npm-package-arg": "^9.0.0",
"npm-packlist": "^5.0.0",
"npm-packlist": "^5.1.0",
"npm-pick-manifest": "^7.0.0",
"npm-registry-fetch": "^13.0.1",
"proc-log": "^2.0.0",
@@ -5009,7 +5008,7 @@
}
},
"node_modules/npm/node_modules/rimraf/node_modules/glob": {
"version": "7.2.0",
"version": "7.2.3",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -5018,7 +5017,7 @@
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
@@ -5141,7 +5140,7 @@
}
},
"node_modules/npm/node_modules/socks-proxy-agent": {
"version": "6.2.0",
"version": "7.0.0",
"dev": true,
"inBundle": true,
"license": "MIT",
@@ -5192,7 +5191,7 @@
"peer": true
},
"node_modules/npm/node_modules/ssri": {
"version": "9.0.0",
"version": "9.0.1",
"dev": true,
"inBundle": true,
"license": "ISC",
@@ -5911,9 +5910,9 @@
"peer": true
},
"node_modules/semantic-release": {
"version": "19.0.2",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-19.0.2.tgz",
"integrity": "sha512-7tPonjZxukKECmClhsfyMKDt0GR38feIC2HxgyYaBi+9tDySBLjK/zYDLhh+m6yjnHIJa9eBTKYE7k63ZQcYbw==",
"version": "19.0.3",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-19.0.3.tgz",
"integrity": "sha512-HaFbydST1cDKZHuFZxB8DTrBLJVK/AnDExpK0s3EqLIAAUAHUgnd+VSJCUtTYQKkAkauL8G9CucODrVCc7BuAA==",
"dev": true,
"peer": true,
"dependencies": {
@@ -9019,9 +9018,9 @@
"peer": true
},
"npm": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/npm/-/npm-8.10.0.tgz",
"integrity": "sha512-6oo65q9Quv9mRPGZJufmSH+C/UFdgelwzRXiglT/2mDB50zdy/lZK5dFY0TJ9fJ/8gHqnxcX1NM206KLjTBMlQ==",
"version": "8.12.2",
"resolved": "https://registry.npmjs.org/npm/-/npm-8.12.2.tgz",
"integrity": "sha512-TArexqro9wpl/6wz6t6YdYhOoiy/UArqiSsSsqI7fieEhQEswDQSJcgt/LuCDjl6mfCDi0So7S2UZ979qLYRPg==",
"dev": true,
"peer": true,
"requires": {
@@ -9035,7 +9034,7 @@
"@npmcli/run-script": "^3.0.1",
"abbrev": "~1.1.1",
"archy": "~1.0.0",
"cacache": "^16.0.7",
"cacache": "^16.1.1",
"chalk": "^4.1.2",
"chownr": "^2.0.0",
"cli-columns": "^4.0.0",
@@ -9060,7 +9059,7 @@
"libnpmsearch": "^5.0.2",
"libnpmteam": "^4.0.2",
"libnpmversion": "^3.0.1",
"make-fetch-happen": "^10.1.3",
"make-fetch-happen": "^10.1.7",
"minipass": "^3.1.6",
"minipass-pipeline": "^1.2.4",
"mkdirp": "^1.0.4",
@@ -9077,7 +9076,7 @@
"npm-user-validate": "^1.0.1",
"npmlog": "^6.0.2",
"opener": "^1.5.2",
"pacote": "^13.3.0",
"pacote": "^13.6.0",
"parse-conflict-json": "^2.0.2",
"proc-log": "^2.0.1",
"qrcode-terminal": "^0.12.0",
@@ -9087,7 +9086,7 @@
"readdir-scoped-modules": "^1.1.0",
"rimraf": "^3.0.2",
"semver": "^7.3.7",
"ssri": "^9.0.0",
"ssri": "^9.0.1",
"tar": "^6.1.11",
"text-table": "~0.2.0",
"tiny-relative-date": "^1.3.0",
@@ -9117,7 +9116,7 @@
"peer": true
},
"@npmcli/arborist": {
"version": "5.2.0",
"version": "5.2.1",
"bundled": true,
"dev": true,
"peer": true,
@@ -9432,7 +9431,7 @@
}
},
"cacache": {
"version": "16.0.7",
"version": "16.1.1",
"bundled": true,
"dev": true,
"peer": true,
@@ -9704,7 +9703,7 @@
}
},
"glob": {
"version": "8.0.1",
"version": "8.0.3",
"bundled": true,
"dev": true,
"peer": true,
@@ -9713,8 +9712,7 @@
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.0.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
"once": "^1.3.0"
}
},
"graceful-fs": {
@@ -9970,7 +9968,7 @@
}
},
"libnpmexec": {
"version": "4.0.5",
"version": "4.0.6",
"bundled": true,
"dev": true,
"peer": true,
@@ -10019,14 +10017,14 @@
}
},
"libnpmpack": {
"version": "4.0.3",
"version": "4.1.0",
"bundled": true,
"dev": true,
"peer": true,
"requires": {
"@npmcli/run-script": "^3.0.0",
"npm-package-arg": "^9.0.1",
"pacote": "^13.0.5"
"pacote": "^13.5.0"
}
},
"libnpmpublish": {
@@ -10081,13 +10079,13 @@
"peer": true
},
"make-fetch-happen": {
"version": "10.1.3",
"version": "10.1.7",
"bundled": true,
"dev": true,
"peer": true,
"requires": {
"agentkeepalive": "^4.2.1",
"cacache": "^16.0.2",
"cacache": "^16.1.0",
"http-cache-semantics": "^4.1.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
@@ -10100,12 +10098,12 @@
"minipass-pipeline": "^1.2.4",
"negotiator": "^0.6.3",
"promise-retry": "^2.0.1",
"socks-proxy-agent": "^6.1.1",
"socks-proxy-agent": "^7.0.0",
"ssri": "^9.0.0"
}
},
"minimatch": {
"version": "5.0.1",
"version": "5.1.0",
"bundled": true,
"dev": true,
"peer": true,
@@ -10254,7 +10252,7 @@
}
},
"glob": {
"version": "7.2.0",
"version": "7.2.3",
"bundled": true,
"dev": true,
"peer": true,
@@ -10262,7 +10260,7 @@
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
@@ -10344,7 +10342,7 @@
}
},
"npm-packlist": {
"version": "5.0.3",
"version": "5.1.0",
"bundled": true,
"dev": true,
"peer": true,
@@ -10435,7 +10433,7 @@
}
},
"pacote": {
"version": "13.3.0",
"version": "13.6.0",
"bundled": true,
"dev": true,
"peer": true,
@@ -10451,7 +10449,7 @@
"minipass": "^3.1.6",
"mkdirp": "^1.0.4",
"npm-package-arg": "^9.0.0",
"npm-packlist": "^5.0.0",
"npm-packlist": "^5.1.0",
"npm-pick-manifest": "^7.0.0",
"npm-registry-fetch": "^13.0.1",
"proc-log": "^2.0.0",
@@ -10615,7 +10613,7 @@
}
},
"glob": {
"version": "7.2.0",
"version": "7.2.3",
"bundled": true,
"dev": true,
"peer": true,
@@ -10623,7 +10621,7 @@
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
@@ -10701,7 +10699,7 @@
}
},
"socks-proxy-agent": {
"version": "6.2.0",
"version": "7.0.0",
"bundled": true,
"dev": true,
"peer": true,
@@ -10744,7 +10742,7 @@
"peer": true
},
"ssri": {
"version": "9.0.0",
"version": "9.0.1",
"bundled": true,
"dev": true,
"peer": true,
@@ -11270,9 +11268,9 @@
"peer": true
},
"semantic-release": {
"version": "19.0.2",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-19.0.2.tgz",
"integrity": "sha512-7tPonjZxukKECmClhsfyMKDt0GR38feIC2HxgyYaBi+9tDySBLjK/zYDLhh+m6yjnHIJa9eBTKYE7k63ZQcYbw==",
"version": "19.0.3",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-19.0.3.tgz",
"integrity": "sha512-HaFbydST1cDKZHuFZxB8DTrBLJVK/AnDExpK0s3EqLIAAUAHUgnd+VSJCUtTYQKkAkauL8G9CucODrVCc7BuAA==",
"dev": true,
"peer": true,
"requires": {

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import axios from 'axios'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import React, { useEffect, useState } from 'react'
import { ToastContainer, toast } from 'react-toastify'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import { useLocation } from 'react-router-dom'
@@ -71,8 +71,6 @@ const AuthCode = () => {
>
<Button variant="contained">Copy to Clipboard</Button>
</CopyToClipboard>
<ToastContainer />
</Box>
)
}

View File

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

View File

@@ -0,0 +1,150 @@
import React, { useState, useEffect, useContext } from 'react'
import axios from 'axios'
import {
Grid,
CircularProgress,
Card,
CardHeader,
Divider,
CardContent,
TextField,
CardActions,
Button,
FormGroup,
FormControlLabel,
Checkbox
} from '@mui/material'
import { toast } from 'react-toastify'
import { AppContext, ModeType } from '../../context/appContext'
const Profile = () => {
const [isLoading, setIsLoading] = useState(false)
const appContext = useContext(AppContext)
const [user, setUser] = useState({} as any)
useEffect(() => {
setIsLoading(true)
axios
.get(`/SASjsApi/user/${appContext.userId}`)
.then((res: any) => {
setUser(res.data)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
setIsLoading(false)
})
}, [])
const handleChange = (event: any) => {
const { name, value } = event.target
setUser({ ...user, [name]: value })
}
const handleSubmit = () => {
setIsLoading(true)
axios
.patch(`/SASjsApi/user/${appContext.userId}`, {
username: user.username,
displayName: user.displayName,
autoExec: user.autoExec
})
.then((res: any) => {
toast.success('User information updated', {
theme: 'dark',
position: toast.POSITION.BOTTOM_RIGHT
})
})
.catch((err) => {
toast.error('Failed: ' + err.response?.data || err.text, {
theme: 'dark',
position: toast.POSITION.BOTTOM_RIGHT
})
})
.finally(() => {
setIsLoading(false)
})
}
return isLoading ? (
<CircularProgress
style={{ position: 'absolute', left: '50%', top: '50%' }}
/>
) : (
<Card>
<CardHeader title="Profile Information" />
<Divider />
<CardContent>
<Grid container spacing={4}>
<Grid item md={6} xs={12}>
<TextField
fullWidth
error={user.displayName?.length === 0}
helperText="Please specify display name"
label="Display Name"
name="displayName"
onChange={handleChange}
required
value={user.displayName}
variant="outlined"
disabled={appContext.mode === ModeType.Desktop}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
error={user.username?.length === 0}
helperText="Please specify username"
label="Username"
name="username"
onChange={handleChange}
required
value={user.username}
variant="outlined"
disabled={appContext.mode === ModeType.Desktop}
/>
</Grid>
<Grid item lg={6} md={8} sm={12} xs={12}>
<TextField
fullWidth
label="autoExec"
name="autoExec"
onChange={handleChange}
multiline
rows="10"
value={user.autoExec}
variant="outlined"
/>
</Grid>
<Grid item xs={6}>
<FormGroup row>
<FormControlLabel
disabled
control={<Checkbox checked={user.isActive} />}
label="isActive"
/>
<FormControlLabel
disabled
control={<Checkbox checked={user.isAdmin} />}
label="isAdmin"
/>
</FormGroup>
</Grid>
</Grid>
</CardContent>
<Divider />
<CardActions>
<Button type="submit" variant="contained" onClick={handleSubmit}>
Save Changes
</Button>
</CardActions>
</Card>
)
}
export default Profile

View File

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