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

Compare commits

...

26 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
24 changed files with 1563 additions and 121 deletions

View File

@@ -1,3 +1,65 @@
# [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)

384
api/package-lock.json generated
View File

@@ -25,12 +25,14 @@
"morgan": "^1.10.0",
"multer": "^1.4.3",
"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",
@@ -45,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",
@@ -1753,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",
@@ -2176,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",
@@ -2272,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",
@@ -2684,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",
@@ -2710,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",
@@ -2881,6 +2937,22 @@
"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",
@@ -3011,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",
@@ -3780,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",
@@ -4450,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",
@@ -7064,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",
@@ -8910,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",
@@ -9648,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",
@@ -9926,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",
@@ -11700,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",
@@ -12097,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",
@@ -12177,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",
@@ -12500,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",
@@ -12525,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",
@@ -12651,6 +12900,16 @@
"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",
@@ -12748,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",
@@ -13374,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",
@@ -13896,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",
@@ -15839,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",
@@ -17219,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",
@@ -17785,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",
@@ -17972,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",

View File

@@ -64,9 +64,11 @@
"morgan": "^1.10.0",
"multer": "^1.4.3",
"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",
@@ -81,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",

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
@@ -364,21 +383,6 @@ components:
- 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:
@@ -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

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

View File

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

View File

@@ -18,6 +18,7 @@ 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
@@ -32,6 +33,7 @@ interface UserDetailsResponse {
isActive: boolean
isAdmin: boolean
autoExec?: string
groups?: GroupResponse[]
}
@Security('bearerAuth')
@@ -77,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.
@@ -94,7 +116,32 @@ export class UserController {
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)
}
/**
@@ -119,7 +166,21 @@ export class UserController {
if (MODE === ModeType.Desktop)
return updateDesktopAutoExec(body.autoExec ?? '')
return updateUser(userId, body)
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)
}
/**
@@ -133,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)
}
}
@@ -174,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.')
@@ -188,7 +260,8 @@ 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
}
}
@@ -201,7 +274,7 @@ const getDesktopAutoExec = async () => {
}
const updateUser = async (
id: number,
findBy: GetUserBy,
data: Partial<UserPayload>
): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive, autoExec } = data
@@ -211,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
}
@@ -221,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,
@@ -245,11 +324,11 @@ const updateDesktopAutoExec = async (autoExec: string) => {
}
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) {
@@ -257,5 +336,5 @@ const deleteUser = async (
if (!validPass) throw new Error('Invalid password.')
}
await User.deleteOne({ id })
await User.deleteOne(findBy)
}

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

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

@@ -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,7 +3,7 @@ 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'
@@ -270,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', () => {
@@ -363,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', () => {
@@ -392,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 () => {
@@ -409,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 () => {
@@ -431,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 () => {
@@ -455,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

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

@@ -2,11 +2,7 @@ import path from 'path'
import express, { Request } from 'express'
import { folderExists } from '@sasjs/utils'
import {
addEntryToAppStreamConfig,
getFilesFolder,
getFullUrl
} from '../../utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
import { appStreamHtml } from './appStreamHtml'
const appStreams: { [key: string]: string } = {}
@@ -73,14 +69,16 @@ export const publishAppStream = async (
router.get(`/*`, function (req: Request, res, next) {
const reqPath = req.path.replace(/^\//, '')
// Redirecting to url with trailing slash for base appStream URL only
// Redirecting to url with trailing slash for appStream base URL only
if (reqPath.split('/').length === 1 && !reqPath.endsWith('/'))
return res.redirect(301, `${getFullUrl(req)}/`)
// navigating to same url with slash at start
return res.redirect(301, `${reqPath}/`)
const appStream = reqPath.split('/')[0]
const appStreamFilesPath = appStreams[appStream]
if (appStreamFilesPath) {
const resourcePath = reqPath.split('/')[1] || 'index.html'
// resourcePath is without appStream base path
const resourcePath = reqPath.split('/').slice(1).join('/') || 'index.html'
req.url = resourcePath

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

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

@@ -3,6 +3,7 @@ export * from './connectDB'
export * from './copySASjsCore'
export * from './desktopAutoExec'
export * from './extractHeaders'
export * from './extractName'
export * from './file'
export * from './generateAccessToken'
export * from './generateAuthCode'
@@ -13,6 +14,7 @@ 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

@@ -5,6 +5,11 @@ 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(),

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": {