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

chore: Merge branch 'main' into issue-184

This commit is contained in:
Saad Jutt
2022-06-18 00:51:48 +05:00
23 changed files with 1418 additions and 125 deletions

84
.all-contributorsrc Normal file
View File

@@ -0,0 +1,84 @@
{
"projectName": "server",
"projectOwner": "sasjs",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": true,
"commitConvention": "angular",
"contributors": [
{
"login": "saadjutt01",
"name": "Saad Jutt",
"avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
"profile": "https://github.com/saadjutt01",
"contributions": [
"code",
"test"
]
},
{
"login": "sabhas",
"name": "Sabir Hassan",
"avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4",
"profile": "https://github.com/sabhas",
"contributions": [
"code",
"test"
]
},
{
"login": "YuryShkoda",
"name": "Yury Shkoda",
"avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
"profile": "https://www.erudicat.com/",
"contributions": [
"code",
"test"
]
},
{
"login": "medjedovicm",
"name": "Mihajlo Medjedovic",
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
"profile": "https://github.com/medjedovicm",
"contributions": [
"code",
"test"
]
},
{
"login": "allanbowe",
"name": "Allan Bowe",
"avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
"profile": "https://4gl.io/",
"contributions": [
"code",
"doc"
]
},
{
"login": "VladislavParhomchik",
"name": "Vladislav Parhomchik",
"avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
"profile": "https://github.com/VladislavParhomchik",
"contributions": [
"test"
]
},
{
"login": "kknapen",
"name": "Koen Knapen",
"avatar_url": "https://avatars.githubusercontent.com/u/78609432?v=4",
"profile": "https://github.com/kknapen",
"contributions": [
"userTesting"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
}

View File

@@ -1,3 +1,36 @@
## [0.6.1](https://github.com/sasjs/server/compare/v0.6.0...v0.6.1) (2022-06-17)
### Bug Fixes
* home page wording. Using fix to force previous change through.. ([8702a4e](https://github.com/sasjs/server/commit/8702a4e8fd1bbfaf4f426b75e8b85a87ede0e0b0))
# [0.6.0](https://github.com/sasjs/server/compare/v0.5.0...v0.6.0) (2022-06-16)
### Features
* get group by group name ([6b0b94a](https://github.com/sasjs/server/commit/6b0b94ad38215ae58e62279a4f73ac3ed2d9d0e8))
# [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)

View File

@@ -1,4 +1,7 @@
# SASjs Server
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:
@@ -196,3 +199,29 @@ The following credentials can be used for the initial connection to SASjs/server
- CLIENTID: `clientID1`
- USERNAME: `secretuser`
- PASSWORD: `secretpassword`
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Saad Jutt</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=sabhas" title="Tests">⚠️</a></td>
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Tests">⚠️</a></td>
<td align="center"><a href="https://4gl.io/"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vladislav Parhomchik</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=VladislavParhomchik" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/kknapen"><img src="https://avatars.githubusercontent.com/u/78609432?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Koen Knapen</b></sub></a><br /><a href="#userTesting-kknapen" title="User Testing">📓</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

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

@@ -435,6 +435,27 @@ components:
- description
type: object
additionalProperties: false
_LeanDocument__LeanDocument_T__:
properties: {}
type: object
Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
properties:
id:
description: 'The string version of this documents _id.'
_id:
$ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
description: 'This documents _id.'
__v:
description: 'This documents __v.'
type: object
description: 'From T, pick a set of properties whose keys are in the union K'
Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_:
$ref: '#/components/schemas/Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__'
description: 'Construct a type with the properties of T except for those in type K.'
LeanDocument_this_:
$ref: '#/components/schemas/Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_'
IGroup:
$ref: '#/components/schemas/LeanDocument_this_'
InfoResponse:
properties:
mode:
@@ -742,7 +763,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:
@@ -1230,6 +1252,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/GroupPayload'
'/SASjsApi/group/by/groupname/{name}':
get:
operationId: GetGroupByGroupName
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/GroupDetailsResponse'
summary: 'Get list of members of a group (userName). All users can request this.'
tags:
- Group
security:
-
bearerAuth: []
parameters:
-
description: 'The group''s name'
in: path
name: name
required: true
schema:
type: string
'/SASjsApi/group/{groupId}':
get:
operationId: GetGroup
@@ -1259,8 +1305,14 @@ paths:
delete:
operationId: DeleteGroup
responses:
'204':
description: 'No content'
'200':
description: Ok
content:
application/json:
schema:
allOf:
- {$ref: '#/components/schemas/IGroup'}
- {properties: {_id: {}}, required: [_id], type: object}
summary: 'Delete a group. Admin task only.'
tags:
- Group

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

@@ -28,6 +28,11 @@ interface GroupDetailsResponse {
users: UserResponse[]
}
interface GetGroupBy {
groupId?: number
name?: string
}
@Security('bearerAuth')
@Route('SASjsApi/group')
@Tags('Group')
@@ -66,6 +71,18 @@ export class GroupController {
return createGroup(body)
}
/**
* @summary Get list of members of a group (userName). All users can request this.
* @param name The group's name
* @example dcgroup
*/
@Get('by/groupname/{name}')
public async getGroupByGroupName(
@Path() name: string
): Promise<GroupDetailsResponse> {
return getGroup({ name })
}
/**
* @summary Get list of members of a group (userName). All users can request this.
* @param groupId The group's identifier
@@ -75,7 +92,7 @@ export class GroupController {
public async getGroup(
@Path() groupId: number
): Promise<GroupDetailsResponse> {
return getGroup(groupId)
return getGroup({ groupId })
}
/**
@@ -129,9 +146,13 @@ export class GroupController {
*/
@Delete('{groupId}')
public async deleteGroup(@Path() groupId: number) {
const { deletedCount } = await Group.deleteOne({ groupId })
if (deletedCount) return
throw new Error('No Group deleted!')
const group = await Group.findOne({ groupId })
if (group) return await group.remove()
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
}
}
@@ -145,6 +166,15 @@ const createGroup = async ({
description,
isActive
}: GroupPayload): Promise<GroupDetailsResponse> => {
// Checking if user is already in the database
const groupnameExist = await Group.findOne({ name })
if (groupnameExist)
throw {
code: 409,
status: 'Conflict',
message: 'Group name already exists.'
}
const group = new Group({
name,
description,
@@ -162,15 +192,20 @@ const createGroup = async ({
}
}
const getGroup = async (groupId: number): Promise<GroupDetailsResponse> => {
const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
const group = (await Group.findOne(
{ groupId },
findBy,
'groupId name description isActive users -_id'
).populate(
'users',
'id username displayName -_id'
)) as unknown as GroupDetailsResponse
if (!group) throw new Error('Group not found.')
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
return {
groupId: group.groupId,
@@ -199,16 +234,31 @@ const updateUsersListInGroup = async (
action: 'addUser' | 'removeUser'
): Promise<GroupDetailsResponse> => {
const group = await Group.findOne({ groupId })
if (!group) throw new Error('Group not found.')
if (!group)
throw {
code: 404,
status: 'Not Found',
message: 'Group not found.'
}
const user = await User.findOne({ id: userId })
if (!user) throw new Error('User not found.')
if (!user)
throw {
code: 404,
status: 'Not Found',
message: 'User not found.'
}
const updatedGroup = (action === 'addUser'
? await group.addUser(user._id)
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
if (!updatedGroup) throw new Error('Unable to update group')
if (!updatedGroup)
throw {
code: 400,
status: 'Bad Request',
message: 'Unable to update group.'
}
if (action === 'addUser') user.addGroup(group._id)
else user.removeGroup(group._id)

View File

@@ -86,6 +86,8 @@ ${autoExecContent}`
// however we also need a promise so that we can update the
// session array to say that it has (eventually) finished.
// Additional windows specific options to avoid the desktop popups.
execFilePromise(process.sasLoc, [
'-SYSIN',
codePath,
@@ -99,7 +101,9 @@ ${autoExecContent}`
autoExecPath,
'-ENCODING',
'UTF-8',
process.platform === 'win32' ? '-nosplash' : ''
process.platform === 'win32' ? '-nosplash' : '',
process.platform === 'win32' ? '-icon' : '',
process.platform === 'win32' ? '-nologo' : ''
])
.then(() => {
session.completed = true

View File

@@ -1,4 +1,5 @@
import mongoose, { Schema, model, Document, Model } from 'mongoose'
import User from './User'
const AutoIncrement = require('mongoose-sequence')(mongoose)
export interface GroupPayload {
@@ -34,7 +35,8 @@ interface IGroupModel extends Model<IGroup> {}
const groupSchema = new Schema<IGroupDocument>({
name: {
type: String,
required: true
required: true,
unique: true
},
description: {
type: String,
@@ -46,6 +48,7 @@ const groupSchema = new Schema<IGroupDocument>({
},
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
})
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
// Hooks
@@ -55,6 +58,17 @@ groupSchema.post('save', function (group: IGroup, next: Function) {
})
})
// pre remove hook to remove all references of group from users
groupSchema.pre('remove', async function () {
const userIds = this.users
await Promise.all(
userIds.map(async (userId) => {
const user = await User.findById(userId)
user?.removeGroup(this._id)
})
)
})
// Instance Methods
groupSchema.method(
'addUser',

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

@@ -1,7 +1,7 @@
import express from 'express'
import { GroupController } from '../../controllers/'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
import { registerGroupValidation } from '../../utils'
import { getGroupValidation, registerGroupValidation } from '../../utils'
const groupRouter = express.Router()
@@ -18,7 +18,11 @@ groupRouter.post(
const response = await controller.createGroup(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
@@ -29,7 +33,11 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
const response = await controller.getAllGroups()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
@@ -41,10 +49,37 @@ groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
const response = await controller.getGroup(parseInt(groupId))
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
})
groupRouter.get(
'/by/groupname/:name',
authenticateAccessToken,
async (req, res) => {
const { error, value: params } = getGroupValidation(req.params)
if (error) return res.status(400).send(error.details[0].message)
const { name } = params
const controller = new GroupController()
try {
const response = await controller.getGroupByGroupName(name)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
groupRouter.post(
'/:groupId/:userId',
authenticateAccessToken,
@@ -60,7 +95,11 @@ groupRouter.post(
)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
@@ -80,7 +119,11 @@ groupRouter.delete(
)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)
@@ -97,7 +140,11 @@ groupRouter.delete(
await controller.deleteGroup(parseInt(groupId))
res.status(200).send('Group Deleted!')
} catch (err: any) {
res.status(403).send(err.toString())
const statusCode = err.code
delete err.code
res.status(statusCode).send(err.message)
}
}
)

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

@@ -23,7 +23,7 @@ const user = {
}
const group = {
name: 'DCGroup1',
name: 'dcgroup1',
description: 'DC group for testing purposes.'
}
@@ -70,6 +70,32 @@ describe('group', () => {
expect(res.body.users).toEqual([])
})
it('should respond with Conflict when group already exists with same name', async () => {
await groupController.createGroup(group)
const res = await request(app)
.post('/SASjsApi/group')
.auth(adminAccessToken, { type: 'bearer' })
.send(group)
.expect(409)
expect(res.text).toEqual('Group name already exists.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request when group name does not match the group name schema', async () => {
const res = await request(app)
.post('/SASjsApi/group')
.auth(adminAccessToken, { type: 'bearer' })
.send({ ...group, name: 'Wrong Group Name' })
.expect(400)
expect(res.text).toEqual(
'"name" must only contain alpha-numeric characters'
)
expect(res.body).toEqual({})
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).post('/SASjsApi/group').send().expect(401)
@@ -125,14 +151,51 @@ describe('group', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if groupId is incorrect', async () => {
it(`should delete group's reference from users' groups array`, async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser1 = await userController.createUser({
...user,
username: 'deletegroup1'
})
const dbUser2 = await userController.createUser({
...user,
username: 'deletegroup2'
})
await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id)
await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id)
await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res1 = await request(app)
.get(`/SASjsApi/user/${dbUser1.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res1.body.groups).toEqual([])
const res2 = await request(app)
.get(`/SASjsApi/user/${dbUser2.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res2.body.groups).toEqual([])
})
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.delete(`/SASjsApi/group/1234`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: No Group deleted!')
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
@@ -216,16 +279,76 @@ describe('group', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if groupId is incorrect', async () => {
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.get('/SASjsApi/group/1234')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: Group not found.')
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
describe('by group name', () => {
it('should respond with group', async () => {
const { name } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/by/groupname/${name}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([])
})
it('should respond with group when access token is not of an admin account', async () => {
const accessToken = await generateSaveTokenAndCreateUser({
...user,
username: 'getbyname' + user.username
})
const { name } = await groupController.createGroup(group)
const res = await request(app)
.get(`/SASjsApi/group/by/groupname/${name}`)
.auth(accessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groupId).toBeTruthy()
expect(res.body.name).toEqual(group.name)
expect(res.body.description).toEqual(group.description)
expect(res.body.isActive).toEqual(true)
expect(res.body.users).toEqual([])
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.get('/SASjsApi/group/by/groupname/dcgroup')
.send()
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Not Found if groupname is incorrect', async () => {
const res = await request(app)
.get('/SASjsApi/group/by/groupname/randomCharacters')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(404)
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
})
})
describe('getAll', () => {
@@ -245,8 +368,8 @@ describe('group', () => {
expect(res.body).toEqual([
{
groupId: expect.anything(),
name: 'DCGroup1',
description: 'DC group for testing purposes.'
name: group.name,
description: group.description
}
])
})
@@ -267,8 +390,8 @@ describe('group', () => {
expect(res.body).toEqual([
{
groupId: expect.anything(),
name: 'DCGroup1',
description: 'DC group for testing purposes.'
name: group.name,
description: group.description
}
])
})
@@ -309,6 +432,34 @@ describe('group', () => {
])
})
it(`should add group to user's groups array`, async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
...user,
username: 'addUserToGroup'
})
await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res = await request(app)
.get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groups).toEqual([
{
groupId: expect.anything(),
name: group.name,
description: group.description
}
])
})
it('should respond with group without duplicating user', async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
@@ -362,26 +513,26 @@ describe('group', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if groupId is incorrect', async () => {
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.post('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: Group not found.')
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if userId is incorrect', async () => {
it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.post(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User not found.')
expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
})
@@ -412,6 +563,29 @@ describe('group', () => {
expect(res.body.users).toEqual([])
})
it(`should remove group from user's groups array`, async () => {
const dbGroup = await groupController.createGroup(group)
const dbUser = await userController.createUser({
...user,
username: 'removeGroupFromUser'
})
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const res = await request(app)
.get(`/SASjsApi/user/${dbUser.id}`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
expect(res.body.groups).toEqual([])
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
@@ -438,26 +612,26 @@ describe('group', () => {
expect(res.body).toEqual({})
})
it('should respond with Forbidden if groupId is incorrect', async () => {
it('should respond with Not Found if groupId is incorrect', async () => {
const res = await request(app)
.delete('/SASjsApi/group/123/123')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: Group not found.')
expect(res.text).toEqual('Group not found.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if userId is incorrect', async () => {
it('should respond with Not Found if userId is incorrect', async () => {
const dbGroup = await groupController.createGroup(group)
const res = await request(app)
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(403)
.expect(404)
expect(res.text).toEqual('Error: User not found.')
expect(res.text).toEqual('User not found.')
expect(res.body).toEqual({})
})
})

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

@@ -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'
@@ -14,6 +15,7 @@ export * from './getRunTimeAndFilePath'
export * from './getServerUrl'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './zipped'
export * from './parseLogToArray'
export * from './removeTokensInDB'
export * from './saveTokensInDB'

View File

@@ -3,6 +3,7 @@ import { RunTimeType } from '.'
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
const passwordSchema = Joi.string().min(6).max(1024)
const groupnameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
@@ -30,11 +31,16 @@ export const tokenValidation = (data: any): Joi.ValidationResult =>
export const registerGroupValidation = (data: any): Joi.ValidationResult =>
Joi.object({
name: Joi.string().min(6).required(),
name: groupnameSchema.required(),
description: Joi.string(),
isActive: Joi.boolean()
}).validate(data)
export const getGroupValidation = (data: any): Joi.ValidationResult =>
Joi.object({
name: groupnameSchema.required()
}).validate(data)
export const registerUserValidation = (data: any): Joi.ValidationResult =>
Joi.object({
displayName: Joi.string().min(6).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": {

View File

@@ -9,8 +9,8 @@ const Home = () => {
<CssBaseline />
<h2>Welcome to SASjs Server!</h2>
<p>
This portal provides an interface for executing Stored Programs (drive)
and ad hoc code (studio) against a SAS executable. The source code is
SASjs Server provides a REST interface for executing Stored Programs
and ad hoc code (studio) against SAS and JS executables. The source is
available on{' '}
<a
href="https://github.com/sasjs/server"