mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e2c08a8d3 | ||
|
|
473fbd62c0 | ||
|
|
b1a0fe7060 | ||
| dde293c852 | |||
| f738a6d7a3 | |||
|
|
3e0a2de2ad | ||
|
|
91cb7bd946 | ||
|
|
a501a300dc | ||
|
|
b446baa822 | ||
| 9023cf33b5 | |||
| 23b6692f02 | |||
|
|
6de91618ff | ||
|
|
e06d66f312 | ||
|
|
1ffaf2e0ef | ||
|
|
393d3327db | ||
|
|
14cfb9a663 | ||
|
|
dd1f2b3ed7 | ||
|
|
9f5dbbc8da | ||
|
|
9423bb2b23 | ||
|
|
5bfcdc4dbb | ||
|
|
ecd8ed9032 | ||
|
|
a8d89ff1d6 | ||
|
|
8702a4e8fd | ||
| ab222cbaab | |||
|
|
5f06132ece | ||
|
|
56c80b0979 | ||
| 158acf1f97 | |||
|
|
c19a20c1d4 | ||
|
|
f8eaadae7b | ||
| 90e0973a7f | |||
| 869a13fc69 | |||
| 1790e10fc1 | |||
|
|
6d12b900ad | ||
|
|
ae5aa02733 | ||
|
|
28a6a36bb7 | ||
|
|
4e7579dc10 | ||
| 6b0b94ad38 | |||
|
|
b81d742c6c | ||
|
|
a61adbcac2 | ||
|
|
12000f4fc7 | ||
| 73792fb574 | |||
| 53854d0012 | |||
|
|
81501d17ab | ||
|
|
11a7f920f1 | ||
|
|
c08cfcbc38 | ||
|
|
8d38d5ac64 | ||
| e08bbcc543 | |||
|
|
eef3cb270d | ||
|
|
9cfbca23f8 | ||
|
|
aef411a0ea | ||
|
|
e359265c4b | ||
|
|
8e7c9e671c | ||
|
|
c830f44e29 | ||
|
|
806ea4cb5c | ||
|
|
7205072358 | ||
|
|
32d372b42f | ||
|
|
e059bee7dc | ||
|
|
6f56aafab1 | ||
|
|
8734489cf0 | ||
| de9ed15286 | |||
| 325285f447 | |||
|
|
7e6635f40f | ||
|
|
c0022a22f4 | ||
|
|
3fa2a7e2e3 | ||
| 8a617a73ae | |||
| 16856165fb | |||
|
|
e7babb9f55 | ||
|
|
5ab35b02c4 | ||
| 058b3b0081 | |||
| 9d5a5e051f | |||
| 2c704a544f | |||
| 6d6bda5626 | |||
| dffe6d7121 | |||
| b4443819d4 | |||
| e5a7674fa1 | |||
| 596ada7ca8 | |||
| f561ba4bf0 | |||
| c58666eb81 | |||
| 5df619b3f6 | |||
| 07295aa151 | |||
| 194eaec7d4 | |||
|
|
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 | ||
|
|
c2b5e353a5 | ||
|
|
f89389bbc6 | ||
|
|
fadcc9bd29 | ||
|
|
182def2f3e | ||
|
|
06a5f39fea | ||
|
|
143b367a0e | ||
|
|
b5fd800300 | ||
|
|
a0b52d9982 | ||
|
|
c4212665c8 | ||
|
|
97d9bc191c | ||
|
|
dd2a403985 | ||
|
|
7cfa2398e1 | ||
|
|
5888f04e08 | ||
|
|
b40de8fa6a | ||
|
|
45a2a01532 | ||
|
|
c61fec47c4 | ||
| 24d7f00c02 | |||
| b0fdaaaa79 | |||
|
|
2467616296 | ||
|
|
ceefbe48e9 | ||
|
|
426e90471e | ||
|
|
c0b57b9e76 | ||
|
|
4a8e32dd20 | ||
|
|
636301e664 | ||
|
|
25dc5dd215 | ||
|
|
503994dbd2 | ||
|
|
0dceb5c3c3 | ||
|
|
1af04fa3b3 | ||
|
|
efa81fec77 | ||
|
|
10caf1918a | ||
|
|
4ed20a3b75 | ||
|
|
98b2c5fa25 | ||
|
|
3ad327b85f | ||
|
|
dd3acce393 | ||
|
|
8065727b9b | ||
|
|
e1223ec3f8 | ||
|
|
1f89279264 | ||
|
|
a07f47a1ba | ||
|
|
2548c82dfe | ||
|
|
238aa1006f | ||
|
|
35cba97611 | ||
|
|
5f29dec16f | ||
|
|
e2a97fcb7c | ||
|
|
6adeeefcf5 | ||
|
|
c9d66b8576 | ||
|
|
5aaac24080 | ||
|
|
6d34206bbc | ||
|
|
7b39cc06d3 | ||
|
|
6e7f28a6f8 | ||
|
|
5689169ce4 | ||
|
|
6139e7bff6 | ||
|
|
2c77317bb9 | ||
|
|
57b63db9cb | ||
|
|
60a2a4fe32 | ||
|
|
09611cb416 | ||
|
|
2a9bb6e6b1 | ||
|
|
b4b60c69cf | ||
|
|
b060ad1b8e | ||
|
|
d47ed6d0e8 | ||
|
|
a6993ef5ae | ||
|
|
2571fc2ca8 | ||
|
|
992f39b63a | ||
|
|
1ea3f6d8b3 | ||
|
|
e462aebdc0 | ||
|
|
13403517a4 | ||
|
|
c3c2048e75 | ||
|
|
1d8acc36eb | ||
|
|
4c7ad56326 | ||
|
|
e57443f1ed | ||
|
|
5da93f318a | ||
|
|
a30fb1a241 | ||
|
|
4ae8f35e9a | ||
|
|
ebb46f51b6 | ||
|
|
fe24f51ca2 | ||
|
|
fd15f3fb41 | ||
|
|
7d31ee7696 | ||
|
|
667e26b080 | ||
|
|
d09876c05f | ||
|
|
fb8e18be75 | ||
|
|
7ac7a4e083 | ||
|
|
8e23786dd4 | ||
|
|
4bd01bcf29 | ||
|
|
4ad8c81e49 | ||
|
|
51f6aa34a1 | ||
|
|
486207128d | ||
|
|
1e4b0b9171 | ||
|
|
1ff820605a | ||
|
|
9c1a781b3a | ||
| 36628551ae | |||
| 23cf8fa06f | |||
| 84ee743eae | |||
|
|
19e5bd7d2d | ||
|
|
e251747302 | ||
|
|
7e7558d4cf | ||
|
|
f02996facf | ||
|
|
803c51f400 | ||
|
|
c35b2b3f59 | ||
|
|
fe0866ace7 | ||
|
|
1513c3623d | ||
|
|
7fe43ae0b7 | ||
|
|
c4cea4a12b | ||
|
|
9fc7a132ba | ||
|
|
d55a619d64 | ||
|
|
737d2a24c2 | ||
|
|
2e63831b90 | ||
|
|
c7ffde1a3b | ||
|
|
db70b1ce55 | ||
|
|
8a3fe8b217 | ||
| 9dca552e82 | |||
|
|
505f2089c7 | ||
|
|
3344c400a8 | ||
| fa6248e3ef | |||
| 9fb5f1f8e7 | |||
|
|
92e0b8a088 | ||
|
|
b484306ed8 | ||
| 5e08aacc51 | |||
| a9e4eb685d | |||
| 31b09f27cc | |||
| 9f3ec92f8e | |||
| 6c9e449614 | |||
| 68e84b0994 | |||
| f0bb51a0d5 | |||
| b93a0da3a3 | |||
|
|
e5facbf54c | ||
|
|
cb2bebbe76 | ||
|
|
9e1e0ce8cc | ||
|
|
29928753b7 | ||
|
|
edd69ecaae | ||
|
|
74ba65f9f3 | ||
|
|
f257602834 | ||
|
|
61080d4694 | ||
|
|
82633adbc4 | ||
|
|
23db7e7b7d | ||
|
|
cbaa687c9b | ||
|
|
527f70e90d | ||
|
|
122faad55f | ||
|
|
3ff6f5e865 | ||
|
|
7d5128c0d6 | ||
|
|
e1ebbfd087 | ||
|
|
e430bdb0d4 | ||
|
|
9d9769eef3 | ||
|
|
9d167abe2a | ||
|
|
18d0604bdd | ||
|
|
7b7bc6b778 | ||
|
|
fb4f3442d5 | ||
|
|
09d1b7d5d4 | ||
|
|
99839ae62f | ||
|
|
f700561e1a |
84
.all-contributorsrc
Normal file
84
.all-contributorsrc
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"projectName": "server",
|
||||
"projectOwner": "sasjs",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": true,
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "saadjutt01",
|
||||
"name": "Saad Jutt",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8914650?v=4",
|
||||
"profile": "https://github.com/saadjutt01",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sabhas",
|
||||
"name": "Sabir Hassan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/82647447?v=4",
|
||||
"profile": "https://github.com/sabhas",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "YuryShkoda",
|
||||
"name": "Yury Shkoda",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/25773492?v=4",
|
||||
"profile": "https://www.erudicat.com/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "medjedovicm",
|
||||
"name": "Mihajlo Medjedovic",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4",
|
||||
"profile": "https://github.com/medjedovicm",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "allanbowe",
|
||||
"name": "Allan Bowe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4",
|
||||
"profile": "https://4gl.io/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "VladislavParhomchik",
|
||||
"name": "Vladislav Parhomchik",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/83717836?v=4",
|
||||
"profile": "https://github.com/VladislavParhomchik",
|
||||
"contributions": [
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kknapen",
|
||||
"name": "Koen Knapen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/78609432?v=4",
|
||||
"profile": "https://github.com/kknapen",
|
||||
"contributions": [
|
||||
"userTesting"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true
|
||||
}
|
||||
77
.github/CONTRIBUTING.md
vendored
77
.github/CONTRIBUTING.md
vendored
@@ -2,25 +2,22 @@
|
||||
|
||||
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
|
||||
|
||||
The app can be deployed using Docker or NodeJS.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is made in the `configuration` section of `package.json`:
|
||||
Configuration is made using `.env` files (per [README.md](https://github.com/sasjs/server#env-var-configuration) settings), _except_ for one case, when running in NodeJS in production - in which case the path to the SAS executable is made in the `configuration` section of `package.json`.
|
||||
|
||||
- Provide path to SAS9 executable.
|
||||
The `.env` file should be created in the location(s) below. Each folder contains a `.env.example` file that may be adjusted and renamed.
|
||||
|
||||
* `.env` - the root .env file is used only for Docker deploys.
|
||||
* `api/.env` - this is the primary file used in NodeJS deploys
|
||||
* `web/.env` - this file is only necessary in NodeJS when running `web` and `api` seperately (on different ports).
|
||||
|
||||
|
||||
### Using dockers:
|
||||
## Using Docker
|
||||
|
||||
There is `.env.example` file present at root of the project. [for Production]
|
||||
|
||||
There is `.env.example` file present at `./api` of the project. [for Development]
|
||||
|
||||
There is `.env.example` file present at `./web` of the project. [for Development]
|
||||
|
||||
Remember to provide enviornment variables.
|
||||
|
||||
#### Development
|
||||
### Docker Development Mode
|
||||
|
||||
Command to run docker for development:
|
||||
|
||||
@@ -38,7 +35,7 @@ It will build following images if running first time:
|
||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||
|
||||
|
||||
#### Production
|
||||
### Docker Production Mode
|
||||
|
||||
Command to run docker for production:
|
||||
|
||||
@@ -54,47 +51,45 @@ It will build following images if running first time:
|
||||
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
|
||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||
|
||||
### Using node:
|
||||
## Using NodeJS:
|
||||
|
||||
#### Development (running api and web seperately):
|
||||
Be sure to use v16 or above, and to set your environment variables in the relevant `.env` file(s) - else defaults will be used.
|
||||
|
||||
##### API
|
||||
### NodeJS Development Mode
|
||||
|
||||
Navigate to `./api`
|
||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
SASjs Server is split between an API server (serving REST requests) and a WEB Server (everything else). These can be run together, or on seperate ports.
|
||||
|
||||
### NodeJS Dev - Single Port
|
||||
|
||||
Here the environment variables should be configured under `api.env`. Then:
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build
|
||||
cd ../api && npm i && npm start
|
||||
```
|
||||
|
||||
### NodeJS Dev - Seperate Ports
|
||||
|
||||
Set the backend variables in `api/.env` and the frontend variables in `web/.env`. Then:
|
||||
|
||||
#### API server
|
||||
```
|
||||
cd api
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
##### Web
|
||||
|
||||
Navigate to `./web`
|
||||
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
#### Web Server
|
||||
|
||||
```
|
||||
cd web
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
#### Development (running only api server and have web build served):
|
||||
#### NodeJS Production Mode
|
||||
|
||||
##### API server also serving Web build files
|
||||
|
||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build && cd ../
|
||||
cd ./api && npm i && npm start
|
||||
```
|
||||
|
||||
#### Production
|
||||
|
||||
##### API & WEB
|
||||
Update the `.env` file in the *api* folder. Then:
|
||||
|
||||
```
|
||||
npm run server
|
||||
@@ -105,7 +100,7 @@ This will install/build `web` and install `api`, then start prod server.
|
||||
|
||||
## Executables
|
||||
|
||||
Command to generate executables
|
||||
In order to generate the final executables:
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build && cd ../
|
||||
@@ -113,3 +108,7 @@ cd ./api && npm i && npm run exe
|
||||
```
|
||||
|
||||
This will install/build web app and install/create executables of sasjs server at root `./executables`
|
||||
|
||||
## Releases
|
||||
|
||||
To cut a release, run `npm run release` on the main branch, then push the tags (per the console log link)
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -54,6 +54,10 @@ jobs:
|
||||
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
||||
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
||||
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
||||
SESSION_SECRET: ${{secrets.SESSION_SECRET}}
|
||||
RUN_TIMES: 'sas,js'
|
||||
SAS_PATH: '/some/path/to/sas'
|
||||
NODE_PATH: '/some/path/to/node'
|
||||
|
||||
- name: Build Package
|
||||
working-directory: ./api
|
||||
|
||||
27
.github/workflows/release.yml
vendored
27
.github/workflows/release.yml
vendored
@@ -2,16 +2,26 @@ name: SASjs Server Executable Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/*]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install Dependencies WEB
|
||||
working-directory: ./web
|
||||
run: npm ci
|
||||
@@ -39,10 +49,11 @@ jobs:
|
||||
zip macos.zip api-macos
|
||||
zip windows.zip api-win.exe
|
||||
|
||||
- name: Install Semantic Release and plugins
|
||||
run: |
|
||||
npm i
|
||||
npm i -g semantic-release
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
./executables/linux.zip
|
||||
./executables/macos.zip
|
||||
./executables/windows.zip
|
||||
run: |
|
||||
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules/
|
||||
.DS_Store
|
||||
.env*
|
||||
sas/
|
||||
sasjs_root/
|
||||
tmp/
|
||||
build/
|
||||
sasjsbuild/
|
||||
@@ -11,3 +12,4 @@ sasjscore/
|
||||
certificates/
|
||||
executables/
|
||||
.env
|
||||
api/csp.config.json
|
||||
|
||||
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'"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
423
CHANGELOG.md
423
CHANGELOG.md
@@ -1,6 +1,425 @@
|
||||
# Changelog
|
||||
# [0.7.0](https://github.com/sasjs/server/compare/v0.6.1...v0.7.0) (2022-06-19)
|
||||
|
||||
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 runtimes to global process object ([194eaec](https://github.com/sasjs/server/commit/194eaec7d4a561468f83bf6efce484909ee532eb))
|
||||
* code fixes for executing program from program path including file extension ([53854d0](https://github.com/sasjs/server/commit/53854d001279462104b24c0e59a8c94ab4938a94))
|
||||
* code/execute controller logic to handle different runtimes ([23b6692](https://github.com/sasjs/server/commit/23b6692f02e4afa33c9dc95d242eb8645c19d546))
|
||||
* convert single executeProgram method to two methods i.e. executeSASProgram and executeJSProgram ([c58666e](https://github.com/sasjs/server/commit/c58666eb81514de500519e7b96c1981778ec149b))
|
||||
* no need to stringify _webout in preProgramVarStatements, developer should have _webout as string in actual code ([9d5a5e0](https://github.com/sasjs/server/commit/9d5a5e051fd821295664ddb3a1fd64629894a44c))
|
||||
* pass _program to execute file without extension ([5df619b](https://github.com/sasjs/server/commit/5df619b3f63571e8e326261d8114869d33881d91))
|
||||
* refactor code for session selection in preUploadMiddleware function ([b444381](https://github.com/sasjs/server/commit/b4443819d42afecebc0f382c58afb9010d4775ef))
|
||||
* refactor code in executeFile method of session controller ([dffe6d7](https://github.com/sasjs/server/commit/dffe6d7121d569e5c7d13023c6ca68d8c901c88e))
|
||||
* refactor code in preUploadMiddleware function ([6d6bda5](https://github.com/sasjs/server/commit/6d6bda56267babde7b98cf69e32973d56d719f75))
|
||||
* refactor sas/js session controller classes to inherit from base session controller class ([2c704a5](https://github.com/sasjs/server/commit/2c704a544f4e31a8e8e833a9a62ba016bcfa6c7c))
|
||||
* **Studio:** style fix for runtime dropdown ([9023cf3](https://github.com/sasjs/server/commit/9023cf33b5fa4b13c2d5e9b80ae307df69c7fc02))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* configure child process with writeStream to write logs to log file ([058b3b0](https://github.com/sasjs/server/commit/058b3b00816e582e143953c2f0b8330bde2181b8))
|
||||
* conver single session controller to two controller i.e. SASSessionController and JSSessionController ([07295aa](https://github.com/sasjs/server/commit/07295aa151175db8c93eeef806fc3b7fde40ac72))
|
||||
* create and inject code for uploaded files to code.js ([1685616](https://github.com/sasjs/server/commit/16856165fb292dc9ffa897189ba105bd9f362267))
|
||||
* validate sasjs_runtimes env var ([596ada7](https://github.com/sasjs/server/commit/596ada7ca88798d6d71f6845633a006fd22438ea))
|
||||
|
||||
## [0.6.1](https://github.com/sasjs/server/compare/v0.6.0...v0.6.1) (2022-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* home page wording. Using fix to force previous change through.. ([8702a4e](https://github.com/sasjs/server/commit/8702a4e8fd1bbfaf4f426b75e8b85a87ede0e0b0))
|
||||
|
||||
# [0.6.0](https://github.com/sasjs/server/compare/v0.5.0...v0.6.0) (2022-06-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* get group by group name ([6b0b94a](https://github.com/sasjs/server/commit/6b0b94ad38215ae58e62279a4f73ac3ed2d9d0e8))
|
||||
|
||||
# [0.5.0](https://github.com/sasjs/server/compare/v0.4.2...v0.5.0) (2022-06-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* npm audit fix to avoid warnings on npm i ([28a6a36](https://github.com/sasjs/server/commit/28a6a36bb708b93fb5c2b74d587e9b2e055582be))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** deployment through zipped/compressed file ([b81d742](https://github.com/sasjs/server/commit/b81d742c6c70d4cf1cab365b0e3efc087441db00))
|
||||
|
||||
## [0.4.2](https://github.com/sasjs/server/compare/v0.4.1...v0.4.2) (2022-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* appStream redesign ([73792fb](https://github.com/sasjs/server/commit/73792fb574c90bd280c4324e0b41c6fee7d572b6))
|
||||
|
||||
## [0.4.1](https://github.com/sasjs/server/compare/v0.4.0...v0.4.1) (2022-06-15)
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* get csrf token from cookie if not present in header ([f89389b](https://github.com/sasjs/server/commit/f89389bbc6f1f8f7060db2bdeb89746cbd60f533))
|
||||
|
||||
### [0.0.75](https://github.com/sasjs/server/compare/v0.0.69...v0.0.75) (2022-05-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
|
||||
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
|
||||
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
|
||||
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
|
||||
* moved getAuthCode from api to web routes ([b40de8f](https://github.com/sasjs/server/commit/b40de8fa6a5aa763ed25a6fe6a381e483e0ab824))
|
||||
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
|
||||
* **web:** seperate container for auth code ([5888f04](https://github.com/sasjs/server/commit/5888f04e08a32c6d2c7bcfcbc3a1d32425bff3b3))
|
||||
|
||||
### [0.0.74](https://github.com/sasjs/server/compare/v0.0.73...v0.0.74) (2022-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
|
||||
|
||||
### [0.0.73](https://github.com/sasjs/server/compare/v0.0.72...v0.0.73) (2022-05-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
|
||||
|
||||
### [0.0.72](https://github.com/sasjs/server/compare/v0.0.71...v0.0.72) (2022-05-09)
|
||||
|
||||
### [0.0.71](https://github.com/sasjs/server/compare/v0.0.70...v0.0.71) (2022-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
|
||||
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
|
||||
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
|
||||
|
||||
### [0.0.70](https://github.com/sasjs/server/compare/v0.0.69...v0.0.70) (2022-05-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
|
||||
|
||||
### [0.0.69](https://github.com/sasjs/server/compare/v0.0.68...v0.0.69) (2022-05-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **upload:** appStream uses CSRF + Session authentication ([1f89279](https://github.com/sasjs/server/commit/1f8927926405887f3d134c0a1dd6452ffa33876e))
|
||||
|
||||
### [0.0.68](https://github.com/sasjs/server/compare/v0.0.67...v0.0.68) (2022-05-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* using monaco editor locally ([2548c82](https://github.com/sasjs/server/commit/2548c82dfe1149e62a570a00546dddd9e30049b1))
|
||||
|
||||
### [0.0.67](https://github.com/sasjs/server/compare/v0.0.66...v0.0.67) (2022-05-01)
|
||||
|
||||
### [0.0.66](https://github.com/sasjs/server/compare/v0.0.64...v0.0.66) (2022-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added swagger ui init file manually ([e2a97fc](https://github.com/sasjs/server/commit/e2a97fcb7c54a57a7ca118677cfce93fe9430d8f))
|
||||
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
|
||||
|
||||
### [0.0.65](https://github.com/sasjs/server/compare/v0.0.64...v0.0.65) (2022-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
|
||||
|
||||
### [0.0.64](https://github.com/sasjs/server/compare/v0.0.63...v0.0.64) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removed fileExists for serving web ([7b39cc0](https://github.com/sasjs/server/commit/7b39cc06d358f5ffecb87955040c4eb0fcc7469e))
|
||||
|
||||
### [0.0.63](https://github.com/sasjs/server/compare/v0.0.62...v0.0.63) (2022-04-30)
|
||||
|
||||
### [0.0.62](https://github.com/sasjs/server/compare/v0.0.61...v0.0.62) (2022-04-30)
|
||||
|
||||
### [0.0.61](https://github.com/sasjs/server/compare/v0.0.59...v0.0.61) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||
|
||||
### [0.0.60](https://github.com/sasjs/server/compare/v0.0.59...v0.0.60) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||
|
||||
### [0.0.59](https://github.com/sasjs/server/compare/v0.0.58...v0.0.59) (2022-04-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* enabled csrf tokens for web component ([e462aeb](https://github.com/sasjs/server/commit/e462aebdc01f3c0068ed0074473a2063412dcf45))
|
||||
* enabled session based authentication for web ([5da93f3](https://github.com/sasjs/server/commit/5da93f318aad10b1c67032a467191e4dbb99f411))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fetch client from DB for each request ([4ad8c81](https://github.com/sasjs/server/commit/4ad8c81e4927c1a82220ec015a781b095c8e859e))
|
||||
* **web:** show display name instead of username ([e57443f](https://github.com/sasjs/server/commit/e57443f1ed662a022494bb93d79c3d2f10a2d082))
|
||||
|
||||
### [0.0.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping core library to get latest user management macros ([4862071](https://github.com/sasjs/server/commit/486207128da58fc4866bd0919c1bed2bd98097ea))
|
||||
* missing dependency ([d09876c](https://github.com/sasjs/server/commit/d09876c05f89166eec20064f7aa7ed5b867be081))
|
||||
|
||||
### [0.0.57](https://github.com/sasjs/server/compare/v0.0.56...v0.0.57) (2022-04-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* create AppContext ([84ee743](https://github.com/sasjs/server/commit/84ee743eae16e87eaa91969393bebf01e2d15a44))
|
||||
|
||||
### [0.0.56](https://github.com/sasjs/server/compare/v0.0.55...v0.0.56) (2022-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* shortening min length of username. Closes [#61](https://github.com/sasjs/server/issues/61) ([f02996f](https://github.com/sasjs/server/commit/f02996facf1019ec4022ccfbc99c1d0137074e1b))
|
||||
|
||||
### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||
* drive path in server mode ([c4cea4a](https://github.com/sasjs/server/commit/c4cea4a12b7eda4daeed995f41c0b10bcea79871))
|
||||
|
||||
### [0.0.54](https://github.com/sasjs/server/compare/v0.0.53...v0.0.54) (2022-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||
|
||||
### [0.0.53](https://github.com/sasjs/server/compare/v0.0.49...v0.0.53) (2022-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* provide clientId to web component ([db70b1c](https://github.com/sasjs/server/commit/db70b1ce555df6b29fb09c0c960d38b911c97b1b))
|
||||
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||
|
||||
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||
|
||||
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||
|
||||
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||
|
||||
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
|
||||
|
||||
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deploy:** new route added for deploy with build.json ([18d0604](https://github.com/sasjs/server/commit/18d0604bdd0b20ad468f9345474b4de034ee3a67))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove uploaded build.json from temp folder in all cases ([9d167ab](https://github.com/sasjs/server/commit/9d167abe2adb743bca161862b4561bf573182c00))
|
||||
* **stp:** return log+webout for debug on ([3ff6f5e](https://github.com/sasjs/server/commit/3ff6f5e86581cd2ac23bbe0b8e2c367fbea890ed))
|
||||
|
||||
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** updated STUDIO log and webout ([f700561](https://github.com/sasjs/server/commit/f700561e1a8d06c18ca2bdbe4605d7ab34f7a761))
|
||||
|
||||
### [0.0.46](https://github.com/sasjs/server/compare/v0.0.45...v0.0.46) (2022-03-29)
|
||||
|
||||
|
||||
133
README.md
133
README.md
@@ -1,5 +1,11 @@
|
||||
# SASjs Server
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
|
||||
[](#contributors-)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:
|
||||
|
||||
- Virtual filesystem for storing SAS programs and other content
|
||||
@@ -48,14 +54,26 @@ When launching the app, it will make use of specific environment variables. Thes
|
||||
Example contents of a `.env` file:
|
||||
|
||||
```
|
||||
# options: [desktop|server] default: `desktop`
|
||||
#
|
||||
## Core Settings
|
||||
#
|
||||
|
||||
|
||||
# MODE options: [desktop|server] default: `desktop`
|
||||
# Desktop mode is single user and designed for workstation use
|
||||
# Server mode is multi-user and suitable for intranet / internet use
|
||||
MODE=
|
||||
|
||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||
CORS=
|
||||
# Path to SAS executable (sas.exe / sas.sh)
|
||||
SAS_PATH=/path/to/sas/executable.exe
|
||||
|
||||
# Path to Node.js executable
|
||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||
|
||||
# Path to working directory
|
||||
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
||||
SASJS_ROOT=./sasjs_root
|
||||
|
||||
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||
WHITELIST=
|
||||
|
||||
# options: [http|https] default: http
|
||||
PROTOCOL=
|
||||
@@ -64,16 +82,22 @@ PROTOCOL=
|
||||
PORT=
|
||||
|
||||
|
||||
# optional
|
||||
# for MODE: `desktop`, prompts user
|
||||
# for MODE: `server` gets value from api/package.json `configuration.sasPath`
|
||||
SAS_PATH=/path/to/sas/executable.exe
|
||||
#
|
||||
## Additional SAS Options
|
||||
#
|
||||
|
||||
|
||||
# optional
|
||||
# for MODE: `desktop`, prompts user
|
||||
# for MODE: `server` defaults to /tmp
|
||||
DRIVE_PATH=/tmp
|
||||
# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
|
||||
# Any options set here are automatically applied in the SAS session
|
||||
# See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm
|
||||
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
|
||||
SAS_OPTIONS= -NOXCMD
|
||||
SASV9_OPTIONS= -NOXCMD
|
||||
|
||||
|
||||
#
|
||||
## Additional Web Server Options
|
||||
#
|
||||
|
||||
# ENV variables required for PROTOCOL: `https`
|
||||
PRIVATE_KEY=privkey.pem
|
||||
@@ -83,17 +107,68 @@ FULL_CHAIN=fullchain.pem
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
SESSION_SECRET=<secret>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
||||
CORS=
|
||||
|
||||
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||
WHITELIST=
|
||||
|
||||
# HELMET Cross Origin Embedder Policy
|
||||
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
|
||||
# options: [true|false] default: true
|
||||
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
|
||||
HELMET_COEP=
|
||||
|
||||
# HELMET Content Security Policy
|
||||
# Path to a json file containing HELMET `contentSecurityPolicy` directives
|
||||
# Docs: https://helmetjs.github.io/#reference
|
||||
#
|
||||
# Example config:
|
||||
# {
|
||||
# "img-src": ["'self'", "data:"],
|
||||
# "script-src": ["'self'", "'unsafe-inline'"],
|
||||
# "script-src-attr": ["'self'", "'unsafe-inline'"]
|
||||
# }
|
||||
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=
|
||||
|
||||
# A comma separated string that defines the available runTimes.
|
||||
# Priority is given to the runtime that cSAS_PATHomes first in string.
|
||||
# Possible options at the moment are sas and js
|
||||
|
||||
# options: [sas,js|js,sas|sas|js] default:sas,js
|
||||
RUN_TIMES=
|
||||
|
||||
```
|
||||
|
||||
## Persisting the Session
|
||||
|
||||
Normally the server process will stop when your terminal dies. To keep it going you can use the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) as follows:
|
||||
Normally the server process will stop when your terminal dies. To keep it going you can use the following suggested approaches:
|
||||
|
||||
1. Linux Background Job
|
||||
2. NPM package `pm2`
|
||||
|
||||
### Background Job
|
||||
|
||||
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
|
||||
|
||||
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
|
||||
|
||||
### PM2
|
||||
|
||||
Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) and execute, eg as follows:
|
||||
|
||||
```bash
|
||||
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||
export PORT=5001
|
||||
export DRIVE_PATH=./tmp
|
||||
export SASJS_ROOT=./sasjs_root
|
||||
|
||||
pm2 start api-linux
|
||||
```
|
||||
@@ -122,8 +197,34 @@ Instead of `app_name` you can pass:
|
||||
|
||||
## Server Version
|
||||
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is highly recommended to change these on first use.
|
||||
|
||||
- CLIENTID: `clientID1`
|
||||
- USERNAME: `secretuser`
|
||||
- PASSWORD: `secretpassword`
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/saadjutt01"><img src="https://avatars.githubusercontent.com/u/8914650?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Saad Jutt</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=saadjutt01" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/sabhas"><img src="https://avatars.githubusercontent.com/u/82647447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sabir Hassan</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=sabhas" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=sabhas" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://www.erudicat.com/"><img src="https://avatars.githubusercontent.com/u/25773492?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yury Shkoda</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=YuryShkoda" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/medjedovicm"><img src="https://avatars.githubusercontent.com/u/18329105?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mihajlo Medjedovic</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=medjedovicm" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://4gl.io/"><img src="https://avatars.githubusercontent.com/u/4420615?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allan Bowe</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Code">💻</a> <a href="https://github.com/sasjs/server/commits?author=allanbowe" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/VladislavParhomchik"><img src="https://avatars.githubusercontent.com/u/83717836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Vladislav Parhomchik</b></sub></a><br /><a href="https://github.com/sasjs/server/commits?author=VladislavParhomchik" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/kknapen"><img src="https://avatars.githubusercontent.com/u/78609432?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Koen Knapen</b></sub></a><br /><a href="#userTesting-kknapen" title="User Testing">📓</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
MODE=[desktop|server] default considered as desktop
|
||||
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
|
||||
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
||||
|
||||
PROTOCOL=[http|https] default considered as http
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
|
||||
PORT=[5000] default value is 5000
|
||||
|
||||
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
||||
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
||||
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
SESSION_SECRET=<secret>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas,js
|
||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||
DRIVE_PATH=./tmp
|
||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||
|
||||
SASJS_ROOT=./sasjs_root
|
||||
|
||||
LOG_FORMAT_MORGAN=common
|
||||
@@ -1 +1 @@
|
||||
v16.14.0
|
||||
v16.15.1
|
||||
5
api/csp.config.example.json
Normal file
5
api/csp.config.example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"img-src": ["'self'", "data:"],
|
||||
"script-src": ["'self'", "'unsafe-inline'"],
|
||||
"script-src-attr": ["'self'", "'unsafe-inline'"]
|
||||
}
|
||||
991
api/package-lock.json
generated
991
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
"prestart": "npm run initial",
|
||||
"prebuild": "npm run initial",
|
||||
"start": "nodemon ./src/server.ts",
|
||||
"start:prod": "node ./build/src/server.js",
|
||||
"build": "rimraf build && tsc",
|
||||
"postbuild": "npm run copy:files",
|
||||
"swagger": "tsoa spec",
|
||||
@@ -46,25 +47,34 @@
|
||||
},
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "4.9.0",
|
||||
"@sasjs/utils": "2.36.2",
|
||||
"@sasjs/core": "^4.27.3",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"csurf": "^1.11.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"helmet": "^5.0.2",
|
||||
"joi": "^17.4.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.3",
|
||||
"swagger-ui-express": "^4.1.6"
|
||||
"swagger-ui-express": "4.3.0",
|
||||
"unzipper": "^0.10.11",
|
||||
"url": "^0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/csurf": "^1.11.2",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
@@ -73,12 +83,14 @@
|
||||
"@types/node": "^15.12.2",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"@types/unzipper": "^0.10.5",
|
||||
"adm-zip": "^0.5.9",
|
||||
"dotenv": "^10.0.0",
|
||||
"http-headers-validation": "^0.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "^8.0.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"pkg": "5.5.2",
|
||||
"pkg": "5.6.0",
|
||||
"prettier": "^2.3.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"supertest": "^6.1.3",
|
||||
@@ -87,12 +99,9 @@
|
||||
"tsoa": "3.14.1",
|
||||
"typescript": "^4.3.2"
|
||||
},
|
||||
"configuration": {
|
||||
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"tmp/**/*"
|
||||
"sasjs_root/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
50
api/public/SASjsApi/swagger-ui-init.js
Normal file
50
api/public/SASjsApi/swagger-ui-init.js
Normal file
@@ -0,0 +1,50 @@
|
||||
window.onload = function () {
|
||||
// Build a system
|
||||
var url = window.location.search.match(/url=([^&]+)/)
|
||||
if (url && url.length > 1) {
|
||||
url = decodeURIComponent(url[1])
|
||||
} else {
|
||||
url = window.location.origin
|
||||
}
|
||||
var options = {
|
||||
customOptions: {
|
||||
url: '/swagger.yaml',
|
||||
requestInterceptor: function (request) {
|
||||
request.credentials = 'include'
|
||||
var cookie = document.cookie
|
||||
var startIndex = cookie.indexOf('XSRF-TOKEN')
|
||||
var csrf = cookie.slice(startIndex + 11).split('; ')[0]
|
||||
request.headers['X-XSRF-TOKEN'] = csrf
|
||||
return request
|
||||
}
|
||||
}
|
||||
}
|
||||
url = options.swaggerUrl || url
|
||||
var urls = options.swaggerUrls
|
||||
var customOptions = options.customOptions
|
||||
var spec1 = options.swaggerDoc
|
||||
var swaggerOptions = {
|
||||
spec: spec1,
|
||||
url: url,
|
||||
urls: urls,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||
layout: 'StandaloneLayout'
|
||||
}
|
||||
for (var attrname in customOptions) {
|
||||
swaggerOptions[attrname] = customOptions[attrname]
|
||||
}
|
||||
var ui = SwaggerUIBundle(swaggerOptions)
|
||||
|
||||
if (customOptions.oauth) {
|
||||
ui.initOAuth(customOptions.oauth)
|
||||
}
|
||||
|
||||
if (customOptions.authAction) {
|
||||
ui.authActions.authorize(customOptions.authAction)
|
||||
}
|
||||
|
||||
window.ui = ui
|
||||
}
|
||||
49
api/public/app-streams-script.js
Normal file
49
api/public/app-streams-script.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const inputElement = document.getElementById('fileId')
|
||||
|
||||
document.getElementById('uploadButton').addEventListener('click', function () {
|
||||
inputElement.click()
|
||||
})
|
||||
|
||||
inputElement.addEventListener(
|
||||
'change',
|
||||
function () {
|
||||
const fileList = this.files /* now you can work with the file list */
|
||||
|
||||
updateFileUploadMessage('Requesting ...')
|
||||
|
||||
const file = fileList[0]
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('file', file)
|
||||
|
||||
axios
|
||||
.post('/SASjsApi/drive/deploy/upload', formData)
|
||||
.then((res) => res.data)
|
||||
.then((data) => {
|
||||
return (
|
||||
data.message +
|
||||
'\nstreamServiceName: ' +
|
||||
data.streamServiceName +
|
||||
'\nrefreshing page once alert box closes.'
|
||||
)
|
||||
})
|
||||
.then((message) => {
|
||||
alert(message)
|
||||
location.reload()
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(error.response.data)
|
||||
resetFileUpload()
|
||||
updateFileUploadMessage('Upload New App')
|
||||
})
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
function updateFileUploadMessage(message) {
|
||||
document.getElementById('uploadMessage').innerHTML = message
|
||||
}
|
||||
|
||||
function resetFileUpload() {
|
||||
inputElement.value = null
|
||||
}
|
||||
3
api/public/axios.min.js
vendored
Normal file
3
api/public/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
api/public/plus.png
Normal file
BIN
api/public/plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 899 B |
@@ -5,36 +5,6 @@ components:
|
||||
requestBodies: {}
|
||||
responses: {}
|
||||
schemas:
|
||||
AuthorizeResponse:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 'Authorization code'
|
||||
example: someRandomCryptoString
|
||||
required:
|
||||
- code
|
||||
type: object
|
||||
additionalProperties: false
|
||||
AuthorizePayload:
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: 'Username for user'
|
||||
example: secretuser
|
||||
password:
|
||||
type: string
|
||||
description: 'Password for user'
|
||||
example: secretpassword
|
||||
clientId:
|
||||
type: string
|
||||
description: 'Client ID'
|
||||
example: clientID1
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
- clientId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
TokenResponse:
|
||||
properties:
|
||||
accessToken:
|
||||
@@ -77,6 +47,41 @@ components:
|
||||
- userId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
LoginPayload:
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: 'Username for user'
|
||||
example: secretuser
|
||||
password:
|
||||
type: string
|
||||
description: 'Password for user'
|
||||
example: secretpassword
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
type: object
|
||||
additionalProperties: false
|
||||
AuthorizeResponse:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 'Authorization code'
|
||||
example: someRandomCryptoString
|
||||
required:
|
||||
- code
|
||||
type: object
|
||||
additionalProperties: false
|
||||
AuthorizePayload:
|
||||
properties:
|
||||
clientId:
|
||||
type: string
|
||||
description: 'Client ID'
|
||||
example: clientID1
|
||||
required:
|
||||
- clientId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
ClientPayload:
|
||||
properties:
|
||||
clientId:
|
||||
@@ -134,14 +139,24 @@ components:
|
||||
- httpHeaders
|
||||
type: object
|
||||
additionalProperties: false
|
||||
RunTimeType:
|
||||
enum:
|
||||
- sas
|
||||
- js
|
||||
type: string
|
||||
ExecuteSASCodePayload:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 'Code of SAS program'
|
||||
example: '* SAS Code HERE;'
|
||||
description: 'Code of program'
|
||||
example: '* Code HERE;'
|
||||
runTime:
|
||||
$ref: '#/components/schemas/RunTimeType'
|
||||
description: 'runtime for program'
|
||||
example: js
|
||||
required:
|
||||
- code
|
||||
- runTime
|
||||
type: object
|
||||
additionalProperties: false
|
||||
MemberType.folder:
|
||||
@@ -305,6 +320,21 @@ components:
|
||||
- displayName
|
||||
type: object
|
||||
additionalProperties: false
|
||||
GroupResponse:
|
||||
properties:
|
||||
groupId:
|
||||
type: number
|
||||
format: double
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
required:
|
||||
- groupId
|
||||
- name
|
||||
- description
|
||||
type: object
|
||||
additionalProperties: false
|
||||
UserDetailsResponse:
|
||||
properties:
|
||||
id:
|
||||
@@ -318,6 +348,12 @@ components:
|
||||
type: boolean
|
||||
isAdmin:
|
||||
type: boolean
|
||||
autoExec:
|
||||
type: string
|
||||
groups:
|
||||
items:
|
||||
$ref: '#/components/schemas/GroupResponse'
|
||||
type: array
|
||||
required:
|
||||
- id
|
||||
- displayName
|
||||
@@ -347,27 +383,16 @@ components:
|
||||
type: boolean
|
||||
description: 'Account should be active or not, defaults to true'
|
||||
example: 'true'
|
||||
autoExec:
|
||||
type: string
|
||||
description: 'User-specific auto-exec code'
|
||||
example: ""
|
||||
required:
|
||||
- displayName
|
||||
- username
|
||||
- password
|
||||
type: object
|
||||
additionalProperties: false
|
||||
GroupResponse:
|
||||
properties:
|
||||
groupId:
|
||||
type: number
|
||||
format: double
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
required:
|
||||
- groupId
|
||||
- name
|
||||
- description
|
||||
type: object
|
||||
additionalProperties: false
|
||||
GroupDetailsResponse:
|
||||
properties:
|
||||
groupId:
|
||||
@@ -410,6 +435,51 @@ components:
|
||||
- description
|
||||
type: object
|
||||
additionalProperties: false
|
||||
_LeanDocument__LeanDocument_T__:
|
||||
properties: {}
|
||||
type: object
|
||||
Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__:
|
||||
properties:
|
||||
id:
|
||||
description: 'The string version of this documents _id.'
|
||||
_id:
|
||||
$ref: '#/components/schemas/_LeanDocument__LeanDocument_T__'
|
||||
description: 'This documents _id.'
|
||||
__v:
|
||||
description: 'This documents __v.'
|
||||
type: object
|
||||
description: 'From T, pick a set of properties whose keys are in the union K'
|
||||
Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_:
|
||||
$ref: '#/components/schemas/Pick__LeanDocument_T_.Exclude_keyof_LeanDocument_T_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested__'
|
||||
description: 'Construct a type with the properties of T except for those in type K.'
|
||||
LeanDocument_this_:
|
||||
$ref: '#/components/schemas/Omit__LeanDocument_this_.Exclude_keyofDocument._id-or-id-or-__v_-or-%24isSingleNested_'
|
||||
IGroup:
|
||||
$ref: '#/components/schemas/LeanDocument_this_'
|
||||
InfoResponse:
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
cors:
|
||||
type: string
|
||||
whiteList:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
protocol:
|
||||
type: string
|
||||
runTimes:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- mode
|
||||
- cors
|
||||
- whiteList
|
||||
- protocol
|
||||
- runTimes
|
||||
type: object
|
||||
additionalProperties: false
|
||||
ExecuteReturnJsonPayload:
|
||||
properties:
|
||||
_program:
|
||||
@@ -431,30 +501,6 @@ info:
|
||||
name: '4GL Ltd'
|
||||
openapi: 3.0.0
|
||||
paths:
|
||||
/SASjsApi/auth/authorize:
|
||||
post:
|
||||
operationId: Authorize
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizeResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {code: someRandomCryptoString}
|
||||
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
||||
tags:
|
||||
- Auth
|
||||
security: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizePayload'
|
||||
/SASjsApi/auth/token:
|
||||
post:
|
||||
operationId: Token
|
||||
@@ -512,6 +558,86 @@ paths:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
/:
|
||||
get:
|
||||
operationId: Home
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
summary: 'Render index.html'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
/SASLogon/login:
|
||||
post:
|
||||
operationId: Login
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
|
||||
loggedIn: {type: boolean}
|
||||
required:
|
||||
- user
|
||||
- loggedIn
|
||||
type: object
|
||||
summary: 'Accept a valid username/password'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginPayload'
|
||||
/SASLogon/authorize:
|
||||
post:
|
||||
operationId: Authorize
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizeResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {code: someRandomCryptoString}
|
||||
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizePayload'
|
||||
/logout:
|
||||
get:
|
||||
operationId: Logout
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
summary: 'Accept a valid username/password'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
/SASjsApi/client:
|
||||
post:
|
||||
operationId: CreateClient
|
||||
@@ -606,6 +732,57 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployPayload'
|
||||
/SASjsApi/drive/deploy/upload:
|
||||
post:
|
||||
operationId: DeployUpload
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'}
|
||||
'400':
|
||||
description: 'Invalid Format'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'Provided not supported data format.'}
|
||||
'500':
|
||||
description: 'Execution Error'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'Deployment failed!'}
|
||||
description: "Accepts JSON file and zipped compressed JSON file as well.\nCompressed file should only contain one JSON file and should have same name\nas of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip\nAny other file or JSON file in zipped will be ignored!"
|
||||
summary: 'Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.'
|
||||
tags:
|
||||
- Drive
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
required:
|
||||
- file
|
||||
/SASjsApi/drive/file:
|
||||
get:
|
||||
operationId: GetFile
|
||||
@@ -849,6 +1026,94 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserPayload'
|
||||
'/SASjsApi/user/by/username/{username}':
|
||||
get:
|
||||
operationId: GetUserByUsername
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserDetailsResponse'
|
||||
description: 'Only Admin or user itself will get user autoExec code.'
|
||||
summary: 'Get user properties - such as group memberships, userName, displayName.'
|
||||
tags:
|
||||
- User
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'The User''s username'
|
||||
in: path
|
||||
name: username
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: johnSnow01
|
||||
patch:
|
||||
operationId: UpdateUserByUsername
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserDetailsResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {id: 1234, displayName: 'John Snow', username: johnSnow01, isAdmin: false, isActive: true}
|
||||
summary: 'Update user properties - such as displayName. Can be performed either by admins, or the user in question.'
|
||||
tags:
|
||||
- User
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'The User''s username'
|
||||
in: path
|
||||
name: username
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: johnSnow01
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserPayload'
|
||||
delete:
|
||||
operationId: DeleteUserByUsername
|
||||
responses:
|
||||
'204':
|
||||
description: 'No content'
|
||||
summary: 'Delete a user. Can be performed either by admins, or the user in question.'
|
||||
tags:
|
||||
- User
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'The User''s username'
|
||||
in: path
|
||||
name: username
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: johnSnow01
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
type: object
|
||||
'/SASjsApi/user/{userId}':
|
||||
get:
|
||||
operationId: GetUser
|
||||
@@ -859,6 +1124,7 @@ paths:
|
||||
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
|
||||
@@ -986,6 +1252,30 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupPayload'
|
||||
'/SASjsApi/group/by/groupname/{name}':
|
||||
get:
|
||||
operationId: GetGroupByGroupName
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GroupDetailsResponse'
|
||||
summary: 'Get list of members of a group (userName). All users can request this.'
|
||||
tags:
|
||||
- Group
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'The group''s name'
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
'/SASjsApi/group/{groupId}':
|
||||
get:
|
||||
operationId: GetGroup
|
||||
@@ -1015,8 +1305,14 @@ paths:
|
||||
delete:
|
||||
operationId: DeleteGroup
|
||||
responses:
|
||||
'204':
|
||||
description: 'No content'
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/IGroup'}
|
||||
- {properties: {_id: {}}, required: [_id], type: object}
|
||||
summary: 'Delete a group. Admin task only.'
|
||||
tags:
|
||||
- Group
|
||||
@@ -1108,6 +1404,24 @@ paths:
|
||||
format: double
|
||||
type: number
|
||||
example: '6789'
|
||||
/SASjsApi/info:
|
||||
get:
|
||||
operationId: Info
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InfoResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http, runTimes: [sas, js]}
|
||||
summary: 'Get server info (mode, cors, whiteList, protocol).'
|
||||
tags:
|
||||
- Info
|
||||
security: []
|
||||
parameters: []
|
||||
/SASjsApi/session:
|
||||
get:
|
||||
operationId: Session
|
||||
@@ -1140,8 +1454,8 @@ paths:
|
||||
anyOf:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. Setting _debug=131 will\ncause the log to be streamed in the output.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response headers can be adjusted using the mfs_httpheader() macro. Any\nfile type can be returned, including binary files such as zip or xls.\n\nIf _debug is >= 131, response headers will contain Content-Type: 'text/plain'\n\nThis behaviour differs for POST requests, in which case the response is\nalways JSON."
|
||||
summary: 'Execute Stored Program, return raw _webout content.'
|
||||
description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
|
||||
summary: 'Execute a Stored Program, returns raw _webout content.'
|
||||
tags:
|
||||
- STP
|
||||
security:
|
||||
@@ -1149,13 +1463,13 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'Location of SAS program'
|
||||
description: 'Location of SAS or JS code'
|
||||
in: query
|
||||
name: _program
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder/some.file
|
||||
example: /Projects/myApp/some/program
|
||||
post:
|
||||
operationId: ExecuteReturnJson
|
||||
responses:
|
||||
@@ -1168,8 +1482,8 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}}
|
||||
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. In any case, the log is\nalways returned in the log object.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response will be a JSON object with the following root attributes: log,\nwebout, headers.\n\nThe webout will be a nested JSON object ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content.\n\nResponse headers from the mfs_httpheader macro are simply listed in the\nheaders object, for POST requests they have no effect on the actual\nresponse header."
|
||||
summary: 'Execute Stored Program, return JSON'
|
||||
description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content."
|
||||
summary: 'Execute a Stored Program, return a JSON object'
|
||||
tags:
|
||||
- STP
|
||||
security:
|
||||
@@ -1177,13 +1491,13 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'Location of SAS program'
|
||||
description: 'Location of SAS or JS code'
|
||||
in: query
|
||||
name: _program
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder/some.file
|
||||
example: /Projects/myApp/some/program
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
@@ -1194,6 +1508,9 @@ servers:
|
||||
-
|
||||
url: /
|
||||
tags:
|
||||
-
|
||||
name: Info
|
||||
description: 'Get Server Info'
|
||||
-
|
||||
name: Session
|
||||
description: 'Get Session information'
|
||||
@@ -1218,3 +1535,6 @@ tags:
|
||||
-
|
||||
name: CODE
|
||||
description: 'Operations on SAS code'
|
||||
-
|
||||
name: Web
|
||||
description: 'Operations on Web'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
CompileTree,
|
||||
createFile,
|
||||
loadDependenciesFile,
|
||||
readFile,
|
||||
@@ -18,7 +19,8 @@ const compiledSystemInit = async (systemInit: string) =>
|
||||
macroFolders: [],
|
||||
buildSourceFolder: '',
|
||||
binaryFolders: [],
|
||||
macroCorePath
|
||||
macroCorePath,
|
||||
compileTree: new CompileTree('') // dummy compileTree
|
||||
}))
|
||||
|
||||
const createSysInitFile = async () => {
|
||||
|
||||
@@ -5,23 +5,12 @@
|
||||
_before_ any user-provided content.
|
||||
|
||||
A number of useful CORE macros are also compiled below, so that they can be
|
||||
available "out of the box".
|
||||
available by default for Stored Programs.
|
||||
|
||||
Note that the full CORE library is available to sessions in SASjs Studio.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mcf_stpsrv_header.sas
|
||||
@li mf_getuser.sas
|
||||
@li mf_getvarlist.sas
|
||||
@li mf_mkdir.sas
|
||||
@li mf_nobs.sas
|
||||
@li mf_uid.sas
|
||||
@li mfs_httpheader.sas
|
||||
@li mp_dirlist.sas
|
||||
@li mp_ds2ddl.sas
|
||||
@li mp_ds2md.sas
|
||||
@li mp_getdbml.sas
|
||||
@li mp_init.sas
|
||||
@li mp_makedata.sas
|
||||
@li mp_zip.sas
|
||||
|
||||
@li ms_webout.sas
|
||||
**/
|
||||
|
||||
|
||||
118
api/src/app.ts
118
api/src/app.ts
@@ -1,43 +1,136 @@
|
||||
import path from 'path'
|
||||
import express, { ErrorRequestHandler } from 'express'
|
||||
import csrf from 'csurf'
|
||||
import session from 'express-session'
|
||||
import MongoStore from 'connect-mongo'
|
||||
import morgan from 'morgan'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import dotenv from 'dotenv'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import {
|
||||
connectDB,
|
||||
copySASjsCore,
|
||||
getWebBuildFolderPath,
|
||||
CorsType,
|
||||
getWebBuildFolder,
|
||||
HelmetCoepType,
|
||||
instantiateLogger,
|
||||
loadAppStreamConfig,
|
||||
ModeType,
|
||||
ProtocolType,
|
||||
ReturnCode,
|
||||
setProcessVariables,
|
||||
setupFolders
|
||||
setupFolders,
|
||||
verifyEnvVariables
|
||||
} from './utils'
|
||||
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
instantiateLogger()
|
||||
|
||||
if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
||||
|
||||
const app = express()
|
||||
|
||||
const { MODE, CORS, WHITELIST } = process.env
|
||||
app.use(cookieParser())
|
||||
|
||||
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
||||
const whiteList: string[] = []
|
||||
WHITELIST?.split(' ')?.forEach((url) => {
|
||||
if (url.startsWith('http'))
|
||||
// removing trailing slash of URLs listing for CORS
|
||||
whiteList.push(url.replace(/\/$/, ''))
|
||||
const {
|
||||
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 = {
|
||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
|
||||
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
||||
HELMET_CSP_CONFIG_PATH
|
||||
)
|
||||
if (PROTOCOL === ProtocolType.HTTP)
|
||||
cspConfigJson['upgrade-insecure-requests'] = null
|
||||
|
||||
/***********************************
|
||||
* CSRF Protection *
|
||||
***********************************/
|
||||
export const csrfProtection = csrf({ cookie: cookieOptions })
|
||||
|
||||
/***********************************
|
||||
* Handle security and origin *
|
||||
***********************************/
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
||||
...cspConfigJson
|
||||
}
|
||||
},
|
||||
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
||||
})
|
||||
)
|
||||
|
||||
/***********************************
|
||||
* Enabling CORS *
|
||||
***********************************/
|
||||
if (CORS === CorsType.ENABLED) {
|
||||
const whiteList: string[] = []
|
||||
WHITELIST?.split(' ')
|
||||
?.filter((url) => !!url)
|
||||
.forEach((url) => {
|
||||
if (url.startsWith('http'))
|
||||
// removing trailing slash of URLs listing for CORS
|
||||
whiteList.push(url.replace(/\/$/, ''))
|
||||
})
|
||||
|
||||
console.log('All CORS Requests are enabled for:', whiteList)
|
||||
app.use(cors({ credentials: true, origin: whiteList }))
|
||||
}
|
||||
|
||||
app.use(cookieParser())
|
||||
app.use(morgan('tiny'))
|
||||
/***********************************
|
||||
* DB Connection & *
|
||||
* Express Sessions *
|
||||
* With Mongo Store *
|
||||
***********************************/
|
||||
if (MODE === ModeType.Server) {
|
||||
let store: MongoStore | undefined
|
||||
|
||||
// NOTE: when exporting app.js as agent for supertest
|
||||
// we should exclude connecting to the real database
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
||||
|
||||
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
|
||||
}
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: process.env.SESSION_SECRET as string,
|
||||
saveUninitialized: false, // don't create session until something stored
|
||||
resave: false, //don't save session if unmodified
|
||||
store,
|
||||
cookie: cookieOptions
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
app.use(express.json({ limit: '100mb' }))
|
||||
app.use(express.static(path.join(__dirname, '../public')))
|
||||
|
||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
if (err.code === 'EBADCSRFTOKEN')
|
||||
return res.status(400).send('Invalid CSRF token!')
|
||||
|
||||
console.error(err.stack)
|
||||
res.status(500).send('Something broke!')
|
||||
}
|
||||
@@ -55,10 +148,9 @@ export default setProcessVariables().then(async () => {
|
||||
|
||||
// should be served after setting up web route
|
||||
// index.html needs to be injected with some js script.
|
||||
app.use(express.static(getWebBuildFolderPath()))
|
||||
app.use(express.static(getWebBuildFolder()))
|
||||
|
||||
app.use(onError)
|
||||
|
||||
await connectDB()
|
||||
return app
|
||||
})
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import User from '../model/User'
|
||||
import { InfoJWT } from '../types'
|
||||
import {
|
||||
generateAccessToken,
|
||||
generateAuthCode,
|
||||
generateRefreshToken,
|
||||
removeTokensInDB,
|
||||
saveTokensInDB
|
||||
@@ -24,20 +22,6 @@ export class AuthController {
|
||||
static deleteCode = (userId: number, clientId: string) =>
|
||||
delete AuthController.authCodes[userId][clientId]
|
||||
|
||||
/**
|
||||
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
|
||||
*
|
||||
*/
|
||||
@Example<AuthorizeResponse>({
|
||||
code: 'someRandomCryptoString'
|
||||
})
|
||||
@Post('/authorize')
|
||||
public async authorize(
|
||||
@Body() body: AuthorizePayload
|
||||
): Promise<AuthorizeResponse> {
|
||||
return authorize(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Accepts client/auth code and returns access/refresh tokens
|
||||
*
|
||||
@@ -78,30 +62,6 @@ export class AuthController {
|
||||
}
|
||||
}
|
||||
|
||||
const authorize = async (data: any): Promise<AuthorizeResponse> => {
|
||||
const { username, password, clientId } = data
|
||||
|
||||
// Authenticate User
|
||||
const user = await User.findOne({ username })
|
||||
if (!user) throw new Error('Username is not found.')
|
||||
|
||||
const validPass = user.comparePassword(password)
|
||||
if (!validPass) throw new Error('Invalid password.')
|
||||
|
||||
// generate authorization code against clientId
|
||||
const userInfo: InfoJWT = {
|
||||
clientId,
|
||||
userId: user.id
|
||||
}
|
||||
const code = AuthController.saveCode(
|
||||
user.id,
|
||||
clientId,
|
||||
generateAuthCode(userInfo)
|
||||
)
|
||||
|
||||
return { code }
|
||||
}
|
||||
|
||||
const token = async (data: any): Promise<TokenResponse> => {
|
||||
const { clientId, code } = data
|
||||
|
||||
@@ -139,32 +99,6 @@ const logout = async (userInfo: InfoJWT) => {
|
||||
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
||||
}
|
||||
|
||||
interface AuthorizePayload {
|
||||
/**
|
||||
* Username for user
|
||||
* @example "secretuser"
|
||||
*/
|
||||
username: string
|
||||
/**
|
||||
* Password for user
|
||||
* @example "secretpassword"
|
||||
*/
|
||||
password: string
|
||||
/**
|
||||
* Client ID
|
||||
* @example "clientID1"
|
||||
*/
|
||||
clientId: string
|
||||
}
|
||||
|
||||
interface AuthorizeResponse {
|
||||
/**
|
||||
* Authorization code
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
code: string
|
||||
}
|
||||
|
||||
interface TokenPayload {
|
||||
/**
|
||||
* Client ID
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import express from 'express'
|
||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
||||
import { PreProgramVars } from '../types'
|
||||
import { ExecuteReturnJsonResponse } from '.'
|
||||
import { parseLogToArray } from '../utils'
|
||||
import {
|
||||
getPreProgramVariables,
|
||||
getUserAutoExec,
|
||||
ModeType,
|
||||
parseLogToArray,
|
||||
RunTimeType
|
||||
} from '../utils'
|
||||
|
||||
interface ExecuteSASCodePayload {
|
||||
interface ExecuteCodePayload {
|
||||
/**
|
||||
* Code of SAS program
|
||||
* @example "* SAS Code HERE;"
|
||||
* Code of program
|
||||
* @example "* Code HERE;"
|
||||
*/
|
||||
code: string
|
||||
/**
|
||||
* runtime for program
|
||||
* @example "js"
|
||||
*/
|
||||
runTime: RunTimeType
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@@ -22,24 +32,34 @@ export class CodeController {
|
||||
* @summary Run SAS Code and returns log
|
||||
*/
|
||||
@Post('/execute')
|
||||
public async executeSASCode(
|
||||
public async executeCode(
|
||||
@Request() request: express.Request,
|
||||
@Body() body: ExecuteSASCodePayload
|
||||
@Body() body: ExecuteCodePayload
|
||||
): Promise<ExecuteReturnJsonResponse> {
|
||||
return executeSASCode(request, body)
|
||||
return executeCode(request, body)
|
||||
}
|
||||
}
|
||||
|
||||
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
||||
const executeCode = async (
|
||||
req: express.Request,
|
||||
{ code, runTime }: ExecuteCodePayload
|
||||
) => {
|
||||
const { user } = req
|
||||
const userAutoExec =
|
||||
process.env.MODE === ModeType.Server
|
||||
? user?.autoExec
|
||||
: await getUserAutoExec()
|
||||
|
||||
try {
|
||||
const { webout, log, httpHeaders } =
|
||||
(await new ExecutionController().executeProgram(
|
||||
code,
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, _debug: 131 },
|
||||
undefined,
|
||||
true
|
||||
)) as ExecuteReturnJson
|
||||
(await new ExecutionController().executeProgram({
|
||||
program: code,
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: { ...req.query, _debug: 131 },
|
||||
otherArgs: { userAutoExec },
|
||||
returnJson: true,
|
||||
runTime: runTime
|
||||
})) as ExecuteReturnJson
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
@@ -56,16 +76,3 @@ const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getPreProgramVariables = (req: any): PreProgramVars => {
|
||||
const host = req.get('host')
|
||||
const protocol = req.protocol + '://'
|
||||
const { user, accessToken } = req
|
||||
return {
|
||||
username: user.username,
|
||||
userId: user.userId,
|
||||
displayName: user.displayName,
|
||||
serverUrl: protocol + host,
|
||||
accessToken
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
Patch,
|
||||
UploadedFile,
|
||||
FormField,
|
||||
Delete
|
||||
Delete,
|
||||
Hidden
|
||||
} from 'tsoa'
|
||||
import {
|
||||
fileExists,
|
||||
@@ -22,15 +23,16 @@ import {
|
||||
createFolder,
|
||||
deleteFile as deleteFileOnSystem,
|
||||
folderExists,
|
||||
listFilesAndSubFoldersInFolder,
|
||||
listFilesInFolder,
|
||||
listSubFoldersInFolder,
|
||||
isFolder
|
||||
isFolder,
|
||||
FileTree,
|
||||
isFileTree
|
||||
} from '@sasjs/utils'
|
||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||
|
||||
import { FileTree, isFileTree, TreeNode } from '../types'
|
||||
import { getTmpFilesFolderPath } from '../utils'
|
||||
import { TreeNode } from '../types'
|
||||
import { getFilesFolder } from '../utils'
|
||||
|
||||
interface DeployPayload {
|
||||
appLoc: string
|
||||
@@ -93,6 +95,26 @@ export class DriveController {
|
||||
return deploy(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts JSON file and zipped compressed JSON file as well.
|
||||
* Compressed file should only contain one JSON file and should have same name
|
||||
* as of compressed file e.g. deploy.JSON should be compressed to deploy.JSON.zip
|
||||
* Any other file or JSON file in zipped will be ignored!
|
||||
*
|
||||
* @summary Creates/updates files within SASjs Drive using uploaded JSON/compressed JSON file.
|
||||
*
|
||||
*/
|
||||
@Example<DeployResponse>(successDeployResponse)
|
||||
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
|
||||
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
|
||||
@Post('/deploy/upload')
|
||||
public async deployUpload(
|
||||
@UploadedFile() file: Express.Multer.File, // passing here for API docs
|
||||
@Query() @Hidden() body?: DeployPayload // Hidden decorator has be optional
|
||||
): Promise<DeployResponse> {
|
||||
return deploy(body!)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Get file from SASjs Drive
|
||||
@@ -197,12 +219,12 @@ const getFileTree = () => {
|
||||
}
|
||||
|
||||
const deploy = async (data: DeployPayload) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
||||
|
||||
const appLocPath = path
|
||||
.join(getTmpFilesFolderPath(), ...appLocParts)
|
||||
.join(getFilesFolder(), ...appLocParts)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!appLocPath.includes(driveFilesPath)) {
|
||||
@@ -221,10 +243,10 @@ const deploy = async (data: DeployPayload) => {
|
||||
}
|
||||
|
||||
const getFile = async (req: express.Request, filePath: string) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const filePathFull = path
|
||||
.join(getTmpFilesFolderPath(), filePath)
|
||||
.join(getFilesFolder(), filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
@@ -244,11 +266,11 @@ const getFile = async (req: express.Request, filePath: string) => {
|
||||
}
|
||||
|
||||
const getFolder = async (folderPath?: string) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
if (folderPath) {
|
||||
const folderPathFull = path
|
||||
.join(getTmpFilesFolderPath(), folderPath)
|
||||
.join(getFilesFolder(), folderPath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!folderPathFull.includes(driveFilesPath)) {
|
||||
@@ -274,10 +296,10 @@ const getFolder = async (folderPath?: string) => {
|
||||
}
|
||||
|
||||
const deleteFile = async (filePath: string) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const filePathFull = path
|
||||
.join(getTmpFilesFolderPath(), filePath)
|
||||
.join(getFilesFolder(), filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
@@ -297,7 +319,7 @@ const saveFile = async (
|
||||
filePath: string,
|
||||
multerFile: Express.Multer.File
|
||||
): Promise<GetFileResponse> => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const filePathFull = path
|
||||
.join(driveFilesPath, filePath)
|
||||
@@ -322,7 +344,7 @@ const updateFile = async (
|
||||
filePath: string,
|
||||
multerFile: Express.Multer.File
|
||||
): Promise<GetFileResponse> => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const filePathFull = path
|
||||
.join(driveFilesPath, filePath)
|
||||
|
||||
@@ -14,7 +14,7 @@ import Group, { GroupPayload } from '../model/Group'
|
||||
import User from '../model/User'
|
||||
import { UserResponse } from './user'
|
||||
|
||||
interface GroupResponse {
|
||||
export interface GroupResponse {
|
||||
groupId: number
|
||||
name: string
|
||||
description: string
|
||||
@@ -28,6 +28,11 @@ interface GroupDetailsResponse {
|
||||
users: UserResponse[]
|
||||
}
|
||||
|
||||
interface GetGroupBy {
|
||||
groupId?: number
|
||||
name?: string
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/group')
|
||||
@Tags('Group')
|
||||
@@ -66,6 +71,18 @@ export class GroupController {
|
||||
return createGroup(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get list of members of a group (userName). All users can request this.
|
||||
* @param name The group's name
|
||||
* @example dcgroup
|
||||
*/
|
||||
@Get('by/groupname/{name}')
|
||||
public async getGroupByGroupName(
|
||||
@Path() name: string
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return getGroup({ name })
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get list of members of a group (userName). All users can request this.
|
||||
* @param groupId The group's identifier
|
||||
@@ -75,7 +92,7 @@ export class GroupController {
|
||||
public async getGroup(
|
||||
@Path() groupId: number
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return getGroup(groupId)
|
||||
return getGroup({ groupId })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,9 +146,13 @@ export class GroupController {
|
||||
*/
|
||||
@Delete('{groupId}')
|
||||
public async deleteGroup(@Path() groupId: number) {
|
||||
const { deletedCount } = await Group.deleteOne({ groupId })
|
||||
if (deletedCount) return
|
||||
throw new Error('No Group deleted!')
|
||||
const group = await Group.findOne({ groupId })
|
||||
if (group) return await group.remove()
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'Group not found.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +166,15 @@ const createGroup = async ({
|
||||
description,
|
||||
isActive
|
||||
}: GroupPayload): Promise<GroupDetailsResponse> => {
|
||||
// Checking if user is already in the database
|
||||
const groupnameExist = await Group.findOne({ name })
|
||||
if (groupnameExist)
|
||||
throw {
|
||||
code: 409,
|
||||
status: 'Conflict',
|
||||
message: 'Group name already exists.'
|
||||
}
|
||||
|
||||
const group = new Group({
|
||||
name,
|
||||
description,
|
||||
@@ -162,15 +192,20 @@ const createGroup = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const getGroup = async (groupId: number): Promise<GroupDetailsResponse> => {
|
||||
const getGroup = async (findBy: GetGroupBy): Promise<GroupDetailsResponse> => {
|
||||
const group = (await Group.findOne(
|
||||
{ groupId },
|
||||
findBy,
|
||||
'groupId name description isActive users -_id'
|
||||
).populate(
|
||||
'users',
|
||||
'id username displayName -_id'
|
||||
)) as unknown as GroupDetailsResponse
|
||||
if (!group) throw new Error('Group not found.')
|
||||
if (!group)
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'Group not found.'
|
||||
}
|
||||
|
||||
return {
|
||||
groupId: group.groupId,
|
||||
@@ -199,16 +234,34 @@ const updateUsersListInGroup = async (
|
||||
action: 'addUser' | 'removeUser'
|
||||
): Promise<GroupDetailsResponse> => {
|
||||
const group = await Group.findOne({ groupId })
|
||||
if (!group) throw new Error('Group not found.')
|
||||
if (!group)
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'Group not found.'
|
||||
}
|
||||
|
||||
const user = await User.findOne({ id: userId })
|
||||
if (!user) throw new Error('User not found.')
|
||||
if (!user)
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'Not Found',
|
||||
message: 'User not found.'
|
||||
}
|
||||
|
||||
const updatedGroup = (action === 'addUser'
|
||||
? await group.addUser(user._id)
|
||||
: await group.removeUser(user._id)) as unknown as GroupDetailsResponse
|
||||
|
||||
if (!updatedGroup) throw new Error('Unable to update group')
|
||||
if (!updatedGroup)
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: 'Unable to update group.'
|
||||
}
|
||||
|
||||
if (action === 'addUser') user.addGroup(group._id)
|
||||
else user.removeGroup(group._id)
|
||||
|
||||
return {
|
||||
groupId: updatedGroup.groupId,
|
||||
|
||||
@@ -3,6 +3,8 @@ export * from './client'
|
||||
export * from './code'
|
||||
export * from './drive'
|
||||
export * from './group'
|
||||
export * from './info'
|
||||
export * from './session'
|
||||
export * from './stp'
|
||||
export * from './user'
|
||||
export * from './web'
|
||||
|
||||
39
api/src/controllers/info.ts
Normal file
39
api/src/controllers/info.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Route, Tags, Example, Get } from 'tsoa'
|
||||
|
||||
export interface InfoResponse {
|
||||
mode: string
|
||||
cors: string
|
||||
whiteList: string[]
|
||||
protocol: string
|
||||
runTimes: string[]
|
||||
}
|
||||
|
||||
@Route('SASjsApi/info')
|
||||
@Tags('Info')
|
||||
export class InfoController {
|
||||
/**
|
||||
* @summary Get server info (mode, cors, whiteList, protocol).
|
||||
*
|
||||
*/
|
||||
@Example<InfoResponse>({
|
||||
mode: 'desktop',
|
||||
cors: 'enable',
|
||||
whiteList: ['http://example.com', 'http://example2.com'],
|
||||
protocol: 'http',
|
||||
runTimes: ['sas', 'js']
|
||||
})
|
||||
@Get('/')
|
||||
public info(): InfoResponse {
|
||||
const response = {
|
||||
mode: process.env.MODE ?? 'desktop',
|
||||
cors:
|
||||
process.env.CORS ||
|
||||
(process.env.MODE === 'server' ? 'disable' : 'enable'),
|
||||
whiteList:
|
||||
process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [],
|
||||
protocol: process.env.PROTOCOL ?? 'http',
|
||||
runTimes: process.runTimes
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,18 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { getSessionController } from './'
|
||||
import {
|
||||
readFile,
|
||||
fileExists,
|
||||
createFile,
|
||||
moveFile,
|
||||
readFileBinary
|
||||
} from '@sasjs/utils'
|
||||
getSASSessionController,
|
||||
getJSSessionController,
|
||||
processProgram
|
||||
} from './'
|
||||
import { readFile, fileExists, createFile, readFileBinary } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||
import {
|
||||
extractHeaders,
|
||||
generateFileUploadSasCode,
|
||||
getTmpFilesFolderPath,
|
||||
getTmpMacrosPath,
|
||||
getFilesFolder,
|
||||
HTTPHeaders,
|
||||
isDebugOn
|
||||
isDebugOn,
|
||||
RunTimeType
|
||||
} from '../../utils'
|
||||
|
||||
export interface ExecutionVars {
|
||||
@@ -33,39 +30,56 @@ export interface ExecuteReturnJson {
|
||||
log?: string
|
||||
}
|
||||
|
||||
export class ExecutionController {
|
||||
async executeFile(
|
||||
programPath: string,
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean,
|
||||
session?: Session
|
||||
) {
|
||||
if (!(await fileExists(programPath)))
|
||||
throw 'ExecutionController: SAS file does not exist.'
|
||||
interface ExecuteFileParams {
|
||||
programPath: string
|
||||
preProgramVariables: PreProgramVars
|
||||
vars: ExecutionVars
|
||||
otherArgs?: any
|
||||
returnJson?: boolean
|
||||
session?: Session
|
||||
runTime: RunTimeType
|
||||
}
|
||||
|
||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
||||
program: string
|
||||
}
|
||||
|
||||
export class ExecutionController {
|
||||
async executeFile({
|
||||
programPath,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
otherArgs,
|
||||
returnJson,
|
||||
session,
|
||||
runTime
|
||||
}: ExecuteFileParams) {
|
||||
const program = await readFile(programPath)
|
||||
|
||||
return this.executeProgram(
|
||||
return this.executeProgram({
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
otherArgs,
|
||||
returnJson,
|
||||
session
|
||||
)
|
||||
session,
|
||||
runTime
|
||||
})
|
||||
}
|
||||
|
||||
async executeProgram(
|
||||
program: string,
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean,
|
||||
sessionByFileUpload?: Session
|
||||
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||
const sessionController = getSessionController()
|
||||
async executeProgram({
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
otherArgs,
|
||||
returnJson,
|
||||
session: sessionByFileUpload,
|
||||
runTime
|
||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||
const sessionController =
|
||||
runTime === RunTimeType.SAS
|
||||
? getSASSessionController()
|
||||
: getJSSessionController()
|
||||
|
||||
const session =
|
||||
sessionByFileUpload ?? (await sessionController.getSession())
|
||||
@@ -75,89 +89,35 @@ export class ExecutionController {
|
||||
const logPath = path.join(session.path, 'log.log')
|
||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||
const weboutPath = path.join(session.path, 'webout.txt')
|
||||
const tokenFile = path.join(session.path, 'accessToken.txt')
|
||||
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
||||
|
||||
await createFile(weboutPath, '')
|
||||
await createFile(
|
||||
tokenFile,
|
||||
preProgramVariables?.accessToken ?? 'accessToken'
|
||||
preProgramVariables?.httpHeaders.join('\n') ?? ''
|
||||
)
|
||||
|
||||
const varStatments = Object.keys(vars).reduce(
|
||||
(computed: string, key: string) =>
|
||||
`${computed}%let ${key}=${vars[key]};\n`,
|
||||
''
|
||||
await processProgram(
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
session,
|
||||
weboutPath,
|
||||
tokenFile,
|
||||
runTime,
|
||||
logPath,
|
||||
otherArgs
|
||||
)
|
||||
|
||||
const preProgramVarStatments = `
|
||||
%let _sasjs_tokenfile=${tokenFile};
|
||||
%let _sasjs_username=${preProgramVariables?.username};
|
||||
%let _sasjs_userid=${preProgramVariables?.userId};
|
||||
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
||||
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||
%let _metaperson=&_sasjs_displayname;
|
||||
%let _metauser=&_sasjs_username;
|
||||
%let sasjsprocessmode=Stored Program;
|
||||
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
||||
|
||||
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
||||
%macro _sasjs_server_init();
|
||||
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
|
||||
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
||||
%mend;
|
||||
%_sasjs_server_init()
|
||||
`
|
||||
|
||||
program = `
|
||||
options insert=(SASAUTOS="${getTmpMacrosPath()}");
|
||||
|
||||
/* runtime vars */
|
||||
${varStatments}
|
||||
filename _webout "${weboutPath}" mod;
|
||||
|
||||
/* dynamic user-provided vars */
|
||||
${preProgramVarStatments}
|
||||
|
||||
/* actual job code */
|
||||
${program}`
|
||||
|
||||
// if no files are uploaded filesNamesMap will be undefined
|
||||
if (otherArgs?.filesNamesMap) {
|
||||
const uploadSasCode = await generateFileUploadSasCode(
|
||||
otherArgs.filesNamesMap,
|
||||
session.path
|
||||
)
|
||||
|
||||
//If sas code for the file is generated it will be appended to the top of sasCode
|
||||
if (uploadSasCode.length > 0) {
|
||||
program = `${uploadSasCode}` + program
|
||||
}
|
||||
}
|
||||
|
||||
const codePath = path.join(session.path, 'code.sas')
|
||||
|
||||
// Creating this file in a RUNNING session will break out
|
||||
// the autoexec loop and actually execute the program
|
||||
// but - given it will take several milliseconds to create
|
||||
// (which can mean SAS trying to run a partial program, or
|
||||
// failing due to file lock) we first create the file THEN
|
||||
// we rename it.
|
||||
await createFile(codePath + '.bkp', program)
|
||||
await moveFile(codePath + '.bkp', codePath)
|
||||
|
||||
// we now need to poll the session status
|
||||
while (!session.completed) {
|
||||
await delay(50)
|
||||
}
|
||||
|
||||
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
|
||||
const headersContent = (await fileExists(headersPath))
|
||||
? await readFile(headersPath)
|
||||
: ''
|
||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||
const fileResponse: boolean =
|
||||
httpHeaders.hasOwnProperty('content-type') && !returnJson
|
||||
httpHeaders.hasOwnProperty('content-type') &&
|
||||
!returnJson && // not a POST Request
|
||||
!isDebugOn(vars) // Debug is not enabled
|
||||
|
||||
const webout = (await fileExists(weboutPath))
|
||||
? fileResponse
|
||||
@@ -178,11 +138,10 @@ ${program}`
|
||||
|
||||
return {
|
||||
httpHeaders,
|
||||
result: fileResponse
|
||||
? webout
|
||||
: isDebugOn(vars) || session.crashed
|
||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||
: webout
|
||||
result:
|
||||
isDebugOn(vars) || session.crashed
|
||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||
: webout
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +149,7 @@ ${program}`
|
||||
const root: TreeNode = {
|
||||
name: 'files',
|
||||
relativePath: '',
|
||||
absolutePath: getTmpFilesFolderPath(),
|
||||
absolutePath: getFilesFolder(),
|
||||
children: []
|
||||
}
|
||||
|
||||
@@ -223,5 +182,3 @@ ${program}`
|
||||
return root
|
||||
}
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { Request, RequestHandler } from 'express'
|
||||
import multer from 'multer'
|
||||
import { uuidv4 } from '@sasjs/utils'
|
||||
import { getSessionController } from '.'
|
||||
import { getSASSessionController, getJSSessionController } from '.'
|
||||
import {
|
||||
executeProgramRawValidation,
|
||||
getRunTimeAndFilePath,
|
||||
RunTimeType
|
||||
} from '../../utils'
|
||||
|
||||
export class FileUploadController {
|
||||
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
|
||||
cb(null, req.sasSession.path)
|
||||
cb(null, req.sasjsSession?.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
|
||||
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
|
||||
}
|
||||
@@ -18,16 +24,37 @@ export class FileUploadController {
|
||||
|
||||
//It will intercept request and generate unique uuid to be used as a subfolder name
|
||||
//that will store the files uploaded
|
||||
public preUploadMiddleware = async (req: any, res: any, next: any) => {
|
||||
let session
|
||||
public preUploadMiddleware: RequestHandler = async (req, res, next) => {
|
||||
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
||||
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
||||
|
||||
const sessionController = getSessionController()
|
||||
session = await sessionController.getSession()
|
||||
if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||
|
||||
const programPath = (query?._program ?? body?._program) as string
|
||||
|
||||
let runTime
|
||||
|
||||
try {
|
||||
;({ runTime } = await getRunTimeAndFilePath(programPath))
|
||||
} catch (err: any) {
|
||||
res.status(400).send({
|
||||
status: 'failure',
|
||||
message: 'Job execution failed',
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
})
|
||||
}
|
||||
|
||||
const sessionController =
|
||||
runTime === RunTimeType.SAS
|
||||
? getSASSessionController()
|
||||
: getJSSessionController()
|
||||
|
||||
const session = await sessionController.getSession()
|
||||
// marking consumed true, so that it's not available
|
||||
// as readySession for any other request
|
||||
session.consumed = true
|
||||
|
||||
req.sasSession = session
|
||||
req.sasjsSession = session
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Session } from '../../types'
|
||||
import { promisify } from 'util'
|
||||
import { execFile } from 'child_process'
|
||||
import {
|
||||
getTmpSessionsFolderPath,
|
||||
getSessionsFolder,
|
||||
generateUniqueFileName,
|
||||
sysInitCompiledPath
|
||||
} from '../../utils'
|
||||
@@ -12,18 +12,19 @@ import {
|
||||
createFile,
|
||||
fileExists,
|
||||
generateTimestamp,
|
||||
readFile,
|
||||
moveFile
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
|
||||
export class SessionController {
|
||||
private sessions: Session[] = []
|
||||
abstract class SessionController {
|
||||
protected sessions: Session[] = []
|
||||
|
||||
private getReadySessions = (): Session[] =>
|
||||
protected getReadySessions = (): Session[] =>
|
||||
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
||||
|
||||
protected abstract createSession(): Promise<Session>
|
||||
|
||||
public async getSession() {
|
||||
const readySessions = this.getReadySessions()
|
||||
|
||||
@@ -35,12 +36,15 @@ export class SessionController {
|
||||
|
||||
return session
|
||||
}
|
||||
}
|
||||
|
||||
private async createSession(): Promise<Session> {
|
||||
export class SASSessionController extends SessionController {
|
||||
protected async createSession(): Promise<Session> {
|
||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||
|
||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||
// death time of session is 15 mins from creation
|
||||
const deathTimeStamp = (
|
||||
parseInt(creationTimeStamp) +
|
||||
15 * 60 * 1000 -
|
||||
@@ -82,6 +86,8 @@ ${autoExecContent}`
|
||||
// however we also need a promise so that we can update the
|
||||
// session array to say that it has (eventually) finished.
|
||||
|
||||
// Additional windows specific options to avoid the desktop popups.
|
||||
|
||||
execFilePromise(process.sasLoc, [
|
||||
'-SYSIN',
|
||||
codePath,
|
||||
@@ -93,7 +99,11 @@ ${autoExecContent}`
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
autoExecPath,
|
||||
process.platform === 'win32' ? '-nosplash' : ''
|
||||
'-ENCODING',
|
||||
'UTF-8',
|
||||
process.platform === 'win32' ? '-nosplash' : '',
|
||||
process.platform === 'win32' ? '-icon' : '',
|
||||
process.platform === 'win32' ? '-nologo' : ''
|
||||
])
|
||||
.then(() => {
|
||||
session.completed = true
|
||||
@@ -140,7 +150,9 @@ ${autoExecContent}`
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
setTimeout(async () => {
|
||||
if (session.inUse) {
|
||||
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
@@ -150,12 +162,52 @@ ${autoExecContent}`
|
||||
}
|
||||
}
|
||||
|
||||
export const getSessionController = (): SessionController => {
|
||||
if (process.sessionController) return process.sessionController
|
||||
export class JSSessionController extends SessionController {
|
||||
protected async createSession(): Promise<Session> {
|
||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||
|
||||
process.sessionController = new SessionController()
|
||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||
// death time of session is 15 mins from creation
|
||||
const deathTimeStamp = (
|
||||
parseInt(creationTimeStamp) +
|
||||
15 * 60 * 1000 -
|
||||
1000
|
||||
).toString()
|
||||
|
||||
return process.sessionController
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
ready: true,
|
||||
inUse: true,
|
||||
consumed: false,
|
||||
completed: false,
|
||||
creationTimeStamp,
|
||||
deathTimeStamp,
|
||||
path: sessionFolder
|
||||
}
|
||||
|
||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||
await createFile(headersPath, 'Content-type: application/json')
|
||||
|
||||
this.sessions.push(session)
|
||||
return session
|
||||
}
|
||||
}
|
||||
|
||||
export const getSASSessionController = (): SASSessionController => {
|
||||
if (process.sasSessionController) return process.sasSessionController
|
||||
|
||||
process.sasSessionController = new SASSessionController()
|
||||
|
||||
return process.sasSessionController
|
||||
}
|
||||
|
||||
export const getJSSessionController = (): JSSessionController => {
|
||||
if (process.jsSessionController) return process.jsSessionController
|
||||
|
||||
process.jsSessionController = new JSSessionController()
|
||||
|
||||
return process.jsSessionController
|
||||
}
|
||||
|
||||
const autoExecContent = `
|
||||
|
||||
64
api/src/controllers/internal/createJSProgram.ts
Normal file
64
api/src/controllers/internal/createJSProgram.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadJSCode } from '../../utils'
|
||||
import { ExecutionVars } from './'
|
||||
|
||||
export const createJSProgram = async (
|
||||
program: string,
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
session: Session,
|
||||
weboutPath: string,
|
||||
tokenFile: string,
|
||||
otherArgs?: any
|
||||
) => {
|
||||
const varStatments = Object.keys(vars).reduce(
|
||||
(computed: string, key: string) =>
|
||||
`${computed}const ${key} = '${vars[key]}';\n`,
|
||||
''
|
||||
)
|
||||
|
||||
const preProgramVarStatments = `
|
||||
let _webout = '';
|
||||
const weboutPath = '${weboutPath}';
|
||||
const _sasjs_tokenfile = '${tokenFile}';
|
||||
const _sasjs_username = '${preProgramVariables?.username}';
|
||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
||||
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
||||
const _metaperson = _sasjs_displayname;
|
||||
const _metauser = _sasjs_username;
|
||||
const sasjsprocessmode = 'Stored Program';
|
||||
`
|
||||
|
||||
const requiredModules = `const fs = require('fs')`
|
||||
|
||||
program = `
|
||||
/* runtime vars */
|
||||
${varStatments}
|
||||
|
||||
/* dynamic user-provided vars */
|
||||
${preProgramVarStatments}
|
||||
|
||||
/* actual job code */
|
||||
${program}
|
||||
|
||||
/* write webout file only if webout exists*/
|
||||
if (_webout) {
|
||||
fs.writeFile(weboutPath, _webout, function (err) {
|
||||
if (err) throw err;
|
||||
})
|
||||
}
|
||||
`
|
||||
// if no files are uploaded filesNamesMap will be undefined
|
||||
if (otherArgs?.filesNamesMap) {
|
||||
const uploadJSCode = await generateFileUploadJSCode(
|
||||
otherArgs.filesNamesMap,
|
||||
session.path
|
||||
)
|
||||
|
||||
//If js code for the file is generated it will be appended to the top of jsCode
|
||||
if (uploadJSCode.length > 0) {
|
||||
program = `${uploadJSCode}\n` + program
|
||||
}
|
||||
}
|
||||
return requiredModules + program
|
||||
}
|
||||
69
api/src/controllers/internal/createSASProgram.ts
Normal file
69
api/src/controllers/internal/createSASProgram.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { generateFileUploadSasCode, getMacrosFolder } from '../../utils'
|
||||
import { ExecutionVars } from './'
|
||||
|
||||
export const createSASProgram = async (
|
||||
program: string,
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
session: Session,
|
||||
weboutPath: string,
|
||||
tokenFile: string,
|
||||
otherArgs?: any
|
||||
) => {
|
||||
const varStatments = Object.keys(vars).reduce(
|
||||
(computed: string, key: string) => `${computed}%let ${key}=${vars[key]};\n`,
|
||||
''
|
||||
)
|
||||
|
||||
const preProgramVarStatments = `
|
||||
%let _sasjs_tokenfile=${tokenFile};
|
||||
%let _sasjs_username=${preProgramVariables?.username};
|
||||
%let _sasjs_userid=${preProgramVariables?.userId};
|
||||
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
||||
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||
%let _metaperson=&_sasjs_displayname;
|
||||
%let _metauser=&_sasjs_username;
|
||||
%let sasjsprocessmode=Stored Program;
|
||||
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
||||
|
||||
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
||||
%macro _sasjs_server_init();
|
||||
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
|
||||
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
||||
%mend;
|
||||
%_sasjs_server_init()
|
||||
`
|
||||
|
||||
program = `
|
||||
options insert=(SASAUTOS="${getMacrosFolder()}");
|
||||
|
||||
/* runtime vars */
|
||||
${varStatments}
|
||||
filename _webout "${weboutPath}" mod;
|
||||
|
||||
/* dynamic user-provided vars */
|
||||
${preProgramVarStatments}
|
||||
|
||||
/* user autoexec starts */
|
||||
${otherArgs?.userAutoExec ?? ''}
|
||||
/* user autoexec ends */
|
||||
|
||||
/* actual job code */
|
||||
${program}`
|
||||
|
||||
// if no files are uploaded filesNamesMap will be undefined
|
||||
if (otherArgs?.filesNamesMap) {
|
||||
const uploadSasCode = await generateFileUploadSasCode(
|
||||
otherArgs.filesNamesMap,
|
||||
session.path
|
||||
)
|
||||
|
||||
//If sas code for the file is generated it will be appended to the top of sasCode
|
||||
if (uploadSasCode.length > 0) {
|
||||
program = `${uploadSasCode}` + program
|
||||
}
|
||||
}
|
||||
return program
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import path from 'path'
|
||||
import { getFilesFolder } from '../../utils/file'
|
||||
import {
|
||||
MemberType,
|
||||
createFolder,
|
||||
createFile,
|
||||
asyncForEach,
|
||||
FolderMember,
|
||||
ServiceMember,
|
||||
FileTree,
|
||||
FileMember
|
||||
} from '../../types'
|
||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
||||
FileMember,
|
||||
MemberType,
|
||||
FileTree
|
||||
} from '@sasjs/utils'
|
||||
|
||||
// REFACTOR: export FileTreeCpntroller
|
||||
export const createFileTree = async (
|
||||
@@ -15,7 +17,7 @@ export const createFileTree = async (
|
||||
parentFolders: string[] = []
|
||||
) => {
|
||||
const destinationPath = path.join(
|
||||
getTmpFilesFolderPath(),
|
||||
getFilesFolder(),
|
||||
path.join(...parentFolders)
|
||||
)
|
||||
|
||||
|
||||
@@ -2,3 +2,6 @@ export * from './deploy'
|
||||
export * from './Session'
|
||||
export * from './Execution'
|
||||
export * from './FileUploadController'
|
||||
export * from './createSASProgram'
|
||||
export * from './createJSProgram'
|
||||
export * from './processProgram'
|
||||
|
||||
86
api/src/controllers/internal/processProgram.ts
Normal file
86
api/src/controllers/internal/processProgram.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { execFileSync } from 'child_process'
|
||||
import { once } from 'stream'
|
||||
import { createFile, moveFile } from '@sasjs/utils'
|
||||
import { PreProgramVars, Session } from '../../types'
|
||||
import { RunTimeType } from '../../utils'
|
||||
import { ExecutionVars, createSASProgram, createJSProgram } from './'
|
||||
|
||||
export const processProgram = async (
|
||||
program: string,
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
session: Session,
|
||||
weboutPath: string,
|
||||
tokenFile: string,
|
||||
runTime: RunTimeType,
|
||||
logPath: string,
|
||||
otherArgs?: any
|
||||
) => {
|
||||
if (runTime === RunTimeType.JS) {
|
||||
program = await createJSProgram(
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
session,
|
||||
weboutPath,
|
||||
tokenFile,
|
||||
otherArgs
|
||||
)
|
||||
|
||||
const codePath = path.join(session.path, 'code.js')
|
||||
|
||||
try {
|
||||
await createFile(codePath, program)
|
||||
|
||||
// create a stream that will write to console outputs to log file
|
||||
const writeStream = fs.createWriteStream(logPath)
|
||||
|
||||
// waiting for the open event so that we can have underlying file descriptor
|
||||
await once(writeStream, 'open')
|
||||
|
||||
execFileSync(process.nodeLoc, [codePath], {
|
||||
stdio: ['ignore', writeStream, writeStream]
|
||||
})
|
||||
|
||||
// copy the code.js program to log and end write stream
|
||||
writeStream.end(program)
|
||||
|
||||
session.completed = true
|
||||
console.log('session completed', session)
|
||||
} catch (err: any) {
|
||||
session.completed = true
|
||||
session.crashed = err.toString()
|
||||
console.log('session crashed', session.id, session.crashed)
|
||||
}
|
||||
} else {
|
||||
program = await createSASProgram(
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
session,
|
||||
weboutPath,
|
||||
tokenFile,
|
||||
otherArgs
|
||||
)
|
||||
|
||||
const codePath = path.join(session.path, 'code.sas')
|
||||
|
||||
// Creating this file in a RUNNING session will break out
|
||||
// the autoexec loop and actually execute the program
|
||||
// but - given it will take several milliseconds to create
|
||||
// (which can mean SAS trying to run a partial program, or
|
||||
// failing due to file lock) we first create the file THEN
|
||||
// we rename it.
|
||||
await createFile(codePath + '.bkp', program)
|
||||
await moveFile(codePath + '.bkp', codePath)
|
||||
|
||||
// we now need to poll the session status
|
||||
while (!session.completed) {
|
||||
await delay(50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
@@ -23,8 +23,8 @@ export class SessionController {
|
||||
}
|
||||
}
|
||||
|
||||
const session = (req: any) => ({
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
displayName: req.user.displayName
|
||||
const session = (req: express.Request) => ({
|
||||
id: req.user!.userId,
|
||||
username: req.user!.username,
|
||||
displayName: req.user!.displayName
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import {
|
||||
Request,
|
||||
Security,
|
||||
@@ -17,15 +16,16 @@ import {
|
||||
ExecutionController,
|
||||
ExecutionVars
|
||||
} from './internal'
|
||||
import { PreProgramVars } from '../types'
|
||||
import {
|
||||
getTmpFilesFolderPath,
|
||||
getPreProgramVariables,
|
||||
HTTPHeaders,
|
||||
isDebugOn,
|
||||
LogLine,
|
||||
makeFilesNamesMap,
|
||||
parseLogToArray
|
||||
parseLogToArray,
|
||||
getRunTimeAndFilePath
|
||||
} from '../utils'
|
||||
import { MulterFile } from '../types/Upload'
|
||||
|
||||
interface ExecuteReturnJsonPayload {
|
||||
/**
|
||||
@@ -51,26 +51,15 @@ export interface ExecuteReturnJsonResponse {
|
||||
@Tags('STP')
|
||||
export class STPController {
|
||||
/**
|
||||
* Trigger a SAS program using it's location in the _program URL parameter.
|
||||
* Enable debugging using the _debug URL parameter. Setting _debug=131 will
|
||||
* cause the log to be streamed in the output.
|
||||
* Trigger a SAS or JS program using the _program URL parameter.
|
||||
*
|
||||
* Additional URL parameters are turned into SAS macro variables.
|
||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||
*
|
||||
* Any files provided in the request body are placed into the SAS session with
|
||||
* corresponding _WEBIN_XXX variables created.
|
||||
* https://server.sasjs.io/storedprograms
|
||||
*
|
||||
* The response headers can be adjusted using the mfs_httpheader() macro. Any
|
||||
* file type can be returned, including binary files such as zip or xls.
|
||||
*
|
||||
* If _debug is >= 131, response headers will contain Content-Type: 'text/plain'
|
||||
*
|
||||
* This behaviour differs for POST requests, in which case the response is
|
||||
* always JSON.
|
||||
*
|
||||
* @summary Execute Stored Program, return raw _webout content.
|
||||
* @param _program Location of SAS program
|
||||
* @example _program "/Public/somefolder/some.file"
|
||||
* @summary Execute a Stored Program, returns raw _webout content.
|
||||
* @param _program Location of SAS or JS code
|
||||
* @example _program "/Projects/myApp/some/program"
|
||||
*/
|
||||
@Get('/execute')
|
||||
public async executeReturnRaw(
|
||||
@@ -81,29 +70,22 @@ export class STPController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a SAS program using it's location in the _program URL parameter.
|
||||
* Enable debugging using the _debug URL parameter. In any case, the log is
|
||||
* always returned in the log object.
|
||||
* Trigger a SAS or JS program using the _program URL parameter.
|
||||
*
|
||||
* Additional URL parameters are turned into SAS macro variables.
|
||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||
*
|
||||
* Any files provided in the request body are placed into the SAS session with
|
||||
* corresponding _WEBIN_XXX variables created.
|
||||
* https://server.sasjs.io/storedprograms
|
||||
*
|
||||
* The response will be a JSON object with the following root attributes: log,
|
||||
* webout, headers.
|
||||
* The response will be a JSON object with the following root attributes:
|
||||
* log, webout, headers.
|
||||
*
|
||||
* The webout will be a nested JSON object ONLY if the response-header
|
||||
* The webout attribute will be nested JSON ONLY if the response-header
|
||||
* contains a content-type of application/json AND it is valid JSON.
|
||||
* Otherwise it will be a stringified version of the webout content.
|
||||
*
|
||||
* Response headers from the mfs_httpheader macro are simply listed in the
|
||||
* headers object, for POST requests they have no effect on the actual
|
||||
* response header.
|
||||
*
|
||||
* @summary Execute Stored Program, return JSON
|
||||
* @param _program Location of SAS program
|
||||
* @example _program "/Public/somefolder/some.file"
|
||||
* @summary Execute a Stored Program, return a JSON object
|
||||
* @param _program Location of SAS or JS code
|
||||
* @example _program "/Projects/myApp/some/program"
|
||||
*/
|
||||
@Example<ExecuteReturnJsonResponse>({
|
||||
status: 'success',
|
||||
@@ -130,18 +112,17 @@ const executeReturnRaw = async (
|
||||
_program: string
|
||||
): Promise<string | Buffer> => {
|
||||
const query = req.query as ExecutionVars
|
||||
const sasCodePath =
|
||||
path
|
||||
.join(getTmpFilesFolderPath(), _program)
|
||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||
|
||||
try {
|
||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
||||
|
||||
const { result, httpHeaders } =
|
||||
(await new ExecutionController().executeFile(
|
||||
sasCodePath,
|
||||
getPreProgramVariables(req),
|
||||
query
|
||||
)) as ExecuteReturnRaw
|
||||
(await new ExecutionController().executeFile({
|
||||
programPath: codePath,
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: query,
|
||||
runTime
|
||||
})) as ExecuteReturnRaw
|
||||
|
||||
// Should over-ride response header for debug
|
||||
// on GET request to see entire log rendering on browser.
|
||||
@@ -167,26 +148,26 @@ const executeReturnRaw = async (
|
||||
}
|
||||
|
||||
const executeReturnJson = async (
|
||||
req: any,
|
||||
req: express.Request,
|
||||
_program: string
|
||||
): Promise<ExecuteReturnJsonResponse> => {
|
||||
const sasCodePath =
|
||||
path
|
||||
.join(getTmpFilesFolderPath(), _program)
|
||||
.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 {
|
||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
||||
|
||||
const { webout, log, httpHeaders } =
|
||||
(await new ExecutionController().executeFile(
|
||||
sasCodePath,
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, ...req.body },
|
||||
{ filesNamesMap: filesNamesMap },
|
||||
true,
|
||||
req.sasSession
|
||||
)) as ExecuteReturnJson
|
||||
(await new ExecutionController().executeFile({
|
||||
programPath: codePath,
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: { ...req.query, ...req.body },
|
||||
otherArgs: { filesNamesMap: filesNamesMap },
|
||||
returnJson: true,
|
||||
session: req.sasjsSession,
|
||||
runTime
|
||||
})) as ExecuteReturnJson
|
||||
|
||||
let weboutRes: string | IRecordOfAny = webout
|
||||
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
|
||||
@@ -210,16 +191,3 @@ const executeReturnJson = async (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getPreProgramVariables = (req: any): PreProgramVars => {
|
||||
const host = req.get('host')
|
||||
const protocol = req.protocol + '://'
|
||||
const { user, accessToken } = req
|
||||
return {
|
||||
username: user.username,
|
||||
userId: user.userId,
|
||||
displayName: user.displayName,
|
||||
serverUrl: protocol + host,
|
||||
accessToken
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import express from 'express'
|
||||
import {
|
||||
Security,
|
||||
Route,
|
||||
@@ -10,10 +11,14 @@ import {
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Hidden
|
||||
Hidden,
|
||||
Request
|
||||
} from 'tsoa'
|
||||
import { desktopUser } from '../middlewares'
|
||||
|
||||
import User, { UserPayload } from '../model/User'
|
||||
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
|
||||
import { GroupResponse } from './group'
|
||||
|
||||
export interface UserResponse {
|
||||
id: number
|
||||
@@ -27,6 +32,8 @@ interface UserDetailsResponse {
|
||||
username: string
|
||||
isActive: boolean
|
||||
isAdmin: boolean
|
||||
autoExec?: string
|
||||
groups?: GroupResponse[]
|
||||
}
|
||||
|
||||
@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.
|
||||
* @param userId The user's identifier
|
||||
* @example userId 1234
|
||||
*/
|
||||
@Get('{userId}')
|
||||
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
|
||||
return getUser(userId)
|
||||
public async getUser(
|
||||
@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,
|
||||
@Body() body: UserPayload
|
||||
): Promise<UserDetailsResponse> {
|
||||
return updateUser(userId, body)
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE === ModeType.Desktop)
|
||||
return updateDesktopAutoExec(body.autoExec ?? '')
|
||||
|
||||
return updateUser({ id: userId }, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Delete a user. Can be performed either by admins, or the user in question.
|
||||
* @param username The User's username
|
||||
* @example username "johnSnow01"
|
||||
*/
|
||||
@Delete('by/username/{username}')
|
||||
public async deleteUserByUsername(
|
||||
@Path() username: string,
|
||||
@Body() body: { password?: string },
|
||||
@Query() @Hidden() isAdmin: boolean = false
|
||||
) {
|
||||
return deleteUser({ username }, isAdmin, body)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,7 +194,7 @@ export class UserController {
|
||||
@Body() body: { password?: string },
|
||||
@Query() @Hidden() isAdmin: boolean = false
|
||||
) {
|
||||
return deleteUser(userId, isAdmin, body)
|
||||
return deleteUser({ id: userId }, isAdmin, body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +204,7 @@ const getAllUsers = async (): Promise<UserResponse[]> =>
|
||||
.exec()
|
||||
|
||||
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
|
||||
const usernameExist = await User.findOne({ username })
|
||||
@@ -138,7 +219,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
username,
|
||||
password: hashPassword,
|
||||
isAdmin,
|
||||
isActive
|
||||
isActive,
|
||||
autoExec
|
||||
})
|
||||
|
||||
const savedUser = await user.save()
|
||||
@@ -148,38 +230,67 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
displayName: savedUser.displayName,
|
||||
username: savedUser.username,
|
||||
isActive: savedUser.isActive,
|
||||
isAdmin: savedUser.isAdmin
|
||||
isAdmin: savedUser.isAdmin,
|
||||
autoExec: savedUser.autoExec
|
||||
}
|
||||
}
|
||||
|
||||
const getUser = async (id: number): Promise<UserDetailsResponse> => {
|
||||
const user = await User.findOne({ id })
|
||||
.select({
|
||||
_id: 0,
|
||||
id: 1,
|
||||
username: 1,
|
||||
displayName: 1,
|
||||
isAdmin: 1,
|
||||
isActive: 1
|
||||
})
|
||||
.exec()
|
||||
interface GetUserBy {
|
||||
id?: number
|
||||
username?: string
|
||||
}
|
||||
|
||||
const getUser = async (
|
||||
findBy: GetUserBy,
|
||||
getAutoExec: boolean
|
||||
): Promise<UserDetailsResponse> => {
|
||||
const user = (await User.findOne(
|
||||
findBy,
|
||||
`id displayName username isActive isAdmin autoExec -_id`
|
||||
).populate(
|
||||
'groups',
|
||||
'groupId name description -_id'
|
||||
)) as unknown as UserDetailsResponse
|
||||
|
||||
if (!user) throw new Error('User is not found.')
|
||||
|
||||
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 (
|
||||
id: number,
|
||||
data: UserPayload
|
||||
findBy: GetUserBy,
|
||||
data: Partial<UserPayload>
|
||||
): 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) {
|
||||
// Checking if user is already in the database
|
||||
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
|
||||
}
|
||||
|
||||
@@ -188,27 +299,36 @@ const updateUser = async (
|
||||
params.password = User.hashPassword(password)
|
||||
}
|
||||
|
||||
const updatedUser = await User.findOneAndUpdate({ id }, 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')
|
||||
const updatedUser = await User.findOneAndUpdate(findBy, params, { new: true })
|
||||
|
||||
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 (
|
||||
id: number,
|
||||
findBy: GetUserBy,
|
||||
isAdmin: boolean,
|
||||
{ password }: { password?: string }
|
||||
) => {
|
||||
const user = await User.findOne({ id })
|
||||
const user = await User.findOne(findBy)
|
||||
if (!user) throw new Error('User is not found.')
|
||||
|
||||
if (!isAdmin) {
|
||||
@@ -216,5 +336,5 @@ const deleteUser = async (
|
||||
if (!validPass) throw new Error('Invalid password.')
|
||||
}
|
||||
|
||||
await User.deleteOne({ id })
|
||||
await User.deleteOne(findBy)
|
||||
}
|
||||
|
||||
158
api/src/controllers/web.ts
Normal file
158
api/src/controllers/web.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
|
||||
import { readFile } from '@sasjs/utils'
|
||||
|
||||
import User from '../model/User'
|
||||
import Client from '../model/Client'
|
||||
import { getWebBuildFolder, generateAuthCode } from '../utils'
|
||||
import { InfoJWT } from '../types'
|
||||
import { AuthController } from './auth'
|
||||
|
||||
@Route('/')
|
||||
@Tags('Web')
|
||||
export class WebController {
|
||||
/**
|
||||
* @summary Render index.html
|
||||
*
|
||||
*/
|
||||
@Get('/')
|
||||
public async home() {
|
||||
return home()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Accept a valid username/password
|
||||
*
|
||||
*/
|
||||
@Post('/SASLogon/login')
|
||||
public async login(
|
||||
@Request() req: express.Request,
|
||||
@Body() body: LoginPayload
|
||||
) {
|
||||
return login(req, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
|
||||
*
|
||||
*/
|
||||
@Example<AuthorizeResponse>({
|
||||
code: 'someRandomCryptoString'
|
||||
})
|
||||
@Post('/SASLogon/authorize')
|
||||
public async authorize(
|
||||
@Request() req: express.Request,
|
||||
@Body() body: AuthorizePayload
|
||||
): Promise<AuthorizeResponse> {
|
||||
return authorize(req, body.clientId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Accept a valid username/password
|
||||
*
|
||||
*/
|
||||
@Get('/logout')
|
||||
public async logout(@Request() req: express.Request) {
|
||||
return new Promise((resolve) => {
|
||||
req.session.destroy(() => {
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const home = async () => {
|
||||
const indexHtmlPath = path.join(getWebBuildFolder(), 'index.html')
|
||||
|
||||
// Attention! Cannot use fileExists here,
|
||||
// due to limitation after building executable
|
||||
const content = await readFile(indexHtmlPath)
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
const login = async (
|
||||
req: express.Request,
|
||||
{ username, password }: LoginPayload
|
||||
) => {
|
||||
// Authenticate User
|
||||
const user = await User.findOne({ username })
|
||||
if (!user) throw new Error('Username is not found.')
|
||||
|
||||
const validPass = user.comparePassword(password)
|
||||
if (!validPass) throw new Error('Invalid password.')
|
||||
|
||||
req.session.loggedIn = true
|
||||
req.session.user = {
|
||||
userId: user.id,
|
||||
clientId: 'web_app',
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
isAdmin: user.isAdmin,
|
||||
isActive: user.isActive,
|
||||
autoExec: user.autoExec
|
||||
}
|
||||
|
||||
return {
|
||||
loggedIn: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const authorize = async (
|
||||
req: express.Request,
|
||||
clientId: string
|
||||
): Promise<AuthorizeResponse> => {
|
||||
const userId = req.session.user?.userId
|
||||
if (!userId) throw new Error('Invalid userId.')
|
||||
|
||||
const client = await Client.findOne({ clientId })
|
||||
if (!client) throw new Error('Invalid clientId.')
|
||||
|
||||
// generate authorization code against clientId
|
||||
const userInfo: InfoJWT = {
|
||||
clientId,
|
||||
userId
|
||||
}
|
||||
const code = AuthController.saveCode(
|
||||
userId,
|
||||
clientId,
|
||||
generateAuthCode(userInfo)
|
||||
)
|
||||
|
||||
return { code }
|
||||
}
|
||||
|
||||
interface LoginPayload {
|
||||
/**
|
||||
* Username for user
|
||||
* @example "secretuser"
|
||||
*/
|
||||
username: string
|
||||
/**
|
||||
* Password for user
|
||||
* @example "secretpassword"
|
||||
*/
|
||||
password: string
|
||||
}
|
||||
|
||||
interface AuthorizePayload {
|
||||
/**
|
||||
* Client ID
|
||||
* @example "clientID1"
|
||||
*/
|
||||
clientId: string
|
||||
}
|
||||
|
||||
interface AuthorizeResponse {
|
||||
/**
|
||||
* Authorization code
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
code: string
|
||||
}
|
||||
@@ -1,7 +1,36 @@
|
||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { verifyTokenInDB } from '../utils'
|
||||
import { csrfProtection } from '../app'
|
||||
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
|
||||
import { desktopUser } from './desktop'
|
||||
|
||||
export const authenticateAccessToken: RequestHandler = async (
|
||||
req,
|
||||
res,
|
||||
next
|
||||
) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE === ModeType.Desktop) {
|
||||
req.user = desktopUser
|
||||
return next()
|
||||
}
|
||||
|
||||
// if request is coming from web and has valid session
|
||||
// it can be validated.
|
||||
if (req.session?.loggedIn) {
|
||||
if (req.session.user) {
|
||||
const user = await fetchLatestAutoExec(req.session.user)
|
||||
|
||||
if (user) {
|
||||
if (user.isActive) {
|
||||
req.user = user
|
||||
return csrfProtection(req, res, next)
|
||||
} else return res.sendStatus(401)
|
||||
}
|
||||
}
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
||||
authenticateToken(
|
||||
req,
|
||||
res,
|
||||
@@ -11,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(
|
||||
req,
|
||||
res,
|
||||
@@ -22,16 +51,16 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
||||
}
|
||||
|
||||
const authenticateToken = (
|
||||
req: any,
|
||||
res: any,
|
||||
next: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
key: string,
|
||||
tokenType: 'accessToken' | 'refreshToken'
|
||||
) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') {
|
||||
req.user = {
|
||||
userId: '1234',
|
||||
userId: 1234,
|
||||
clientId: 'desktopModeClientId',
|
||||
username: 'desktopModeUsername',
|
||||
displayName: 'desktopModeDisplayName',
|
||||
@@ -43,9 +72,7 @@ const authenticateToken = (
|
||||
}
|
||||
|
||||
const authHeader = req.headers['authorization']
|
||||
const token =
|
||||
authHeader?.split(' ')[1] ??
|
||||
(tokenType === 'accessToken' ? req.cookies.accessToken : '')
|
||||
const token = authHeader?.split(' ')[1]
|
||||
if (!token) return res.sendStatus(401)
|
||||
|
||||
jwt.verify(token, key, async (err: any, data: any) => {
|
||||
|
||||
@@ -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
|
||||
if (MODE?.trim() !== 'server')
|
||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||
|
||||
if (MODE === ModeType.Desktop) {
|
||||
if (!reqAllowedInDesktopMode(req))
|
||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
export const desktopUsername = (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 { Request } from 'express'
|
||||
import multer, { FileFilterCallback, Options } from 'multer'
|
||||
import { blockFileRegex, getTmpUploadsPath } from '../utils'
|
||||
import { blockFileRegex, getUploadsFolder } from '../utils'
|
||||
|
||||
const fieldNameSize = 300
|
||||
const fileSize = 10485760 // 10 MB
|
||||
const fileSize = 104857600 // 100 MB
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: getTmpUploadsPath(),
|
||||
destination: getUploadsFolder(),
|
||||
filename: function (
|
||||
_req: Request,
|
||||
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
|
||||
if (MODE?.trim() !== 'server') return next()
|
||||
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => {
|
||||
const { user } = req
|
||||
const userId = parseInt(req.params.userId)
|
||||
import { RequestHandler } from 'express'
|
||||
|
||||
if (!user.isAdmin && user.userId !== userId) {
|
||||
return res.status(401).send('Admin account required')
|
||||
// This middleware checks if a non-admin user trying to
|
||||
// access information of other user
|
||||
export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
|
||||
const { user } = req
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
import User from './User'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
|
||||
export interface GroupPayload {
|
||||
@@ -34,7 +35,8 @@ interface IGroupModel extends Model<IGroup> {}
|
||||
const groupSchema = new Schema<IGroupDocument>({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
@@ -46,6 +48,7 @@ const groupSchema = new Schema<IGroupDocument>({
|
||||
},
|
||||
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
|
||||
})
|
||||
|
||||
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
|
||||
|
||||
// Hooks
|
||||
@@ -55,6 +58,17 @@ groupSchema.post('save', function (group: IGroup, next: Function) {
|
||||
})
|
||||
})
|
||||
|
||||
// pre remove hook to remove all references of group from users
|
||||
groupSchema.pre('remove', async function () {
|
||||
const userIds = this.users
|
||||
await Promise.all(
|
||||
userIds.map(async (userId) => {
|
||||
const user = await User.findById(userId)
|
||||
user?.removeGroup(this._id)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// Instance Methods
|
||||
groupSchema.method(
|
||||
'addUser',
|
||||
|
||||
@@ -27,18 +27,26 @@ export interface UserPayload {
|
||||
* @example "true"
|
||||
*/
|
||||
isActive?: boolean
|
||||
/**
|
||||
* User-specific auto-exec code
|
||||
* @example ""
|
||||
*/
|
||||
autoExec?: string
|
||||
}
|
||||
|
||||
interface IUserDocument extends UserPayload, Document {
|
||||
id: number
|
||||
isAdmin: boolean
|
||||
isActive: boolean
|
||||
autoExec: string
|
||||
groups: Schema.Types.ObjectId[]
|
||||
tokens: [{ [key: string]: string }]
|
||||
}
|
||||
|
||||
interface IUser extends IUserDocument {
|
||||
comparePassword(password: string): boolean
|
||||
addGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||
removeGroup(groupObjectId: Schema.Types.ObjectId): Promise<IUser>
|
||||
}
|
||||
interface IUserModel extends Model<IUser> {
|
||||
hashPassword(password: string): string
|
||||
@@ -66,6 +74,9 @@ const userSchema = new Schema<IUserDocument>({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoExec: {
|
||||
type: String
|
||||
},
|
||||
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
||||
tokens: [
|
||||
{
|
||||
@@ -97,6 +108,28 @@ userSchema.method('comparePassword', function (password: string): boolean {
|
||||
if (bcrypt.compareSync(password, this.password)) return true
|
||||
return false
|
||||
})
|
||||
userSchema.method(
|
||||
'addGroup',
|
||||
async function (groupObjectId: Schema.Types.ObjectId) {
|
||||
const groupIdIndex = this.groups.indexOf(groupObjectId)
|
||||
if (groupIdIndex === -1) {
|
||||
this.groups.push(groupObjectId)
|
||||
}
|
||||
this.markModified('groups')
|
||||
return this.save()
|
||||
}
|
||||
)
|
||||
userSchema.method(
|
||||
'removeGroup',
|
||||
async function (groupObjectId: Schema.Types.ObjectId) {
|
||||
const groupIdIndex = this.groups.indexOf(groupObjectId)
|
||||
if (groupIdIndex > -1) {
|
||||
this.groups.splice(groupIdIndex, 1)
|
||||
}
|
||||
this.markModified('groups')
|
||||
return this.save()
|
||||
}
|
||||
)
|
||||
|
||||
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema)
|
||||
|
||||
|
||||
@@ -1,46 +1,24 @@
|
||||
import express from 'express'
|
||||
|
||||
import { AuthController } from '../../controllers/'
|
||||
import Client from '../../model/Client'
|
||||
|
||||
import {
|
||||
authenticateAccessToken,
|
||||
authenticateRefreshToken
|
||||
} from '../../middlewares'
|
||||
|
||||
import {
|
||||
authorizeValidation,
|
||||
getDesktopFields,
|
||||
tokenValidation
|
||||
} from '../../utils'
|
||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
||||
import { InfoJWT } from '../../types'
|
||||
|
||||
const authRouter = express.Router()
|
||||
const controller = new AuthController()
|
||||
|
||||
const clientIDs = new Set()
|
||||
|
||||
export const populateClients = async () => {
|
||||
const result = await Client.find()
|
||||
clientIDs.clear()
|
||||
result.forEach((r) => {
|
||||
clientIDs.add(r.clientId)
|
||||
})
|
||||
}
|
||||
|
||||
authRouter.post('/authorize', async (req, res) => {
|
||||
const { error, value: body } = authorizeValidation(req.body)
|
||||
authRouter.post('/token', async (req, res) => {
|
||||
const { error, value: body } = tokenValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const { clientId } = body
|
||||
|
||||
// Verify client ID
|
||||
if (!clientIDs.has(clientId)) {
|
||||
return res.status(403).send('Invalid clientId.')
|
||||
}
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.authorize(body)
|
||||
const response = await controller.token(body)
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
@@ -48,25 +26,12 @@ authRouter.post('/authorize', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
authRouter.post('/token', async (req, res) => {
|
||||
const { error, value: body } = tokenValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.token(body)
|
||||
const { accessToken } = response
|
||||
|
||||
res.cookie('accessToken', accessToken).send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
|
||||
const userInfo: InfoJWT = {
|
||||
userId: req.user!.userId!,
|
||||
clientId: req.user!.clientId!
|
||||
}
|
||||
})
|
||||
|
||||
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
||||
const userInfo: InfoJWT = req.user
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.refresh(userInfo)
|
||||
|
||||
@@ -76,10 +41,12 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
|
||||
const userInfo: InfoJWT = req.user
|
||||
authRouter.delete('/logout', authenticateAccessToken, async (req, res) => {
|
||||
const userInfo: InfoJWT = {
|
||||
userId: req.user!.userId!,
|
||||
clientId: req.user!.clientId!
|
||||
}
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
await controller.logout(userInfo)
|
||||
} catch (e) {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express'
|
||||
import { runSASValidation } from '../../utils'
|
||||
import { runCodeValidation } from '../../utils'
|
||||
import { CodeController } from '../../controllers/'
|
||||
|
||||
const runRouter = express.Router()
|
||||
@@ -7,11 +7,11 @@ const runRouter = express.Router()
|
||||
const controller = new CodeController()
|
||||
|
||||
runRouter.post('/execute', async (req, res) => {
|
||||
const { error, value: body } = runSASValidation(req.body)
|
||||
const { error, value: body } = runCodeValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.executeSASCode(req, body)
|
||||
const response = await controller.executeCode(req, body)
|
||||
|
||||
if (response instanceof Buffer) {
|
||||
res.writeHead(200, (req as any).sasHeaders)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express'
|
||||
import { deleteFile } from '@sasjs/utils'
|
||||
import { deleteFile, readFile } from '@sasjs/utils'
|
||||
|
||||
import { publishAppStream } from '../appStream'
|
||||
|
||||
@@ -7,9 +7,12 @@ import { multerSingle } from '../../middlewares/multer'
|
||||
import { DriveController } from '../../controllers/'
|
||||
import {
|
||||
deployValidation,
|
||||
extractJSONFromZip,
|
||||
extractName,
|
||||
fileBodyValidation,
|
||||
fileParamValidation,
|
||||
folderParamValidation
|
||||
folderParamValidation,
|
||||
isZipFile
|
||||
} from '../../utils'
|
||||
|
||||
const controller = new DriveController()
|
||||
@@ -43,6 +46,71 @@ driveRouter.post('/deploy', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.post(
|
||||
'/deploy/upload',
|
||||
(...arg) => multerSingle('file', arg),
|
||||
async (req, res) => {
|
||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||
|
||||
let fileContent: string = ''
|
||||
|
||||
const { value: zipFile } = isZipFile(req.file)
|
||||
if (zipFile) {
|
||||
fileContent = await extractJSONFromZip(zipFile)
|
||||
const fileInZip = extractName(zipFile.originalname)
|
||||
|
||||
if (!fileContent) {
|
||||
deleteFile(req.file.path)
|
||||
return res
|
||||
.status(400)
|
||||
.send(
|
||||
`No content present in ${fileInZip} of compressed file ${zipFile.originalname}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
fileContent = await readFile(req.file.path)
|
||||
}
|
||||
|
||||
let jsonContent
|
||||
try {
|
||||
jsonContent = JSON.parse(fileContent)
|
||||
} catch (err) {
|
||||
deleteFile(req.file.path)
|
||||
return res.status(400).send('File containing invalid JSON content.')
|
||||
}
|
||||
|
||||
const { error, value: body } = deployValidation(jsonContent)
|
||||
if (error) {
|
||||
deleteFile(req.file.path)
|
||||
return res.status(400).send(error.details[0].message)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await controller.deployUpload(req.file, body)
|
||||
|
||||
if (body.streamWebFolder) {
|
||||
const { streamServiceName } = await publishAppStream(
|
||||
body.appLoc,
|
||||
body.streamWebFolder,
|
||||
body.streamServiceName,
|
||||
body.streamLogo
|
||||
)
|
||||
response.streamServiceName = streamServiceName
|
||||
}
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err)
|
||||
} finally {
|
||||
deleteFile(req.file.path)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
driveRouter.get('/file', async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import express from 'express'
|
||||
import { GroupController } from '../../controllers/'
|
||||
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
||||
import { registerGroupValidation } from '../../utils'
|
||||
import { getGroupValidation, registerGroupValidation } from '../../utils'
|
||||
|
||||
const groupRouter = express.Router()
|
||||
|
||||
@@ -18,7 +18,11 @@ groupRouter.post(
|
||||
const response = await controller.createGroup(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -29,35 +33,73 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
|
||||
const response = await controller.getAllGroups()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
groupRouter.get('/:groupId', authenticateAccessToken, async (req: any, res) => {
|
||||
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
|
||||
const { groupId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.getGroup(groupId)
|
||||
const response = await controller.getGroup(parseInt(groupId))
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
})
|
||||
|
||||
groupRouter.get(
|
||||
'/by/groupname/:name',
|
||||
authenticateAccessToken,
|
||||
async (req, res) => {
|
||||
const { error, value: params } = getGroupValidation(req.params)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const { name } = params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.getGroupByGroupName(name)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
groupRouter.post(
|
||||
'/:groupId/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
const { groupId, userId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.addUserToGroup(groupId, userId)
|
||||
const response = await controller.addUserToGroup(
|
||||
parseInt(groupId),
|
||||
parseInt(userId)
|
||||
)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -66,15 +108,22 @@ groupRouter.delete(
|
||||
'/:groupId/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
const { groupId, userId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.removeUserFromGroup(groupId, userId)
|
||||
const response = await controller.removeUserFromGroup(
|
||||
parseInt(groupId),
|
||||
parseInt(userId)
|
||||
)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -83,15 +132,19 @@ groupRouter.delete(
|
||||
'/:groupId',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
const { groupId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
await controller.deleteGroup(groupId)
|
||||
await controller.deleteGroup(parseInt(groupId))
|
||||
res.status(200).send('Group Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,10 +5,10 @@ import swaggerUi from 'swagger-ui-express'
|
||||
import {
|
||||
authenticateAccessToken,
|
||||
desktopRestrict,
|
||||
desktopUsername,
|
||||
verifyAdmin
|
||||
} from '../../middlewares'
|
||||
|
||||
import infoRouter from './info'
|
||||
import driveRouter from './drive'
|
||||
import stpRouter from './stp'
|
||||
import codeRouter from './code'
|
||||
@@ -20,7 +20,8 @@ import sessionRouter from './session'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||
router.use('/info', infoRouter)
|
||||
router.use('/session', authenticateAccessToken, sessionRouter)
|
||||
router.use('/auth', desktopRestrict, authRouter)
|
||||
router.use(
|
||||
'/client',
|
||||
@@ -34,12 +35,22 @@ router.use('/group', desktopRestrict, groupRouter)
|
||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||
router.use('/code', authenticateAccessToken, codeRouter)
|
||||
router.use('/user', desktopRestrict, userRouter)
|
||||
|
||||
router.use(
|
||||
'/',
|
||||
swaggerUi.serve,
|
||||
swaggerUi.setup(undefined, {
|
||||
swaggerOptions: {
|
||||
url: '/swagger.yaml'
|
||||
url: '/swagger.yaml',
|
||||
requestInterceptor: (request: any) => {
|
||||
request.credentials = 'include'
|
||||
|
||||
const cookie = document.cookie
|
||||
const startIndex = cookie.indexOf('XSRF-TOKEN')
|
||||
const csrf = cookie.slice(startIndex + 11).split('; ')[0]
|
||||
request.headers['X-XSRF-TOKEN'] = csrf
|
||||
return request
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
16
api/src/routes/api/info.ts
Normal file
16
api/src/routes/api/info.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import express from 'express'
|
||||
import { InfoController } from '../../controllers'
|
||||
|
||||
const infoRouter = express.Router()
|
||||
|
||||
infoRouter.get('/', async (req, res) => {
|
||||
const controller = new InfoController()
|
||||
try {
|
||||
const response = controller.info()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default infoRouter
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
ClientController,
|
||||
AuthController
|
||||
} from '../../../controllers/'
|
||||
import { populateClients } from '../auth'
|
||||
import { InfoJWT } from '../../../types'
|
||||
import {
|
||||
generateAccessToken,
|
||||
@@ -18,11 +17,6 @@ import {
|
||||
verifyTokenInDB
|
||||
} from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const clientSecret = 'someclientSecret'
|
||||
const user = {
|
||||
@@ -35,16 +29,18 @@ const user = {
|
||||
}
|
||||
|
||||
describe('auth', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
await clientController.createClient({ clientId, clientSecret })
|
||||
await populateClients()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -53,114 +49,6 @@ describe('auth', () => {
|
||||
await mongoServer.stop()
|
||||
})
|
||||
|
||||
describe('authorize', () => {
|
||||
afterEach(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond with authorization code', async () => {
|
||||
await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
clientId
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toHaveProperty('code')
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if username is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
password: user.password,
|
||||
clientId
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"username" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if password is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
clientId
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"password" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if clientId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"clientId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if username is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
clientId
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Username is not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if password is incorrect', async () => {
|
||||
await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: 'WrongPassword',
|
||||
clientId
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Invalid password.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||
await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
clientId: 'WrongClientID'
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Invalid clientId.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('token', () => {
|
||||
const userInfo: InfoJWT = {
|
||||
clientId,
|
||||
|
||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
||||
import { UserController, ClientController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const client = {
|
||||
clientId: 'someclientID',
|
||||
clientSecret: 'someclientSecret'
|
||||
@@ -28,12 +23,15 @@ const newClient = {
|
||||
}
|
||||
|
||||
describe('client', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import AdmZip from 'adm-zip'
|
||||
|
||||
import {
|
||||
folderExists,
|
||||
@@ -12,30 +13,26 @@ import {
|
||||
generateTimestamp,
|
||||
copy,
|
||||
createFolder,
|
||||
createFile
|
||||
createFile,
|
||||
ServiceMember,
|
||||
FolderMember
|
||||
} from '@sasjs/utils'
|
||||
import * as fileUtilModules from '../../../utils/file'
|
||||
|
||||
const timestamp = generateTimestamp()
|
||||
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
||||
jest
|
||||
.spyOn(fileUtilModules, 'getTmpFolderPath')
|
||||
.spyOn(fileUtilModules, 'getSasjsRootFolder')
|
||||
.mockImplementation(() => tmpFolder)
|
||||
jest
|
||||
.spyOn(fileUtilModules, 'getTmpUploadsPath')
|
||||
.spyOn(fileUtilModules, 'getUploadsFolder')
|
||||
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||
|
||||
import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import { getTreeExample } from '../../../controllers/internal'
|
||||
import { FolderMember, ServiceMember } from '../../../types'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||
const { getTmpFilesFolderPath } = fileUtilModules
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
const { getFilesFolder } = fileUtilModules
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const user = {
|
||||
@@ -47,6 +44,7 @@ const user = {
|
||||
}
|
||||
|
||||
describe('drive', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const controller = new UserController()
|
||||
@@ -54,6 +52,8 @@ describe('drive', () => {
|
||||
let accessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
@@ -73,11 +73,52 @@ describe('drive', () => {
|
||||
})
|
||||
|
||||
describe('deploy', () => {
|
||||
const shouldFailAssertion = async (payload: any) => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/deploy')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ appLoc: '/Public', fileTree: payload })
|
||||
const makeRequest = async (payload: any, type: string = 'payload') => {
|
||||
const requestUrl =
|
||||
type === 'payload'
|
||||
? '/SASjsApi/drive/deploy'
|
||||
: '/SASjsApi/drive/deploy/upload'
|
||||
|
||||
if (type === 'payload') {
|
||||
return await request(app)
|
||||
.post(requestUrl)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ appLoc: '/Public', fileTree: payload })
|
||||
}
|
||||
if (type === 'file') {
|
||||
const deployContents = JSON.stringify({
|
||||
appLoc: '/Public',
|
||||
fileTree: payload
|
||||
})
|
||||
return await request(app)
|
||||
.post(requestUrl)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.attach('file', Buffer.from(deployContents), 'deploy.json')
|
||||
} else {
|
||||
const deployContents = JSON.stringify({
|
||||
appLoc: '/Public',
|
||||
fileTree: payload
|
||||
})
|
||||
const zip = new AdmZip()
|
||||
// add file directly
|
||||
zip.addFile(
|
||||
'deploy.json',
|
||||
Buffer.from(deployContents, 'utf8'),
|
||||
'entry comment goes here'
|
||||
)
|
||||
|
||||
return await request(app)
|
||||
.post(requestUrl)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.attach('file', zip.toBuffer(), 'deploy.json.zip')
|
||||
}
|
||||
}
|
||||
|
||||
const shouldFailAssertion = async (
|
||||
payload: any,
|
||||
type: string = 'payload'
|
||||
) => {
|
||||
const res = await makeRequest(payload, type)
|
||||
|
||||
expect(res.statusCode).toEqual(400)
|
||||
|
||||
@@ -158,10 +199,10 @@ describe('drive', () => {
|
||||
expect(res.text).toEqual(
|
||||
'{"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(
|
||||
getTmpFilesFolderPath(),
|
||||
getFilesFolder(),
|
||||
'public',
|
||||
'jobs',
|
||||
'extract'
|
||||
@@ -175,7 +216,241 @@ describe('drive', () => {
|
||||
|
||||
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
||||
|
||||
await deleteFolder(path.join(getTmpFilesFolderPath(), 'public'))
|
||||
await deleteFolder(path.join(getFilesFolder(), 'public'))
|
||||
})
|
||||
|
||||
describe('upload', () => {
|
||||
it('should respond with payload example if valid JSON file was not provided', async () => {
|
||||
await shouldFailAssertion(null, 'file')
|
||||
await shouldFailAssertion(undefined, 'file')
|
||||
await shouldFailAssertion('data', 'file')
|
||||
await shouldFailAssertion({}, 'file')
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
userId: 1,
|
||||
title: 'test is cool'
|
||||
},
|
||||
'file'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
membersWRONG: []
|
||||
},
|
||||
'file'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
members: {}
|
||||
},
|
||||
'file'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
members: [
|
||||
{
|
||||
nameWRONG: 'jobs',
|
||||
type: 'folder',
|
||||
members: []
|
||||
}
|
||||
]
|
||||
},
|
||||
'file'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
members: [
|
||||
{
|
||||
name: 'jobs',
|
||||
type: 'WRONG',
|
||||
members: []
|
||||
}
|
||||
]
|
||||
},
|
||||
'file'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
members: [
|
||||
{
|
||||
name: 'jobs',
|
||||
type: 'folder',
|
||||
members: [
|
||||
{
|
||||
name: 'extract',
|
||||
type: 'folder',
|
||||
members: [
|
||||
{
|
||||
name: 'makedata1',
|
||||
type: 'service',
|
||||
codeWRONG: '%put Hello World!;'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'file'
|
||||
)
|
||||
})
|
||||
|
||||
it('should successfully deploy if valid JSON file was provided', async () => {
|
||||
const deployContents = JSON.stringify({
|
||||
appLoc: '/public',
|
||||
fileTree: getTreeExample()
|
||||
})
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/deploy/upload')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.attach('file', Buffer.from(deployContents), 'deploy.json')
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.text).toEqual(
|
||||
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
||||
)
|
||||
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
|
||||
|
||||
const testJobFolder = path.join(
|
||||
getFilesFolder(),
|
||||
'public',
|
||||
'jobs',
|
||||
'extract'
|
||||
)
|
||||
await expect(folderExists(testJobFolder)).resolves.toEqual(true)
|
||||
|
||||
const exampleService = getExampleService()
|
||||
const testJobFile =
|
||||
path.join(testJobFolder, exampleService.name) + '.sas'
|
||||
|
||||
await expect(fileExists(testJobFile)).resolves.toEqual(true)
|
||||
|
||||
await expect(readFile(testJobFile)).resolves.toEqual(
|
||||
exampleService.code
|
||||
)
|
||||
|
||||
await deleteFolder(path.join(getFilesFolder(), 'public'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('upload - zipped', () => {
|
||||
it('should respond with payload example if valid Zipped file was not provided', async () => {
|
||||
await shouldFailAssertion(null, 'zip')
|
||||
await shouldFailAssertion(undefined, 'zip')
|
||||
await shouldFailAssertion('data', 'zip')
|
||||
await shouldFailAssertion({}, 'zip')
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
userId: 1,
|
||||
title: 'test is cool'
|
||||
},
|
||||
'zip'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
membersWRONG: []
|
||||
},
|
||||
'zip'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
members: {}
|
||||
},
|
||||
'zip'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
members: [
|
||||
{
|
||||
nameWRONG: 'jobs',
|
||||
type: 'folder',
|
||||
members: []
|
||||
}
|
||||
]
|
||||
},
|
||||
'zip'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
members: [
|
||||
{
|
||||
name: 'jobs',
|
||||
type: 'WRONG',
|
||||
members: []
|
||||
}
|
||||
]
|
||||
},
|
||||
'zip'
|
||||
)
|
||||
await shouldFailAssertion(
|
||||
{
|
||||
members: [
|
||||
{
|
||||
name: 'jobs',
|
||||
type: 'folder',
|
||||
members: [
|
||||
{
|
||||
name: 'extract',
|
||||
type: 'folder',
|
||||
members: [
|
||||
{
|
||||
name: 'makedata1',
|
||||
type: 'service',
|
||||
codeWRONG: '%put Hello World!;'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'zip'
|
||||
)
|
||||
})
|
||||
|
||||
it('should successfully deploy if valid Zipped file was provided', async () => {
|
||||
const deployContents = JSON.stringify({
|
||||
appLoc: '/public',
|
||||
fileTree: getTreeExample()
|
||||
})
|
||||
|
||||
const zip = new AdmZip()
|
||||
// add file directly
|
||||
zip.addFile(
|
||||
'deploy.json',
|
||||
Buffer.from(deployContents, 'utf8'),
|
||||
'entry comment goes here'
|
||||
)
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/deploy/upload')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.attach('file', zip.toBuffer(), 'deploy.json.zip')
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.text).toEqual(
|
||||
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
||||
)
|
||||
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
|
||||
|
||||
const testJobFolder = path.join(
|
||||
getFilesFolder(),
|
||||
'public',
|
||||
'jobs',
|
||||
'extract'
|
||||
)
|
||||
await expect(folderExists(testJobFolder)).resolves.toEqual(true)
|
||||
|
||||
const exampleService = getExampleService()
|
||||
const testJobFile =
|
||||
path.join(testJobFolder, exampleService.name) + '.sas'
|
||||
|
||||
await expect(fileExists(testJobFile)).resolves.toEqual(true)
|
||||
|
||||
await expect(readFile(testJobFile)).resolves.toEqual(
|
||||
exampleService.code
|
||||
)
|
||||
|
||||
await deleteFolder(path.join(getFilesFolder(), 'public'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -193,7 +468,7 @@ describe('drive', () => {
|
||||
})
|
||||
|
||||
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 dirLevel2 = 'level2'
|
||||
@@ -268,10 +543,7 @@ describe('drive', () => {
|
||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const filePath = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
filePath
|
||||
)
|
||||
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
|
||||
await copy(fileToCopyPath, pathToCopy)
|
||||
|
||||
const res = await request(app)
|
||||
@@ -334,7 +606,7 @@ describe('drive', () => {
|
||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
fileUtilModules.getFilesFolder(),
|
||||
pathToUpload
|
||||
)
|
||||
await copy(fileToAttachPath, pathToCopy)
|
||||
@@ -424,7 +696,7 @@ describe('drive', () => {
|
||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
@@ -434,7 +706,7 @@ describe('drive', () => {
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 10 MB'
|
||||
'File size is over limit. File limit is: 100 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
@@ -446,7 +718,7 @@ describe('drive', () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
fileUtilModules.getFilesFolder(),
|
||||
pathToUpload
|
||||
)
|
||||
await copy(fileToAttachPath, pathToCopy)
|
||||
@@ -468,7 +740,7 @@ describe('drive', () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
fileUtilModules.getFilesFolder(),
|
||||
pathToUpload
|
||||
)
|
||||
await copy(fileToAttachPath, pathToCopy)
|
||||
@@ -582,7 +854,7 @@ describe('drive', () => {
|
||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
@@ -592,7 +864,7 @@ describe('drive', () => {
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 10 MB'
|
||||
'File size is over limit. File limit is: 100 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
@@ -604,10 +876,7 @@ describe('drive', () => {
|
||||
const fileToCopyContent = await readFile(fileToCopyPath)
|
||||
const filePath = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
filePath
|
||||
)
|
||||
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
|
||||
await copy(fileToCopyPath, pathToCopy)
|
||||
|
||||
const res = await request(app)
|
||||
|
||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
||||
import { UserController, GroupController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
@@ -28,7 +23,7 @@ const user = {
|
||||
}
|
||||
|
||||
const group = {
|
||||
name: 'DCGroup1',
|
||||
name: 'dcgroup1',
|
||||
description: 'DC group for testing purposes.'
|
||||
}
|
||||
|
||||
@@ -36,11 +31,14 @@ const userController = new UserController()
|
||||
const groupController = new GroupController()
|
||||
|
||||
describe('group', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
@@ -72,6 +70,32 @@ describe('group', () => {
|
||||
expect(res.body.users).toEqual([])
|
||||
})
|
||||
|
||||
it('should respond with Conflict when group already exists with same name', async () => {
|
||||
await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/group')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send(group)
|
||||
.expect(409)
|
||||
|
||||
expect(res.text).toEqual('Group name already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request when group name does not match the group name schema', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/group')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ ...group, name: 'Wrong Group Name' })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'"name" must only contain alpha-numeric characters'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app).post('/SASjsApi/group').send().expect(401)
|
||||
|
||||
@@ -127,14 +151,51 @@ describe('group', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||
it(`should delete group's reference from users' groups array`, async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const dbUser1 = await userController.createUser({
|
||||
...user,
|
||||
username: 'deletegroup1'
|
||||
})
|
||||
const dbUser2 = await userController.createUser({
|
||||
...user,
|
||||
username: 'deletegroup2'
|
||||
})
|
||||
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser1.id)
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser2.id)
|
||||
|
||||
await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
const res1 = await request(app)
|
||||
.get(`/SASjsApi/user/${dbUser1.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res1.body.groups).toEqual([])
|
||||
|
||||
const res2 = await request(app)
|
||||
.get(`/SASjsApi/user/${dbUser2.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res2.body.groups).toEqual([])
|
||||
})
|
||||
|
||||
it('should respond with Not Found if groupId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/1234`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('Error: No Group deleted!')
|
||||
expect(res.text).toEqual('Group not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -218,16 +279,76 @@ describe('group', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||
it('should respond with Not Found if groupId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/group/1234')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('Error: Group not found.')
|
||||
expect(res.text).toEqual('Group not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
describe('by group name', () => {
|
||||
it('should respond with group', async () => {
|
||||
const { name } = await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/group/by/groupname/${name}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.name).toEqual(group.name)
|
||||
expect(res.body.description).toEqual(group.description)
|
||||
expect(res.body.isActive).toEqual(true)
|
||||
expect(res.body.users).toEqual([])
|
||||
})
|
||||
|
||||
it('should respond with group when access token is not of an admin account', async () => {
|
||||
const accessToken = await generateSaveTokenAndCreateUser({
|
||||
...user,
|
||||
username: 'getbyname' + user.username
|
||||
})
|
||||
|
||||
const { name } = await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/group/by/groupname/${name}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groupId).toBeTruthy()
|
||||
expect(res.body.name).toEqual(group.name)
|
||||
expect(res.body.description).toEqual(group.description)
|
||||
expect(res.body.isActive).toEqual(true)
|
||||
expect(res.body.users).toEqual([])
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/group/by/groupname/dcgroup')
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Not Found if groupname is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/group/by/groupname/randomCharacters')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('Group not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAll', () => {
|
||||
@@ -247,8 +368,8 @@ describe('group', () => {
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
groupId: expect.anything(),
|
||||
name: 'DCGroup1',
|
||||
description: 'DC group for testing purposes.'
|
||||
name: group.name,
|
||||
description: group.description
|
||||
}
|
||||
])
|
||||
})
|
||||
@@ -269,8 +390,8 @@ describe('group', () => {
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
groupId: expect.anything(),
|
||||
name: 'DCGroup1',
|
||||
description: 'DC group for testing purposes.'
|
||||
name: group.name,
|
||||
description: group.description
|
||||
}
|
||||
])
|
||||
})
|
||||
@@ -311,6 +432,34 @@ describe('group', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it(`should add group to user's groups array`, async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const dbUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'addUserToGroup'
|
||||
})
|
||||
|
||||
await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groups).toEqual([
|
||||
{
|
||||
groupId: expect.anything(),
|
||||
name: group.name,
|
||||
description: group.description
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should respond with group without duplicating user', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const dbUser = await userController.createUser({
|
||||
@@ -364,26 +513,26 @@ describe('group', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||
it('should respond with Not Found if groupId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/group/123/123')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('Error: Group not found.')
|
||||
expect(res.text).toEqual('Group not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if userId is incorrect', async () => {
|
||||
it('should respond with Not Found if userId is incorrect', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/123`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('Error: User not found.')
|
||||
expect(res.text).toEqual('User not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
@@ -414,6 +563,29 @@ describe('group', () => {
|
||||
expect(res.body.users).toEqual([])
|
||||
})
|
||||
|
||||
it(`should remove group from user's groups array`, async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const dbUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'removeGroupFromUser'
|
||||
})
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
||||
|
||||
await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.groups).toEqual([])
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.delete('/SASjsApi/group/123/123')
|
||||
@@ -440,26 +612,26 @@ describe('group', () => {
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||
it('should respond with Not Found if groupId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.delete('/SASjsApi/group/123/123')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('Error: Group not found.')
|
||||
expect(res.text).toEqual('Group not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if userId is incorrect', async () => {
|
||||
it('should respond with Not Found if userId is incorrect', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/123`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
.expect(404)
|
||||
|
||||
expect(res.text).toEqual('Error: User not found.')
|
||||
expect(res.text).toEqual('User not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
20
api/src/routes/api/spec/info.spec.ts
Normal file
20
api/src/routes/api/spec/info.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Express } from 'express'
|
||||
import request from 'supertest'
|
||||
import appPromise from '../../../app'
|
||||
|
||||
describe('Info', () => {
|
||||
let app: Express
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
})
|
||||
|
||||
it('should should return configured information of the server instance', async () => {
|
||||
const res = await request(app).get('/SASjsApi/info').expect(200)
|
||||
|
||||
expect(res.body.mode).toEqual('server')
|
||||
expect(res.body.cors).toEqual('disable')
|
||||
expect(res.body.whiteList).toEqual([])
|
||||
expect(res.body.protocol).toEqual('http')
|
||||
})
|
||||
})
|
||||
383
api/src/routes/api/spec/stp.spec.ts
Normal file
383
api/src/routes/api/spec/stp.spec.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
import path from 'path'
|
||||
import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import {
|
||||
generateAccessToken,
|
||||
saveTokensInDB,
|
||||
getFilesFolder,
|
||||
RunTimeType,
|
||||
generateUniqueFileName,
|
||||
getSessionsFolder
|
||||
} from '../../../utils'
|
||||
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
||||
import {
|
||||
SASSessionController,
|
||||
JSSessionController
|
||||
} from '../../../controllers/internal'
|
||||
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
||||
import { Session } from '../../../types'
|
||||
|
||||
const clientId = 'someclientID'
|
||||
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
username: 'testUsername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
const sampleSasProgram = '%put hello world!;'
|
||||
const sampleJsProgram = `console.log('hello world!/')`
|
||||
|
||||
const filesFolder = getFilesFolder()
|
||||
|
||||
describe('stp', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
let accessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
accessToken = await generateSaveTokenAndCreateUser(user)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await con.connection.dropDatabase()
|
||||
await con.connection.close()
|
||||
await mongoServer.stop()
|
||||
})
|
||||
|
||||
describe('execute', () => {
|
||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||
|
||||
describe('get', () => {
|
||||
describe('with runtime js', () => {
|
||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||
|
||||
beforeAll(() => {
|
||||
process.runTimes = [RunTimeType.JS]
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules() // it clears the cache
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
jest.resetAllMocks()
|
||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||
})
|
||||
|
||||
it('should execute js program when both js and sas program are present', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
||||
await createFile(sasProgramPath, sampleSasProgram)
|
||||
await createFile(jsProgramPath, sampleJsProgram)
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
RunTimeType.JS,
|
||||
expect.anything(),
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when js program is not present but sas program exists', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
||||
await createFile(sasProgramPath, sampleSasProgram)
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with runtime sas', () => {
|
||||
beforeAll(() => {
|
||||
process.runTimes = [RunTimeType.SAS]
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules() // it clears the cache
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
jest.resetAllMocks()
|
||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||
})
|
||||
|
||||
it('should execute sas program when both sas and js programs are present', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
||||
await createFile(sasProgramPath, sampleSasProgram)
|
||||
await createFile(jsProgramPath, sampleJsProgram)
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
RunTimeType.SAS,
|
||||
expect.anything(),
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when sas program do not exit but js exists', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
||||
await createFile(jsProgramPath, sampleJsProgram)
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with runtime js and sas', () => {
|
||||
beforeAll(() => {
|
||||
process.runTimes = [RunTimeType.JS, RunTimeType.SAS]
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules() // it clears the cache
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
jest.resetAllMocks()
|
||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||
})
|
||||
|
||||
it('should execute js program when both js and sas program are present', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
||||
await createFile(sasProgramPath, sampleSasProgram)
|
||||
await createFile(jsProgramPath, sampleJsProgram)
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
RunTimeType.JS,
|
||||
expect.anything(),
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute sas program when js program is not present but sas program exists', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
||||
await createFile(sasProgramPath, sampleSasProgram)
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
RunTimeType.SAS,
|
||||
expect.anything(),
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when both sas and js programs do not exist', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with runtime sas and js', () => {
|
||||
beforeAll(() => {
|
||||
process.runTimes = [RunTimeType.SAS, RunTimeType.JS]
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules() // it clears the cache
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
jest.resetAllMocks()
|
||||
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||
})
|
||||
|
||||
it('should execute sas program when both sas and js programs exist', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
||||
await createFile(sasProgramPath, sampleSasProgram)
|
||||
await createFile(jsProgramPath, sampleJsProgram)
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
RunTimeType.SAS,
|
||||
expect.anything(),
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute js program when sas program is not present but js program exists', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
||||
await createFile(jsProgramPath, sampleJsProgram)
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
RunTimeType.JS,
|
||||
expect.anything(),
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it('should throw error when both sas and js programs do not exist', async () => {
|
||||
const programPath = path.join(testFilesFolder, 'program')
|
||||
|
||||
await request(app)
|
||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const generateSaveTokenAndCreateUser = async (
|
||||
someUser: any
|
||||
): Promise<string> => {
|
||||
const userController = new UserController()
|
||||
const dbUser = await userController.createUser(someUser)
|
||||
|
||||
return generateAndSaveToken(dbUser.id)
|
||||
}
|
||||
|
||||
const generateAndSaveToken = async (userId: number) => {
|
||||
const accessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId
|
||||
})
|
||||
await saveTokensInDB(userId, clientId, accessToken, 'refreshToken')
|
||||
return accessToken
|
||||
}
|
||||
|
||||
const setupMocks = async () => {
|
||||
jest
|
||||
.spyOn(SASSessionController.prototype, 'getSession')
|
||||
.mockImplementation(mockedGetSession)
|
||||
|
||||
jest
|
||||
.spyOn(JSSessionController.prototype, 'getSession')
|
||||
.mockImplementation(mockedGetSession)
|
||||
|
||||
jest
|
||||
.spyOn(ProcessProgramModule, 'processProgram')
|
||||
.mockImplementation(() => Promise.resolve())
|
||||
}
|
||||
|
||||
const mockedGetSession = async () => {
|
||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||
|
||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||
// death time of session is 15 mins from creation
|
||||
const deathTimeStamp = (
|
||||
parseInt(creationTimeStamp) +
|
||||
15 * 60 * 1000 -
|
||||
1000
|
||||
).toString()
|
||||
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
ready: true,
|
||||
inUse: true,
|
||||
consumed: false,
|
||||
completed: false,
|
||||
creationTimeStamp,
|
||||
deathTimeStamp,
|
||||
path: sessionFolder
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
@@ -3,37 +3,36 @@ import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import { UserController, GroupController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
username: 'testAdminUsername',
|
||||
username: 'testadminusername',
|
||||
password: '12345678',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
username: 'testUsername',
|
||||
username: 'testusername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
autoExec: 'some sas code for auto exec;'
|
||||
}
|
||||
|
||||
const controller = new UserController()
|
||||
|
||||
describe('user', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
@@ -66,6 +65,21 @@ describe('user', () => {
|
||||
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 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 () => {
|
||||
@@ -244,7 +258,7 @@ describe('user', () => {
|
||||
const dbUser1 = await controller.createUser(user)
|
||||
const dbUser2 = await controller.createUser({
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
username: 'randomuser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
@@ -256,6 +270,102 @@ describe('user', () => {
|
||||
expect(res.text).toEqual('Error: Username already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
describe('by username', () => {
|
||||
it('should respond with updated user when admin user requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const newDisplayName = 'My new display Name'
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/user/by/username/${user.username}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ ...user, displayName: newDisplayName })
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.username).toEqual(user.username)
|
||||
expect(res.body.displayName).toEqual(newDisplayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
})
|
||||
|
||||
it('should respond with updated user when user himself requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const newDisplayName = 'My new display Name'
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/user/by/username/${user.username}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({
|
||||
displayName: newDisplayName,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.username).toEqual(user.username)
|
||||
expect(res.body.displayName).toEqual(newDisplayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
})
|
||||
|
||||
it('should respond with Bad Request, only admin can update isAdmin/isActive', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
const newDisplayName = 'My new display Name'
|
||||
|
||||
await request(app)
|
||||
.patch(`/SASjsApi/user/by/username/${user.username}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ ...user, displayName: newDisplayName })
|
||||
.expect(400)
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/user/by/username/1234')
|
||||
.send(user)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
|
||||
const dbUser1 = await controller.createUser(user)
|
||||
const dbUser2 = await controller.createUser({
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser2.id)
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/user/${dbUser1.id}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(user)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Admin account required')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if username is already present', async () => {
|
||||
const dbUser1 = await controller.createUser(user)
|
||||
const dbUser2 = await controller.createUser({
|
||||
...user,
|
||||
username: 'randomuser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ username: dbUser2.username })
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Username already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete', () => {
|
||||
@@ -349,6 +459,89 @@ describe('user', () => {
|
||||
expect(res.text).toEqual('Error: Invalid password.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
describe('by username', () => {
|
||||
it('should respond with OK when admin user requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with OK when user himself requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ password: user.password })
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request when user himself requests and password is missing', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"password" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized when access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.delete('/SASjsApi/user/by/username/RandomUsername')
|
||||
.send(user)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized when access token is not of an admin account or himself', async () => {
|
||||
const dbUser1 = await controller.createUser(user)
|
||||
const dbUser2 = await controller.createUser({
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
})
|
||||
const accessToken = await generateAndSaveToken(dbUser2.id)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser1.username}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(user)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Admin account required')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden when user himself requests and password is incorrect', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = await generateAndSaveToken(dbUser.id)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ password: 'incorrectpassword' })
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Invalid password.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('get', () => {
|
||||
@@ -362,7 +555,26 @@ describe('user', () => {
|
||||
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 userId = dbUser.id
|
||||
|
||||
@@ -376,6 +588,8 @@ describe('user', () => {
|
||||
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 when access token is not of an admin account', async () => {
|
||||
@@ -397,6 +611,35 @@ describe('user', () => {
|
||||
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()
|
||||
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 () => {
|
||||
@@ -421,6 +664,86 @@ describe('user', () => {
|
||||
expect(res.text).toEqual('Error: User is not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
describe('by username', () => {
|
||||
it('should respond with user autoExec when same user requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const userId = dbUser.id
|
||||
const accessToken = await generateAndSaveToken(userId)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.username).toEqual(user.username)
|
||||
expect(res.body.displayName).toEqual(user.displayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||
})
|
||||
|
||||
it('should respond with user autoExec when admin user requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.username).toEqual(user.username)
|
||||
expect(res.body.displayName).toEqual(user.displayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||
})
|
||||
|
||||
it('should respond with user when access token is not of an admin account', async () => {
|
||||
const accessToken = await generateSaveTokenAndCreateUser({
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
})
|
||||
|
||||
const dbUser = await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/by/username/${dbUser.username}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.username).toEqual(user.username)
|
||||
expect(res.body.displayName).toEqual(user.displayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
expect(res.body.autoExec).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/user/by/username/randomUsername')
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if username is incorrect', async () => {
|
||||
await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/user/by/username/randomUsername')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: User is not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAll', () => {
|
||||
|
||||
182
api/src/routes/api/spec/web.spec.ts
Normal file
182
api/src/routes/api/spec/web.spec.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController, ClientController } from '../../../controllers/'
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const clientSecret = 'someclientSecret'
|
||||
const user = {
|
||||
id: 1234,
|
||||
displayName: 'Test User',
|
||||
username: 'testusername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
describe('web', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
await clientController.createClient({ clientId, clientSecret })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await con.connection.dropDatabase()
|
||||
await con.connection.close()
|
||||
await mongoServer.stop()
|
||||
})
|
||||
|
||||
describe('home', () => {
|
||||
it('should respond with CSRF Token', async () => {
|
||||
await request(app)
|
||||
.get('/')
|
||||
.expect(
|
||||
'set-cookie',
|
||||
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SASLogon/login', () => {
|
||||
let csrfToken: string
|
||||
let cookies: string
|
||||
|
||||
beforeAll(async () => {
|
||||
;({ csrfToken, cookies } = await getCSRF(app))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond with successful login', async () => {
|
||||
await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/login')
|
||||
.set('Cookie', cookies)
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.loggedIn).toBeTruthy()
|
||||
expect(res.body.user).toEqual({
|
||||
id: expect.any(Number),
|
||||
username: user.username,
|
||||
displayName: user.displayName
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SASLogon/authorize', () => {
|
||||
let csrfToken: string
|
||||
let cookies: string
|
||||
let authCookies: string
|
||||
|
||||
beforeAll(async () => {
|
||||
;({ csrfToken, cookies } = await getCSRF(app))
|
||||
|
||||
await userController.createUser(user)
|
||||
|
||||
const credentials = {
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
|
||||
;({ cookies: authCookies } = await performLogin(
|
||||
app,
|
||||
credentials,
|
||||
cookies,
|
||||
csrfToken
|
||||
))
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond with authorization code', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies, cookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({ clientId })
|
||||
|
||||
expect(res.body).toHaveProperty('code')
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if clientId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies, cookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"clientId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASLogon/authorize')
|
||||
.set('Cookie', [authCookies, cookies].join('; '))
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send({
|
||||
clientId: 'WrongClientID'
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Invalid clientId.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const getCSRF = async (app: Express) => {
|
||||
// make request to get CSRF
|
||||
const { header } = await request(app).get('/')
|
||||
const cookies = header['set-cookie'].join()
|
||||
|
||||
const csrfToken = extractCSRF(cookies)
|
||||
return { csrfToken, cookies }
|
||||
}
|
||||
|
||||
const performLogin = async (
|
||||
app: Express,
|
||||
credentials: { username: string; password: string },
|
||||
cookies: string,
|
||||
csrfToken: string
|
||||
) => {
|
||||
const { header } = await request(app)
|
||||
.post('/SASLogon/login')
|
||||
.set('Cookie', cookies)
|
||||
.set('x-xsrf-token', csrfToken)
|
||||
.send(credentials)
|
||||
|
||||
const newCookies: string = header['set-cookie'].join()
|
||||
return { cookies: newCookies }
|
||||
}
|
||||
|
||||
const extractCSRF = (cookies: string) =>
|
||||
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
|
||||
cookies
|
||||
)![2]
|
||||
@@ -34,23 +34,25 @@ stpRouter.post(
|
||||
'/execute',
|
||||
fileUploadController.preUploadMiddleware,
|
||||
fileUploadController.getMulterUploadObject().any(),
|
||||
async (req: any, res: any) => {
|
||||
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
||||
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
||||
async (req, res: any) => {
|
||||
// below validations are moved to preUploadMiddleware
|
||||
// const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
||||
// const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
||||
|
||||
if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.executeReturnJson(
|
||||
req,
|
||||
body,
|
||||
query?._program
|
||||
req.body,
|
||||
req.query?._program as string
|
||||
)
|
||||
|
||||
if (response instanceof Buffer) {
|
||||
res.writeHead(200, (req as any).sasHeaders)
|
||||
return res.end(response)
|
||||
}
|
||||
// TODO: investigate if this code is required
|
||||
// if (response instanceof Buffer) {
|
||||
// res.writeHead(200, (req as any).sasHeaders)
|
||||
// return res.end(response)
|
||||
// }
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '../../middlewares'
|
||||
import {
|
||||
deleteUserValidation,
|
||||
getUserValidation,
|
||||
registerUserValidation,
|
||||
updateUserValidation
|
||||
} 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 controller = new UserController()
|
||||
try {
|
||||
const response = await controller.getUser(userId)
|
||||
const response = await controller.getUser(req, parseInt(userId))
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
@@ -49,20 +69,26 @@ userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
|
||||
})
|
||||
|
||||
userRouter.patch(
|
||||
'/:userId',
|
||||
'/by/username/:username',
|
||||
authenticateAccessToken,
|
||||
verifyAdminIfNeeded,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
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
|
||||
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)
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
const response = await controller.updateUser(userId, body)
|
||||
const response = await controller.updateUserByUsername(username, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
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(
|
||||
'/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdminIfNeeded,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
const { user } = req
|
||||
const { userId } = req.params
|
||||
|
||||
// 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)
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
await controller.deleteUser(userId, data, user.isAdmin)
|
||||
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
|
||||
res.status(200).send('Account Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
import { AppStreamConfig } from '../../types'
|
||||
|
||||
const style = `<style>
|
||||
* {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
}
|
||||
.app-container .app {
|
||||
width: 150px;
|
||||
margin: 10px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
.app-container .app img{
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>`
|
||||
import { style } from './style'
|
||||
|
||||
const defaultAppLogo = '/sasjs-logo.svg'
|
||||
|
||||
@@ -45,13 +23,30 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
|
||||
${style}
|
||||
</head>
|
||||
<body>
|
||||
<h1>App Stream</h1>
|
||||
<header>
|
||||
<a href="/"><img src="/logo.png" alt="logo" class="logo"></a>
|
||||
<h1>App Stream</h1>
|
||||
</header>
|
||||
<div class="app-container">
|
||||
${Object.entries(appStreamConfig)
|
||||
.map(([streamServiceName, entry]) =>
|
||||
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
|
||||
)
|
||||
.join('')}
|
||||
${Object.entries(appStreamConfig)
|
||||
.map(([streamServiceName, entry]) =>
|
||||
singleAppStreamHtml(
|
||||
streamServiceName,
|
||||
entry.appLoc,
|
||||
entry.streamLogo
|
||||
)
|
||||
)
|
||||
.join('')}
|
||||
|
||||
<a class="app" title="Upload build.json">
|
||||
<input id="fileId" type="file" hidden />
|
||||
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
|
||||
<img src="/plus.png" />
|
||||
</button>
|
||||
<span id="uploadMessage">Upload New App</span>
|
||||
</a>
|
||||
</div>
|
||||
<script src="/axios.min.js"></script>
|
||||
<script src="/app-streams-script.js"></script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import express, { Request } from 'express'
|
||||
import { folderExists } from '@sasjs/utils'
|
||||
|
||||
import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
|
||||
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
||||
import { appStreamHtml } from './appStreamHtml'
|
||||
|
||||
const appStreams: { [key: string]: string } = {}
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', async (_, res) => {
|
||||
router.get('/', async (req, res) => {
|
||||
const content = appStreamHtml(process.appStreamConfig)
|
||||
|
||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||
|
||||
return res.send(content)
|
||||
})
|
||||
|
||||
@@ -20,7 +24,7 @@ export const publishAppStream = async (
|
||||
streamLogo?: string,
|
||||
addEntryToFile: boolean = true
|
||||
) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
||||
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
||||
@@ -42,7 +46,7 @@ export const publishAppStream = async (
|
||||
streamServiceName = `AppStreamName${appCount + 1}`
|
||||
}
|
||||
|
||||
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
|
||||
appStreams[streamServiceName] = pathToDeployment
|
||||
|
||||
addEntryToAppStreamConfig(
|
||||
streamServiceName,
|
||||
@@ -52,7 +56,7 @@ export const publishAppStream = async (
|
||||
addEntryToFile
|
||||
)
|
||||
|
||||
const sasJsPort = process.env.PORT ?? 5000
|
||||
const sasJsPort = process.env.PORT || 5000
|
||||
console.log(
|
||||
'Serving Stream App: ',
|
||||
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
||||
@@ -62,4 +66,26 @@ export const publishAppStream = async (
|
||||
return {}
|
||||
}
|
||||
|
||||
router.get(`/*`, function (req: Request, res, next) {
|
||||
const reqPath = req.path.replace(/^\//, '')
|
||||
|
||||
// Redirecting to url with trailing slash for appStream base URL only
|
||||
if (reqPath.split('/').length === 1 && !reqPath.endsWith('/'))
|
||||
// navigating to same url with slash at start
|
||||
return res.redirect(301, `${reqPath}/`)
|
||||
|
||||
const appStream = reqPath.split('/')[0]
|
||||
const appStreamFilesPath = appStreams[appStream]
|
||||
if (appStreamFilesPath) {
|
||||
// resourcePath is without appStream base path
|
||||
const resourcePath = reqPath.split('/').slice(1).join('/') || 'index.html'
|
||||
|
||||
req.url = resourcePath
|
||||
|
||||
return express.static(appStreamFilesPath)(req, res, next)
|
||||
}
|
||||
|
||||
return res.send("There's no App Stream available here.")
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
75
api/src/routes/appStream/style.ts
Normal file
75
api/src/routes/appStream/style.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
export const style = `<style>
|
||||
* {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 50px;
|
||||
}
|
||||
.app-container .app {
|
||||
width: 150px;
|
||||
height: 180px;
|
||||
margin: 10px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
|
||||
background: #efefef;
|
||||
padding: 10px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid #d7d7d7;
|
||||
}
|
||||
.app-container .app img{
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
#uploadButton {
|
||||
border: 0
|
||||
}
|
||||
|
||||
#uploadButton:focus {
|
||||
outline: 0
|
||||
}
|
||||
|
||||
#uploadMessage {
|
||||
position: relative;
|
||||
bottom: -5px;
|
||||
}
|
||||
|
||||
header {
|
||||
transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
box-shadow: rgb(0 0 0 / 20%) 0px 2px 4px -1px, rgb(0 0 0 / 14%) 0px 4px 5px 0px, rgb(0 0 0 / 12%) 0px 1px 10px 0px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: auto;
|
||||
right: 0px;
|
||||
background-color: rgb(0, 0, 0);
|
||||
color: rgb(255, 255, 255);
|
||||
z-index: 1201;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 13px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
header a {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
header .logo {
|
||||
width: 35px;
|
||||
margin-left: 10px;
|
||||
align-self: center;
|
||||
}
|
||||
</style>`
|
||||
@@ -4,13 +4,16 @@ import webRouter from './web'
|
||||
import apiRouter from './api'
|
||||
import appStreamRouter from './appStream'
|
||||
|
||||
import { csrfProtection } from '../app'
|
||||
|
||||
export const setupRoutes = (app: Express) => {
|
||||
app.use('/', webRouter)
|
||||
app.use('/SASjsApi', apiRouter)
|
||||
|
||||
app.use('/AppStream', function (req, res, next) {
|
||||
app.use('/AppStream', csrfProtection, function (req, res, next) {
|
||||
// this needs to be a function to hook on
|
||||
// whatever the current router is
|
||||
appStreamRouter(req, res, next)
|
||||
})
|
||||
|
||||
app.use('/', csrfProtection, webRouter)
|
||||
}
|
||||
|
||||
@@ -1,34 +1,60 @@
|
||||
import { readFile } from '@sasjs/utils'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import { getWebBuildFolderPath } from '../../utils'
|
||||
import { WebController } from '../../controllers/web'
|
||||
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
|
||||
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||
|
||||
const webRouter = express.Router()
|
||||
const controller = new WebController()
|
||||
|
||||
const codeToInject = `
|
||||
<script>
|
||||
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
|
||||
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
|
||||
</script>`
|
||||
|
||||
webRouter.get('/', async (_, res) => {
|
||||
let content: string
|
||||
webRouter.get('/', async (req, res) => {
|
||||
let response
|
||||
try {
|
||||
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
||||
content = await readFile(indexHtmlPath)
|
||||
response = await controller.home()
|
||||
} catch (_) {
|
||||
return res.send('Web Build is not present')
|
||||
response = 'Web Build is not present'
|
||||
} finally {
|
||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||
|
||||
return res.send(response)
|
||||
}
|
||||
})
|
||||
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') {
|
||||
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
|
||||
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
|
||||
const { error, value: body } = loginWebValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
return res.send(injectedContent)
|
||||
try {
|
||||
const response = await controller.login(req, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
return res.send(content)
|
||||
webRouter.post(
|
||||
'/SASLogon/authorize',
|
||||
desktopRestrict,
|
||||
authenticateAccessToken,
|
||||
async (req, res) => {
|
||||
const { error, value: body } = authorizeValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.authorize(req, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
webRouter.get('/logout', desktopRestrict, async (req, res) => {
|
||||
try {
|
||||
await controller.logout(req)
|
||||
res.status(200).send('OK!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default webRouter
|
||||
|
||||
@@ -4,8 +4,8 @@ import appPromise from './app'
|
||||
import { getCertificates } from './utils'
|
||||
|
||||
appPromise.then(async (app) => {
|
||||
const protocol = process.env.PROTOCOL ?? 'http'
|
||||
const sasJsPort = process.env.PORT ?? 5000
|
||||
const protocol = process.env.PROTOCOL || 'http'
|
||||
const sasJsPort = process.env.PORT || 5000
|
||||
|
||||
console.log('PROTOCOL: ', protocol)
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
export enum MemberType {
|
||||
service = 'service',
|
||||
file = 'file',
|
||||
folder = 'folder'
|
||||
}
|
||||
|
||||
export interface ServiceMember {
|
||||
name: string
|
||||
type: MemberType.service
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface FileMember {
|
||||
name: string
|
||||
type: MemberType.file
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface FolderMember {
|
||||
name: string
|
||||
type: MemberType.folder
|
||||
members: (FolderMember | ServiceMember | FileMember)[]
|
||||
}
|
||||
export interface FileTree {
|
||||
members: (FolderMember | ServiceMember | FileMember)[]
|
||||
}
|
||||
|
||||
export const isFileTree = (arg: any): arg is FileTree =>
|
||||
arg &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: ServiceMember | FileMember | FolderMember) =>
|
||||
!isServiceMember(member, '-') &&
|
||||
!isFileMember(member, '-') &&
|
||||
!isFolderMember(member, '-')
|
||||
).length === 0
|
||||
|
||||
const isServiceMember = (arg: any, pre: string): arg is ServiceMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.service &&
|
||||
typeof arg.code === 'string'
|
||||
|
||||
const isFileMember = (arg: any, pre: string): arg is ServiceMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.file &&
|
||||
typeof arg.code === 'string'
|
||||
|
||||
const isFolderMember = (arg: any, pre: string): arg is FolderMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.folder &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isServiceMember(member, pre + '-') &&
|
||||
!isFileMember(member, pre + '-') &&
|
||||
!isFolderMember(member, pre + '-')
|
||||
).length === 0
|
||||
@@ -3,5 +3,5 @@ export interface PreProgramVars {
|
||||
userId: number
|
||||
displayName: string
|
||||
serverUrl: string
|
||||
accessToken: string
|
||||
httpHeaders: string[]
|
||||
}
|
||||
|
||||
8
api/src/types/Process.d.ts
vendored
8
api/src/types/Process.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
sasLoc: string
|
||||
driveLoc: string
|
||||
sessionController?: import('../controllers/internal').SessionController
|
||||
appStreamConfig: import('./').AppStreamConfig
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MacroVars } from '@sasjs/utils'
|
||||
|
||||
export interface ExecutionQuery {
|
||||
_program: string
|
||||
macroVars?: MacroVars
|
||||
_debug?: number
|
||||
}
|
||||
|
||||
export interface FileQuery {
|
||||
filePath: string
|
||||
}
|
||||
|
||||
export const isExecutionQuery = (arg: any): arg is ExecutionQuery =>
|
||||
arg && !Array.isArray(arg) && typeof arg._program === 'string'
|
||||
|
||||
export const isFileQuery = (arg: any): arg is FileQuery =>
|
||||
arg && !Array.isArray(arg) && typeof arg.filePath === 'string'
|
||||
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
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
// TODO: uppercase types
|
||||
export * from './AppStreamConfig'
|
||||
export * from './Execution'
|
||||
export * from './FileTree'
|
||||
export * from './InfoJWT'
|
||||
export * from './PreProgramVars'
|
||||
export * from './Request'
|
||||
export * from './Session'
|
||||
export * from './TreeNode'
|
||||
export * from './RequestUser'
|
||||
|
||||
7
api/src/types/system/express-session.d.ts
vendored
Normal file
7
api/src/types/system/express-session.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import express from 'express'
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
loggedIn: boolean
|
||||
user: import('../').RequestUser
|
||||
}
|
||||
}
|
||||
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
|
||||
sasjsSession?: import('../').Session
|
||||
}
|
||||
}
|
||||
1
api/src/types/system/global.d.ts
vendored
Normal file
1
api/src/types/system/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import 'jest-extended'
|
||||
12
api/src/types/system/process.d.ts
vendored
Normal file
12
api/src/types/system/process.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
sasLoc: string
|
||||
nodeLoc: string
|
||||
driveLoc: string
|
||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
||||
appStreamConfig: import('../').AppStreamConfig
|
||||
logger: import('@sasjs/utils/logger').Logger
|
||||
runTimes: import('../../utils').RunTimeType[]
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import { createFile, fileExists, readFile } from '@sasjs/utils'
|
||||
import { publishAppStream } from '../routes/appStream'
|
||||
import { AppStreamConfig } from '../types'
|
||||
|
||||
import { getTmpAppStreamConfigPath } from './file'
|
||||
import { getAppStreamConfigPath } from './file'
|
||||
|
||||
export const loadAppStreamConfig = async () => {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
|
||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
||||
const appStreamConfigPath = getAppStreamConfigPath()
|
||||
|
||||
const content = (await fileExists(appStreamConfigPath))
|
||||
? await readFile(appStreamConfigPath)
|
||||
@@ -63,7 +63,7 @@ export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
|
||||
}
|
||||
|
||||
const saveAppStreamConfig = async () => {
|
||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
||||
const appStreamConfigPath = getAppStreamConfigPath()
|
||||
|
||||
try {
|
||||
await createFile(
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { populateClients } from '../routes/api/auth'
|
||||
import { seedDB } from './seedDB'
|
||||
|
||||
export const connectDB = async () => {
|
||||
// NOTE: when exporting app.js as agent for supertest
|
||||
// we should exclude connecting to the real database
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return
|
||||
} else {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server') {
|
||||
console.log('Running in Destop Mode, no DB to connect.')
|
||||
return
|
||||
}
|
||||
|
||||
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
|
||||
if (err) throw err
|
||||
|
||||
console.log('Connected to db!')
|
||||
|
||||
await populateClients()
|
||||
})
|
||||
try {
|
||||
await mongoose.connect(process.env.DB_CONNECT as string)
|
||||
} catch (err) {
|
||||
throw new Error('Unable to connect to DB!')
|
||||
}
|
||||
|
||||
console.log('Connected to DB!')
|
||||
await seedDB()
|
||||
|
||||
return mongoose.connection
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||
import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||
|
||||
export const copySASjsCore = async () => {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
|
||||
console.log('Copying Macros from container to drive(tmp).')
|
||||
|
||||
const macrosDrivePath = getTmpMacrosPath()
|
||||
const macrosDrivePath = getMacrosFolder()
|
||||
|
||||
await deleteFolder(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)
|
||||
6
api/src/utils/extractName.ts
Normal file
6
api/src/utils/extractName.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import path from 'path'
|
||||
|
||||
export const extractName = (filePath: string) => {
|
||||
const extension = path.extname(filePath)
|
||||
return path.basename(filePath, extension)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import path from 'path'
|
||||
import { homedir } from 'os'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
export const apiRoot = path.join(__dirname, '..', '..')
|
||||
export const codebaseRoot = path.join(apiRoot, '..')
|
||||
@@ -9,30 +11,33 @@ export const sysInitCompiledPath = path.join(
|
||||
)
|
||||
|
||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||
export const sasJSCoreMacrosInfo = path.join(apiRoot, 'sasjscore', '.macrolist')
|
||||
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||
|
||||
export const getWebBuildFolderPath = () =>
|
||||
path.join(codebaseRoot, 'web', 'build')
|
||||
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
|
||||
|
||||
export const getTmpFolderPath = () => process.driveLoc
|
||||
export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
|
||||
|
||||
export const getTmpAppStreamConfigPath = () =>
|
||||
path.join(getTmpFolderPath(), 'appStreamConfig.json')
|
||||
export const getDesktopUserAutoExecPath = () =>
|
||||
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 = () =>
|
||||
path.join(getTmpFolderPath(), 'files')
|
||||
export const getMacrosFolder = () =>
|
||||
path.join(getSasjsRootFolder(), 'sasjscore')
|
||||
|
||||
export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs')
|
||||
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
||||
|
||||
export const getTmpWeboutFolderPath = () =>
|
||||
path.join(getTmpFolderPath(), 'webouts')
|
||||
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
||||
|
||||
export const getTmpSessionsFolderPath = () =>
|
||||
path.join(getTmpFolderPath(), 'sessions')
|
||||
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
|
||||
|
||||
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||
|
||||
export const getSessionsFolder = () =>
|
||||
path.join(getSasjsRootFolder(), 'sessions')
|
||||
|
||||
export const generateUniqueFileName = (fileName: string, extension = '') =>
|
||||
[
|
||||
@@ -43,3 +48,6 @@ export const generateUniqueFileName = (fileName: string, extension = '') =>
|
||||
new Date().getTime(),
|
||||
extension
|
||||
].join('')
|
||||
|
||||
export const createReadStream = async (filePath: string) =>
|
||||
fs.createReadStream(filePath)
|
||||
|
||||
@@ -5,12 +5,13 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
||||
const isWindows = () => process.platform === 'win32'
|
||||
|
||||
export const getDesktopFields = async () => {
|
||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||
const { SAS_PATH, NODE_PATH } = process.env
|
||||
|
||||
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
||||
const nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
||||
|
||||
return { sasLoc, driveLoc }
|
||||
return { sasLoc, nodeLoc }
|
||||
}
|
||||
|
||||
const getDriveLocation = async (): Promise<string> => {
|
||||
@@ -61,3 +62,27 @@ const getSASLocation = async (): Promise<string> => {
|
||||
|
||||
return targetName
|
||||
}
|
||||
|
||||
const getNodeLocation = async (): Promise<string> => {
|
||||
const validator = async (filePath: string) => {
|
||||
if (!filePath) return 'Path to NodeJS executable is required.'
|
||||
|
||||
if (!(await fileExists(filePath))) {
|
||||
return 'No file found at provided path.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const defaultLocation = isWindows()
|
||||
? 'C:\\Program Files\\nodejs\\'
|
||||
: '/usr/local/nodejs/bin'
|
||||
|
||||
const targetName = await getString(
|
||||
'Please enter path to nodejs executable (absolute path): ',
|
||||
validator,
|
||||
defaultLocation
|
||||
)
|
||||
|
||||
return targetName
|
||||
}
|
||||
|
||||
30
api/src/utils/getPreProgramVariables.ts
Normal file
30
api/src/utils/getPreProgramVariables.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Request } from 'express'
|
||||
import { PreProgramVars } from '../types'
|
||||
|
||||
export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
||||
const host = req.get('host')
|
||||
const protocol = req.protocol + '://'
|
||||
const { user, accessToken } = req
|
||||
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']
|
||||
const sessionId = req.cookies['connect.sid']
|
||||
const { _csrf } = req.cookies
|
||||
|
||||
const httpHeaders: string[] = []
|
||||
|
||||
if (accessToken) httpHeaders.push(`Authorization: Bearer ${accessToken}`)
|
||||
if (csrfToken) httpHeaders.push(`x-xsrf-token: ${csrfToken}`)
|
||||
|
||||
const cookies: string[] = []
|
||||
if (sessionId) cookies.push(`connect.sid=${sessionId}`)
|
||||
if (_csrf) cookies.push(`_csrf=${_csrf}`)
|
||||
|
||||
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
||||
|
||||
return {
|
||||
username: user!.username,
|
||||
userId: user!.userId,
|
||||
displayName: user!.displayName,
|
||||
serverUrl: protocol + host,
|
||||
httpHeaders
|
||||
}
|
||||
}
|
||||
37
api/src/utils/getRunTimeAndFilePath.ts
Normal file
37
api/src/utils/getRunTimeAndFilePath.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import path from 'path'
|
||||
import { fileExists } from '@sasjs/utils'
|
||||
import { getFilesFolder } from './file'
|
||||
import { RunTimeType } from '.'
|
||||
|
||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||
const ext = path.extname(programPath)
|
||||
// if program path is provided with extension we should split that into code path and ext as run time
|
||||
if (ext) {
|
||||
const runTime = ext.slice(1)
|
||||
const runTimeTypes = Object.values(RunTimeType)
|
||||
|
||||
if (!runTimeTypes.includes(runTime as RunTimeType)) {
|
||||
throw `The '${runTime}' runtime is not supported.`
|
||||
}
|
||||
|
||||
const codePath = path
|
||||
.join(getFilesFolder(), programPath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (await fileExists(codePath)) {
|
||||
return { codePath, runTime: runTime as RunTimeType }
|
||||
}
|
||||
} else {
|
||||
for (const runTime of process.runTimes) {
|
||||
const codePath =
|
||||
path
|
||||
.join(getFilesFolder(), programPath)
|
||||
.replace(new RegExp('/', 'g'), path.sep) +
|
||||
'.' +
|
||||
runTime
|
||||
|
||||
if (await fileExists(codePath)) return { codePath, runTime }
|
||||
}
|
||||
}
|
||||
throw `The Program at (${programPath}) does not exist.`
|
||||
}
|
||||
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,20 +1,28 @@
|
||||
export * from './appStreamConfig'
|
||||
export * from './connectDB'
|
||||
export * from './copySASjsCore'
|
||||
export * from './desktopAutoExec'
|
||||
export * from './extractHeaders'
|
||||
export * from './extractName'
|
||||
export * from './file'
|
||||
export * from './generateAccessToken'
|
||||
export * from './generateAuthCode'
|
||||
export * from './generateRefreshToken'
|
||||
export * from './getCertificates'
|
||||
export * from './getDesktopFields'
|
||||
export * from './getPreProgramVariables'
|
||||
export * from './getRunTimeAndFilePath'
|
||||
export * from './getServerUrl'
|
||||
export * from './instantiateLogger'
|
||||
export * from './isDebugOn'
|
||||
export * from './zipped'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
export * from './saveTokensInDB'
|
||||
export * from './seedDB'
|
||||
export * from './setProcessVariables'
|
||||
export * from './setupFolders'
|
||||
export * from './sleep'
|
||||
export * from './upload'
|
||||
export * from './validation'
|
||||
export * from './verifyEnvVariables'
|
||||
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
|
||||
}
|
||||
35
api/src/utils/parseHelmetConfig.ts
Normal file
35
api/src/utils/parseHelmetConfig.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
export const getEnvCSPDirectives = (
|
||||
HELMET_CSP_CONFIG_PATH: string | undefined
|
||||
) => {
|
||||
let cspConfigJson = {
|
||||
'img-src': ["'self'", 'data:'],
|
||||
'script-src': ["'self'", "'unsafe-inline'"],
|
||||
'script-src-attr': ["'self'", "'unsafe-inline'"]
|
||||
}
|
||||
|
||||
if (
|
||||
typeof HELMET_CSP_CONFIG_PATH === 'string' &&
|
||||
HELMET_CSP_CONFIG_PATH.length > 0
|
||||
) {
|
||||
const cspConfigPath = path.join(process.cwd(), HELMET_CSP_CONFIG_PATH)
|
||||
|
||||
try {
|
||||
let file = fs.readFileSync(cspConfigPath).toString()
|
||||
|
||||
try {
|
||||
cspConfigJson = JSON.parse(file)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Parsing Content Security Policy JSON config failed. Make sure it is valid json'
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error reading HELMET CSP config file', e)
|
||||
}
|
||||
}
|
||||
|
||||
return cspConfigJson
|
||||
}
|
||||
35
api/src/utils/seedDB.ts
Normal file
35
api/src/utils/seedDB.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import Client from '../model/Client'
|
||||
import User from '../model/User'
|
||||
|
||||
const CLIENT = {
|
||||
clientId: 'clientID1',
|
||||
clientSecret: 'clientSecret'
|
||||
}
|
||||
const ADMIN_USER = {
|
||||
id: 1,
|
||||
displayName: 'Super Admin',
|
||||
username: 'secretuser',
|
||||
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
export const seedDB = async () => {
|
||||
// Checking if client is already in the database
|
||||
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
||||
if (!clientExist) {
|
||||
const client = new Client(CLIENT)
|
||||
await client.save()
|
||||
|
||||
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||
}
|
||||
|
||||
// Checking if user is already in the database
|
||||
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||
if (!usernameExist) {
|
||||
const user = new User(ADMIN_USER)
|
||||
await user.save()
|
||||
|
||||
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,35 @@
|
||||
import path from 'path'
|
||||
import { getRealPath } from '@sasjs/utils'
|
||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||
|
||||
import { configuration } from '../../package.json'
|
||||
import { getDesktopFields } from '.'
|
||||
import { getDesktopFields, ModeType, RunTimeType } from '.'
|
||||
|
||||
export const setProcessVariables = async () => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
process.driveLoc = path.join(process.cwd(), 'tmp')
|
||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
||||
return
|
||||
}
|
||||
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server') {
|
||||
const { sasLoc, driveLoc } = await getDesktopFields()
|
||||
if (MODE === ModeType.Server) {
|
||||
process.sasLoc = process.env.SAS_PATH as string
|
||||
process.nodeLoc = process.env.NODE_PATH as string
|
||||
} else {
|
||||
const { sasLoc, nodeLoc } = await getDesktopFields()
|
||||
|
||||
process.sasLoc = sasLoc
|
||||
process.driveLoc = driveLoc
|
||||
} else {
|
||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||
|
||||
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
||||
process.driveLoc = getRealPath(
|
||||
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
|
||||
)
|
||||
process.nodeLoc = nodeLoc
|
||||
}
|
||||
|
||||
const { SASJS_ROOT } = process.env
|
||||
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||
await createFolder(absPath)
|
||||
process.driveLoc = getRealPath(absPath)
|
||||
|
||||
const { RUN_TIMES } = process.env
|
||||
process.runTimes = (RUN_TIMES as string).split(',') as RunTimeType[]
|
||||
|
||||
console.log('sasLoc: ', process.sasLoc)
|
||||
console.log('sasDrive: ', process.driveLoc)
|
||||
console.log('runTimes: ', process.runTimes)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { createFolder } from '@sasjs/utils'
|
||||
import { getTmpFilesFolderPath } from './file'
|
||||
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
||||
import { getDesktopUserAutoExecPath, getFilesFolder } from './file'
|
||||
import { ModeType } from './verifyEnvVariables'
|
||||
|
||||
export const setupFolders = async () => {
|
||||
const drivePath = getTmpFilesFolderPath()
|
||||
const drivePath = getFilesFolder()
|
||||
await createFolder(drivePath)
|
||||
|
||||
if (process.env.MODE === ModeType.Desktop) {
|
||||
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
||||
await createFile(getDesktopUserAutoExecPath(), '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const sleep = async (delay: number) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import { MulterFile } from '../types/Upload'
|
||||
import { listFilesInFolder } from '@sasjs/utils'
|
||||
import { listFilesInFolder, readFileBinary } from '@sasjs/utils'
|
||||
|
||||
interface FilenameMapSingle {
|
||||
fieldName: string
|
||||
@@ -98,3 +99,34 @@ export const generateFileUploadSasCode = async (
|
||||
|
||||
return uploadSasCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the js code that references uploaded files in the concurrent request
|
||||
* @param filesNamesMap object that maps hashed file names and original file names
|
||||
* @param sessionFolder name of the folder that is created for the purpose of files in concurrent request
|
||||
* @returns generated js code
|
||||
*/
|
||||
export const generateFileUploadJSCode = async (
|
||||
filesNamesMap: FilenamesMap,
|
||||
sessionFolder: string
|
||||
) => {
|
||||
let uploadCode = ''
|
||||
let fileCount = 0
|
||||
|
||||
const sessionFolderList: string[] = await listFilesInFolder(sessionFolder)
|
||||
sessionFolderList.forEach(async (fileName) => {
|
||||
if (fileName.includes('req_file')) {
|
||||
fileCount++
|
||||
const filePath = path.join(sessionFolder, fileName)
|
||||
uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${filePath}')`
|
||||
uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
||||
uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
||||
}
|
||||
})
|
||||
|
||||
if (fileCount) {
|
||||
uploadCode = `\nconst _WEBIN_FILE_COUNT = ${fileCount}` + uploadCode
|
||||
}
|
||||
|
||||
return uploadCode
|
||||
}
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import Joi from 'joi'
|
||||
import { RunTimeType } from '.'
|
||||
|
||||
const usernameSchema = Joi.string().alphanum().min(6).max(20)
|
||||
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||
const passwordSchema = Joi.string().min(6).max(1024)
|
||||
const groupnameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||
|
||||
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||
|
||||
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
||||
export const getUserValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
username: usernameSchema.required()
|
||||
}).validate(data)
|
||||
|
||||
export const loginWebValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
username: usernameSchema.required(),
|
||||
password: passwordSchema.required(),
|
||||
password: passwordSchema.required()
|
||||
}).validate(data)
|
||||
|
||||
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
clientId: Joi.string().required()
|
||||
}).validate(data)
|
||||
|
||||
@@ -20,18 +31,24 @@ export const tokenValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
export const registerGroupValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
name: Joi.string().min(6).required(),
|
||||
name: groupnameSchema.required(),
|
||||
description: Joi.string(),
|
||||
isActive: Joi.boolean()
|
||||
}).validate(data)
|
||||
|
||||
export const getGroupValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
name: groupnameSchema.required()
|
||||
}).validate(data)
|
||||
|
||||
export const registerUserValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
displayName: Joi.string().min(6).required(),
|
||||
username: usernameSchema.required(),
|
||||
password: passwordSchema.required(),
|
||||
isAdmin: Joi.boolean(),
|
||||
isActive: Joi.boolean()
|
||||
isActive: Joi.boolean(),
|
||||
autoExec: Joi.string().allow('')
|
||||
}).validate(data)
|
||||
|
||||
export const deleteUserValidation = (
|
||||
@@ -53,7 +70,8 @@ export const updateUserValidation = (
|
||||
const validationChecks: any = {
|
||||
displayName: Joi.string().min(6),
|
||||
username: usernameSchema,
|
||||
password: passwordSchema
|
||||
password: passwordSchema,
|
||||
autoExec: Joi.string().allow('')
|
||||
}
|
||||
if (isAdmin) {
|
||||
validationChecks.isAdmin = Joi.boolean()
|
||||
@@ -103,9 +121,10 @@ export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
||||
_folderPath: Joi.string()
|
||||
}).validate(data)
|
||||
|
||||
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||
export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
code: Joi.string().required()
|
||||
code: Joi.string().required(),
|
||||
runTime: Joi.string().valid(...Object.values(RunTimeType))
|
||||
}).validate(data)
|
||||
|
||||
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
262
api/src/utils/verifyEnvVariables.ts
Normal file
262
api/src/utils/verifyEnvVariables.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
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 RunTimeType {
|
||||
SAS = 'sas',
|
||||
JS = 'js'
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
errors.push(...verifyRUN_TIMES())
|
||||
|
||||
errors.push(...verifyExecutablePaths())
|
||||
|
||||
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 verifyRUN_TIMES = (): string[] => {
|
||||
const errors: string[] = []
|
||||
const { RUN_TIMES } = process.env
|
||||
|
||||
if (RUN_TIMES) {
|
||||
const runTimes = RUN_TIMES.split(',')
|
||||
|
||||
const runTimeTypes = Object.values(RunTimeType)
|
||||
|
||||
runTimes.forEach((runTime) => {
|
||||
if (!runTimeTypes.includes(runTime as RunTimeType)) {
|
||||
errors.push(
|
||||
`- Invalid '${runTime}' runtime\n - valid options ${runTimeTypes}`
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
process.env.RUN_TIMES = DEFAULTS.RUN_TIMES
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
const verifyExecutablePaths = () => {
|
||||
const errors: string[] = []
|
||||
const { RUN_TIMES, SAS_PATH, NODE_PATH, MODE } = process.env
|
||||
|
||||
if (MODE === ModeType.Server) {
|
||||
const runTimes = RUN_TIMES?.split(',')
|
||||
|
||||
if (runTimes?.includes(RunTimeType.SAS) && !SAS_PATH) {
|
||||
errors.push(`- SAS_PATH is required for ${RunTimeType.SAS} run time`)
|
||||
}
|
||||
|
||||
if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) {
|
||||
errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const DEFAULTS = {
|
||||
MODE: ModeType.Desktop,
|
||||
PROTOCOL: ProtocolType.HTTP,
|
||||
PORT: '5000',
|
||||
HELMET_COEP: HelmetCoepType.TRUE,
|
||||
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
||||
RUN_TIMES: `${RunTimeType.SAS},${RunTimeType.JS}`
|
||||
}
|
||||
@@ -1,11 +1,30 @@
|
||||
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 (
|
||||
userId: number,
|
||||
clientId: string,
|
||||
token: string,
|
||||
tokenType: 'accessToken' | 'refreshToken'
|
||||
) => {
|
||||
): Promise<RequestUser | undefined> => {
|
||||
const dbUser = await User.findOne({ id: userId })
|
||||
|
||||
if (!dbUser) return undefined
|
||||
@@ -21,7 +40,8 @@ export const verifyTokenInDB = async (
|
||||
username: dbUser.username,
|
||||
displayName: dbUser.displayName,
|
||||
isAdmin: dbUser.isAdmin,
|
||||
isActive: dbUser.isActive
|
||||
isActive: dbUser.isActive,
|
||||
autoExec: dbUser.autoExec
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user