mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c08cfcbc38 | ||
|
|
8d38d5ac64 | ||
| e08bbcc543 | |||
|
|
eef3cb270d | ||
|
|
9cfbca23f8 | ||
|
|
aef411a0ea | ||
|
|
806ea4cb5c | ||
|
|
7205072358 | ||
|
|
32d372b42f | ||
|
|
e059bee7dc | ||
|
|
6f56aafab1 | ||
|
|
8734489cf0 | ||
|
|
7e6635f40f | ||
|
|
c0022a22f4 | ||
|
|
3fa2a7e2e3 | ||
| 8a617a73ae | |||
|
|
e7babb9f55 | ||
|
|
5ab35b02c4 | ||
|
|
ad82ee7106 | ||
|
|
d2e9456d81 | ||
|
|
e6d1989847 | ||
|
|
7a932383b4 | ||
|
|
576e18347e | ||
|
|
61815f8ae1 | ||
|
|
afff27fd21 | ||
|
|
a8ba378fd1 | ||
|
|
73c81a45dc | ||
|
|
12d424acce | ||
|
|
414fb19de3 | ||
|
|
cfddf1fb0c | ||
|
|
1f483b1afc | ||
|
|
0470239ef1 | ||
|
|
2c259fe1de | ||
|
|
b066734398 | ||
|
|
3b698fce5f | ||
|
|
5ad6ee5e0f | ||
|
|
7d11cc7916 | ||
|
|
ff1def6436 | ||
|
|
c275db184e | ||
|
|
e4239fbcc3 | ||
|
|
c6fd8fdd70 | ||
|
|
79dc2dba23 | ||
|
|
2a7223ad7d | ||
|
|
1fed5ea6ac | ||
|
|
97f689f292 | ||
|
|
53bf68a6af | ||
|
|
f37f8e95d1 | ||
|
|
80b33c7a18 | ||
|
|
b1803fe385 | ||
|
|
7dd08c3b5b | ||
|
|
b780b59b66 | ||
|
|
7b457eaec5 | ||
|
|
c017d13061 |
17
.github/workflows/release.yml
vendored
17
.github/workflows/release.yml
vendored
@@ -2,8 +2,8 @@ name: SASjs Server Executable Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
branches:
|
||||||
- 'v*.*.*'
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -49,10 +49,11 @@ jobs:
|
|||||||
zip macos.zip api-macos
|
zip macos.zip api-macos
|
||||||
zip windows.zip api-win.exe
|
zip windows.zip api-win.exe
|
||||||
|
|
||||||
|
- name: Install Semantic Release and plugins
|
||||||
|
run: |
|
||||||
|
npm i
|
||||||
|
npm i -g semantic-release
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
run: |
|
||||||
with:
|
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
|
||||||
files: |
|
|
||||||
./executables/linux.zip
|
|
||||||
./executables/macos.zip
|
|
||||||
./executables/windows.zip
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.env*
|
.env*
|
||||||
sas/
|
sas/
|
||||||
|
sasjs_root/
|
||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
|
|||||||
43
.releaserc
Normal file
43
.releaserc
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"CHANGELOG.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"path": "./executables/linux.zip",
|
||||||
|
"label": "Linux Executable Binary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./executables/macos.zip",
|
||||||
|
"label": "Macos Executable Binary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./executables/windows.zip",
|
||||||
|
"label": "Windows Executable Binary"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"publishCmd": "echo 'publish command'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
125
CHANGELOG.md
125
CHANGELOG.md
@@ -1,6 +1,127 @@
|
|||||||
# Changelog
|
## [0.4.1](https://github.com/sasjs/server/compare/v0.4.0...v0.4.1) (2022-06-15)
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add/remove group to User when adding/removing user from group and return group membership on getting user ([e08bbcc](https://github.com/sasjs/server/commit/e08bbcc5435cbabaee40a41a7fb667d4a1f078e6))
|
||||||
|
|
||||||
|
# [0.4.0](https://github.com/sasjs/server/compare/v0.3.10...v0.4.0) (2022-06-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* new APIs added for GET|PATCH|DELETE of user by username ([aef411a](https://github.com/sasjs/server/commit/aef411a0eac625c33274dfe3e88b6f75115c44d8))
|
||||||
|
|
||||||
|
## [0.3.10](https://github.com/sasjs/server/compare/v0.3.9...v0.3.10) (2022-06-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* correct syntax for encoding option ([32d372b](https://github.com/sasjs/server/commit/32d372b42fbf56b6c0779e8f704164eaae1c7548))
|
||||||
|
|
||||||
|
## [0.3.9](https://github.com/sasjs/server/compare/v0.3.8...v0.3.9) (2022-06-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* forcing utf 8 encoding. Closes [#76](https://github.com/sasjs/server/issues/76) ([8734489](https://github.com/sasjs/server/commit/8734489cf014aedaca3f325e689493e4fe0b71ca))
|
||||||
|
|
||||||
|
## [0.3.8](https://github.com/sasjs/server/compare/v0.3.7...v0.3.8) (2022-06-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* execution controller better error handling ([8a617a7](https://github.com/sasjs/server/commit/8a617a73ae63233332f5788c90f173d6cd5e1283))
|
||||||
|
* execution controller error details ([3fa2a7e](https://github.com/sasjs/server/commit/3fa2a7e2e32f90050f6b09e30ce3ef725eb0b15f))
|
||||||
|
|
||||||
|
## [0.3.7](https://github.com/sasjs/server/compare/v0.3.6...v0.3.7) (2022-06-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **appstream:** redirect to relative + nested resource should be accessed ([5ab35b0](https://github.com/sasjs/server/commit/5ab35b02c4417132dddb5a800982f31d0d50ef66))
|
||||||
|
|
||||||
|
## [0.3.6](https://github.com/sasjs/server/compare/v0.3.5...v0.3.6) (2022-06-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **appstream:** should serve only new files for same app stream name with new deployment ([e6d1989](https://github.com/sasjs/server/commit/e6d1989847761fbe562d7861ffa0ee542839b125))
|
||||||
|
|
||||||
|
## [0.3.5](https://github.com/sasjs/server/compare/v0.3.4...v0.3.5) (2022-05-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bumping sasjs/core library ([61815f8](https://github.com/sasjs/server/commit/61815f8ae18be132e17c199cd8e3afbcc2fa0b60))
|
||||||
|
|
||||||
|
## [0.3.4](https://github.com/sasjs/server/compare/v0.3.3...v0.3.4) (2022-05-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** system username for DESKTOP mode ([a8ba378](https://github.com/sasjs/server/commit/a8ba378fd1ff374ba025a96fdfae5c6c36954465))
|
||||||
|
|
||||||
|
## [0.3.3](https://github.com/sasjs/server/compare/v0.3.2...v0.3.3) (2022-05-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* usage of autoexec API in DESKTOP mode ([12d424a](https://github.com/sasjs/server/commit/12d424acce8108a6f53aefbac01fddcdc5efb48f))
|
||||||
|
|
||||||
|
## [0.3.2](https://github.com/sasjs/server/compare/v0.3.1...v0.3.2) (2022-05-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** ability to use get/patch User API in desktop mode. ([2c259fe](https://github.com/sasjs/server/commit/2c259fe1de95d84e6929e311aaa6b895e66b42a3))
|
||||||
|
|
||||||
|
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** username should be lowercase ([5ad6ee5](https://github.com/sasjs/server/commit/5ad6ee5e0f5d7d6faa45b72215f1d9d55cfc37db))
|
||||||
|
* **web:** reduced width for autoexec input ([7d11cc7](https://github.com/sasjs/server/commit/7d11cc79161e5a07f6c5392d742ef6b9d8658071))
|
||||||
|
|
||||||
|
# [0.3.0](https://github.com/sasjs/server/compare/v0.2.0...v0.3.0) (2022-05-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **web:** added profile + edit + autoexec changes ([c275db1](https://github.com/sasjs/server/commit/c275db184e874f0ee3a4f08f2592cfacf1e90742))
|
||||||
|
|
||||||
|
# [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **autoexec:** usage in case of desktop from file ([79dc2db](https://github.com/sasjs/server/commit/79dc2dba23dc48ec218a973119392a45cb3856b5))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** added autoexec + major type setting changes ([2a7223a](https://github.com/sasjs/server/commit/2a7223ad7d6b8f3d4682447fd25d9426a7c79ac3))
|
||||||
|
|
||||||
|
# [0.1.0](https://github.com/sasjs/server/compare/v0.0.77...v0.1.0) (2022-05-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* issue174 + issue175 + issue146 ([80b33c7](https://github.com/sasjs/server/commit/80b33c7a18c1b7727316ffeca71658346733e935))
|
||||||
|
* **web:** click to copy + notification ([f37f8e9](https://github.com/sasjs/server/commit/f37f8e95d1a85e00ceca2413dbb5e1f3f3f72255))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **env:** added new env variable LOG_FORMAT_MORGAN ([53bf68a](https://github.com/sasjs/server/commit/53bf68a6aff44bb7b2f40d40d6554809253a01a8))
|
||||||
|
|
||||||
|
## [0.0.77](https://github.com/sasjs/server/compare/v0.0.76...v0.0.77) (2022-05-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **release:** Github workflow without npm token ([c017d13](https://github.com/sasjs/server/commit/c017d13061d21aeacd0690367992d12ca57a115b))
|
||||||
|
|
||||||
### [0.0.76](https://github.com/sasjs/server/compare/v0.0.75...v0.0.76) (2022-05-16)
|
### [0.0.76](https://github.com/sasjs/server/compare/v0.0.75...v0.0.76) (2022-05-16)
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ SAS_PATH=/path/to/sas/executable.exe
|
|||||||
|
|
||||||
# Path to working directory
|
# Path to working directory
|
||||||
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
||||||
DRIVE_PATH=/tmp
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
# options: [http|https] default: http
|
# options: [http|https] default: http
|
||||||
PROTOCOL=
|
PROTOCOL=
|
||||||
@@ -125,6 +125,10 @@ HELMET_COEP=
|
|||||||
# }
|
# }
|
||||||
HELMET_CSP_CONFIG_PATH=./csp.config.json
|
HELMET_CSP_CONFIG_PATH=./csp.config.json
|
||||||
|
|
||||||
|
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
||||||
|
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
||||||
|
LOG_FORMAT_MORGAN=
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Persisting the Session
|
## Persisting the Session
|
||||||
@@ -147,7 +151,7 @@ Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install p
|
|||||||
```bash
|
```bash
|
||||||
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||||
export PORT=5001
|
export PORT=5001
|
||||||
export DRIVE_PATH=./tmp
|
export SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
pm2 start api-linux
|
pm2 start api-linux
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -18,4 +18,6 @@ SESSION_SECRET=<secret>
|
|||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
DRIVE_PATH=./tmp
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
|
LOG_FORMAT_MORGAN=common
|
||||||
82
api/package-lock.json
generated
82
api/package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^4.23.1",
|
"@sasjs/core": "^4.27.3",
|
||||||
"@sasjs/utils": "2.42.1",
|
"@sasjs/utils": "2.42.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"connect-mongo": "^4.6.0",
|
"connect-mongo": "^4.6.0",
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
"swagger-ui-express": "4.3.0"
|
"swagger-ui-express": "4.3.0",
|
||||||
|
"url": "^0.10.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"api": "build/src/server.js"
|
"api": "build/src/server.js"
|
||||||
@@ -1385,9 +1386,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sasjs/core": {
|
"node_modules/@sasjs/core": {
|
||||||
"version": "4.23.1",
|
"version": "4.27.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
|
||||||
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
|
"integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
|
||||||
},
|
},
|
||||||
"node_modules/@sasjs/utils": {
|
"node_modules/@sasjs/utils": {
|
||||||
"version": "2.42.1",
|
"version": "2.42.1",
|
||||||
@@ -2883,7 +2884,7 @@
|
|||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
||||||
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
|
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dicer": "0.2.5",
|
"dicer": "0.2.5",
|
||||||
"readable-stream": "1.1.x"
|
"readable-stream": "1.1.x"
|
||||||
@@ -3677,7 +3678,7 @@
|
|||||||
"node_modules/dicer": {
|
"node_modules/dicer": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
||||||
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
|
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readable-stream": "1.1.x",
|
"readable-stream": "1.1.x",
|
||||||
"streamsearch": "0.1.2"
|
"streamsearch": "0.1.2"
|
||||||
@@ -7590,9 +7591,10 @@
|
|||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
},
|
},
|
||||||
"node_modules/multer": {
|
"node_modules/multer": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
|
||||||
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==",
|
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
|
||||||
|
"deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"append-field": "^1.0.0",
|
"append-field": "^1.0.0",
|
||||||
"busboy": "^0.2.11",
|
"busboy": "^0.2.11",
|
||||||
@@ -8552,6 +8554,15 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/querystring": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
|
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
|
||||||
|
"deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -9942,6 +9953,15 @@
|
|||||||
"url": "https://github.com/yeoman/update-notifier?sponsor=1"
|
"url": "https://github.com/yeoman/update-notifier?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url": {
|
||||||
|
"version": "0.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
|
||||||
|
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "1.3.2",
|
||||||
|
"querystring": "0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/url-parse-lax": {
|
"node_modules/url-parse-lax": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
||||||
@@ -9954,6 +9974,11 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url/node_modules/punycode": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||||
|
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -11364,9 +11389,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sasjs/core": {
|
"@sasjs/core": {
|
||||||
"version": "4.23.1",
|
"version": "4.27.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.27.3.tgz",
|
||||||
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
|
"integrity": "sha512-8AaPPRGMwhmjw244CDSnTqHXdp/77ZBjIJMgwqw4wTrCf8Vzs2Y5hVihbvAniIGQctZHLMR6X5a3X4ccn9gRjg=="
|
||||||
},
|
},
|
||||||
"@sasjs/utils": {
|
"@sasjs/utils": {
|
||||||
"version": "2.42.1",
|
"version": "2.42.1",
|
||||||
@@ -12629,7 +12654,7 @@
|
|||||||
"busboy": {
|
"busboy": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
||||||
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
|
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"dicer": "0.2.5",
|
"dicer": "0.2.5",
|
||||||
"readable-stream": "1.1.x"
|
"readable-stream": "1.1.x"
|
||||||
@@ -13267,7 +13292,7 @@
|
|||||||
"dicer": {
|
"dicer": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
||||||
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
|
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"readable-stream": "1.1.x",
|
"readable-stream": "1.1.x",
|
||||||
"streamsearch": "0.1.2"
|
"streamsearch": "0.1.2"
|
||||||
@@ -16220,9 +16245,9 @@
|
|||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
},
|
},
|
||||||
"multer": {
|
"multer": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
|
||||||
"integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==",
|
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"append-field": "^1.0.0",
|
"append-field": "^1.0.0",
|
||||||
"busboy": "^0.2.11",
|
"busboy": "^0.2.11",
|
||||||
@@ -16933,6 +16958,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||||
},
|
},
|
||||||
|
"querystring": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
|
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
|
||||||
|
},
|
||||||
"queue-microtask": {
|
"queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -17963,6 +17993,22 @@
|
|||||||
"xdg-basedir": "^4.0.0"
|
"xdg-basedir": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"url": {
|
||||||
|
"version": "0.10.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
|
||||||
|
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
|
||||||
|
"requires": {
|
||||||
|
"punycode": "1.3.2",
|
||||||
|
"querystring": "0.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||||
|
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"url-parse-lax": {
|
"url-parse-lax": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
},
|
},
|
||||||
"author": "4GL Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^4.23.1",
|
"@sasjs/core": "^4.27.3",
|
||||||
"@sasjs/utils": "2.42.1",
|
"@sasjs/utils": "2.42.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"connect-mongo": "^4.6.0",
|
"connect-mongo": "^4.6.0",
|
||||||
@@ -63,7 +63,8 @@
|
|||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
"swagger-ui-express": "4.3.0"
|
"swagger-ui-express": "4.3.0",
|
||||||
|
"url": "^0.10.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
@@ -94,12 +95,9 @@
|
|||||||
"tsoa": "3.14.1",
|
"tsoa": "3.14.1",
|
||||||
"typescript": "^4.3.2"
|
"typescript": "^4.3.2"
|
||||||
},
|
},
|
||||||
"configuration": {
|
|
||||||
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
|
||||||
},
|
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"tmp/**/*"
|
"sasjs_root/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,6 +310,21 @@ components:
|
|||||||
- displayName
|
- displayName
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
GroupResponse:
|
||||||
|
properties:
|
||||||
|
groupId:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- groupId
|
||||||
|
- name
|
||||||
|
- description
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
UserDetailsResponse:
|
UserDetailsResponse:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
@@ -323,6 +338,12 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
isAdmin:
|
isAdmin:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
autoExec:
|
||||||
|
type: string
|
||||||
|
groups:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/GroupResponse'
|
||||||
|
type: array
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- displayName
|
- displayName
|
||||||
@@ -352,27 +373,16 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
description: 'Account should be active or not, defaults to true'
|
description: 'Account should be active or not, defaults to true'
|
||||||
example: 'true'
|
example: 'true'
|
||||||
|
autoExec:
|
||||||
|
type: string
|
||||||
|
description: 'User-specific auto-exec code'
|
||||||
|
example: ""
|
||||||
required:
|
required:
|
||||||
- displayName
|
- displayName
|
||||||
- username
|
- username
|
||||||
- password
|
- password
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
GroupResponse:
|
|
||||||
properties:
|
|
||||||
groupId:
|
|
||||||
type: number
|
|
||||||
format: double
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- groupId
|
|
||||||
- name
|
|
||||||
- description
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
GroupDetailsResponse:
|
GroupDetailsResponse:
|
||||||
properties:
|
properties:
|
||||||
groupId:
|
groupId:
|
||||||
@@ -537,7 +547,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
properties:
|
properties:
|
||||||
user: {properties: {displayName: {type: string}, username: {type: string}}, required: [displayName, username], type: object}
|
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
|
||||||
loggedIn: {type: boolean}
|
loggedIn: {type: boolean}
|
||||||
required:
|
required:
|
||||||
- user
|
- user
|
||||||
@@ -979,6 +989,94 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UserPayload'
|
$ref: '#/components/schemas/UserPayload'
|
||||||
|
'/SASjsApi/user/by/username/{username}':
|
||||||
|
get:
|
||||||
|
operationId: GetUserByUsername
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserDetailsResponse'
|
||||||
|
description: 'Only Admin or user itself will get user autoExec code.'
|
||||||
|
summary: 'Get user properties - such as group memberships, userName, displayName.'
|
||||||
|
tags:
|
||||||
|
- User
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'The User''s username'
|
||||||
|
in: path
|
||||||
|
name: username
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: johnSnow01
|
||||||
|
patch:
|
||||||
|
operationId: UpdateUserByUsername
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserDetailsResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
|
||||||
|
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
|
||||||
|
tags:
|
||||||
|
- User
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'The User''s username'
|
||||||
|
in: path
|
||||||
|
name: username
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: johnSnow01
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserPayload'
|
||||||
|
delete:
|
||||||
|
operationId: DeleteUserByUsername
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: 'No content'
|
||||||
|
summary: 'Delete a user. Can be performed either by admins, or the user in question.'
|
||||||
|
tags:
|
||||||
|
- User
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'The User''s username'
|
||||||
|
in: path
|
||||||
|
name: username
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: johnSnow01
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
'/SASjsApi/user/{userId}':
|
'/SASjsApi/user/{userId}':
|
||||||
get:
|
get:
|
||||||
operationId: GetUser
|
operationId: GetUser
|
||||||
@@ -989,6 +1087,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UserDetailsResponse'
|
$ref: '#/components/schemas/UserDetailsResponse'
|
||||||
|
description: 'Only Admin or user itself will get user autoExec code.'
|
||||||
summary: 'Get user properties - such as group memberships, userName, displayName.'
|
summary: 'Get user properties - such as group memberships, userName, displayName.'
|
||||||
tags:
|
tags:
|
||||||
- User
|
- User
|
||||||
|
|||||||
@@ -12,25 +12,44 @@ import helmet from 'helmet'
|
|||||||
import {
|
import {
|
||||||
connectDB,
|
connectDB,
|
||||||
copySASjsCore,
|
copySASjsCore,
|
||||||
getWebBuildFolderPath,
|
CorsType,
|
||||||
|
getWebBuildFolder,
|
||||||
|
HelmetCoepType,
|
||||||
|
instantiateLogger,
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
|
ModeType,
|
||||||
|
ProtocolType,
|
||||||
|
ReturnCode,
|
||||||
setProcessVariables,
|
setProcessVariables,
|
||||||
setupFolders
|
setupFolders,
|
||||||
|
verifyEnvVariables
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
|
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
instantiateLogger()
|
||||||
|
|
||||||
|
if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.use(cookieParser())
|
app.use(cookieParser())
|
||||||
app.use(morgan('tiny'))
|
|
||||||
|
|
||||||
const { MODE, CORS, WHITELIST, PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } =
|
const {
|
||||||
process.env
|
MODE,
|
||||||
|
CORS,
|
||||||
|
WHITELIST,
|
||||||
|
PROTOCOL,
|
||||||
|
HELMET_CSP_CONFIG_PATH,
|
||||||
|
HELMET_COEP,
|
||||||
|
LOG_FORMAT_MORGAN
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
app.use(morgan(LOG_FORMAT_MORGAN as string))
|
||||||
|
|
||||||
export const cookieOptions = {
|
export const cookieOptions = {
|
||||||
secure: PROTOCOL === 'https',
|
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
}
|
}
|
||||||
@@ -38,9 +57,8 @@ export const cookieOptions = {
|
|||||||
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
||||||
HELMET_CSP_CONFIG_PATH
|
HELMET_CSP_CONFIG_PATH
|
||||||
)
|
)
|
||||||
const coepFlag =
|
if (PROTOCOL === ProtocolType.HTTP)
|
||||||
HELMET_COEP === 'true' || HELMET_COEP === undefined ? true : false
|
cspConfigJson['upgrade-insecure-requests'] = null
|
||||||
if (PROTOCOL === 'http') cspConfigJson['upgrade-insecure-requests'] = null
|
|
||||||
|
|
||||||
/***********************************
|
/***********************************
|
||||||
* CSRF Protection *
|
* CSRF Protection *
|
||||||
@@ -58,14 +76,14 @@ app.use(
|
|||||||
...cspConfigJson
|
...cspConfigJson
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
crossOriginEmbedderPolicy: coepFlag
|
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
/***********************************
|
/***********************************
|
||||||
* Enabling CORS *
|
* Enabling CORS *
|
||||||
***********************************/
|
***********************************/
|
||||||
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
if (CORS === CorsType.ENABLED) {
|
||||||
const whiteList: string[] = []
|
const whiteList: string[] = []
|
||||||
WHITELIST?.split(' ')
|
WHITELIST?.split(' ')
|
||||||
?.filter((url) => !!url)
|
?.filter((url) => !!url)
|
||||||
@@ -84,7 +102,7 @@ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
|||||||
* Express Sessions *
|
* Express Sessions *
|
||||||
* With Mongo Store *
|
* With Mongo Store *
|
||||||
***********************************/
|
***********************************/
|
||||||
if (MODE?.trim() === 'server') {
|
if (MODE === ModeType.Server) {
|
||||||
let store: MongoStore | undefined
|
let store: MongoStore | undefined
|
||||||
|
|
||||||
// NOTE: when exporting app.js as agent for supertest
|
// NOTE: when exporting app.js as agent for supertest
|
||||||
@@ -105,6 +123,7 @@ if (MODE?.trim() === 'server') {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.json({ limit: '100mb' }))
|
app.use(express.json({ limit: '100mb' }))
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
@@ -129,7 +148,7 @@ export default setProcessVariables().then(async () => {
|
|||||||
|
|
||||||
// should be served after setting up web route
|
// should be served after setting up web route
|
||||||
// index.html needs to be injected with some js script.
|
// index.html needs to be injected with some js script.
|
||||||
app.use(express.static(getWebBuildFolderPath()))
|
app.use(express.static(getWebBuildFolder()))
|
||||||
|
|
||||||
app.use(onError)
|
app.use(onError)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
import { ExecuteReturnJson, ExecutionController } from './internal'
|
||||||
import { PreProgramVars } from '../types'
|
|
||||||
import { ExecuteReturnJsonResponse } from '.'
|
import { ExecuteReturnJsonResponse } from '.'
|
||||||
import { getPreProgramVariables, parseLogToArray } from '../utils'
|
import {
|
||||||
|
getPreProgramVariables,
|
||||||
|
getUserAutoExec,
|
||||||
|
ModeType,
|
||||||
|
parseLogToArray
|
||||||
|
} from '../utils'
|
||||||
|
|
||||||
interface ExecuteSASCodePayload {
|
interface ExecuteSASCodePayload {
|
||||||
/**
|
/**
|
||||||
@@ -30,14 +34,23 @@ export class CodeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
const executeSASCode = async (
|
||||||
|
req: express.Request,
|
||||||
|
{ code }: ExecuteSASCodePayload
|
||||||
|
) => {
|
||||||
|
const { user } = req
|
||||||
|
const userAutoExec =
|
||||||
|
process.env.MODE === ModeType.Server
|
||||||
|
? user?.autoExec
|
||||||
|
: await getUserAutoExec()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { webout, log, httpHeaders } =
|
||||||
(await new ExecutionController().executeProgram(
|
(await new ExecutionController().executeProgram(
|
||||||
code,
|
code,
|
||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
{ ...req.query, _debug: 131 },
|
{ ...req.query, _debug: 131 },
|
||||||
undefined,
|
{ userAutoExec },
|
||||||
true
|
true
|
||||||
)) as ExecuteReturnJson
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||||
|
|
||||||
import { TreeNode } from '../types'
|
import { TreeNode } from '../types'
|
||||||
import { getTmpFilesFolderPath } from '../utils'
|
import { getFilesFolder } from '../utils'
|
||||||
|
|
||||||
interface DeployPayload {
|
interface DeployPayload {
|
||||||
appLoc: string
|
appLoc: string
|
||||||
@@ -214,12 +214,12 @@ const getFileTree = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deploy = async (data: DeployPayload) => {
|
const deploy = async (data: DeployPayload) => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
||||||
|
|
||||||
const appLocPath = path
|
const appLocPath = path
|
||||||
.join(getTmpFilesFolderPath(), ...appLocParts)
|
.join(getFilesFolder(), ...appLocParts)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!appLocPath.includes(driveFilesPath)) {
|
if (!appLocPath.includes(driveFilesPath)) {
|
||||||
@@ -238,10 +238,10 @@ const deploy = async (data: DeployPayload) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFile = async (req: express.Request, filePath: string) => {
|
const getFile = async (req: express.Request, filePath: string) => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
@@ -261,11 +261,11 @@ const getFile = async (req: express.Request, filePath: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFolder = async (folderPath?: string) => {
|
const getFolder = async (folderPath?: string) => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
if (folderPath) {
|
if (folderPath) {
|
||||||
const folderPathFull = path
|
const folderPathFull = path
|
||||||
.join(getTmpFilesFolderPath(), folderPath)
|
.join(getFilesFolder(), folderPath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!folderPathFull.includes(driveFilesPath)) {
|
if (!folderPathFull.includes(driveFilesPath)) {
|
||||||
@@ -291,10 +291,10 @@ const getFolder = async (folderPath?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteFile = async (filePath: string) => {
|
const deleteFile = async (filePath: string) => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
@@ -314,7 +314,7 @@ const saveFile = async (
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
): Promise<GetFileResponse> => {
|
): Promise<GetFileResponse> => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
@@ -339,7 +339,7 @@ const updateFile = async (
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
): Promise<GetFileResponse> => {
|
): Promise<GetFileResponse> => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Group, { GroupPayload } from '../model/Group'
|
|||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import { UserResponse } from './user'
|
import { UserResponse } from './user'
|
||||||
|
|
||||||
interface GroupResponse {
|
export interface GroupResponse {
|
||||||
groupId: number
|
groupId: number
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
@@ -210,6 +210,9 @@ const updateUsersListInGroup = async (
|
|||||||
|
|
||||||
if (!updatedGroup) throw new Error('Unable to update group')
|
if (!updatedGroup) throw new Error('Unable to update group')
|
||||||
|
|
||||||
|
if (action === 'addUser') user.addGroup(group._id)
|
||||||
|
else user.removeGroup(group._id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groupId: updatedGroup.groupId,
|
groupId: updatedGroup.groupId,
|
||||||
name: updatedGroup.name,
|
name: updatedGroup.name,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { PreProgramVars, Session, TreeNode } from '../../types'
|
|||||||
import {
|
import {
|
||||||
extractHeaders,
|
extractHeaders,
|
||||||
generateFileUploadSasCode,
|
generateFileUploadSasCode,
|
||||||
getTmpFilesFolderPath,
|
getFilesFolder,
|
||||||
getTmpMacrosPath,
|
getMacrosFolder,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn
|
isDebugOn
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@@ -43,7 +43,7 @@ export class ExecutionController {
|
|||||||
session?: Session
|
session?: Session
|
||||||
) {
|
) {
|
||||||
if (!(await fileExists(programPath)))
|
if (!(await fileExists(programPath)))
|
||||||
throw 'ExecutionController: SAS file does not exist.'
|
throw `The Stored Program at (${vars._program}) does not exist, or you do not have permission to view it.`
|
||||||
|
|
||||||
const program = await readFile(programPath)
|
const program = await readFile(programPath)
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ export class ExecutionController {
|
|||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
options insert=(SASAUTOS="${getTmpMacrosPath()}");
|
options insert=(SASAUTOS="${getMacrosFolder()}");
|
||||||
|
|
||||||
/* runtime vars */
|
/* runtime vars */
|
||||||
${varStatments}
|
${varStatments}
|
||||||
@@ -119,6 +119,10 @@ filename _webout "${weboutPath}" mod;
|
|||||||
/* dynamic user-provided vars */
|
/* dynamic user-provided vars */
|
||||||
${preProgramVarStatments}
|
${preProgramVarStatments}
|
||||||
|
|
||||||
|
/* user autoexec starts */
|
||||||
|
${otherArgs?.userAutoExec ?? ''}
|
||||||
|
/* user autoexec ends */
|
||||||
|
|
||||||
/* actual job code */
|
/* actual job code */
|
||||||
${program}`
|
${program}`
|
||||||
|
|
||||||
@@ -191,7 +195,7 @@ ${program}`
|
|||||||
const root: TreeNode = {
|
const root: TreeNode = {
|
||||||
name: 'files',
|
name: 'files',
|
||||||
relativePath: '',
|
relativePath: '',
|
||||||
absolutePath: getTmpFilesFolderPath(),
|
absolutePath: getFilesFolder(),
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import { Request, RequestHandler } from 'express'
|
||||||
import multer from 'multer'
|
import multer from 'multer'
|
||||||
import { uuidv4 } from '@sasjs/utils'
|
import { uuidv4 } from '@sasjs/utils'
|
||||||
import { getSessionController } from '.'
|
import { getSessionController } from '.'
|
||||||
|
|
||||||
export class FileUploadController {
|
export class FileUploadController {
|
||||||
private storage = multer.diskStorage({
|
private storage = multer.diskStorage({
|
||||||
destination: function (req: any, file: any, cb: any) {
|
destination: function (req: Request, file: any, cb: any) {
|
||||||
//Sending the intercepted files to the sessions subfolder
|
//Sending the intercepted files to the sessions subfolder
|
||||||
cb(null, req.sasSession.path)
|
cb(null, req.sasSession?.path)
|
||||||
},
|
},
|
||||||
filename: function (req: any, file: any, cb: any) {
|
filename: function (req: Request, file: any, cb: any) {
|
||||||
//req_file prefix + unique hash added to sas request files
|
//req_file prefix + unique hash added to sas request files
|
||||||
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
|
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
|
||||||
}
|
}
|
||||||
@@ -18,7 +19,7 @@ export class FileUploadController {
|
|||||||
|
|
||||||
//It will intercept request and generate unique uuid to be used as a subfolder name
|
//It will intercept request and generate unique uuid to be used as a subfolder name
|
||||||
//that will store the files uploaded
|
//that will store the files uploaded
|
||||||
public preUploadMiddleware = async (req: any, res: any, next: any) => {
|
public preUploadMiddleware: RequestHandler = async (req, res, next) => {
|
||||||
let session
|
let session
|
||||||
|
|
||||||
const sessionController = getSessionController()
|
const sessionController = getSessionController()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Session } from '../../types'
|
|||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import {
|
import {
|
||||||
getTmpSessionsFolderPath,
|
getSessionsFolder,
|
||||||
generateUniqueFileName,
|
generateUniqueFileName,
|
||||||
sysInitCompiledPath
|
sysInitCompiledPath
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@@ -37,7 +37,7 @@ export class SessionController {
|
|||||||
|
|
||||||
private async createSession(): Promise<Session> {
|
private async createSession(): Promise<Session> {
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
// death time of session is 15 mins from creation
|
// death time of session is 15 mins from creation
|
||||||
@@ -93,6 +93,8 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
|
'-ENCODING',
|
||||||
|
'UTF-8',
|
||||||
process.platform === 'win32' ? '-nosplash' : ''
|
process.platform === 'win32' ? '-nosplash' : ''
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
import { getFilesFolder } from '../../utils/file'
|
||||||
import {
|
import {
|
||||||
createFolder,
|
createFolder,
|
||||||
createFile,
|
createFile,
|
||||||
@@ -17,7 +17,7 @@ export const createFileTree = async (
|
|||||||
parentFolders: string[] = []
|
parentFolders: string[] = []
|
||||||
) => {
|
) => {
|
||||||
const destinationPath = path.join(
|
const destinationPath = path.join(
|
||||||
getTmpFilesFolderPath(),
|
getFilesFolder(),
|
||||||
path.join(...parentFolders)
|
path.join(...parentFolders)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = (req: any) => ({
|
const session = (req: express.Request) => ({
|
||||||
id: req.user.userId,
|
id: req.user!.userId,
|
||||||
username: req.user.username,
|
username: req.user!.username,
|
||||||
displayName: req.user.displayName
|
displayName: req.user!.displayName
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,13 +19,14 @@ import {
|
|||||||
} from './internal'
|
} from './internal'
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getPreProgramVariables,
|
||||||
getTmpFilesFolderPath,
|
getFilesFolder,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
isDebugOn,
|
||||||
LogLine,
|
LogLine,
|
||||||
makeFilesNamesMap,
|
makeFilesNamesMap,
|
||||||
parseLogToArray
|
parseLogToArray
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
import { MulterFile } from '../types/Upload'
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecuteReturnJsonPayload {
|
||||||
/**
|
/**
|
||||||
@@ -132,7 +133,7 @@ const executeReturnRaw = async (
|
|||||||
const query = req.query as ExecutionVars
|
const query = req.query as ExecutionVars
|
||||||
const sasCodePath =
|
const sasCodePath =
|
||||||
path
|
path
|
||||||
.join(getTmpFilesFolderPath(), _program)
|
.join(getFilesFolder(), _program)
|
||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -167,15 +168,17 @@ const executeReturnRaw = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnJson = async (
|
const executeReturnJson = async (
|
||||||
req: any,
|
req: express.Request,
|
||||||
_program: string
|
_program: string
|
||||||
): Promise<ExecuteReturnJsonResponse> => {
|
): Promise<ExecuteReturnJsonResponse> => {
|
||||||
const sasCodePath =
|
const sasCodePath =
|
||||||
path
|
path
|
||||||
.join(getTmpFilesFolderPath(), _program)
|
.join(getFilesFolder(), _program)
|
||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
const filesNamesMap = req.files?.length
|
||||||
|
? makeFilesNamesMap(req.files as MulterFile[])
|
||||||
|
: null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { webout, log, httpHeaders } =
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import express from 'express'
|
||||||
import {
|
import {
|
||||||
Security,
|
Security,
|
||||||
Route,
|
Route,
|
||||||
@@ -10,10 +11,14 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Hidden
|
Hidden,
|
||||||
|
Request
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
|
import { desktopUser } from '../middlewares'
|
||||||
|
|
||||||
import User, { UserPayload } from '../model/User'
|
import User, { UserPayload } from '../model/User'
|
||||||
|
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
|
||||||
|
import { GroupResponse } from './group'
|
||||||
|
|
||||||
export interface UserResponse {
|
export interface UserResponse {
|
||||||
id: number
|
id: number
|
||||||
@@ -27,6 +32,8 @@ interface UserDetailsResponse {
|
|||||||
username: string
|
username: string
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
|
autoExec?: string
|
||||||
|
groups?: GroupResponse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@@ -73,13 +80,68 @@ export class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Only Admin or user itself will get user autoExec code.
|
||||||
|
* @summary Get user properties - such as group memberships, userName, displayName.
|
||||||
|
* @param username The User's username
|
||||||
|
* @example username "johnSnow01"
|
||||||
|
*/
|
||||||
|
@Get('by/username/{username}')
|
||||||
|
public async getUserByUsername(
|
||||||
|
@Request() req: express.Request,
|
||||||
|
@Path() username: string
|
||||||
|
): Promise<UserDetailsResponse> {
|
||||||
|
const { MODE } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
|
||||||
|
|
||||||
|
const { user } = req
|
||||||
|
const getAutoExec = user!.isAdmin || user!.username == username
|
||||||
|
return getUser({ username }, getAutoExec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only Admin or user itself will get user autoExec code.
|
||||||
* @summary Get user properties - such as group memberships, userName, displayName.
|
* @summary Get user properties - such as group memberships, userName, displayName.
|
||||||
* @param userId The user's identifier
|
* @param userId The user's identifier
|
||||||
* @example userId 1234
|
* @example userId 1234
|
||||||
*/
|
*/
|
||||||
@Get('{userId}')
|
@Get('{userId}')
|
||||||
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
|
public async getUser(
|
||||||
return getUser(userId)
|
@Request() req: express.Request,
|
||||||
|
@Path() userId: number
|
||||||
|
): Promise<UserDetailsResponse> {
|
||||||
|
const { MODE } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
|
||||||
|
|
||||||
|
const { user } = req
|
||||||
|
const getAutoExec = user!.isAdmin || user!.userId == userId
|
||||||
|
return getUser({ id: userId }, getAutoExec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Update user properties - such as displayName. Can be performed either by admins, or the user in question.
|
||||||
|
* @param username The User's username
|
||||||
|
* @example username "johnSnow01"
|
||||||
|
*/
|
||||||
|
@Example<UserDetailsResponse>({
|
||||||
|
id: 1234,
|
||||||
|
displayName: 'John Snow',
|
||||||
|
username: 'johnSnow01',
|
||||||
|
isAdmin: false,
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
@Patch('by/username/{username}')
|
||||||
|
public async updateUserByUsername(
|
||||||
|
@Path() username: string,
|
||||||
|
@Body() body: UserPayload
|
||||||
|
): Promise<UserDetailsResponse> {
|
||||||
|
const { MODE } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Desktop)
|
||||||
|
return updateDesktopAutoExec(body.autoExec ?? '')
|
||||||
|
|
||||||
|
return updateUser({ username }, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,7 +161,26 @@ export class UserController {
|
|||||||
@Path() userId: number,
|
@Path() userId: number,
|
||||||
@Body() body: UserPayload
|
@Body() body: UserPayload
|
||||||
): Promise<UserDetailsResponse> {
|
): Promise<UserDetailsResponse> {
|
||||||
return updateUser(userId, body)
|
const { MODE } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Desktop)
|
||||||
|
return updateDesktopAutoExec(body.autoExec ?? '')
|
||||||
|
|
||||||
|
return updateUser({ id: userId }, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Delete a user. Can be performed either by admins, or the user in question.
|
||||||
|
* @param username The User's username
|
||||||
|
* @example username "johnSnow01"
|
||||||
|
*/
|
||||||
|
@Delete('by/username/{username}')
|
||||||
|
public async deleteUserByUsername(
|
||||||
|
@Path() username: string,
|
||||||
|
@Body() body: { password?: string },
|
||||||
|
@Query() @Hidden() isAdmin: boolean = false
|
||||||
|
) {
|
||||||
|
return deleteUser({ username }, isAdmin, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,7 +194,7 @@ export class UserController {
|
|||||||
@Body() body: { password?: string },
|
@Body() body: { password?: string },
|
||||||
@Query() @Hidden() isAdmin: boolean = false
|
@Query() @Hidden() isAdmin: boolean = false
|
||||||
) {
|
) {
|
||||||
return deleteUser(userId, isAdmin, body)
|
return deleteUser({ id: userId }, isAdmin, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +204,7 @@ const getAllUsers = async (): Promise<UserResponse[]> =>
|
|||||||
.exec()
|
.exec()
|
||||||
|
|
||||||
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||||
const { displayName, username, password, isAdmin, isActive } = data
|
const { displayName, username, password, isAdmin, isActive, autoExec } = data
|
||||||
|
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
const usernameExist = await User.findOne({ username })
|
const usernameExist = await User.findOne({ username })
|
||||||
@@ -138,7 +219,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
|||||||
username,
|
username,
|
||||||
password: hashPassword,
|
password: hashPassword,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isActive
|
isActive,
|
||||||
|
autoExec
|
||||||
})
|
})
|
||||||
|
|
||||||
const savedUser = await user.save()
|
const savedUser = await user.save()
|
||||||
@@ -148,38 +230,67 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
|||||||
displayName: savedUser.displayName,
|
displayName: savedUser.displayName,
|
||||||
username: savedUser.username,
|
username: savedUser.username,
|
||||||
isActive: savedUser.isActive,
|
isActive: savedUser.isActive,
|
||||||
isAdmin: savedUser.isAdmin
|
isAdmin: savedUser.isAdmin,
|
||||||
|
autoExec: savedUser.autoExec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUser = async (id: number): Promise<UserDetailsResponse> => {
|
interface GetUserBy {
|
||||||
const user = await User.findOne({ id })
|
id?: number
|
||||||
.select({
|
username?: string
|
||||||
_id: 0,
|
}
|
||||||
id: 1,
|
|
||||||
username: 1,
|
const getUser = async (
|
||||||
displayName: 1,
|
findBy: GetUserBy,
|
||||||
isAdmin: 1,
|
getAutoExec: boolean
|
||||||
isActive: 1
|
): Promise<UserDetailsResponse> => {
|
||||||
})
|
const user = (await User.findOne(
|
||||||
.exec()
|
findBy,
|
||||||
|
`id displayName username isActive isAdmin autoExec -_id`
|
||||||
|
).populate(
|
||||||
|
'groups',
|
||||||
|
'groupId name description -_id'
|
||||||
|
)) as unknown as UserDetailsResponse
|
||||||
|
|
||||||
if (!user) throw new Error('User is not found.')
|
if (!user) throw new Error('User is not found.')
|
||||||
|
|
||||||
return user
|
return {
|
||||||
|
id: user.id,
|
||||||
|
displayName: user.displayName,
|
||||||
|
username: user.username,
|
||||||
|
isActive: user.isActive,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
autoExec: getAutoExec ? user.autoExec ?? '' : undefined,
|
||||||
|
groups: user.groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDesktopAutoExec = async () => {
|
||||||
|
return {
|
||||||
|
...desktopUser,
|
||||||
|
id: desktopUser.userId,
|
||||||
|
autoExec: await getUserAutoExec()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUser = async (
|
const updateUser = async (
|
||||||
id: number,
|
findBy: GetUserBy,
|
||||||
data: UserPayload
|
data: Partial<UserPayload>
|
||||||
): Promise<UserDetailsResponse> => {
|
): Promise<UserDetailsResponse> => {
|
||||||
const { displayName, username, password, isAdmin, isActive } = data
|
const { displayName, username, password, isAdmin, isActive, autoExec } = data
|
||||||
|
|
||||||
const params: any = { displayName, isAdmin, isActive }
|
const params: any = { displayName, isAdmin, isActive, autoExec }
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
const usernameExist = await User.findOne({ username })
|
const usernameExist = await User.findOne({ username })
|
||||||
if (usernameExist?.id != id) throw new Error('Username already exists.')
|
if (usernameExist) {
|
||||||
|
if (
|
||||||
|
(findBy.id && usernameExist.id != findBy.id) ||
|
||||||
|
(findBy.username && usernameExist.username != findBy.username)
|
||||||
|
)
|
||||||
|
throw new Error('Username already exists.')
|
||||||
|
}
|
||||||
params.username = username
|
params.username = username
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,27 +299,36 @@ const updateUser = async (
|
|||||||
params.password = User.hashPassword(password)
|
params.password = User.hashPassword(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true })
|
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
|
||||||
.select({
|
|
||||||
_id: 0,
|
|
||||||
id: 1,
|
|
||||||
username: 1,
|
|
||||||
displayName: 1,
|
|
||||||
isAdmin: 1,
|
|
||||||
isActive: 1
|
|
||||||
})
|
|
||||||
.exec()
|
|
||||||
if (!updatedUser) throw new Error('Unable to update user')
|
|
||||||
|
|
||||||
return updatedUser
|
if (!updatedUser)
|
||||||
|
throw new Error(`Unable to find user with ${findBy.id || findBy.username}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updatedUser.id,
|
||||||
|
username: updatedUser.username,
|
||||||
|
displayName: updatedUser.displayName,
|
||||||
|
isAdmin: updatedUser.isAdmin,
|
||||||
|
isActive: updatedUser.isActive,
|
||||||
|
autoExec: updatedUser.autoExec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDesktopAutoExec = async (autoExec: string) => {
|
||||||
|
await updateUserAutoExec(autoExec)
|
||||||
|
return {
|
||||||
|
...desktopUser,
|
||||||
|
id: desktopUser.userId,
|
||||||
|
autoExec
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteUser = async (
|
const deleteUser = async (
|
||||||
id: number,
|
findBy: GetUserBy,
|
||||||
isAdmin: boolean,
|
isAdmin: boolean,
|
||||||
{ password }: { password?: string }
|
{ password }: { password?: string }
|
||||||
) => {
|
) => {
|
||||||
const user = await User.findOne({ id })
|
const user = await User.findOne(findBy)
|
||||||
if (!user) throw new Error('User is not found.')
|
if (!user) throw new Error('User is not found.')
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
@@ -216,5 +336,5 @@ const deleteUser = async (
|
|||||||
if (!validPass) throw new Error('Invalid password.')
|
if (!validPass) throw new Error('Invalid password.')
|
||||||
}
|
}
|
||||||
|
|
||||||
await User.deleteOne({ id })
|
await User.deleteOne(findBy)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { readFile } from '@sasjs/utils'
|
|||||||
|
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
import { getWebBuildFolderPath, generateAuthCode } from '../utils'
|
import { getWebBuildFolder, generateAuthCode } from '../utils'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
import { AuthController } from './auth'
|
import { AuthController } from './auth'
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export class WebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const home = async () => {
|
const home = async () => {
|
||||||
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
const indexHtmlPath = path.join(getWebBuildFolder(), 'index.html')
|
||||||
|
|
||||||
// Attention! Cannot use fileExists here,
|
// Attention! Cannot use fileExists here,
|
||||||
// due to limitation after building executable
|
// due to limitation after building executable
|
||||||
@@ -90,12 +90,14 @@ const login = async (
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isActive: user.isActive
|
isActive: user.isActive,
|
||||||
|
autoExec: user.autoExec
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
user: {
|
user: {
|
||||||
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName
|
displayName: user.displayName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
|
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { csrfProtection } from '../app'
|
import { csrfProtection } from '../app'
|
||||||
import { verifyTokenInDB } from '../utils'
|
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
|
||||||
|
import { desktopUser } from './desktop'
|
||||||
|
|
||||||
|
export const authenticateAccessToken: RequestHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
next
|
||||||
|
) => {
|
||||||
|
const { MODE } = process.env
|
||||||
|
if (MODE === ModeType.Desktop) {
|
||||||
|
req.user = desktopUser
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
|
||||||
// if request is coming from web and has valid session
|
// if request is coming from web and has valid session
|
||||||
// we can validate the request and check for CSRF Token
|
// it can be validated.
|
||||||
if (req.session?.loggedIn) {
|
if (req.session?.loggedIn) {
|
||||||
req.user = req.session.user
|
if (req.session.user) {
|
||||||
|
const user = await fetchLatestAutoExec(req.session.user)
|
||||||
|
|
||||||
return csrfProtection(req, res, next)
|
if (user) {
|
||||||
|
if (user.isActive) {
|
||||||
|
req.user = user
|
||||||
|
return csrfProtection(req, res, next)
|
||||||
|
} else return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticateToken(
|
authenticateToken(
|
||||||
@@ -20,7 +40,7 @@ export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
||||||
authenticateToken(
|
authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@@ -31,16 +51,16 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authenticateToken = (
|
const authenticateToken = (
|
||||||
req: any,
|
req: Request,
|
||||||
res: any,
|
res: Response,
|
||||||
next: any,
|
next: NextFunction,
|
||||||
key: string,
|
key: string,
|
||||||
tokenType: 'accessToken' | 'refreshToken'
|
tokenType: 'accessToken' | 'refreshToken'
|
||||||
) => {
|
) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') {
|
if (MODE?.trim() !== 'server') {
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: '1234',
|
userId: 1234,
|
||||||
clientId: 'desktopModeClientId',
|
clientId: 'desktopModeClientId',
|
||||||
username: 'desktopModeUsername',
|
username: 'desktopModeUsername',
|
||||||
displayName: 'desktopModeDisplayName',
|
displayName: 'desktopModeDisplayName',
|
||||||
|
|||||||
@@ -1,18 +1,37 @@
|
|||||||
export const desktopRestrict = (req: any, res: any, next: any) => {
|
import { RequestHandler, Request } from 'express'
|
||||||
|
import { userInfo } from 'os'
|
||||||
|
import { RequestUser } from '../types'
|
||||||
|
import { ModeType } from '../utils'
|
||||||
|
|
||||||
|
const regexUser = /^\/SASjsApi\/user\/[0-9]*$/ // /SASjsApi/user/1
|
||||||
|
|
||||||
|
const allowedInDesktopMode: { [key: string]: RegExp[] } = {
|
||||||
|
GET: [regexUser],
|
||||||
|
PATCH: [regexUser]
|
||||||
|
}
|
||||||
|
|
||||||
|
const reqAllowedInDesktopMode = (request: Request): boolean => {
|
||||||
|
const { method, originalUrl: url } = request
|
||||||
|
|
||||||
|
return !!allowedInDesktopMode[method]?.find((urlRegex) => urlRegex.test(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const desktopRestrict: RequestHandler = (req, res, next) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server')
|
|
||||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
if (MODE === ModeType.Desktop) {
|
||||||
|
if (!reqAllowedInDesktopMode(req))
|
||||||
|
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||||
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
export const desktopUsername = (req: any, res: any, next: any) => {
|
|
||||||
const { MODE } = process.env
|
|
||||||
if (MODE?.trim() !== 'server')
|
|
||||||
return res.status(200).send({
|
|
||||||
userId: 12345,
|
|
||||||
username: 'DESKTOPusername',
|
|
||||||
displayName: 'DESKTOP User'
|
|
||||||
})
|
|
||||||
|
|
||||||
next()
|
export const desktopUser: RequestUser = {
|
||||||
|
userId: 12345,
|
||||||
|
clientId: 'desktop_app',
|
||||||
|
username: userInfo().username,
|
||||||
|
displayName: userInfo().username,
|
||||||
|
isAdmin: true,
|
||||||
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { Request } from 'express'
|
import { Request } from 'express'
|
||||||
import multer, { FileFilterCallback, Options } from 'multer'
|
import multer, { FileFilterCallback, Options } from 'multer'
|
||||||
import { blockFileRegex, getTmpUploadsPath } from '../utils'
|
import { blockFileRegex, getUploadsFolder } from '../utils'
|
||||||
|
|
||||||
const fieldNameSize = 300
|
const fieldNameSize = 300
|
||||||
const fileSize = 104857600 // 100 MB
|
const fileSize = 104857600 // 100 MB
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: getTmpUploadsPath(),
|
destination: getUploadsFolder(),
|
||||||
filename: function (
|
filename: function (
|
||||||
_req: Request,
|
_req: Request,
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export const verifyAdmin = (req: any, res: any, next: any) => {
|
import { RequestHandler } from 'express'
|
||||||
|
|
||||||
|
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') return next()
|
if (MODE?.trim() !== 'server') return next()
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => {
|
import { RequestHandler } from 'express'
|
||||||
const { user } = req
|
|
||||||
const userId = parseInt(req.params.userId)
|
|
||||||
|
|
||||||
if (!user.isAdmin && user.userId !== userId) {
|
// This middleware checks if a non-admin user trying to
|
||||||
return res.status(401).send('Admin account required')
|
// access information of other user
|
||||||
|
export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
|
||||||
|
const { user } = req
|
||||||
|
|
||||||
|
if (!user?.isAdmin) {
|
||||||
|
let adminAccountRequired: boolean = true
|
||||||
|
|
||||||
|
if (req.params.userId) {
|
||||||
|
adminAccountRequired = user?.userId !== parseInt(req.params.userId)
|
||||||
|
} else if (req.params.username) {
|
||||||
|
adminAccountRequired = user?.username !== req.params.username
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adminAccountRequired)
|
||||||
|
return res.status(401).send('Admin account required')
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,18 +27,26 @@ export interface UserPayload {
|
|||||||
* @example "true"
|
* @example "true"
|
||||||
*/
|
*/
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
/**
|
||||||
|
* User-specific auto-exec code
|
||||||
|
* @example ""
|
||||||
|
*/
|
||||||
|
autoExec?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUserDocument extends UserPayload, Document {
|
interface IUserDocument extends UserPayload, Document {
|
||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
autoExec: string
|
||||||
groups: Schema.Types.ObjectId[]
|
groups: Schema.Types.ObjectId[]
|
||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUser extends IUserDocument {
|
interface IUser extends IUserDocument {
|
||||||
comparePassword(password: string): boolean
|
comparePassword(password: string): boolean
|
||||||
|
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
|
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||||
}
|
}
|
||||||
interface IUserModel extends Model<IUser> {
|
interface IUserModel extends Model<IUser> {
|
||||||
hashPassword(password: string): string
|
hashPassword(password: string): string
|
||||||
@@ -66,6 +74,9 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
autoExec: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
||||||
tokens: [
|
tokens: [
|
||||||
{
|
{
|
||||||
@@ -97,6 +108,28 @@ userSchema.method('comparePassword', function (password: string): boolean {
|
|||||||
if (bcrypt.compareSync(password, this.password)) return true
|
if (bcrypt.compareSync(password, this.password)) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
userSchema.method(
|
||||||
|
'addGroup',
|
||||||
|
async function (groupObjectId: Schema.Types.ObjectId) {
|
||||||
|
const groupIdIndex = this.groups.indexOf(groupObjectId)
|
||||||
|
if (groupIdIndex === -1) {
|
||||||
|
this.groups.push(groupObjectId)
|
||||||
|
}
|
||||||
|
this.markModified('groups')
|
||||||
|
return this.save()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
userSchema.method(
|
||||||
|
'removeGroup',
|
||||||
|
async function (groupObjectId: Schema.Types.ObjectId) {
|
||||||
|
const groupIdIndex = this.groups.indexOf(groupObjectId)
|
||||||
|
if (groupIdIndex > -1) {
|
||||||
|
this.groups.splice(groupIdIndex, 1)
|
||||||
|
}
|
||||||
|
this.markModified('groups')
|
||||||
|
return this.save()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema)
|
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema)
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,11 @@ authRouter.post('/token', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
|
||||||
const userInfo: InfoJWT = req.user
|
const userInfo: InfoJWT = {
|
||||||
|
userId: req.user!.userId!,
|
||||||
|
clientId: req.user!.clientId!
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.refresh(userInfo)
|
const response = await controller.refresh(userInfo)
|
||||||
@@ -38,8 +41,11 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
|
authRouter.delete('/logout', authenticateAccessToken, async (req, res) => {
|
||||||
const userInfo: InfoJWT = req.user
|
const userInfo: InfoJWT = {
|
||||||
|
userId: req.user!.userId!,
|
||||||
|
clientId: req.user!.clientId!
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await controller.logout(userInfo)
|
await controller.logout(userInfo)
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
groupRouter.get('/:groupId', authenticateAccessToken, async (req: any, res) => {
|
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
|
||||||
const { groupId } = req.params
|
const { groupId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.getGroup(groupId)
|
const response = await controller.getGroup(parseInt(groupId))
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -49,12 +49,15 @@ groupRouter.post(
|
|||||||
'/:groupId/:userId',
|
'/:groupId/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { groupId, userId } = req.params
|
const { groupId, userId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.addUserToGroup(groupId, userId)
|
const response = await controller.addUserToGroup(
|
||||||
|
parseInt(groupId),
|
||||||
|
parseInt(userId)
|
||||||
|
)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -66,12 +69,15 @@ groupRouter.delete(
|
|||||||
'/:groupId/:userId',
|
'/:groupId/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { groupId, userId } = req.params
|
const { groupId, userId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.removeUserFromGroup(groupId, userId)
|
const response = await controller.removeUserFromGroup(
|
||||||
|
parseInt(groupId),
|
||||||
|
parseInt(userId)
|
||||||
|
)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -83,12 +89,12 @@ groupRouter.delete(
|
|||||||
'/:groupId',
|
'/:groupId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { groupId } = req.params
|
const { groupId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
await controller.deleteGroup(groupId)
|
await controller.deleteGroup(parseInt(groupId))
|
||||||
res.status(200).send('Group Deleted!')
|
res.status(200).send('Group Deleted!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import swaggerUi from 'swagger-ui-express'
|
|||||||
import {
|
import {
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
desktopRestrict,
|
desktopRestrict,
|
||||||
desktopUsername,
|
|
||||||
verifyAdmin
|
verifyAdmin
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ import sessionRouter from './session'
|
|||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.use('/info', infoRouter)
|
router.use('/info', infoRouter)
|
||||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
router.use('/session', authenticateAccessToken, sessionRouter)
|
||||||
router.use('/auth', desktopRestrict, authRouter)
|
router.use('/auth', desktopRestrict, authRouter)
|
||||||
router.use(
|
router.use(
|
||||||
'/client',
|
'/client',
|
||||||
|
|||||||
@@ -21,17 +21,17 @@ import * as fileUtilModules from '../../../utils/file'
|
|||||||
const timestamp = generateTimestamp()
|
const timestamp = generateTimestamp()
|
||||||
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
||||||
jest
|
jest
|
||||||
.spyOn(fileUtilModules, 'getTmpFolderPath')
|
.spyOn(fileUtilModules, 'getSasjsRootFolder')
|
||||||
.mockImplementation(() => tmpFolder)
|
.mockImplementation(() => tmpFolder)
|
||||||
jest
|
jest
|
||||||
.spyOn(fileUtilModules, 'getTmpUploadsPath')
|
.spyOn(fileUtilModules, 'getUploadsFolder')
|
||||||
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||||
|
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||||
const { getTmpFilesFolderPath } = fileUtilModules
|
const { getFilesFolder } = fileUtilModules
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const user = {
|
const user = {
|
||||||
@@ -157,10 +157,10 @@ describe('drive', () => {
|
|||||||
expect(res.text).toEqual(
|
expect(res.text).toEqual(
|
||||||
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
||||||
)
|
)
|
||||||
await expect(folderExists(getTmpFilesFolderPath())).resolves.toEqual(true)
|
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
|
||||||
|
|
||||||
const testJobFolder = path.join(
|
const testJobFolder = path.join(
|
||||||
getTmpFilesFolderPath(),
|
getFilesFolder(),
|
||||||
'public',
|
'public',
|
||||||
'jobs',
|
'jobs',
|
||||||
'extract'
|
'extract'
|
||||||
@@ -174,7 +174,7 @@ describe('drive', () => {
|
|||||||
|
|
||||||
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
||||||
|
|
||||||
await deleteFolder(path.join(getTmpFilesFolderPath(), 'public'))
|
await deleteFolder(path.join(getFilesFolder(), 'public'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ describe('drive', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should get a SAS folder on drive having _folderPath as query param', async () => {
|
it('should get a SAS folder on drive having _folderPath as query param', async () => {
|
||||||
const pathToDrive = fileUtilModules.getTmpFilesFolderPath()
|
const pathToDrive = fileUtilModules.getFilesFolder()
|
||||||
|
|
||||||
const dirLevel1 = 'level1'
|
const dirLevel1 = 'level1'
|
||||||
const dirLevel2 = 'level2'
|
const dirLevel2 = 'level2'
|
||||||
@@ -267,10 +267,7 @@ describe('drive', () => {
|
|||||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const filePath = '/my/path/code.sas'
|
const filePath = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
|
||||||
fileUtilModules.getTmpFilesFolderPath(),
|
|
||||||
filePath
|
|
||||||
)
|
|
||||||
await copy(fileToCopyPath, pathToCopy)
|
await copy(fileToCopyPath, pathToCopy)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
@@ -333,7 +330,7 @@ describe('drive', () => {
|
|||||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(
|
||||||
fileUtilModules.getTmpFilesFolderPath(),
|
fileUtilModules.getFilesFolder(),
|
||||||
pathToUpload
|
pathToUpload
|
||||||
)
|
)
|
||||||
await copy(fileToAttachPath, pathToCopy)
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
@@ -445,7 +442,7 @@ describe('drive', () => {
|
|||||||
const pathToUpload = '/my/path/code.sas'
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(
|
||||||
fileUtilModules.getTmpFilesFolderPath(),
|
fileUtilModules.getFilesFolder(),
|
||||||
pathToUpload
|
pathToUpload
|
||||||
)
|
)
|
||||||
await copy(fileToAttachPath, pathToCopy)
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
@@ -467,7 +464,7 @@ describe('drive', () => {
|
|||||||
const pathToUpload = '/my/path/code.sas'
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(
|
||||||
fileUtilModules.getTmpFilesFolderPath(),
|
fileUtilModules.getFilesFolder(),
|
||||||
pathToUpload
|
pathToUpload
|
||||||
)
|
)
|
||||||
await copy(fileToAttachPath, pathToCopy)
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
@@ -603,10 +600,7 @@ describe('drive', () => {
|
|||||||
const fileToCopyContent = await readFile(fileToCopyPath)
|
const fileToCopyContent = await readFile(fileToCopyPath)
|
||||||
const filePath = '/my/path/code.sas'
|
const filePath = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
|
||||||
fileUtilModules.getTmpFilesFolderPath(),
|
|
||||||
filePath
|
|
||||||
)
|
|
||||||
await copy(fileToCopyPath, pathToCopy)
|
await copy(fileToCopyPath, pathToCopy)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
|
|||||||
@@ -3,23 +3,24 @@ import mongoose, { Mongoose } from 'mongoose'
|
|||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import { UserController, GroupController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
displayName: 'Test Admin',
|
displayName: 'Test Admin',
|
||||||
username: 'testAdminUsername',
|
username: 'testadminusername',
|
||||||
password: '12345678',
|
password: '12345678',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
const user = {
|
const user = {
|
||||||
displayName: 'Test User',
|
displayName: 'Test User',
|
||||||
username: 'testUsername',
|
username: 'testusername',
|
||||||
password: '87654321',
|
password: '87654321',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
autoExec: 'some sas code for auto exec;'
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
@@ -64,6 +65,21 @@ describe('user', () => {
|
|||||||
expect(res.body.displayName).toEqual(user.displayName)
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with new user having username as lowercase', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/user')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({ ...user, username: user.username.toUpperCase() })
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
@@ -242,7 +258,7 @@ describe('user', () => {
|
|||||||
const dbUser1 = await controller.createUser(user)
|
const dbUser1 = await controller.createUser(user)
|
||||||
const dbUser2 = await controller.createUser({
|
const dbUser2 = await controller.createUser({
|
||||||
...user,
|
...user,
|
||||||
username: 'randomUser'
|
username: 'randomuser'
|
||||||
})
|
})
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
@@ -254,6 +270,102 @@ describe('user', () => {
|
|||||||
expect(res.text).toEqual('Error: Username already exists.')
|
expect(res.text).toEqual('Error: Username already exists.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('by username', () => {
|
||||||
|
it('should respond with updated user when admin user requests', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const newDisplayName = 'My new display Name'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/SASjsApi/user/by/username/${user.username}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({ ...user, displayName: newDisplayName })
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(newDisplayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with updated user when user himself requests', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
const newDisplayName = 'My new display Name'
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/SASjsApi/user/by/username/${user.username}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({
|
||||||
|
displayName: newDisplayName,
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(newDisplayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
const newDisplayName = 'My new display Name'
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(`/SASjsApi/user/by/username/${user.username}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ ...user, displayName: newDisplayName })
|
||||||
|
.expect(400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/user/by/username/1234')
|
||||||
|
.send(user)
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
|
||||||
|
const dbUser1 = await controller.createUser(user)
|
||||||
|
const dbUser2 = await controller.createUser({
|
||||||
|
...user,
|
||||||
|
username: 'randomUser'
|
||||||
|
})
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser2.id)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send(user)
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Admin account required')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if username is already present', async () => {
|
||||||
|
const dbUser1 = await controller.createUser(user)
|
||||||
|
const dbUser2 = await controller.createUser({
|
||||||
|
...user,
|
||||||
|
username: 'randomuser'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({ username: dbUser2.username })
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Username already exists.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
@@ -347,6 +459,89 @@ describe('user', () => {
|
|||||||
expect(res.text).toEqual('Error: Invalid password.')
|
expect(res.text).toEqual('Error: Invalid password.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('by username', () => {
|
||||||
|
it('should respond with OK when admin user requests', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with OK when user himself requests', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ password: user.password })
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request when user himself requests and password is missing', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"password" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized when access token is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.delete('/SASjsApi/user/by/username/RandomUsername')
|
||||||
|
.send(user)
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
|
||||||
|
const dbUser1 = await controller.createUser(user)
|
||||||
|
const dbUser2 = await controller.createUser({
|
||||||
|
...user,
|
||||||
|
username: 'randomUser'
|
||||||
|
})
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser2.id)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send(user)
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Admin account required')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send({ password: 'incorrectpassword' })
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Invalid password.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
@@ -360,7 +555,26 @@ describe('user', () => {
|
|||||||
await deleteAllUsers()
|
await deleteAllUsers()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with user', async () => {
|
it('should respond with user autoExec when same user requests', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const userId = dbUser.id
|
||||||
|
const accessToken = await generateAndSaveToken(userId)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get(`/SASjsApi/user/${userId}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
|
expect(res.body.groups).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with user autoExec when admin user requests', async () => {
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
const userId = dbUser.id
|
const userId = dbUser.id
|
||||||
|
|
||||||
@@ -374,6 +588,8 @@ describe('user', () => {
|
|||||||
expect(res.body.displayName).toEqual(user.displayName)
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
|
expect(res.body.groups).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with user when access token is not of an admin account', async () => {
|
it('should respond with user when access token is not of an admin account', async () => {
|
||||||
@@ -395,6 +611,35 @@ describe('user', () => {
|
|||||||
expect(res.body.displayName).toEqual(user.displayName)
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toBeUndefined()
|
||||||
|
expect(res.body.groups).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with user along with associated groups', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const userId = dbUser.id
|
||||||
|
const accessToken = await generateAndSaveToken(userId)
|
||||||
|
|
||||||
|
const group = {
|
||||||
|
name: 'DCGroup1',
|
||||||
|
description: 'DC group for testing purposes.'
|
||||||
|
}
|
||||||
|
const groupController = new GroupController()
|
||||||
|
const dbGroup = await groupController.createGroup(group)
|
||||||
|
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get(`/SASjsApi/user/${userId}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
|
expect(res.body.groups.length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
@@ -419,6 +664,86 @@ describe('user', () => {
|
|||||||
expect(res.text).toEqual('Error: User is not found.')
|
expect(res.text).toEqual('Error: User is not found.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('by username', () => {
|
||||||
|
it('should respond with user autoExec when same user requests', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const userId = dbUser.id
|
||||||
|
const accessToken = await generateAndSaveToken(userId)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with user autoExec when admin user requests', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with user when access token is not of an admin account', async () => {
|
||||||
|
const accessToken = await generateSaveTokenAndCreateUser({
|
||||||
|
...user,
|
||||||
|
username: 'randomUser'
|
||||||
|
})
|
||||||
|
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/SASjsApi/user/by/username/randomUsername')
|
||||||
|
.send()
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if username is incorrect', async () => {
|
||||||
|
await controller.createUser(user)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/SASjsApi/user/by/username/randomUsername')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: User is not found.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('getAll', () => {
|
describe('getAll', () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const clientSecret = 'someclientSecret'
|
|||||||
const user = {
|
const user = {
|
||||||
id: 1234,
|
id: 1234,
|
||||||
displayName: 'Test User',
|
displayName: 'Test User',
|
||||||
username: 'testUsername',
|
username: 'testusername',
|
||||||
password: '87654321',
|
password: '87654321',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isActive: true
|
isActive: true
|
||||||
@@ -77,6 +77,7 @@ describe('web', () => {
|
|||||||
|
|
||||||
expect(res.body.loggedIn).toBeTruthy()
|
expect(res.body.loggedIn).toBeTruthy()
|
||||||
expect(res.body.user).toEqual({
|
expect(res.body.user).toEqual({
|
||||||
|
id: expect.any(Number),
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName
|
displayName: user.displayName
|
||||||
})
|
})
|
||||||
@@ -155,7 +156,6 @@ const getCSRF = async (app: Express) => {
|
|||||||
const { header } = await request(app).get('/')
|
const { header } = await request(app).get('/')
|
||||||
const cookies = header['set-cookie'].join()
|
const cookies = header['set-cookie'].join()
|
||||||
|
|
||||||
console.log('cookies', cookies)
|
|
||||||
const csrfToken = extractCSRF(cookies)
|
const csrfToken = extractCSRF(cookies)
|
||||||
return { csrfToken, cookies }
|
return { csrfToken, cookies }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ stpRouter.post(
|
|||||||
'/execute',
|
'/execute',
|
||||||
fileUploadController.preUploadMiddleware,
|
fileUploadController.preUploadMiddleware,
|
||||||
fileUploadController.getMulterUploadObject().any(),
|
fileUploadController.getMulterUploadObject().any(),
|
||||||
async (req: any, res: any) => {
|
async (req, res: any) => {
|
||||||
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
||||||
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
||||||
|
|
||||||
@@ -47,10 +47,11 @@ stpRouter.post(
|
|||||||
query?._program
|
query?._program
|
||||||
)
|
)
|
||||||
|
|
||||||
if (response instanceof Buffer) {
|
// TODO: investigate if this code is required
|
||||||
res.writeHead(200, (req as any).sasHeaders)
|
// if (response instanceof Buffer) {
|
||||||
return res.end(response)
|
// res.writeHead(200, (req as any).sasHeaders)
|
||||||
}
|
// return res.end(response)
|
||||||
|
// }
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
import {
|
import {
|
||||||
deleteUserValidation,
|
deleteUserValidation,
|
||||||
|
getUserValidation,
|
||||||
registerUserValidation,
|
registerUserValidation,
|
||||||
updateUserValidation
|
updateUserValidation
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@@ -36,12 +37,31 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
|
userRouter.get(
|
||||||
|
'/by/username/:username',
|
||||||
|
authenticateAccessToken,
|
||||||
|
async (req, res) => {
|
||||||
|
const { error, value: params } = getUserValidation(req.params)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
const { username } = params
|
||||||
|
|
||||||
|
const controller = new UserController()
|
||||||
|
try {
|
||||||
|
const response = await controller.getUserByUsername(req, username)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.getUser(userId)
|
const response = await controller.getUser(req, parseInt(userId))
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -49,20 +69,26 @@ userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
userRouter.patch(
|
userRouter.patch(
|
||||||
'/:userId',
|
'/by/username/:username',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdminIfNeeded,
|
verifyAdminIfNeeded,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
const { userId } = req.params
|
const { error: errorUsername, value: params } = getUserValidation(
|
||||||
|
req.params
|
||||||
|
)
|
||||||
|
if (errorUsername)
|
||||||
|
return res.status(400).send(errorUsername.details[0].message)
|
||||||
|
|
||||||
|
const { username } = params
|
||||||
|
|
||||||
// only an admin can update `isActive` and `isAdmin` fields
|
// only an admin can update `isActive` and `isAdmin` fields
|
||||||
const { error, value: body } = updateUserValidation(req.body, user.isAdmin)
|
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.updateUser(userId, body)
|
const response = await controller.updateUserByUsername(username, body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -70,21 +96,71 @@ userRouter.patch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
userRouter.patch(
|
||||||
|
'/:userId',
|
||||||
|
authenticateAccessToken,
|
||||||
|
verifyAdminIfNeeded,
|
||||||
|
async (req, res) => {
|
||||||
|
const { user } = req
|
||||||
|
const { userId } = req.params
|
||||||
|
|
||||||
|
// only an admin can update `isActive` and `isAdmin` fields
|
||||||
|
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
const controller = new UserController()
|
||||||
|
try {
|
||||||
|
const response = await controller.updateUser(parseInt(userId), body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
userRouter.delete(
|
||||||
|
'/by/username/:username',
|
||||||
|
authenticateAccessToken,
|
||||||
|
verifyAdminIfNeeded,
|
||||||
|
async (req, res) => {
|
||||||
|
const { user } = req
|
||||||
|
const { error: errorUsername, value: params } = getUserValidation(
|
||||||
|
req.params
|
||||||
|
)
|
||||||
|
if (errorUsername)
|
||||||
|
return res.status(400).send(errorUsername.details[0].message)
|
||||||
|
|
||||||
|
const { username } = params
|
||||||
|
|
||||||
|
// only an admin can delete user without providing password
|
||||||
|
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
const controller = new UserController()
|
||||||
|
try {
|
||||||
|
await controller.deleteUserByUsername(username, data, user!.isAdmin)
|
||||||
|
res.status(200).send('Account Deleted!')
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
userRouter.delete(
|
userRouter.delete(
|
||||||
'/:userId',
|
'/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdminIfNeeded,
|
verifyAdminIfNeeded,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
|
|
||||||
// only an admin can delete user without providing password
|
// only an admin can delete user without providing password
|
||||||
const { error, value: data } = deleteUserValidation(req.body, user.isAdmin)
|
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
try {
|
try {
|
||||||
await controller.deleteUser(userId, data, user.isAdmin)
|
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
|
||||||
res.status(200).send('Account Deleted!')
|
res.status(200).send('Account Deleted!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express from 'express'
|
import express, { Request } from 'express'
|
||||||
import { folderExists } from '@sasjs/utils'
|
import { folderExists } from '@sasjs/utils'
|
||||||
|
|
||||||
import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
|
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
||||||
import { appStreamHtml } from './appStreamHtml'
|
import { appStreamHtml } from './appStreamHtml'
|
||||||
|
|
||||||
|
const appStreams: { [key: string]: string } = {}
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
@@ -22,7 +24,7 @@ export const publishAppStream = async (
|
|||||||
streamLogo?: string,
|
streamLogo?: string,
|
||||||
addEntryToFile: boolean = true
|
addEntryToFile: boolean = true
|
||||||
) => {
|
) => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
||||||
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
||||||
@@ -44,7 +46,7 @@ export const publishAppStream = async (
|
|||||||
streamServiceName = `AppStreamName${appCount + 1}`
|
streamServiceName = `AppStreamName${appCount + 1}`
|
||||||
}
|
}
|
||||||
|
|
||||||
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
|
appStreams[streamServiceName] = pathToDeployment
|
||||||
|
|
||||||
addEntryToAppStreamConfig(
|
addEntryToAppStreamConfig(
|
||||||
streamServiceName,
|
streamServiceName,
|
||||||
@@ -64,4 +66,26 @@ export const publishAppStream = async (
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.get(`/*`, function (req: Request, res, next) {
|
||||||
|
const reqPath = req.path.replace(/^\//, '')
|
||||||
|
|
||||||
|
// Redirecting to url with trailing slash for appStream base URL only
|
||||||
|
if (reqPath.split('/').length === 1 && !reqPath.endsWith('/'))
|
||||||
|
// navigating to same url with slash at start
|
||||||
|
return res.redirect(301, `${reqPath}/`)
|
||||||
|
|
||||||
|
const appStream = reqPath.split('/')[0]
|
||||||
|
const appStreamFilesPath = appStreams[appStream]
|
||||||
|
if (appStreamFilesPath) {
|
||||||
|
// resourcePath is without appStream base path
|
||||||
|
const resourcePath = reqPath.split('/').slice(1).join('/') || 'index.html'
|
||||||
|
|
||||||
|
req.url = resourcePath
|
||||||
|
|
||||||
|
return express.static(appStreamFilesPath)(req, res, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send("There's no App Stream available here.")
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { WebController } from '../../controllers/web'
|
import { WebController } from '../../controllers/web'
|
||||||
import { authenticateAccessToken } from '../../middlewares'
|
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
|
||||||
import { authorizeValidation, loginWebValidation } from '../../utils'
|
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||||
|
|
||||||
const webRouter = express.Router()
|
const webRouter = express.Router()
|
||||||
@@ -19,7 +19,7 @@ webRouter.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
webRouter.post('/SASLogon/login', async (req, res) => {
|
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
|
||||||
const { error, value: body } = loginWebValidation(req.body)
|
const { error, value: body } = loginWebValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ webRouter.post('/SASLogon/login', async (req, res) => {
|
|||||||
|
|
||||||
webRouter.post(
|
webRouter.post(
|
||||||
'/SASLogon/authorize',
|
'/SASLogon/authorize',
|
||||||
|
desktopRestrict,
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const { error, value: body } = authorizeValidation(req.body)
|
const { error, value: body } = authorizeValidation(req.body)
|
||||||
@@ -47,7 +48,7 @@ webRouter.post(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
webRouter.get('/logout', async (req, res) => {
|
webRouter.get('/logout', desktopRestrict, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await controller.logout(req)
|
await controller.logout(req)
|
||||||
res.status(200).send('OK!')
|
res.status(200).send('OK!')
|
||||||
|
|||||||
9
api/src/types/RequestUser.ts
Normal file
9
api/src/types/RequestUser.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface RequestUser {
|
||||||
|
userId: number
|
||||||
|
clientId: string
|
||||||
|
username: string
|
||||||
|
displayName: string
|
||||||
|
isAdmin: boolean
|
||||||
|
isActive: boolean
|
||||||
|
autoExec?: string
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@ export * from './InfoJWT'
|
|||||||
export * from './PreProgramVars'
|
export * from './PreProgramVars'
|
||||||
export * from './Session'
|
export * from './Session'
|
||||||
export * from './TreeNode'
|
export * from './TreeNode'
|
||||||
|
export * from './RequestUser'
|
||||||
|
|||||||
9
api/src/types/system/express-session.d.ts
vendored
9
api/src/types/system/express-session.d.ts
vendored
@@ -2,13 +2,6 @@ import express from 'express'
|
|||||||
declare module 'express-session' {
|
declare module 'express-session' {
|
||||||
interface SessionData {
|
interface SessionData {
|
||||||
loggedIn: boolean
|
loggedIn: boolean
|
||||||
user: {
|
user: import('../').RequestUser
|
||||||
userId: number
|
|
||||||
clientId: string
|
|
||||||
username: string
|
|
||||||
displayName: string
|
|
||||||
isAdmin: boolean
|
|
||||||
isActive: boolean
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
api/src/types/system/express.d.ts
vendored
Normal file
7
api/src/types/system/express.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
export interface Request {
|
||||||
|
accessToken?: string
|
||||||
|
user?: import('../').RequestUser
|
||||||
|
sasSession?: import('../').Session
|
||||||
|
}
|
||||||
|
}
|
||||||
1
api/src/types/system/process.d.ts
vendored
1
api/src/types/system/process.d.ts
vendored
@@ -4,5 +4,6 @@ declare namespace NodeJS {
|
|||||||
driveLoc: string
|
driveLoc: string
|
||||||
sessionController?: import('../../controllers/internal').SessionController
|
sessionController?: import('../../controllers/internal').SessionController
|
||||||
appStreamConfig: import('../').AppStreamConfig
|
appStreamConfig: import('../').AppStreamConfig
|
||||||
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { createFile, fileExists, readFile } from '@sasjs/utils'
|
|||||||
import { publishAppStream } from '../routes/appStream'
|
import { publishAppStream } from '../routes/appStream'
|
||||||
import { AppStreamConfig } from '../types'
|
import { AppStreamConfig } from '../types'
|
||||||
|
|
||||||
import { getTmpAppStreamConfigPath } from './file'
|
import { getAppStreamConfigPath } from './file'
|
||||||
|
|
||||||
export const loadAppStreamConfig = async () => {
|
export const loadAppStreamConfig = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
const appStreamConfigPath = getAppStreamConfigPath()
|
||||||
|
|
||||||
const content = (await fileExists(appStreamConfigPath))
|
const content = (await fileExists(appStreamConfigPath))
|
||||||
? await readFile(appStreamConfigPath)
|
? await readFile(appStreamConfigPath)
|
||||||
@@ -63,7 +63,7 @@ export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saveAppStreamConfig = async () => {
|
const saveAppStreamConfig = async () => {
|
||||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
const appStreamConfigPath = getAppStreamConfigPath()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createFile(
|
await createFile(
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import {
|
|||||||
readFile
|
readFile
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||||
|
|
||||||
export const copySASjsCore = async () => {
|
export const copySASjsCore = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
console.log('Copying Macros from container to drive(tmp).')
|
console.log('Copying Macros from container to drive(tmp).')
|
||||||
|
|
||||||
const macrosDrivePath = getTmpMacrosPath()
|
const macrosDrivePath = getMacrosFolder()
|
||||||
|
|
||||||
await deleteFolder(macrosDrivePath)
|
await deleteFolder(macrosDrivePath)
|
||||||
await createFolder(macrosDrivePath)
|
await createFolder(macrosDrivePath)
|
||||||
|
|||||||
8
api/src/utils/desktopAutoExec.ts
Normal file
8
api/src/utils/desktopAutoExec.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createFile, readFile } from '@sasjs/utils'
|
||||||
|
import { getDesktopUserAutoExecPath } from './file'
|
||||||
|
|
||||||
|
export const getUserAutoExec = async (): Promise<string> =>
|
||||||
|
readFile(getDesktopUserAutoExecPath())
|
||||||
|
|
||||||
|
export const updateUserAutoExec = async (autoExecContent: string) =>
|
||||||
|
createFile(getDesktopUserAutoExecPath(), autoExecContent)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { homedir } from 'os'
|
||||||
|
|
||||||
export const apiRoot = path.join(__dirname, '..', '..')
|
export const apiRoot = path.join(__dirname, '..', '..')
|
||||||
export const codebaseRoot = path.join(apiRoot, '..')
|
export const codebaseRoot = path.join(apiRoot, '..')
|
||||||
@@ -11,28 +12,31 @@ export const sysInitCompiledPath = path.join(
|
|||||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||||
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||||
|
|
||||||
export const getWebBuildFolderPath = () =>
|
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
|
||||||
path.join(codebaseRoot, 'web', 'build')
|
|
||||||
|
|
||||||
export const getTmpFolderPath = () => process.driveLoc
|
export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
|
||||||
|
|
||||||
export const getTmpAppStreamConfigPath = () =>
|
export const getDesktopUserAutoExecPath = () =>
|
||||||
path.join(getTmpFolderPath(), 'appStreamConfig.json')
|
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
||||||
|
|
||||||
export const getTmpMacrosPath = () => path.join(getTmpFolderPath(), 'sasjscore')
|
export const getSasjsRootFolder = () => process.driveLoc
|
||||||
|
|
||||||
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
|
export const getAppStreamConfigPath = () =>
|
||||||
|
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
||||||
|
|
||||||
export const getTmpFilesFolderPath = () =>
|
export const getMacrosFolder = () =>
|
||||||
path.join(getTmpFolderPath(), 'files')
|
path.join(getSasjsRootFolder(), 'sasjscore')
|
||||||
|
|
||||||
export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs')
|
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
||||||
|
|
||||||
export const getTmpWeboutFolderPath = () =>
|
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
||||||
path.join(getTmpFolderPath(), 'webouts')
|
|
||||||
|
|
||||||
export const getTmpSessionsFolderPath = () =>
|
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
|
||||||
path.join(getTmpFolderPath(), 'sessions')
|
|
||||||
|
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||||
|
|
||||||
|
export const getSessionsFolder = () =>
|
||||||
|
path.join(getSasjsRootFolder(), 'sessions')
|
||||||
|
|
||||||
export const generateUniqueFileName = (fileName: string, extension = '') =>
|
export const generateUniqueFileName = (fileName: string, extension = '') =>
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
|||||||
const isWindows = () => process.platform === 'win32'
|
const isWindows = () => process.platform === 'win32'
|
||||||
|
|
||||||
export const getDesktopFields = async () => {
|
export const getDesktopFields = async () => {
|
||||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
const { SAS_PATH } = process.env
|
||||||
|
|
||||||
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||||
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
||||||
|
|
||||||
return { sasLoc, driveLoc }
|
return { sasLoc }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDriveLocation = async (): Promise<string> => {
|
const getDriveLocation = async (): Promise<string> => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { Request } from 'express'
|
||||||
import { PreProgramVars } from '../types'
|
import { PreProgramVars } from '../types'
|
||||||
|
|
||||||
export const getPreProgramVariables = (req: any): PreProgramVars => {
|
export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
||||||
const host = req.get('host')
|
const host = req.get('host')
|
||||||
const protocol = req.protocol + '://'
|
const protocol = req.protocol + '://'
|
||||||
const { user, accessToken } = req
|
const { user, accessToken } = req
|
||||||
@@ -20,9 +21,9 @@ export const getPreProgramVariables = (req: any): PreProgramVars => {
|
|||||||
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
username: user.username,
|
username: user!.username,
|
||||||
userId: user.userId,
|
userId: user!.userId,
|
||||||
displayName: user.displayName,
|
displayName: user!.displayName,
|
||||||
serverUrl: protocol + host,
|
serverUrl: protocol + host,
|
||||||
httpHeaders
|
httpHeaders
|
||||||
}
|
}
|
||||||
|
|||||||
15
api/src/utils/getServerUrl.ts
Normal file
15
api/src/utils/getServerUrl.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import url from 'url'
|
||||||
|
|
||||||
|
export const getFullUrl = (req: express.Request) =>
|
||||||
|
url.format({
|
||||||
|
protocol: req.protocol,
|
||||||
|
host: req.get('host'),
|
||||||
|
pathname: req.originalUrl
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getServerUrl = (req: express.Request) =>
|
||||||
|
url.format({
|
||||||
|
protocol: req.protocol,
|
||||||
|
host: req.get('x-forwarded-host') || req.get('host')
|
||||||
|
})
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './appStreamConfig'
|
export * from './appStreamConfig'
|
||||||
export * from './connectDB'
|
export * from './connectDB'
|
||||||
export * from './copySASjsCore'
|
export * from './copySASjsCore'
|
||||||
|
export * from './desktopAutoExec'
|
||||||
export * from './extractHeaders'
|
export * from './extractHeaders'
|
||||||
export * from './file'
|
export * from './file'
|
||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
@@ -9,6 +10,8 @@ export * from './generateRefreshToken'
|
|||||||
export * from './getCertificates'
|
export * from './getCertificates'
|
||||||
export * from './getDesktopFields'
|
export * from './getDesktopFields'
|
||||||
export * from './getPreProgramVariables'
|
export * from './getPreProgramVariables'
|
||||||
|
export * from './getServerUrl'
|
||||||
|
export * from './instantiateLogger'
|
||||||
export * from './isDebugOn'
|
export * from './isDebugOn'
|
||||||
export * from './parseLogToArray'
|
export * from './parseLogToArray'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
@@ -18,4 +21,5 @@ export * from './setProcessVariables'
|
|||||||
export * from './setupFolders'
|
export * from './setupFolders'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
|
export * from './verifyEnvVariables'
|
||||||
export * from './verifyTokenInDB'
|
export * from './verifyTokenInDB'
|
||||||
|
|||||||
7
api/src/utils/instantiateLogger.ts
Normal file
7
api/src/utils/instantiateLogger.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { LogLevel, Logger } from '@sasjs/utils/logger'
|
||||||
|
|
||||||
|
export const instantiateLogger = () => {
|
||||||
|
const logLevel = (process.env.LOG_LEVEL || LogLevel.Info) as LogLevel
|
||||||
|
const logger = new Logger(logLevel)
|
||||||
|
process.logger = logger
|
||||||
|
}
|
||||||
@@ -1,30 +1,29 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getAbsolutePath, getRealPath } from '@sasjs/utils'
|
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||||
|
|
||||||
import { configuration } from '../../package.json'
|
import { getDesktopFields, ModeType } from '.'
|
||||||
import { getDesktopFields } from '.'
|
|
||||||
|
|
||||||
export const setProcessVariables = async () => {
|
export const setProcessVariables = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'tmp')
|
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
|
|
||||||
if (MODE?.trim() === 'server') {
|
if (MODE === ModeType.Server) {
|
||||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
process.sasLoc = process.env.SAS_PATH as string
|
||||||
|
|
||||||
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
|
||||||
const absPath = getAbsolutePath(DRIVE_PATH ?? 'tmp', process.cwd())
|
|
||||||
process.driveLoc = getRealPath(absPath)
|
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc, driveLoc } = await getDesktopFields()
|
const { sasLoc } = await getDesktopFields()
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
process.sasLoc = sasLoc
|
||||||
process.driveLoc = driveLoc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { SASJS_ROOT } = process.env
|
||||||
|
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||||
|
await createFolder(absPath)
|
||||||
|
process.driveLoc = getRealPath(absPath)
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { createFolder } from '@sasjs/utils'
|
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
||||||
import { getTmpFilesFolderPath } from './file'
|
import { getDesktopUserAutoExecPath, getFilesFolder } from './file'
|
||||||
|
import { ModeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
export const setupFolders = async () => {
|
export const setupFolders = async () => {
|
||||||
const drivePath = getTmpFilesFolderPath()
|
const drivePath = getFilesFolder()
|
||||||
await createFolder(drivePath)
|
await createFolder(drivePath)
|
||||||
|
|
||||||
|
if (process.env.MODE === ModeType.Desktop) {
|
||||||
|
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
||||||
|
await createFile(getDesktopUserAutoExecPath(), '')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import Joi from 'joi'
|
import Joi from 'joi'
|
||||||
|
|
||||||
const usernameSchema = Joi.string().alphanum().min(3).max(16)
|
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||||
const passwordSchema = Joi.string().min(6).max(1024)
|
const passwordSchema = Joi.string().min(6).max(1024)
|
||||||
|
|
||||||
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
|
export const getUserValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
username: usernameSchema.required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
export const loginWebValidation = (data: any): Joi.ValidationResult =>
|
export const loginWebValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
username: usernameSchema.required(),
|
username: usernameSchema.required(),
|
||||||
@@ -35,7 +40,8 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
|
|||||||
username: usernameSchema.required(),
|
username: usernameSchema.required(),
|
||||||
password: passwordSchema.required(),
|
password: passwordSchema.required(),
|
||||||
isAdmin: Joi.boolean(),
|
isAdmin: Joi.boolean(),
|
||||||
isActive: Joi.boolean()
|
isActive: Joi.boolean(),
|
||||||
|
autoExec: Joi.string().allow('')
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const deleteUserValidation = (
|
export const deleteUserValidation = (
|
||||||
@@ -57,7 +63,8 @@ export const updateUserValidation = (
|
|||||||
const validationChecks: any = {
|
const validationChecks: any = {
|
||||||
displayName: Joi.string().min(6),
|
displayName: Joi.string().min(6),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
password: passwordSchema
|
password: passwordSchema,
|
||||||
|
autoExec: Joi.string().allow('')
|
||||||
}
|
}
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
validationChecks.isAdmin = Joi.boolean()
|
validationChecks.isAdmin = Joi.boolean()
|
||||||
|
|||||||
211
api/src/utils/verifyEnvVariables.ts
Normal file
211
api/src/utils/verifyEnvVariables.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
export enum ModeType {
|
||||||
|
Server = 'server',
|
||||||
|
Desktop = 'desktop'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProtocolType {
|
||||||
|
HTTP = 'http',
|
||||||
|
HTTPS = 'https'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CorsType {
|
||||||
|
ENABLED = 'enable',
|
||||||
|
DISABLED = 'disable'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HelmetCoepType {
|
||||||
|
TRUE = 'true',
|
||||||
|
FALSE = 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LOG_FORMAT_MORGANType {
|
||||||
|
Combined = 'combined',
|
||||||
|
Common = 'common',
|
||||||
|
Dev = 'dev',
|
||||||
|
Short = 'short',
|
||||||
|
tiny = 'tiny'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ReturnCode {
|
||||||
|
Success,
|
||||||
|
InvalidEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyEnvVariables = (): ReturnCode => {
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
errors.push(...verifyMODE())
|
||||||
|
|
||||||
|
errors.push(...verifyPROTOCOL())
|
||||||
|
|
||||||
|
errors.push(...verifyPORT())
|
||||||
|
|
||||||
|
errors.push(...verifyCORS())
|
||||||
|
|
||||||
|
errors.push(...verifyHELMET_COEP())
|
||||||
|
|
||||||
|
errors.push(...verifyLOG_FORMAT_MORGAN())
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
process.logger?.error(
|
||||||
|
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
||||||
|
)
|
||||||
|
return ReturnCode.InvalidEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnCode.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyMODE = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { MODE } = process.env
|
||||||
|
|
||||||
|
if (MODE) {
|
||||||
|
const modeTypes = Object.values(ModeType)
|
||||||
|
if (!modeTypes.includes(MODE as ModeType))
|
||||||
|
errors.push(`- MODE '${MODE}'\n - valid options ${modeTypes}`)
|
||||||
|
} else {
|
||||||
|
process.env.MODE = DEFAULTS.MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.MODE === ModeType.Server) {
|
||||||
|
const {
|
||||||
|
ACCESS_TOKEN_SECRET,
|
||||||
|
REFRESH_TOKEN_SECRET,
|
||||||
|
AUTH_CODE_SECRET,
|
||||||
|
SESSION_SECRET,
|
||||||
|
DB_CONNECT
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
if (!ACCESS_TOKEN_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!REFRESH_TOKEN_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!AUTH_CODE_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!SESSION_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test')
|
||||||
|
if (!DB_CONNECT)
|
||||||
|
errors.push(
|
||||||
|
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyPROTOCOL = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { PROTOCOL } = process.env
|
||||||
|
|
||||||
|
if (PROTOCOL) {
|
||||||
|
const protocolTypes = Object.values(ProtocolType)
|
||||||
|
if (!protocolTypes.includes(PROTOCOL as ProtocolType))
|
||||||
|
errors.push(`- PROTOCOL '${PROTOCOL}'\n - valid options ${protocolTypes}`)
|
||||||
|
} else {
|
||||||
|
process.env.PROTOCOL = DEFAULTS.PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
|
||||||
|
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
||||||
|
|
||||||
|
if (!PRIVATE_KEY)
|
||||||
|
errors.push(
|
||||||
|
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!FULL_CHAIN)
|
||||||
|
errors.push(
|
||||||
|
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyCORS = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { CORS } = process.env
|
||||||
|
|
||||||
|
if (CORS) {
|
||||||
|
const corsTypes = Object.values(CorsType)
|
||||||
|
if (!corsTypes.includes(CORS as CorsType))
|
||||||
|
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
|
||||||
|
} else {
|
||||||
|
const { MODE } = process.env
|
||||||
|
process.env.CORS =
|
||||||
|
MODE === ModeType.Server ? CorsType.DISABLED : CorsType.ENABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyPORT = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { PORT } = process.env
|
||||||
|
|
||||||
|
if (PORT) {
|
||||||
|
if (Number.isNaN(parseInt(PORT)))
|
||||||
|
errors.push(`- PORT '${PORT}'\n - should be a valid number`)
|
||||||
|
} else {
|
||||||
|
process.env.PORT = DEFAULTS.PORT
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyHELMET_COEP = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { HELMET_COEP } = process.env
|
||||||
|
|
||||||
|
if (HELMET_COEP) {
|
||||||
|
const helmetCoepTypes = Object.values(HelmetCoepType)
|
||||||
|
if (!helmetCoepTypes.includes(HELMET_COEP as HelmetCoepType))
|
||||||
|
errors.push(
|
||||||
|
`- HELMET_COEP '${HELMET_COEP}'\n - valid options ${helmetCoepTypes}`
|
||||||
|
)
|
||||||
|
HELMET_COEP
|
||||||
|
} else {
|
||||||
|
process.env.HELMET_COEP = DEFAULTS.HELMET_COEP
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyLOG_FORMAT_MORGAN = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { LOG_FORMAT_MORGAN } = process.env
|
||||||
|
|
||||||
|
if (LOG_FORMAT_MORGAN) {
|
||||||
|
const logFormatMorganTypes = Object.values(LOG_FORMAT_MORGANType)
|
||||||
|
if (
|
||||||
|
!logFormatMorganTypes.includes(LOG_FORMAT_MORGAN as LOG_FORMAT_MORGANType)
|
||||||
|
)
|
||||||
|
errors.push(
|
||||||
|
`- LOG_FORMAT_MORGAN '${LOG_FORMAT_MORGAN}'\n - valid options ${logFormatMorganTypes}`
|
||||||
|
)
|
||||||
|
LOG_FORMAT_MORGAN
|
||||||
|
} else {
|
||||||
|
process.env.LOG_FORMAT_MORGAN = DEFAULTS.LOG_FORMAT_MORGAN
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
MODE: ModeType.Desktop,
|
||||||
|
PROTOCOL: ProtocolType.HTTP,
|
||||||
|
PORT: '5000',
|
||||||
|
HELMET_COEP: HelmetCoepType.TRUE,
|
||||||
|
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common
|
||||||
|
}
|
||||||
@@ -1,11 +1,30 @@
|
|||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
|
import { RequestUser } from '../types'
|
||||||
|
|
||||||
|
export const fetchLatestAutoExec = async (
|
||||||
|
reqUser: RequestUser
|
||||||
|
): Promise<RequestUser | undefined> => {
|
||||||
|
const dbUser = await User.findOne({ id: reqUser.userId })
|
||||||
|
|
||||||
|
if (!dbUser) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: reqUser.userId,
|
||||||
|
clientId: reqUser.clientId,
|
||||||
|
username: dbUser.username,
|
||||||
|
displayName: dbUser.displayName,
|
||||||
|
isAdmin: dbUser.isAdmin,
|
||||||
|
isActive: dbUser.isActive,
|
||||||
|
autoExec: dbUser.autoExec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const verifyTokenInDB = async (
|
export const verifyTokenInDB = async (
|
||||||
userId: number,
|
userId: number,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
token: string,
|
token: string,
|
||||||
tokenType: 'accessToken' | 'refreshToken'
|
tokenType: 'accessToken' | 'refreshToken'
|
||||||
) => {
|
): Promise<RequestUser | undefined> => {
|
||||||
const dbUser = await User.findOne({ id: userId })
|
const dbUser = await User.findOne({ id: userId })
|
||||||
|
|
||||||
if (!dbUser) return undefined
|
if (!dbUser) return undefined
|
||||||
@@ -21,7 +40,8 @@ export const verifyTokenInDB = async (
|
|||||||
username: dbUser.username,
|
username: dbUser.username,
|
||||||
displayName: dbUser.displayName,
|
displayName: dbUser.displayName,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isActive: dbUser.isActive
|
isActive: dbUser.isActive,
|
||||||
|
autoExec: dbUser.autoExec
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
10580
package-lock.json
generated
10580
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,6 @@
|
|||||||
"server": "npm run server:prepare && npm run server:start",
|
"server": "npm run server:prepare && npm run server:start",
|
||||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
|
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
|
||||||
"server:start": "cd api && npm run start:prod",
|
"server:start": "cd api && npm run start:prod",
|
||||||
"release": "standard-version",
|
|
||||||
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint-web:fix": "npx prettier --write \"web/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-web:fix": "npx prettier --write \"web/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
@@ -16,7 +15,9 @@
|
|||||||
"lint:fix": "npm run lint-api:fix && npm run lint-web:fix"
|
"lint:fix": "npm run lint-api:fix && npm run lint-web:fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.3.1",
|
"@semantic-release/changelog": "^6.0.1",
|
||||||
"standard-version": "^9.3.2"
|
"@semantic-release/exec": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"@semantic-release/github": "^8.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
web/package-lock.json
generated
90
web/package-lock.json
generated
@@ -24,9 +24,11 @@
|
|||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.33.0",
|
||||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-monaco-editor": "^0.48.0",
|
"react-monaco-editor": "^0.48.0",
|
||||||
"react-router-dom": "^5.3.0"
|
"react-router-dom": "^5.3.0",
|
||||||
|
"react-toastify": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.0",
|
"@babel/core": "^7.16.0",
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"@types/dotenv-webpack": "^7.0.3",
|
"@types/dotenv-webpack": "^7.0.3",
|
||||||
"@types/prismjs": "^1.16.6",
|
"@types/prismjs": "^1.16.6",
|
||||||
"@types/react": "^17.0.37",
|
"@types/react": "^17.0.37",
|
||||||
|
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router-dom": "^5.3.1",
|
"@types/react-router-dom": "^5.3.1",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
@@ -3296,6 +3299,15 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-copy-to-clipboard": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
"version": "17.0.11",
|
"version": "17.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
|
||||||
@@ -4863,6 +4875,14 @@
|
|||||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/copy-to-clipboard": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
|
||||||
|
"dependencies": {
|
||||||
|
"toggle-selection": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/copy-webpack-plugin": {
|
"node_modules/copy-webpack-plugin": {
|
||||||
"version": "10.2.4",
|
"version": "10.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz",
|
||||||
@@ -9299,6 +9319,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-copy-to-clipboard": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^15.3.0 || 16 || 17 || 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||||
@@ -9372,6 +9404,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-toastify": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-c2zeZHkCX+WXuItS/JRqQ/8CH8Qm/je+M0rt09xe9fnu5YPJigtNOdD8zX4fwLA093V2am3abkGfOowwpkrwOQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16",
|
||||||
|
"react-dom": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-transition-group": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
||||||
@@ -10335,6 +10379,11 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toggle-selection": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||||
@@ -13641,6 +13690,15 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-copy-to-clipboard": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-dom": {
|
"@types/react-dom": {
|
||||||
"version": "17.0.11",
|
"version": "17.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
|
||||||
@@ -14848,6 +14906,14 @@
|
|||||||
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"copy-to-clipboard": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
|
||||||
|
"requires": {
|
||||||
|
"toggle-selection": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"copy-webpack-plugin": {
|
"copy-webpack-plugin": {
|
||||||
"version": "10.2.4",
|
"version": "10.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz",
|
||||||
@@ -18162,6 +18228,15 @@
|
|||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-copy-to-clipboard": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
|
||||||
|
"requires": {
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||||
@@ -18223,6 +18298,14 @@
|
|||||||
"tiny-warning": "^1.0.0"
|
"tiny-warning": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-toastify": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-c2zeZHkCX+WXuItS/JRqQ/8CH8Qm/je+M0rt09xe9fnu5YPJigtNOdD8zX4fwLA093V2am3abkGfOowwpkrwOQ==",
|
||||||
|
"requires": {
|
||||||
|
"clsx": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-transition-group": {
|
"react-transition-group": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
|
||||||
@@ -18967,6 +19050,11 @@
|
|||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"toggle-selection": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
|
||||||
|
},
|
||||||
"toidentifier": {
|
"toidentifier": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||||
|
|||||||
@@ -23,9 +23,11 @@
|
|||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.33.0",
|
||||||
"monaco-editor-webpack-plugin": "^7.0.1",
|
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-monaco-editor": "^0.48.0",
|
"react-monaco-editor": "^0.48.0",
|
||||||
"react-router-dom": "^5.3.0"
|
"react-router-dom": "^5.3.0",
|
||||||
|
"react-toastify": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.0",
|
"@babel/core": "^7.16.0",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"@types/dotenv-webpack": "^7.0.3",
|
"@types/dotenv-webpack": "^7.0.3",
|
||||||
"@types/prismjs": "^1.16.6",
|
"@types/prismjs": "^1.16.6",
|
||||||
"@types/react": "^17.0.37",
|
"@types/react": "^17.0.37",
|
||||||
|
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router-dom": "^5.3.1",
|
"@types/react-router-dom": "^5.3.1",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import Header from './components/header'
|
|||||||
import Home from './components/home'
|
import Home from './components/home'
|
||||||
import Drive from './containers/Drive'
|
import Drive from './containers/Drive'
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
|
import Settings from './containers/Settings'
|
||||||
|
|
||||||
import { AppContext } from './context/appContext'
|
import { AppContext } from './context/appContext'
|
||||||
import AuthCode from './containers/AuthCode'
|
import AuthCode from './containers/AuthCode'
|
||||||
|
import { ToastContainer } from 'react-toastify'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
@@ -44,10 +46,14 @@ function App() {
|
|||||||
<Route exact path="/SASjsStudio">
|
<Route exact path="/SASjsStudio">
|
||||||
<Studio />
|
<Studio />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/SASjsSettings">
|
||||||
|
<Settings />
|
||||||
|
</Route>
|
||||||
<Route exact path="/SASjsLogon">
|
<Route exact path="/SASjsLogon">
|
||||||
<AuthCode />
|
<AuthCode />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
<ToastContainer />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useContext } from 'react'
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MenuItem
|
MenuItem
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings'
|
||||||
|
|
||||||
import Username from './username'
|
import Username from './username'
|
||||||
import { AppContext } from '../context/appContext'
|
import { AppContext } from '../context/appContext'
|
||||||
@@ -20,15 +21,23 @@ const PORT_API = process.env.PORT_API
|
|||||||
const baseUrl =
|
const baseUrl =
|
||||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||||
|
|
||||||
|
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
||||||
|
|
||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
const [tabValue, setTabValue] = useState(pathname)
|
const [tabValue, setTabValue] = useState(
|
||||||
|
validTabs.includes(pathname) ? pathname : '/'
|
||||||
|
)
|
||||||
const [anchorEl, setAnchorEl] = useState<
|
const [anchorEl, setAnchorEl] = useState<
|
||||||
(EventTarget & HTMLButtonElement) | null
|
(EventTarget & HTMLButtonElement) | null
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTabValue(validTabs.includes(pathname) ? pathname : '/')
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
const handleMenu = (
|
const handleMenu = (
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
) => {
|
) => {
|
||||||
@@ -44,7 +53,10 @@ const Header = (props: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
if (appContext.logout) appContext.logout()
|
if (appContext.logout) {
|
||||||
|
handleClose()
|
||||||
|
appContext.logout()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
@@ -132,6 +144,18 @@ const Header = (props: any) => {
|
|||||||
open={!!anchorEl}
|
open={!!anchorEl}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
>
|
>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/SASjsSettings"
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<SettingsIcon />}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||||
<Button variant="contained" color="primary">
|
<Button variant="contained" color="primary">
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ const Login = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
appContext.setLoggedIn?.(loggedIn)
|
appContext.setUserId?.(user.id)
|
||||||
appContext.setUsername?.(user.username)
|
appContext.setUsername?.(user.username)
|
||||||
appContext.setDisplayName?.(user.displayName)
|
appContext.setDisplayName?.(user.displayName)
|
||||||
|
appContext.setLoggedIn?.(loggedIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import 'react-toastify/dist/ReactToastify.css'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { CssBaseline, Box, Typography } from '@mui/material'
|
import { CssBaseline, Box, Typography, Button } from '@mui/material'
|
||||||
|
|
||||||
const getAuthCode = async (credentials: any) =>
|
const getAuthCode = async (credentials: any) =>
|
||||||
axios.post('/SASLogon/authorize', credentials).then((res) => res.data)
|
axios.post('/SASLogon/authorize', credentials).then((res) => res.data)
|
||||||
|
|
||||||
const AuthCode = () => {
|
const AuthCode = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [displayCode, setDisplayCode] = useState(null)
|
const [displayCode, setDisplayCode] = useState('')
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -56,6 +59,18 @@ const AuthCode = () => {
|
|||||||
{errorMessage && <Typography>{errorMessage}</Typography>}
|
{errorMessage && <Typography>{errorMessage}</Typography>}
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
<CopyToClipboard
|
||||||
|
text={displayCode}
|
||||||
|
onCopy={() =>
|
||||||
|
toast.info('Code copied to ClipBoard', {
|
||||||
|
theme: 'dark',
|
||||||
|
position: toast.POSITION.BOTTOM_RIGHT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button variant="contained">Copy to Clipboard</Button>
|
||||||
|
</CopyToClipboard>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
55
web/src/containers/Settings/index.tsx
Normal file
55
web/src/containers/Settings/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { Box, Paper, Tab, styled } from '@mui/material'
|
||||||
|
import TabContext from '@mui/lab/TabContext'
|
||||||
|
import TabList from '@mui/lab/TabList'
|
||||||
|
import TabPanel from '@mui/lab/TabPanel'
|
||||||
|
|
||||||
|
import Profile from './profile'
|
||||||
|
|
||||||
|
const StyledTab = styled(Tab)({
|
||||||
|
background: 'black',
|
||||||
|
margin: '0 5px 5px 0'
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledTabpanel = styled(TabPanel)({
|
||||||
|
flexGrow: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
const [value, setValue] = React.useState('profile')
|
||||||
|
|
||||||
|
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
|
setValue(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: '65px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabContext value={value}>
|
||||||
|
<Box component={Paper} sx={{ margin: '0 5px', height: '92vh' }}>
|
||||||
|
<TabList
|
||||||
|
TabIndicatorProps={{
|
||||||
|
style: {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
orientation="vertical"
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<StyledTab label="Profile" value="profile" />
|
||||||
|
</TabList>
|
||||||
|
</Box>
|
||||||
|
<StyledTabpanel value="profile">
|
||||||
|
<Profile />
|
||||||
|
</StyledTabpanel>
|
||||||
|
</TabContext>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings
|
||||||
150
web/src/containers/Settings/profile.tsx
Normal file
150
web/src/containers/Settings/profile.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import {
|
||||||
|
Grid,
|
||||||
|
CircularProgress,
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
Divider,
|
||||||
|
CardContent,
|
||||||
|
TextField,
|
||||||
|
CardActions,
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
FormControlLabel,
|
||||||
|
Checkbox
|
||||||
|
} from '@mui/material'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
|
import { AppContext, ModeType } from '../../context/appContext'
|
||||||
|
|
||||||
|
const Profile = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
|
const [user, setUser] = useState({} as any)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.get(`/SASjsApi/user/${appContext.userId}`)
|
||||||
|
.then((res: any) => {
|
||||||
|
setUser(res.data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleChange = (event: any) => {
|
||||||
|
const { name, value } = event.target
|
||||||
|
|
||||||
|
setUser({ ...user, [name]: value })
|
||||||
|
}
|
||||||
|
const handleSubmit = () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.patch(`/SASjsApi/user/${appContext.userId}`, {
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName,
|
||||||
|
autoExec: user.autoExec
|
||||||
|
})
|
||||||
|
.then((res: any) => {
|
||||||
|
toast.success('User information updated', {
|
||||||
|
theme: 'dark',
|
||||||
|
position: toast.POSITION.BOTTOM_RIGHT
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed: ' + err.response?.data || err.text, {
|
||||||
|
theme: 'dark',
|
||||||
|
position: toast.POSITION.BOTTOM_RIGHT
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLoading ? (
|
||||||
|
<CircularProgress
|
||||||
|
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardHeader title="Profile Information" />
|
||||||
|
<Divider />
|
||||||
|
<CardContent>
|
||||||
|
<Grid container spacing={4}>
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
error={user.displayName?.length === 0}
|
||||||
|
helperText="Please specify display name"
|
||||||
|
label="Display Name"
|
||||||
|
name="displayName"
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
value={user.displayName}
|
||||||
|
variant="outlined"
|
||||||
|
disabled={appContext.mode === ModeType.Desktop}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
error={user.username?.length === 0}
|
||||||
|
helperText="Please specify username"
|
||||||
|
label="Username"
|
||||||
|
name="username"
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
value={user.username}
|
||||||
|
variant="outlined"
|
||||||
|
disabled={appContext.mode === ModeType.Desktop}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item lg={6} md={8} sm={12} xs={12}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="autoExec"
|
||||||
|
name="autoExec"
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
rows="10"
|
||||||
|
value={user.autoExec}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<FormGroup row>
|
||||||
|
<FormControlLabel
|
||||||
|
disabled
|
||||||
|
control={<Checkbox checked={user.isActive} />}
|
||||||
|
label="isActive"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
disabled
|
||||||
|
control={<Checkbox checked={user.isAdmin} />}
|
||||||
|
label="isAdmin"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
<Divider />
|
||||||
|
<CardActions>
|
||||||
|
<Button type="submit" variant="contained" onClick={handleSubmit}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profile
|
||||||
@@ -9,14 +9,22 @@ import React, {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export enum ModeType {
|
||||||
|
Server = 'server',
|
||||||
|
Desktop = 'desktop'
|
||||||
|
}
|
||||||
|
|
||||||
interface AppContextProps {
|
interface AppContextProps {
|
||||||
checkingSession: boolean
|
checkingSession: boolean
|
||||||
loggedIn: boolean
|
loggedIn: boolean
|
||||||
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
||||||
|
userId: number
|
||||||
|
setUserId: Dispatch<SetStateAction<number>> | null
|
||||||
username: string
|
username: string
|
||||||
setUsername: Dispatch<SetStateAction<string>> | null
|
setUsername: Dispatch<SetStateAction<string>> | null
|
||||||
displayName: string
|
displayName: string
|
||||||
setDisplayName: Dispatch<SetStateAction<string>> | null
|
setDisplayName: Dispatch<SetStateAction<string>> | null
|
||||||
|
mode: ModeType
|
||||||
logout: (() => void) | null
|
logout: (() => void) | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,10 +32,13 @@ export const AppContext = createContext<AppContextProps>({
|
|||||||
checkingSession: false,
|
checkingSession: false,
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
setLoggedIn: null,
|
setLoggedIn: null,
|
||||||
|
userId: 0,
|
||||||
|
setUserId: null,
|
||||||
username: '',
|
username: '',
|
||||||
setUsername: null,
|
setUsername: null,
|
||||||
displayName: '',
|
displayName: '',
|
||||||
setDisplayName: null,
|
setDisplayName: null,
|
||||||
|
mode: ModeType.Server,
|
||||||
logout: null
|
logout: null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -35,8 +46,10 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
const { children } = props
|
const { children } = props
|
||||||
const [checkingSession, setCheckingSession] = useState(false)
|
const [checkingSession, setCheckingSession] = useState(false)
|
||||||
const [loggedIn, setLoggedIn] = useState(false)
|
const [loggedIn, setLoggedIn] = useState(false)
|
||||||
|
const [userId, setUserId] = useState(0)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [displayName, setDisplayName] = useState('')
|
const [displayName, setDisplayName] = useState('')
|
||||||
|
const [mode, setMode] = useState(ModeType.Server)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCheckingSession(true)
|
setCheckingSession(true)
|
||||||
@@ -46,14 +59,23 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
.then((res) => res.data)
|
.then((res) => res.data)
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
setCheckingSession(false)
|
setCheckingSession(false)
|
||||||
setLoggedIn(true)
|
setUserId(data.id)
|
||||||
setUsername(data.username)
|
setUsername(data.username)
|
||||||
setDisplayName(data.displayName)
|
setDisplayName(data.displayName)
|
||||||
|
setLoggedIn(true)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoggedIn(false)
|
setLoggedIn(false)
|
||||||
axios.get('/') // get CSRF TOKEN
|
axios.get('/') // get CSRF TOKEN
|
||||||
})
|
})
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get('/SASjsApi/info')
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((data: any) => {
|
||||||
|
setMode(data.mode)
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
@@ -70,10 +92,13 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
checkingSession,
|
checkingSession,
|
||||||
loggedIn,
|
loggedIn,
|
||||||
setLoggedIn,
|
setLoggedIn,
|
||||||
|
userId,
|
||||||
|
setUserId,
|
||||||
username,
|
username,
|
||||||
setUsername,
|
setUsername,
|
||||||
displayName,
|
displayName,
|
||||||
setDisplayName,
|
setDisplayName,
|
||||||
|
mode,
|
||||||
logout
|
logout
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user