mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 11:24:35 +00:00
Compare commits
368 Commits
azure-auth
...
v0.0.38
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5758bcd392 | ||
|
|
9e53470947 | ||
|
|
81f6605249 | ||
|
|
0b45402946 | ||
|
|
9ac3191891 | ||
|
|
cd00aa2af8 | ||
|
|
0147bcb701 | ||
|
|
bf53ad30f4 | ||
|
|
a003b8836b | ||
|
|
df6003df94 | ||
|
|
b1d0fdbb02 | ||
|
|
2c34395110 | ||
|
|
534e4e5bf3 | ||
|
|
6146372eba | ||
|
|
aaa469a142 | ||
|
|
4fd5bf948e | ||
|
|
99f91fbce2 | ||
|
|
98a00ec7ac | ||
|
|
b0fb858c49 | ||
|
|
83959ef99e | ||
|
|
08087495d3 | ||
|
|
3f68474839 | ||
|
|
f26886f84d | ||
|
|
ddd50eac8e | ||
|
|
bba3e8d272 | ||
|
|
30944bfa18 | ||
|
|
8822de95df | ||
|
|
02a242fe4b | ||
|
|
1beac914db | ||
|
|
a45b42107e | ||
| 3d89b753f0 | |||
| fb77d99177 | |||
| fa627aabf9 | |||
|
|
fd2629862f | ||
|
|
75291f9397 | ||
|
|
99fb5f4b2b | ||
|
|
5dc3deeb11 | ||
|
|
6b708fcad3 | ||
|
|
bc0ff84d8d | ||
|
|
1ff6965dd2 | ||
|
|
d6fa877941 | ||
|
|
940f705f5d | ||
|
|
7a6e6c8bec | ||
|
|
67d200d817 | ||
|
|
a0c27ea8d3 | ||
|
|
3d583ff21d | ||
|
|
7072e282b1 | ||
|
|
145ac45036 | ||
|
|
698180ab7e | ||
|
|
0f4e38d51d | ||
|
|
e76283daa4 | ||
|
|
6ab42ca486 | ||
|
|
fa4da7624b | ||
|
|
9f5509d2d4 | ||
|
|
efaf38d303 | ||
|
|
95843fa4c7 | ||
|
|
5ba7661a83 | ||
|
|
ed5c58e10e | ||
|
|
5fce7d8f71 | ||
|
|
feeec4eb14 | ||
|
|
8c1941a87b | ||
|
|
765969db11 | ||
|
|
e60f17268d | ||
|
|
ce0a5e1229 | ||
|
|
c5738792b0 | ||
|
|
94e036dd10 | ||
|
|
da375b8086 | ||
|
|
7312763339 | ||
|
|
5005f203b8 | ||
|
|
232a73fd17 | ||
|
|
ef41691e40 | ||
|
|
3e6234e601 | ||
|
|
0a4b202428 | ||
|
|
a11893ece1 | ||
|
|
c5ad72c931 | ||
|
|
034f3173bd | ||
|
|
e2a6810e95 | ||
|
|
373d66f8af | ||
|
|
0b5f958f45 | ||
|
|
da899b90e2 | ||
|
|
2c4aa420b3 | ||
|
|
cd32912379 | ||
|
|
93dcb1753b | ||
|
|
35cf301905 | ||
|
|
5931fc1e71 | ||
|
|
18d845799c | ||
|
|
8c872bde92 | ||
|
|
f953472efd | ||
|
|
f10138b0f2 | ||
|
|
6f19d3d0ea | ||
|
|
a7facb005a | ||
|
|
88acf9df5d | ||
|
|
b0880b142a | ||
|
|
d3674c7f94 | ||
|
|
adccca6c7f | ||
|
|
8b83ccc4c2 | ||
|
|
556944b1d5 | ||
|
|
b14e07ee6e | ||
|
|
048bd9f78c | ||
|
|
d7e1aca7e3 | ||
|
|
de47d78a00 | ||
|
|
58b6f439b3 | ||
|
|
ce9bde5717 | ||
|
|
0cfe724ffa | ||
|
|
fde4bc051d | ||
|
|
367b0f1f89 | ||
|
|
d17a3dd590 | ||
|
|
bee5deed2a | ||
|
|
e6e46838b3 | ||
|
|
404f1ec059 | ||
|
|
09d36bc754 | ||
|
|
3722bbaec3 | ||
|
|
480ee4da83 | ||
|
|
dd853fe13b | ||
|
|
e1142a33a0 | ||
|
|
d4e8d91cae | ||
|
|
9a74ec545d | ||
|
|
f2000a1227 | ||
|
|
bf5767eadf | ||
|
|
e3f5206758 | ||
|
|
fffd21b348 | ||
|
|
2d74ef5e12 | ||
|
|
224743a439 | ||
|
|
f39a76da17 | ||
|
|
6107d02c8e | ||
|
|
1966b17f27 | ||
|
|
87c8aa5146 | ||
|
|
e4c027ad51 | ||
|
|
083355fdba | ||
|
|
a3b57f6e28 | ||
|
|
b0ffa145bc | ||
|
|
a8df5f4afd | ||
|
|
62de960e86 | ||
|
|
31532c0efa | ||
|
|
732230524d | ||
|
|
6dc281313e | ||
|
|
92db3c7c82 | ||
|
|
d8b75a47d3 | ||
|
|
d70fc1032f | ||
|
|
794ee8f6e0 | ||
|
|
43769e711d | ||
|
|
30528a1528 | ||
|
|
b7e1753d25 | ||
|
|
9c5772a303 | ||
|
|
7a3d710153 | ||
|
|
0a6ebe6e62 | ||
|
|
6cbc657da3 | ||
|
|
cd838915fd | ||
|
|
4e486fda69 | ||
|
|
79cac53fdb | ||
|
|
450d99f06e | ||
|
|
51ee8c0825 | ||
|
|
a1151606f2 | ||
|
|
38193c83dd | ||
|
|
59ecc36f2b | ||
|
|
8bc459c9a7 | ||
|
|
f1f1e47f76 | ||
|
|
679e9de245 | ||
|
|
f0ac996b3c | ||
|
|
2d77222ae8 | ||
|
|
e6e5a5fd64 | ||
|
|
e1eb04494a | ||
|
|
b7fa8e5f80 | ||
|
|
ef4fae4496 | ||
|
|
3e5a4e0555 | ||
|
|
cf9a8091ea | ||
|
|
0edc45dd0a | ||
|
|
ceca370e27 | ||
|
|
f235b9c2f9 | ||
|
|
d86c841f1f | ||
|
|
076b866c02 | ||
|
|
19d4430b31 | ||
|
|
e5be0e6789 | ||
|
|
27129a8921 | ||
|
|
da11c03d55 | ||
|
|
4fbdda0365 | ||
|
|
efacb1e916 | ||
|
|
d19ce253b4 | ||
|
|
e11a4b66e7 | ||
|
|
d0a1457f44 | ||
|
|
34e54934fd | ||
|
|
4873e6054f | ||
|
|
b00aa4e17b | ||
|
|
9fccfe6f35 | ||
|
|
0a9d734e09 | ||
|
|
a0822e6b61 | ||
|
|
43545fa04b | ||
|
|
a80e5c8ead | ||
|
|
c8634953ca | ||
|
|
129ad71a15 | ||
|
|
4ff4d39e95 | ||
|
|
6ad67b6dcf | ||
|
|
e6e4cd901d | ||
|
|
70b60af10c | ||
|
|
5e92864f2d | ||
|
|
511a68732b | ||
|
|
19c3298a02 | ||
|
|
77f49386d8 | ||
|
|
b97523e555 | ||
|
|
d2955645f0 | ||
|
|
b75139dda5 | ||
|
|
319743a23e | ||
|
|
3c328dbab2 | ||
|
|
455367f10a | ||
|
|
b3147ec680 | ||
|
|
c55ba5620e | ||
|
|
b20d6ec59c | ||
|
|
44fe149ed8 | ||
|
|
357bccce01 | ||
|
|
f030aa1516 | ||
|
|
9ee7951816 | ||
|
|
ca49aad153 | ||
|
|
8a6e8f54f1 | ||
|
|
4a363c5b97 | ||
|
|
5e7cecf3ea | ||
|
|
4792f15c40 | ||
|
|
d5024012c4 | ||
|
|
64656c2919 | ||
|
|
f2e50eb4cc | ||
|
|
2bb10c7166 | ||
|
|
cbe07b4abb | ||
|
|
a4ac5dc280 | ||
|
|
04a8626570 | ||
|
|
cfdb67a049 | ||
|
|
cd738aa4b8 | ||
|
|
46f2648a95 | ||
|
|
2eb42408d1 | ||
|
|
514a262340 | ||
|
|
7b8cd0892c | ||
|
|
1814c3a1f4 | ||
|
|
7a8123eb52 | ||
|
|
45fbf2df46 | ||
|
|
31959455c3 | ||
|
|
98ef40ffd6 | ||
|
|
652af82dac | ||
|
|
13d2e15d72 | ||
|
|
42dde3eded | ||
|
|
c47782eed2 | ||
|
|
d9057bc33b | ||
|
|
f07f269839 | ||
|
|
cc111675cc | ||
|
|
d2dbbce7ad | ||
|
|
3fad8e94f5 | ||
|
|
9a86419d7a | ||
|
|
61738f6a0d | ||
|
|
ab62dad237 | ||
|
|
9e2123cfe9 | ||
|
|
a056eed6b4 | ||
|
|
d7cbd315a6 | ||
|
|
d3fef0f973 | ||
|
|
2fe9d5ca9c | ||
|
|
14c2def459 | ||
|
|
dbbdaa83f0 | ||
|
|
52c3823f20 | ||
|
|
ae34aa52f0 | ||
|
|
2b7dfeb2ea | ||
|
|
882f36d30e | ||
|
|
728f277f5c | ||
|
|
9f17b17e31 | ||
|
|
46c5a75ac4 | ||
|
|
d6aeb378de | ||
|
|
d7337ce456 | ||
|
|
60f2b34567 | ||
| 4048c1a4aa | |||
| d9814441bb | |||
|
|
b48e674468 | ||
| d93673f2a5 | |||
|
|
f7e0849148 | ||
|
|
22dfcfddb9 | ||
| d52c2ed18c | |||
| 031e492d44 | |||
| 0c6ccddafd | |||
|
|
f1e464d4a4 | ||
|
|
6c7a6b6c6a | ||
|
|
f5e6b56abb | ||
|
|
48fe7994ec | ||
|
|
49c152a398 | ||
|
|
dabef59728 | ||
| bdbf7573be | |||
| 56cb2d1d51 | |||
| 8dd201ecbe | |||
| ac745c8f5c | |||
|
|
c6c24da0e2 | ||
|
|
8f815b7874 | ||
|
|
361b539271 | ||
| a3f47708a8 | |||
| ec6333f6aa | |||
| 0fb4301966 | |||
| 552a3584ec | |||
| 03d1d60660 | |||
| 70a8aaf600 | |||
| e2b12b74f5 | |||
| 9648c51b54 | |||
| 5aeefb7955 | |||
| 20eb64a994 | |||
| f8a79ff451 | |||
| 1d66079e11 | |||
| 9db65c9fa9 | |||
| 6284e72e9b | |||
|
|
4297285b26 | ||
|
|
77cb7ef50f | ||
|
|
c0ec406b2a | ||
|
|
65a57acff6 | ||
|
|
6071b6c054 | ||
|
|
d97fd9b4d7 | ||
|
|
ba2c209fce | ||
|
|
19c60c4c98 | ||
|
|
fd2092ecf3 | ||
|
|
fda5680b0e | ||
|
|
6b23452d6a | ||
|
|
174d94a23c | ||
| 829c88c4a0 | |||
| d530e0801e | |||
| 99d55775aa | |||
| ee053d1a52 | |||
| c72867d5a7 | |||
| a5bfdf9503 | |||
| 49d3a02e85 | |||
| 02f5371f57 | |||
| cff5ba460d | |||
| cb8b34afac | |||
| c760e7bf30 | |||
| a1ae0e3ed0 | |||
| 3fe475d477 | |||
| 299319e2db | |||
| d713d04b20 | |||
|
|
a1de0ae888 | ||
|
|
38db48d59f | ||
|
|
a09e90b010 | ||
|
|
f374acae6e | ||
|
|
91c7c60dc9 | ||
| a42a1693c2 | |||
| 91e2e2bc4a | |||
| bc3cb7bb20 | |||
|
|
94fc242afe | ||
|
|
6efb2d0b51 | ||
| a506bc9dd9 | |||
| 936a205e66 | |||
| 7396f4c952 | |||
| 24d3290366 | |||
| da3feb85e3 | |||
|
|
125cf35722 | ||
|
|
d9555e151b | ||
|
|
38ab27c1ed | ||
|
|
8bcb1c4b7b | ||
|
|
c0a4e1aa14 | ||
|
|
716ae81d92 | ||
| f1a19be07c | |||
| db8eb8dd71 | |||
| 96b5fef302 | |||
| f90dcff7cf | |||
| 41d6e53548 | |||
|
|
17a7a26fc3 | ||
|
|
3f39cab357 | ||
|
|
00c7d2150c | ||
|
|
ba0722b98c | ||
|
|
129cb7c128 | ||
|
|
9cf02b25d0 | ||
|
|
6a34fa1b1d | ||
|
|
8b2564120d | ||
| 22e1378ecd | |||
| 2e32f4e6dc | |||
|
|
37b6936cca | ||
|
|
6e0b04a6e5 | ||
| 279fbf2a9a | |||
| a446f5c4f7 | |||
| bd92b8b983 | |||
|
|
3ffa168c8b |
32
.dockerignore
Normal file
32
.dockerignore
Normal file
@@ -0,0 +1,32 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
!.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
api/build
|
||||
api/coverage
|
||||
api/build
|
||||
api/node_modules
|
||||
api/public
|
||||
api/web
|
||||
web/build
|
||||
web/node_modules
|
||||
README.md
|
||||
7
.env.example
Normal file
7
.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
SAS_EXEC_PATH=<path to folder containing SAS executable>
|
||||
SAS_EXEC_NAME=<name of SAS executable file>
|
||||
PORT_API=<port for sasjs server (api)>
|
||||
PORT_WEB=<port for sasjs web component(react)>
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
115
.github/CONTRIBUTING.md
vendored
Normal file
115
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is made in the `configuration` section of `package.json`:
|
||||
|
||||
- Provide path to SAS9 executable.
|
||||
|
||||
|
||||
### Using dockers:
|
||||
|
||||
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
|
||||
|
||||
Command to run docker for development:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
It uses default docker compose file i.e. `docker-compose.yml` present at root.
|
||||
It will build following images if running first time:
|
||||
|
||||
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
|
||||
- `sasjs_server_web` - image for sasjs web component app based on _ReactJS_
|
||||
- `mongodb` - image for mongo database
|
||||
- `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_
|
||||
|
||||
|
||||
#### Production
|
||||
|
||||
Command to run docker for production:
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
|
||||
It will build following images if running first time:
|
||||
|
||||
- `sasjs_server_prod` - image for sasjs server app containing api and web component's build served at route `/`
|
||||
- `mongodb` - image for mongo database
|
||||
- `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:
|
||||
|
||||
#### Development (running api and web seperately):
|
||||
|
||||
##### API
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
#### Development (running only api server and have web build served):
|
||||
|
||||
##### 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
|
||||
|
||||
```
|
||||
npm run server
|
||||
```
|
||||
|
||||
This will install/build `web` and install `api`, then start prod server.
|
||||
|
||||
|
||||
## Executables
|
||||
|
||||
Command to generate executables
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build && cd ../
|
||||
cd ./api && npm i && npm run exe
|
||||
```
|
||||
|
||||
This will install/build web app and install/create executables of sasjs server at root `./executables`
|
||||
77
.github/workflows/build.yml
vendored
77
.github/workflows/build.yml
vendored
@@ -1,30 +1,91 @@
|
||||
name: SASjs Server Build
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [lts/*]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Check Code Style
|
||||
run: npm run lint
|
||||
|
||||
- name: Check Api Code Style
|
||||
run: npm run lint-api
|
||||
|
||||
- name: Check Web Code Style
|
||||
run: npm run lint-web
|
||||
|
||||
build-api:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/*]
|
||||
|
||||
steps:
|
||||
- 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
|
||||
working-directory: ./api
|
||||
run: npm ci
|
||||
|
||||
- name: Run Unit Tests
|
||||
working-directory: ./api
|
||||
run: npm test
|
||||
- name: Build Package
|
||||
run: npm run package:lib
|
||||
env:
|
||||
CI: true
|
||||
MODE: 'server'
|
||||
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
||||
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
||||
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
||||
|
||||
- name: Build Package
|
||||
working-directory: ./api
|
||||
run: npm run build
|
||||
env:
|
||||
CI: true
|
||||
|
||||
build-web:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/*]
|
||||
|
||||
steps:
|
||||
- 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
|
||||
working-directory: ./web
|
||||
run: npm ci
|
||||
|
||||
# TODO: Uncomment next step when unit tests provided
|
||||
# - name: Run Unit Tests
|
||||
# working-directory: ./web
|
||||
# run: npm test
|
||||
|
||||
- name: Build Package
|
||||
working-directory: ./web
|
||||
run: npm run build
|
||||
env:
|
||||
CI: true
|
||||
|
||||
48
.github/workflows/release.yml
vendored
Normal file
48
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: SASjs Server Executable Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Dependencies WEB
|
||||
working-directory: ./web
|
||||
run: npm ci
|
||||
|
||||
- name: Build WEB
|
||||
working-directory: ./web
|
||||
run: npm run build
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Install Dependencies API
|
||||
working-directory: ./api
|
||||
run: npm ci
|
||||
|
||||
- name: Build Executables
|
||||
working-directory: ./api
|
||||
run: npm run exe
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Compress Executables
|
||||
working-directory: ./executables
|
||||
run: |
|
||||
zip linux.zip api-linux
|
||||
zip macos.zip api-macos
|
||||
zip windows.zip api-win.exe
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
./executables/linux.zip
|
||||
./executables/macos.zip
|
||||
./executables/windows.zip
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -6,5 +6,8 @@ node_modules/
|
||||
sas/
|
||||
tmp/
|
||||
build/
|
||||
sasjsbuild/
|
||||
sasjscore/
|
||||
certificates/
|
||||
executables/
|
||||
.env
|
||||
security.json
|
||||
10
.gitpod.yml
Normal file
10
.gitpod.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
# This configuration file was automatically generated by Gitpod.
|
||||
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
|
||||
# and commit this file to your remote git repository to share the goodness with others.
|
||||
|
||||
tasks:
|
||||
- init: npm install
|
||||
vscode:
|
||||
extensions:
|
||||
- dbaeumer.vscode-eslint
|
||||
- sasjs.sasjs-for-vscode
|
||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Docker Node.js Launch",
|
||||
"type": "docker",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "docker-run: debug",
|
||||
"platform": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"autoexec"
|
||||
]
|
||||
}
|
||||
35
.vscode/tasks.json
vendored
Normal file
35
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "docker-build",
|
||||
"label": "docker-build",
|
||||
"platform": "node",
|
||||
"dockerBuild": {
|
||||
"dockerfile": "${workspaceFolder}/Dockerfile",
|
||||
"context": "${workspaceFolder}",
|
||||
"pull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "docker-run",
|
||||
"label": "docker-run: release",
|
||||
"dependsOn": ["docker-build"],
|
||||
"platform": "node"
|
||||
},
|
||||
{
|
||||
"type": "docker-run",
|
||||
"label": "docker-run: debug",
|
||||
"dependsOn": ["docker-build"],
|
||||
"dockerRun": {
|
||||
"env": {
|
||||
"DEBUG": "*",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"node": {
|
||||
"enableDebugging": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
448
CHANGELOG.md
Normal file
448
CHANGELOG.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# Changelog
|
||||
|
||||
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.
|
||||
|
||||
### [0.0.38](https://github.com/sasjs/server/compare/v0.0.37...v0.0.38) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* quick fix for executables ([9e53470](https://github.com/sasjs/server/commit/9e53470947350f4b8d835a2cb6b70e3dabf247c4))
|
||||
|
||||
### [0.0.37](https://github.com/sasjs/server/compare/v0.0.36...v0.0.37) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* appStream html view ([cd00aa2](https://github.com/sasjs/server/commit/cd00aa2af8c7e0df851050a02152dfeddaec7b0f))
|
||||
* moved macros from codebase to drive ([9ac3191](https://github.com/sasjs/server/commit/9ac3191891bf53ff07135ccec6ddc83b34ea871a))
|
||||
* **webin:** closes [#99](https://github.com/sasjs/server/issues/99) ([0147bcb](https://github.com/sasjs/server/commit/0147bcb701a209266144147a3746baf1eb1ccc63))
|
||||
|
||||
### [0.0.36](https://github.com/sasjs/server/compare/v0.0.35...v0.0.36) (2022-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* App Stream, load on startup, new route added ([98a00ec](https://github.com/sasjs/server/commit/98a00ec7ace5da765f049864799be44ba6538e8a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appstream:** app logo + improvements ([df6003d](https://github.com/sasjs/server/commit/df6003df942fd52b956f3d4069d6d7615441d372))
|
||||
|
||||
### [0.0.35](https://github.com/sasjs/server/compare/v0.0.33...v0.0.35) (2022-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cors:** whitelisting is configurable through .env variables ([99f91fb](https://github.com/sasjs/server/commit/99f91fbce2a029dd963ed30c9007a9b046ea6560))
|
||||
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cors:** removed trailing slashes of urls ([4fd5bf9](https://github.com/sasjs/server/commit/4fd5bf948e4ad8a274d3176d5509163e67980061))
|
||||
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
|
||||
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
|
||||
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
|
||||
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
|
||||
|
||||
### [0.0.34](https://github.com/sasjs/server/compare/v0.0.33...v0.0.34) (2022-03-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
|
||||
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
|
||||
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
|
||||
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
|
||||
|
||||
### [0.0.33](https://github.com/sasjs/server/compare/v0.0.32...v0.0.33) (2022-03-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* serve deployed streaming apps ([d6fa877](https://github.com/sasjs/server/commit/d6fa87794155880adc23c2552c37c86ad606c292))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adde validation + code improvement ([1ff6965](https://github.com/sasjs/server/commit/1ff6965dd2f44ad74136af04b4fba8c76979ecba))
|
||||
* added api button on web component ([6b708fc](https://github.com/sasjs/server/commit/6b708fcad30d92c21713f9c97bca173c148cc875))
|
||||
|
||||
### [0.0.32](https://github.com/sasjs/server/compare/v0.0.31...v0.0.32) (2022-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** added delete option in Drive ([7a6e6c8](https://github.com/sasjs/server/commit/7a6e6c8becab31410d0a36bcc22e13d5359a6cdf))
|
||||
|
||||
### [0.0.31](https://github.com/sasjs/server/compare/v0.0.30...v0.0.31) (2022-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **drive:** new route delete file api ([3d583ff](https://github.com/sasjs/server/commit/3d583ff21d344a71aa861c7e5b1426ebc2d54c22))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added cookie for accessToken ([698180a](https://github.com/sasjs/server/commit/698180ab7e44d67d46c84352ececca5b6c83b230))
|
||||
* **drive:** update file API is same as create file ([7072e28](https://github.com/sasjs/server/commit/7072e282b1cd1a296d81512c57130237610c1c1e))
|
||||
* show content of get file api ([6ab42ca](https://github.com/sasjs/server/commit/6ab42ca4868366874f5f21bd711b7b8b72e36774))
|
||||
* **stp:** return plain/text header for GET & debug ([145ac45](https://github.com/sasjs/server/commit/145ac450365ed39279248ec9321bbe4918bee9fa))
|
||||
|
||||
### [0.0.30](https://github.com/sasjs/server/compare/v0.0.29...v0.0.30) (2022-03-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* parse log to array ([c5ad72c](https://github.com/sasjs/server/commit/c5ad72c931ec8fbd7d5a6475838adcbd380c8aee))
|
||||
* set response headers provded by SAS Code execution ([2c4aa42](https://github.com/sasjs/server/commit/2c4aa420b3119890cafde4265ed5dddbc9d6a636))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added http headers to /code api as well ([da899b9](https://github.com/sasjs/server/commit/da899b90e26d5ee393eefc302be985eb7c9055a5))
|
||||
* code api is updated return type ([e2a6810](https://github.com/sasjs/server/commit/e2a6810e9531a8102d3c51fd8df2e1f78f0d965f))
|
||||
* **file:** fixes response headers ([ef41691](https://github.com/sasjs/server/commit/ef41691e408ef1c1c7a921cc1050bdd533651331))
|
||||
* get file instead of it's content ([efaf38d](https://github.com/sasjs/server/commit/efaf38d3039391392ce0e14a3accddd8f34ea7d6))
|
||||
* hot fix for web component ([0a4b202](https://github.com/sasjs/server/commit/0a4b202428e14effc8014a6813cecf7761ce3715))
|
||||
* improvement in flow of uploading ([8c1941a](https://github.com/sasjs/server/commit/8c1941a87bc184be4e0e09eeff73fc6cb69e3041))
|
||||
* macros are available Sessions with SASAUTOS ([95843fa](https://github.com/sasjs/server/commit/95843fa4c711aa695ee63ad265b8def4ba56360d))
|
||||
* minor changes ([0b5f958](https://github.com/sasjs/server/commit/0b5f958f456d291ec7a8697236657c7819d5c654))
|
||||
* multi-part file upload + validations + specs ([e60f172](https://github.com/sasjs/server/commit/e60f17268d1fa9ab623313026d46bd3f63756f69))
|
||||
* organized code for usage of multer ([ce0a5e1](https://github.com/sasjs/server/commit/ce0a5e1229bed69c450061fac2bc19711448da56))
|
||||
* return buffer in case of file response ([3e6234e](https://github.com/sasjs/server/commit/3e6234e6019c5f3ae4280fac079ecc9cb0effc07))
|
||||
* **stp:** return json for webout ([5005f20](https://github.com/sasjs/server/commit/5005f203b8d6b1d577cdf094b83886bd1fc817a2))
|
||||
* updating docs ([7312763](https://github.com/sasjs/server/commit/7312763339d6769826328561e2c8d11bbfc0c9f4))
|
||||
* **upload:** added query param as well for filepath ([feeec4e](https://github.com/sasjs/server/commit/feeec4eb149e9a47e5a52320d1fc95243bf5eb15))
|
||||
|
||||
### [0.0.29](https://github.com/sasjs/server/compare/v0.0.28...v0.0.29) (2022-02-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding .. in folder path ([5931fc1](https://github.com/sasjs/server/commit/5931fc1e712c545ef80454dea5b36e684017c367))
|
||||
* adding sasjs stpsrv_header() path to autoexec. Relates to [#58](https://github.com/sasjs/server/issues/58) ([ce9bde5](https://github.com/sasjs/server/commit/ce9bde5717369de2d76dc183319be8830b2362b2))
|
||||
|
||||
### [0.0.28](https://github.com/sasjs/server/compare/v0.0.27...v0.0.28) (2022-02-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* default macros and bumping core ([6f19d3d](https://github.com/sasjs/server/commit/6f19d3d0ea3815815f246a3e455495c72c8604c7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* moving core ([f10138b](https://github.com/sasjs/server/commit/f10138b0f2005a958f63cb3a8351e1afa52f086a))
|
||||
|
||||
### [0.0.27](https://github.com/sasjs/server/compare/v0.0.26...v0.0.27) (2022-02-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* removing stpsrv_header and updating README with auth details ([d3674c7](https://github.com/sasjs/server/commit/d3674c7f9449d77977e482cd63ccdf7e974fa838))
|
||||
* **stp-execution:** add returnLog option to execution query ([bf5767e](https://github.com/sasjs/server/commit/bf5767eadfb87f7ed902659347a18361a6a6c74b))
|
||||
|
||||
### [0.0.26](https://github.com/sasjs/server/compare/v0.0.25...v0.0.26) (2022-02-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* refactored + removed unused package ([d7e1aca](https://github.com/sasjs/server/commit/d7e1aca7e33c3264c784d406fa766e29a6b15ae9))
|
||||
* release should also has https protocol ([0cfe724](https://github.com/sasjs/server/commit/0cfe724ffa089b84a9f8bca49c9033b56f51c9cb))
|
||||
* updated token expiry times ([d17a3dd](https://github.com/sasjs/server/commit/d17a3dd5900d5eb88120af8575e3fc7c2cb71ed6))
|
||||
|
||||
### [0.0.25](https://github.com/sasjs/server/compare/v0.0.24...v0.0.25) (2022-02-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding global macvar and bumping sasjs/core with additional server support ([404f1ec](https://github.com/sasjs/server/commit/404f1ec0593a027ed5e84b1d6a84cb9f2d09d99e))
|
||||
|
||||
### [0.0.24](https://github.com/sasjs/server/compare/v0.0.23...v0.0.24) (2022-02-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removing sysmacdelete ([480ee4d](https://github.com/sasjs/server/commit/480ee4da831d2a89888c58ebec26bd89802ee2f5))
|
||||
|
||||
### [0.0.23](https://github.com/sasjs/server/compare/v0.0.22...v0.0.23) (2022-02-08)
|
||||
|
||||
### [0.0.22](https://github.com/sasjs/server/compare/v0.0.17...v0.0.22) (2022-02-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adding missing global vars to autoexec ([1966b17](https://github.com/sasjs/server/commit/1966b17f27e66bf1c9673ef6e1c11f4868b4f816))
|
||||
* avoid uninitialised note ([e4c027a](https://github.com/sasjs/server/commit/e4c027ad5121302b9ae093b2b76dc27f51a94365))
|
||||
* bumping core version ([a8df5f4](https://github.com/sasjs/server/commit/a8df5f4afd6c4522270d0a60ab8153dfbdf79e16))
|
||||
* bumping sasjs/core and updating descriptions ([31532c0](https://github.com/sasjs/server/commit/31532c0efa41e53f87377a2c7c41d21c7909e3a0))
|
||||
* compressing release files for faster download times ([d8b75a4](https://github.com/sasjs/server/commit/d8b75a47d305e0772ccbf8837ba4d7347b94cc93))
|
||||
* fixing versioning blooper ([a3b57f6](https://github.com/sasjs/server/commit/a3b57f6e28448fe98e634383041a5633541c8c02))
|
||||
|
||||
### [0.0.21](https://github.com/sasjs/server/compare/v0.0.20...v0.0.21) (2022-02-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid uninitialised note ([e4c027a](https://github.com/sasjs/server/commit/e4c027ad5121302b9ae093b2b76dc27f51a94365))
|
||||
|
||||
### [0.0.20](https://github.com/sasjs/server/compare/v0.0.2...v0.0.20) (2022-01-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fixing versioning blooper ([a3b57f6](https://github.com/sasjs/server/commit/a3b57f6e28448fe98e634383041a5633541c8c02))
|
||||
|
||||
### [0.0.19](https://github.com/sasjs/server/compare/v0.0.18...v0.0.19) (2022-01-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping sasjs/core and updating descriptions ([31532c0](https://github.com/sasjs/server/commit/31532c0efa41e53f87377a2c7c41d21c7909e3a0))
|
||||
|
||||
### [0.0.18](https://github.com/sasjs/server/compare/v0.0.17...v0.0.18) (2022-01-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* compressing release files for faster download times ([d8b75a4](https://github.com/sasjs/server/commit/d8b75a47d305e0772ccbf8837ba4d7347b94cc93))
|
||||
|
||||
### [0.0.17](https://github.com/sasjs/server/compare/v0.0.16...v0.0.17) (2022-01-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bug removed, log is clean now ([43769e7](https://github.com/sasjs/server/commit/43769e711d37a4f670786545630139a2d926dc76))
|
||||
|
||||
### [0.0.16](https://github.com/sasjs/server/compare/v0.0.15...v0.0.16) (2022-01-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added sas9 server address ([cd83891](https://github.com/sasjs/server/commit/cd838915fdb216ee364ea677747409311b1214fb))
|
||||
* recreate crashed session ([6cbc657](https://github.com/sasjs/server/commit/6cbc657da3eb7fa821a678443a3ae4079c2a1f09))
|
||||
* session should be marked as consumed ([7a3d710](https://github.com/sasjs/server/commit/7a3d710153f37d12160ff45f8f97fb4fcc75d684))
|
||||
|
||||
### [0.0.15](https://github.com/sasjs/server/compare/v0.0.14...v0.0.15) (2022-01-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **studio:** web component updated ([2d77222](https://github.com/sasjs/server/commit/2d77222ae8a139acd9d96466d0e68291c4ebd70e))
|
||||
* updated route for sas code ([e1eb044](https://github.com/sasjs/server/commit/e1eb04494a5650726c95990f74fc719eced4ccb5))
|
||||
* **web:** autosave and autofocus ([51ee8c0](https://github.com/sasjs/server/commit/51ee8c0825f021d1d67b2d765d5b434cbf248a1f))
|
||||
* **web:** parsing of webout ([a115160](https://github.com/sasjs/server/commit/a1151606f21e0007e2b1ca1245d592d96866f62a))
|
||||
* **web:** sticky tabs on Studio + extra run code button removed ([450d99f](https://github.com/sasjs/server/commit/450d99f06e5929eb1679e6203284e4faa44e19b0))
|
||||
|
||||
### [0.0.14](https://github.com/sasjs/server/compare/v0.0.13...v0.0.14) (2021-12-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* actually a README change, the fix was in the previous commit (updating ms_webout) that should have been a PR, to trigger a release ([d86c841](https://github.com/sasjs/server/commit/d86c841f1fb94455ac3500f215a42b4acb8b0017))
|
||||
* bumping sasjs/core with adjustment to ms_webout() ([076b866](https://github.com/sasjs/server/commit/076b866c020fb017512c2764801022a57fe4cca8))
|
||||
* switch to main branch ([ceca370](https://github.com/sasjs/server/commit/ceca370e2757baf2e8ebb90dab6dfd27f7b990fc))
|
||||
|
||||
### [0.0.13](https://github.com/sasjs/server/compare/v0.0.12...v0.0.13) (2021-12-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **studio:** run selected code + open in studio ([27129a8](https://github.com/sasjs/server/commit/27129a8921084c72968383fdbc2ecbd2f417456c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* output for Studio ([e5be0e6](https://github.com/sasjs/server/commit/e5be0e678965b05c64bcc8f55c48a366e0ff55a3))
|
||||
|
||||
### [0.0.12](https://github.com/sasjs/server/compare/v0.0.11...v0.0.12) (2021-12-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use env if provided for desktop mode ([d19ce25](https://github.com/sasjs/server/commit/d19ce253b4e2d2a7dd912d43a553d4c1bd60ba58))
|
||||
|
||||
### [0.0.11](https://github.com/sasjs/server/compare/v0.0.10...v0.0.11) (2021-12-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added authorization route for web ([#37](https://github.com/sasjs/server/issues/37)) ([d0a1457](https://github.com/sasjs/server/commit/d0a1457f44a3d8993b57106e5e681c4e51fe8e7d))
|
||||
|
||||
### [0.0.10](https://github.com/sasjs/server/compare/v0.0.9...v0.0.10) (2021-12-07)
|
||||
|
||||
### [0.0.9](https://github.com/sasjs/server/compare/v0.0.3...v0.0.9) (2021-12-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* release with files ([#35](https://github.com/sasjs/server/issues/35)) ([a0822e6](https://github.com/sasjs/server/commit/a0822e6b61905257475121ffd907fd1f79ed146b))
|
||||
|
||||
### [0.0.8](https://github.com/saadjutt01/server/compare/v0.0.7...v0.0.8) (2021-12-07)
|
||||
|
||||
### [0.0.7](https://github.com/saadjutt01/server/compare/v0.0.6...v0.0.7) (2021-12-07)
|
||||
|
||||
### [0.0.6](https://github.com/saadjutt01/server/compare/v0.0.5...v0.0.6) (2021-12-07)
|
||||
|
||||
### [0.0.5](https://github.com/saadjutt01/server/compare/v0.0.4...v0.0.5) (2021-12-07)
|
||||
|
||||
### 0.0.4 (2021-12-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api endpoint for sasjs drive ([96b5fef](https://github.com/saadjutt01/server/commit/96b5fef3021f67f66e5e3b854319230618421852))
|
||||
* add new type TreeNode ([bc3cb7b](https://github.com/saadjutt01/server/commit/bc3cb7bb20a1202d17aaf8bbcddd1feef4fff724))
|
||||
* add pug and directory tree dependencies ([3ffa168](https://github.com/saadjutt01/server/commit/3ffa168c8bafc989caf1a744cebc20d36c6aa11b))
|
||||
* add sasjsExecutor controller ([279fbf2](https://github.com/saadjutt01/server/commit/279fbf2a9a0bd6bc0938f9a66e9685fb93d86089))
|
||||
* add top app bar with tab navigation ([a506bc9](https://github.com/saadjutt01/server/commit/a506bc9dd9d201b89fc9ffd1a552c16bd170f058))
|
||||
* add views and styles for rendering html ([a446f5c](https://github.com/saadjutt01/server/commit/a446f5c4f73a4e829a2c5eec041e3adffeddff52))
|
||||
* adding _metaperson and _metauser to Stored Programs ([b3147ec](https://github.com/saadjutt01/server/commit/b3147ec680646b3d9c7e89152e472dddc8a36075))
|
||||
* **api-utility:** create getWebBuildFolderPath utility ([9648c51](https://github.com/saadjutt01/server/commit/9648c51b5491d8b6bbe5497273efa2d11e2486d2))
|
||||
* **api:** set up endpoint for sas code execution ([f6046b1](https://github.com/saadjutt01/server/commit/f6046b15ae30cd8ace685cf283339871de658b7d))
|
||||
* authentication with jwt ([22dfcfd](https://github.com/saadjutt01/server/commit/22dfcfddb9abd355a63d1ee5acd925c759e86d69))
|
||||
* compile systemInit and inject to autoExec ([b75139d](https://github.com/saadjutt01/server/commit/b75139dda5cacc7e10a4d635eb2a222f7dfa3fec))
|
||||
* **deploy:** add appLoc ([f0f1e1d](https://github.com/saadjutt01/server/commit/f0f1e1d57ea1e961fc3b1cfcbd4cb259a77a90d0))
|
||||
* **deploy:** add route to deploy a file tree to @sasjs/server ([b4bf72f](https://github.com/saadjutt01/server/commit/b4bf72f70401a81b6d5d0104332a1fbc5f71562b))
|
||||
* **execute:** add macroVars to job execution ([39e486b](https://github.com/saadjutt01/server/commit/39e486b8cb5efbadc86eb7029b60c7073744eb2b))
|
||||
* **execute:** add sas controller ([bf1db4d](https://github.com/saadjutt01/server/commit/bf1db4dd47d2488bac073cd468db920ff9fd533d))
|
||||
* **execution:** add ExecutionController working with session ([8b25641](https://github.com/saadjutt01/server/commit/8b2564120def137f80647064e28062b880d58efe))
|
||||
* **executor:** improved api response ([707b503](https://github.com/saadjutt01/server/commit/707b50394267217e717aa72f74dbeba3852a93e6))
|
||||
* **executor:** response with webout ([52275ba](https://github.com/saadjutt01/server/commit/52275ba67d97d5cbdf6c5511c9bd789bd6ca6b4e))
|
||||
* **express:** increase payload max size ([7b403c1](https://github.com/saadjutt01/server/commit/7b403c151e889cae975944546bb4bb53eff1dd26))
|
||||
* frontend app for sasjs server ([db8eb8d](https://github.com/saadjutt01/server/commit/db8eb8dd7197bbe36f2d10cabbb58b3eb7ce7c33))
|
||||
* generate executables for sasjs/server with web component ([514a262](https://github.com/saadjutt01/server/commit/514a262340dc34007de75caf08ad03969e7110c1))
|
||||
* Groups are added + docs ([2fe9d5c](https://github.com/saadjutt01/server/commit/2fe9d5ca9ce1fb376f03534f8685d65efb2f68a6))
|
||||
* improved deploy and execute endpoints ([5b4e562](https://github.com/saadjutt01/server/commit/5b4e5626fc7ae3e020819e3ebd334cc3712ae8e7))
|
||||
* JWT saved in DB + logout api added ([46c5a75](https://github.com/saadjutt01/server/commit/46c5a75ac4fb26ebec219118eb204f1b5049ae90))
|
||||
* **routes:** separate routes into web and api ([dabef59](https://github.com/saadjutt01/server/commit/dabef597287a59f3bfaff54a18de465f820aa514))
|
||||
* **session:** add SessionController ([6a34fa1](https://github.com/saadjutt01/server/commit/6a34fa1b1dae07fe032352bea0644ab7a6f9c3f9))
|
||||
* **session:** add SessionController and ExecutionController ([6e0b04a](https://github.com/saadjutt01/server/commit/6e0b04a6e548ac31baee726c9249b7e25f50f0bf))
|
||||
* user operation apis added ([728f277](https://github.com/saadjutt01/server/commit/728f277f5ce136d62951071833cd6db478b07e4a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-cdrive-oller:** throw erow error when file not found ([03d1d60](https://github.com/saadjutt01/server/commit/03d1d60660fc46421ef6ad9cee8493dd884e309a))
|
||||
* change api endpoint SASjsExecutor/do -> SASjsApi/stp/execute ([d93673f](https://github.com/saadjutt01/server/commit/d93673f2a51098c6af8abc4a793081d4591e27de))
|
||||
* cors enabled for desktop mode ([2bb10c7](https://github.com/saadjutt01/server/commit/2bb10c71661b5de7ed515c82e5b1967b88449972))
|
||||
* DB names updates + refresh api is added ([9f17b17](https://github.com/saadjutt01/server/commit/9f17b17e3138ce49f24447cd5ae457e3e90ad4da))
|
||||
* debug not passed ([d9555e1](https://github.com/saadjutt01/server/commit/d9555e151b0e1d1a4068efdf8ee9ed53b25b9b89))
|
||||
* **deploy:** fix payload processing ([361b539](https://github.com/saadjutt01/server/commit/361b539271cf95bbe570cca9e44635ab563d3f9e))
|
||||
* **deps:** removed malicious dependency ([c4b9402](https://github.com/saadjutt01/server/commit/c4b9402f017b76dc412a17a10313f1fd5a3891ef))
|
||||
* **docker:** docker-compose for prod+development ([4a363c5](https://github.com/saadjutt01/server/commit/4a363c5b9796283199debcc8afa810c6f561f8e6))
|
||||
* **executor:** create tmp files before execution ([cdbc3fd](https://github.com/saadjutt01/server/commit/cdbc3fd298e2a581773448bdddcad93de3b3544d))
|
||||
* **executor:** fix nosplash argument and api response ([715b1de](https://github.com/saadjutt01/server/commit/715b1dec68377eefe03aa8203a73debe77842436))
|
||||
* fix web route ([6c7a6b6](https://github.com/saadjutt01/server/commit/6c7a6b6c6af28c29b391162e4e332da6524b1c61))
|
||||
* **github:** fixed github flow ([8dab288](https://github.com/saadjutt01/server/commit/8dab28861dfa7c4c7fefc7fe038df50f58d04547))
|
||||
* **github:** removed npm token ([bbb94d6](https://github.com/saadjutt01/server/commit/bbb94d61ce39c84a6c0c44186e89787ab0e76a8c))
|
||||
* immplementation of files api fixed ([299319e](https://github.com/saadjutt01/server/commit/299319e2dbe06c7ca99e403fcbdec2ad1db8b7e4))
|
||||
* load file when url contains filePath ([99d5577](https://github.com/saadjutt01/server/commit/99d55775aaac3b2caaa4b10d4ed698f6cd7fcb2a))
|
||||
* modify the directory tree algorithm to include relative path with each node ([91e2e2b](https://github.com/saadjutt01/server/commit/91e2e2bc4a46da0d149578593559efdb87681bd4))
|
||||
* norefferer issue in home page external links fix ([e2b12b7](https://github.com/saadjutt01/server/commit/e2b12b74f52c3ce4541fde9af6af0093b56b157b))
|
||||
* on clicking execute button open new tab for response ([02f5371](https://github.com/saadjutt01/server/commit/02f5371f57b311ff700ba8108f9d5168da8c22a4))
|
||||
* prettier ([716ae81](https://github.com/saadjutt01/server/commit/716ae81d9293b42dd2a7047ac52d75401b3b8798))
|
||||
* **prod-server:** use port from configuration ([4d8efbb](https://github.com/saadjutt01/server/commit/4d8efbb88d32154d84e80b79780e2e3de2f519e4))
|
||||
* readme overview| ([b3342f0](https://github.com/saadjutt01/server/commit/b3342f00031d19080fb72e3460f023c5f44bac95))
|
||||
* remove .sas extension from _program parameter at the end of string ([56cb2d1](https://github.com/saadjutt01/server/commit/56cb2d1d512beadb5cfdc4ab4034ac917311ff23))
|
||||
* removing renegade dash ([4ff4d39](https://github.com/saadjutt01/server/commit/4ff4d39e954e895b46ddc3e2919f7f2c4e1ce01d))
|
||||
* **root-package.json:** lint:fix command fixed in root package json ([ec6333f](https://github.com/saadjutt01/server/commit/ec6333f6aa67c1b94f54b017ed27eb3b21b4207f))
|
||||
* **routes:** fix routes imports ([49c152a](https://github.com/saadjutt01/server/commit/49c152a398b60f6b0a0c25a68eb4c1c291984872))
|
||||
* **semantic-release:** fixed package.json ([ef45787](https://github.com/saadjutt01/server/commit/ef45787019f1e61d0e4e2acee334236e8aca23cc))
|
||||
* sending _webout as result object in response JSON ([b97523e](https://github.com/saadjutt01/server/commit/b97523e55584cc7d9d682cfeaab8f5b70a10b899))
|
||||
* session refactoring with Saad & Allan ([cbe07b4](https://github.com/saadjutt01/server/commit/cbe07b4abb2e936037874af1a088cd038e0fc731))
|
||||
* **ts:** enable files ([37b6936](https://github.com/saadjutt01/server/commit/37b6936cca3cff9c1ca26ec7b4b938a357c448df))
|
||||
* update api calls from client side ([031e492](https://github.com/saadjutt01/server/commit/031e492d44674dec4f2b3bc1f5bf7affac5716bd))
|
||||
* update api endpoints ([936a205](https://github.com/saadjutt01/server/commit/936a205e66073b9178089c6ab10d6ac3bf323c54))
|
||||
* update sasjs drive controller from function base to class base ([3fe475d](https://github.com/saadjutt01/server/commit/3fe475d477c466556659b48c70eeac5153ff5b0e))
|
||||
* update SASjsApi/stp/execute post api endpoints to capture url params ([d981444](https://github.com/saadjutt01/server/commit/d9814441bb1d269ec2404e50f51124f998c65c40))
|
||||
* use hash router instead of browser router in react app ([c72867d](https://github.com/saadjutt01/server/commit/c72867d5a70550660c8c37220aa33693716a93f1))
|
||||
* **web:** infinite call to api end point fixed ([ac745c8](https://github.com/saadjutt01/server/commit/ac745c8f5c3e4aa2ac8d6ca23bb1276452d4018b))
|
||||
* **web:** remove unnecessary packages and files ([0fb4301](https://github.com/saadjutt01/server/commit/0fb43019668f5a13f6e77fdb4b3e543006b509c0))
|
||||
* **weeb:** add catch block with each axios request ([552a358](https://github.com/saadjutt01/server/commit/552a3584ec9345bc1dec0ff5377bf773a7928d62))
|
||||
* **workflow:** fix 'SASjs Server Build' ([174d94a](https://github.com/saadjutt01/server/commit/174d94a23c5036d61a4f2e11296283f128d4dafa))
|
||||
|
||||
### 0.0.3 (2021-11-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api endpoint for sasjs drive ([96b5fef](https://github.com/sasjs/server/commit/96b5fef3021f67f66e5e3b854319230618421852))
|
||||
* add new type TreeNode ([bc3cb7b](https://github.com/sasjs/server/commit/bc3cb7bb20a1202d17aaf8bbcddd1feef4fff724))
|
||||
* add pug and directory tree dependencies ([3ffa168](https://github.com/sasjs/server/commit/3ffa168c8bafc989caf1a744cebc20d36c6aa11b))
|
||||
* add sasjsExecutor controller ([279fbf2](https://github.com/sasjs/server/commit/279fbf2a9a0bd6bc0938f9a66e9685fb93d86089))
|
||||
* add top app bar with tab navigation ([a506bc9](https://github.com/sasjs/server/commit/a506bc9dd9d201b89fc9ffd1a552c16bd170f058))
|
||||
* add views and styles for rendering html ([a446f5c](https://github.com/sasjs/server/commit/a446f5c4f73a4e829a2c5eec041e3adffeddff52))
|
||||
* adding _metaperson and _metauser to Stored Programs ([b3147ec](https://github.com/sasjs/server/commit/b3147ec680646b3d9c7e89152e472dddc8a36075))
|
||||
* **api-utility:** create getWebBuildFolderPath utility ([9648c51](https://github.com/sasjs/server/commit/9648c51b5491d8b6bbe5497273efa2d11e2486d2))
|
||||
* **api:** set up endpoint for sas code execution ([f6046b1](https://github.com/sasjs/server/commit/f6046b15ae30cd8ace685cf283339871de658b7d))
|
||||
* authentication with jwt ([22dfcfd](https://github.com/sasjs/server/commit/22dfcfddb9abd355a63d1ee5acd925c759e86d69))
|
||||
* compile systemInit and inject to autoExec ([b75139d](https://github.com/sasjs/server/commit/b75139dda5cacc7e10a4d635eb2a222f7dfa3fec))
|
||||
* **deploy:** add appLoc ([f0f1e1d](https://github.com/sasjs/server/commit/f0f1e1d57ea1e961fc3b1cfcbd4cb259a77a90d0))
|
||||
* **deploy:** add route to deploy a file tree to @sasjs/server ([b4bf72f](https://github.com/sasjs/server/commit/b4bf72f70401a81b6d5d0104332a1fbc5f71562b))
|
||||
* **execute:** add macroVars to job execution ([39e486b](https://github.com/sasjs/server/commit/39e486b8cb5efbadc86eb7029b60c7073744eb2b))
|
||||
* **execute:** add sas controller ([bf1db4d](https://github.com/sasjs/server/commit/bf1db4dd47d2488bac073cd468db920ff9fd533d))
|
||||
* **execution:** add ExecutionController working with session ([8b25641](https://github.com/sasjs/server/commit/8b2564120def137f80647064e28062b880d58efe))
|
||||
* **executor:** improved api response ([707b503](https://github.com/sasjs/server/commit/707b50394267217e717aa72f74dbeba3852a93e6))
|
||||
* **executor:** response with webout ([52275ba](https://github.com/sasjs/server/commit/52275ba67d97d5cbdf6c5511c9bd789bd6ca6b4e))
|
||||
* **express:** increase payload max size ([7b403c1](https://github.com/sasjs/server/commit/7b403c151e889cae975944546bb4bb53eff1dd26))
|
||||
* frontend app for sasjs server ([db8eb8d](https://github.com/sasjs/server/commit/db8eb8dd7197bbe36f2d10cabbb58b3eb7ce7c33))
|
||||
* generate executables for sasjs/server with web component ([514a262](https://github.com/sasjs/server/commit/514a262340dc34007de75caf08ad03969e7110c1))
|
||||
* Groups are added + docs ([2fe9d5c](https://github.com/sasjs/server/commit/2fe9d5ca9ce1fb376f03534f8685d65efb2f68a6))
|
||||
* improved deploy and execute endpoints ([5b4e562](https://github.com/sasjs/server/commit/5b4e5626fc7ae3e020819e3ebd334cc3712ae8e7))
|
||||
* JWT saved in DB + logout api added ([46c5a75](https://github.com/sasjs/server/commit/46c5a75ac4fb26ebec219118eb204f1b5049ae90))
|
||||
* **routes:** separate routes into web and api ([dabef59](https://github.com/sasjs/server/commit/dabef597287a59f3bfaff54a18de465f820aa514))
|
||||
* **session:** add SessionController ([6a34fa1](https://github.com/sasjs/server/commit/6a34fa1b1dae07fe032352bea0644ab7a6f9c3f9))
|
||||
* **session:** add SessionController and ExecutionController ([6e0b04a](https://github.com/sasjs/server/commit/6e0b04a6e548ac31baee726c9249b7e25f50f0bf))
|
||||
* user operation apis added ([728f277](https://github.com/sasjs/server/commit/728f277f5ce136d62951071833cd6db478b07e4a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api-cdrive-oller:** throw erow error when file not found ([03d1d60](https://github.com/sasjs/server/commit/03d1d60660fc46421ef6ad9cee8493dd884e309a))
|
||||
* change api endpoint SASjsExecutor/do -> SASjsApi/stp/execute ([d93673f](https://github.com/sasjs/server/commit/d93673f2a51098c6af8abc4a793081d4591e27de))
|
||||
* cors enabled for desktop mode ([2bb10c7](https://github.com/sasjs/server/commit/2bb10c71661b5de7ed515c82e5b1967b88449972))
|
||||
* DB names updates + refresh api is added ([9f17b17](https://github.com/sasjs/server/commit/9f17b17e3138ce49f24447cd5ae457e3e90ad4da))
|
||||
* debug not passed ([d9555e1](https://github.com/sasjs/server/commit/d9555e151b0e1d1a4068efdf8ee9ed53b25b9b89))
|
||||
* **deploy:** fix payload processing ([361b539](https://github.com/sasjs/server/commit/361b539271cf95bbe570cca9e44635ab563d3f9e))
|
||||
* **deps:** removed malicious dependency ([c4b9402](https://github.com/sasjs/server/commit/c4b9402f017b76dc412a17a10313f1fd5a3891ef))
|
||||
* **docker:** docker-compose for prod+development ([4a363c5](https://github.com/sasjs/server/commit/4a363c5b9796283199debcc8afa810c6f561f8e6))
|
||||
* **executor:** create tmp files before execution ([cdbc3fd](https://github.com/sasjs/server/commit/cdbc3fd298e2a581773448bdddcad93de3b3544d))
|
||||
* **executor:** fix nosplash argument and api response ([715b1de](https://github.com/sasjs/server/commit/715b1dec68377eefe03aa8203a73debe77842436))
|
||||
* fix web route ([6c7a6b6](https://github.com/sasjs/server/commit/6c7a6b6c6af28c29b391162e4e332da6524b1c61))
|
||||
* **github:** fixed github flow ([8dab288](https://github.com/sasjs/server/commit/8dab28861dfa7c4c7fefc7fe038df50f58d04547))
|
||||
* **github:** removed npm token ([bbb94d6](https://github.com/sasjs/server/commit/bbb94d61ce39c84a6c0c44186e89787ab0e76a8c))
|
||||
* immplementation of files api fixed ([299319e](https://github.com/sasjs/server/commit/299319e2dbe06c7ca99e403fcbdec2ad1db8b7e4))
|
||||
* load file when url contains filePath ([99d5577](https://github.com/sasjs/server/commit/99d55775aaac3b2caaa4b10d4ed698f6cd7fcb2a))
|
||||
* modify the directory tree algorithm to include relative path with each node ([91e2e2b](https://github.com/sasjs/server/commit/91e2e2bc4a46da0d149578593559efdb87681bd4))
|
||||
* norefferer issue in home page external links fix ([e2b12b7](https://github.com/sasjs/server/commit/e2b12b74f52c3ce4541fde9af6af0093b56b157b))
|
||||
* on clicking execute button open new tab for response ([02f5371](https://github.com/sasjs/server/commit/02f5371f57b311ff700ba8108f9d5168da8c22a4))
|
||||
* prettier ([716ae81](https://github.com/sasjs/server/commit/716ae81d9293b42dd2a7047ac52d75401b3b8798))
|
||||
* **prod-server:** use port from configuration ([4d8efbb](https://github.com/sasjs/server/commit/4d8efbb88d32154d84e80b79780e2e3de2f519e4))
|
||||
* readme overview| ([b3342f0](https://github.com/sasjs/server/commit/b3342f00031d19080fb72e3460f023c5f44bac95))
|
||||
* remove .sas extension from _program parameter at the end of string ([56cb2d1](https://github.com/sasjs/server/commit/56cb2d1d512beadb5cfdc4ab4034ac917311ff23))
|
||||
* removing renegade dash ([4ff4d39](https://github.com/sasjs/server/commit/4ff4d39e954e895b46ddc3e2919f7f2c4e1ce01d))
|
||||
* **root-package.json:** lint:fix command fixed in root package json ([ec6333f](https://github.com/sasjs/server/commit/ec6333f6aa67c1b94f54b017ed27eb3b21b4207f))
|
||||
* **routes:** fix routes imports ([49c152a](https://github.com/sasjs/server/commit/49c152a398b60f6b0a0c25a68eb4c1c291984872))
|
||||
* **semantic-release:** fixed package.json ([ef45787](https://github.com/sasjs/server/commit/ef45787019f1e61d0e4e2acee334236e8aca23cc))
|
||||
* sending _webout as result object in response JSON ([b97523e](https://github.com/sasjs/server/commit/b97523e55584cc7d9d682cfeaab8f5b70a10b899))
|
||||
* session refactoring with Saad & Allan ([cbe07b4](https://github.com/sasjs/server/commit/cbe07b4abb2e936037874af1a088cd038e0fc731))
|
||||
* **ts:** enable files ([37b6936](https://github.com/sasjs/server/commit/37b6936cca3cff9c1ca26ec7b4b938a357c448df))
|
||||
* update api calls from client side ([031e492](https://github.com/sasjs/server/commit/031e492d44674dec4f2b3bc1f5bf7affac5716bd))
|
||||
* update api endpoints ([936a205](https://github.com/sasjs/server/commit/936a205e66073b9178089c6ab10d6ac3bf323c54))
|
||||
* update sasjs drive controller from function base to class base ([3fe475d](https://github.com/sasjs/server/commit/3fe475d477c466556659b48c70eeac5153ff5b0e))
|
||||
* update SASjsApi/stp/execute post api endpoints to capture url params ([d981444](https://github.com/sasjs/server/commit/d9814441bb1d269ec2404e50f51124f998c65c40))
|
||||
* use hash router instead of browser router in react app ([c72867d](https://github.com/sasjs/server/commit/c72867d5a70550660c8c37220aa33693716a93f1))
|
||||
* **web:** infinite call to api end point fixed ([ac745c8](https://github.com/sasjs/server/commit/ac745c8f5c3e4aa2ac8d6ca23bb1276452d4018b))
|
||||
* **web:** remove unnecessary packages and files ([0fb4301](https://github.com/sasjs/server/commit/0fb43019668f5a13f6e77fdb4b3e543006b509c0))
|
||||
* **weeb:** add catch block with each axios request ([552a358](https://github.com/sasjs/server/commit/552a3584ec9345bc1dec0ff5377bf773a7928d62))
|
||||
* **workflow:** fix 'SASjs Server Build' ([174d94a](https://github.com/sasjs/server/commit/174d94a23c5036d61a4f2e11296283f128d4dafa))
|
||||
9
DockerfileApi
Normal file
9
DockerfileApi
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM node:lts-alpine
|
||||
WORKDIR /usr/server/api
|
||||
COPY ["package.json","package-lock.json", "./"]
|
||||
RUN npm ci
|
||||
COPY ./api .
|
||||
COPY ./certificates ../certificates
|
||||
# RUN chown -R node /usr/server/api
|
||||
# USER node
|
||||
CMD ["npm","start"]
|
||||
11
DockerfileProd
Normal file
11
DockerfileProd
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM node:lts-alpine
|
||||
RUN npm install -g @sasjs/cli
|
||||
WORKDIR /usr/server/
|
||||
COPY . .
|
||||
RUN cd web && npm ci --silent
|
||||
RUN cd web && REACT_APP_CLIENT_ID=clientID1 npm run build
|
||||
RUN cd api && npm ci --silent
|
||||
# RUN chown -R node /usr/server/api
|
||||
# USER node
|
||||
WORKDIR /usr/server/api
|
||||
CMD ["npm","run","start:prod"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 SASjs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
121
README.md
121
README.md
@@ -1,16 +1,119 @@
|
||||
# SASjs Server
|
||||
|
||||
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or it could even run locally on your desktop. It provides the following functionality:
|
||||
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
|
||||
* Ability to execute Stored Programs from a URL
|
||||
* Ability to create web apps using simple Desktop SAS
|
||||
- Virtual filesystem for storing SAS programs and other content
|
||||
- Ability to execute Stored Programs from a URL
|
||||
- Ability to create web apps using simple Desktop SAS
|
||||
- REST API with Swagger Docs
|
||||
|
||||
One major benefit of using SASjs Server (alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library) is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
|
||||
One major benefit of using SASjs Server alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library, is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
|
||||
|
||||
## Configuration
|
||||
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentication, and a database)
|
||||
|
||||
Configuration is made in the `configuration` section of `package.json`:
|
||||
## Installation
|
||||
|
||||
- Provide path to SAS9 executable.
|
||||
- Provide `SASjsServer` hostname and port (eg `localhost:5000`).
|
||||
Installation can be made programmatically using command line, or by manually downloading and running the executable.
|
||||
|
||||
### Programmatic
|
||||
|
||||
Fetch the relevant package from github using `curl`, eg as follows (for linux):
|
||||
|
||||
```bash
|
||||
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||
unzip linux.zip
|
||||
```
|
||||
|
||||
The app can then be launched with `./api-linux` and prompts followed (if ENV vars not set).
|
||||
|
||||
### Manual
|
||||
|
||||
1. Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
|
||||
2. Trigger by double clicking (windows) or executing from commandline.
|
||||
|
||||
You are presented with two prompts (if not set as ENV vars):
|
||||
|
||||
- Location of your `sas.exe` / `sas.sh` executable
|
||||
- Path to a filesystem location for Stored Programs and temporary files
|
||||
|
||||
## ENV Var configuration
|
||||
|
||||
When launching the app, it will make use of specific environment variables. These can be set in the following places:
|
||||
|
||||
- Configured globally in `/etc/environment` file
|
||||
- Export in terminal or shell script (`export VAR=VALUE`)
|
||||
- Prepended in the command
|
||||
- Enter in the `.env` file alongside the executable
|
||||
|
||||
Example contents of a `.env` file:
|
||||
|
||||
```
|
||||
MODE=desktop # options: [desktop|server] default: `desktop`
|
||||
CORS=disable # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||
WHITELIST= # options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||
PROTOCOL=http # options: [http|https] default: http
|
||||
PORT=5000 # default: 5000
|
||||
|
||||
# optional
|
||||
# for MODE: `desktop`, prompts user
|
||||
# for MODE: `server` gets value from api/package.json `configuration.sasPath`
|
||||
SAS_PATH=/path/to/sas/executable.exe
|
||||
|
||||
|
||||
# optional
|
||||
# for MODE: `desktop`, prompts user
|
||||
# for MODE: `server` defaults to /tmp
|
||||
DRIVE_PATH=/tmp
|
||||
|
||||
# ENV variables required for PROTOCOL: `https`
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
|
||||
# ENV variables required for MODE: `server`
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||
export PORT=5001
|
||||
export DRIVE_PATH=./tmp
|
||||
|
||||
pm2 start api-linux
|
||||
```
|
||||
|
||||
To get the logs (and some useful commands):
|
||||
|
||||
```bash
|
||||
pm2 [list|ls|status]
|
||||
pm2 logs
|
||||
pm2 logs --lines 200
|
||||
```
|
||||
|
||||
Managing processes:
|
||||
|
||||
```
|
||||
pm2 restart app_name
|
||||
pm2 reload app_name
|
||||
pm2 stop app_name
|
||||
pm2 delete app_name
|
||||
```
|
||||
|
||||
Instead of `app_name` you can pass:
|
||||
|
||||
- `all` to act on all processes
|
||||
- `id` to act on a specific process id
|
||||
|
||||
## Server Version
|
||||
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
|
||||
|
||||
- CLIENTID: `clientID1`
|
||||
- USERNAME: `secretuser`
|
||||
- PASSWORD: `secretpassword`
|
||||
|
||||
89
SASjsServer.drawio
Normal file
89
SASjsServer.drawio
Normal file
@@ -0,0 +1,89 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="HJy_QFGaI9JSrArARLup" name="Page-1">
|
||||
<mxGraphModel dx="1908" dy="2140" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="4" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
|
||||
<mxGeometry x="-360" y="-120" width="40" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="SASjs Server" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;fontSize=30;" vertex="1" parent="1">
|
||||
<mxGeometry x="30" y="-150" width="360" height="850" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="28">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-340" y="23" as="sourcePoint"/>
|
||||
<mxPoint x="115" y="22.586363636363558" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/authorize<br>(username,password,clientId)</span></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="8">
|
||||
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="" style="edgeStyle=none;html=1;exitX=-0.002;exitY=0.874;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="28">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="110" y="80" as="sourcePoint"/>
|
||||
<mxPoint x="-340" y="80" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`code`</span></font>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="14">
|
||||
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
|
||||
<mxGeometry x="-360" y="545" width="40" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="30">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-340" y="165" as="sourcePoint"/>
|
||||
<mxPoint x="115" y="165" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/token</span></div><span style="color: #a31515">(clientId,code)</span></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="22">
|
||||
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="" style="edgeStyle=none;html=1;exitX=0.009;exitY=0.905;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="30">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="210" y="222.5" as="sourcePoint"/>
|
||||
<mxPoint x="-340" y="223" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`</span></font><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">accessToken</span><span style="font-size: 12px ; color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace">` &amp; `</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">refreshToken</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">`</span>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="24">
|
||||
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;" edge="1" parent="1" source="21" target="4">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="40" y="240" as="sourcePoint"/>
|
||||
<mxPoint x="90" y="190" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="<span>Validates</span><br><span>username/password/clientId</span><br><span>and issue short</span><br><span>Authorization code</span>" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="115" width="190" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="Validates<br>clientId &amp; authorization code<br>and issue<br>Access Token &amp; Refresh Token" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="115" y="140" width="190" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="32" value="Protected APIs<br>Authenticate requests <br>with provided Bearer Token" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="50" y="280" width="320" height="400" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="33" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.373;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="32">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="-340" y="432.5" as="sourcePoint"/>
|
||||
<mxPoint x="-10" y="430" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="34" value="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><font color="#a31515">Request with Access Token</font></div></div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33">
|
||||
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
6
api/.dockerignore
Normal file
6
api/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
build
|
||||
coverage
|
||||
node_modules
|
||||
public
|
||||
web
|
||||
Dockerfile
|
||||
14
api/.env.example
Normal file
14
api/.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
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
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||
DRIVE_PATH=./tmp
|
||||
1
api/.nvmrc
Normal file
1
api/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v16.14.0
|
||||
13
api/.vscode/launch.json
vendored
Normal file
13
api/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch via NPM",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["run-script", "start"],
|
||||
"runtimeExecutable": "npm",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
}
|
||||
17864
api/package-lock.json
generated
Normal file
17864
api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
97
api/package.json
Normal file
97
api/package.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "api",
|
||||
"version": "0.0.2",
|
||||
"description": "Api of SASjs server",
|
||||
"main": "./src/server.ts",
|
||||
"scripts": {
|
||||
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
||||
"prestart": "npm run initial",
|
||||
"prebuild": "npm run initial",
|
||||
"start": "nodemon ./src/server.ts",
|
||||
"build": "rimraf build && tsc",
|
||||
"postbuild": "npm run copy:files",
|
||||
"swagger": "tsoa spec",
|
||||
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
|
||||
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --silent --coverage",
|
||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"exe": "npm run build && pkg .",
|
||||
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
||||
"public:copy": "cp -r ./public/ ./build/public/",
|
||||
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
||||
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
|
||||
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
|
||||
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
|
||||
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
|
||||
},
|
||||
"bin": "./build/src/server.js",
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"./build/public/**/*",
|
||||
"./build/sasjsbuild/**/*",
|
||||
"./web/build/**/*"
|
||||
],
|
||||
"targets": [
|
||||
"node16-linux-x64",
|
||||
"node16-macos-x64",
|
||||
"node16-win-x64"
|
||||
],
|
||||
"outputPath": "../executables"
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
"main"
|
||||
]
|
||||
},
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "4.9.0",
|
||||
"@sasjs/utils": "2.36.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^15.12.2",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/swagger-ui-express": "^4.1.3",
|
||||
"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",
|
||||
"prettier": "^2.3.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsoa": "3.14.1",
|
||||
"typescript": "^4.3.2"
|
||||
},
|
||||
"configuration": {
|
||||
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"tmp/appStreamConfig.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
8933
api/public/SASjsApi/swagger-ui.css
Normal file
8933
api/public/SASjsApi/swagger-ui.css
Normal file
File diff suppressed because it is too large
Load Diff
21
api/public/sasjs-logo.svg
Normal file
21
api/public/sasjs-logo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F6E40C;}
|
||||
</style>
|
||||
<rect id="XMLID_1_" width="32" height="32"/>
|
||||
<g id="XMLID_654_">
|
||||
<path id="XMLID_656_" class="st0" d="M27.9,17.4c-1.1,0-2.1,0-3,0c-1.2,0-2.3,0-3.5,0c-0.5,0-0.7,0.2-0.6,0.7c0,2.1,0,4.3,0,6.4
|
||||
c0,0.5-0.2,0.8-0.6,1c-2.5,1.4-4.9,2.8-7.3,4.3c-0.4,0.2-0.6,0.2-1,0c-2.4-1.4-4.9-2.9-7.3-4.3c-0.2-0.1-0.5-0.5-0.5-0.7
|
||||
c0-3.2,0-6.4,0-9.6c0-0.1,0-0.1,0.1-0.3c0.3,0,0.5,0,0.8,0c1.9,0,3.7,0,5.6,0c0.6,0,0.7-0.2,0.7-0.7c0-2.1,0-4.2,0-6.4
|
||||
c0-0.5,0.1-0.8,0.6-1.1c2.5-1.4,4.9-2.9,7.3-4.3c0.2-0.1,0.6-0.1,0.9,0c2.5,1.4,5,2.9,7.5,4.4c0.2,0.1,0.4,0.4,0.4,0.6
|
||||
C27.9,10.6,27.9,13.9,27.9,17.4z M20.8,14.8c1.4,0,2.7,0,4,0c0.5,0,0.7-0.2,0.7-0.7c0-1.7,0-3.3,0-5c0-0.5-0.2-0.7-0.6-1
|
||||
c-1.6-0.9-3.2-1.9-4.8-2.8c-0.2-0.1-0.7-0.1-0.9,0c-1.6,0.9-3.2,1.9-4.8,2.8c-0.4,0.2-0.6,0.5-0.6,1c0,3.2,0,6.3,0,9.5
|
||||
c0,1.9,0,1.9-1.9,1.9c-0.4,0-0.6-0.1-0.6-0.6c0-0.6,0-1.3,0-1.9c0-0.5-0.2-0.6-0.6-0.6c-1.1,0-2.2,0-3.3,0c-0.5,0-0.7,0.2-0.7,0.7
|
||||
c0,1.6,0,3.3,0,4.9c0,0.5,0.2,0.8,0.6,1c1.6,0.9,3.2,1.9,4.8,2.8c0.2,0.1,0.7,0.1,0.9,0c1.6-0.9,3.2-1.9,4.8-2.8
|
||||
c0.4-0.2,0.6-0.5,0.6-1c0-3.1,0-6.1,0-9.2c0-1.9,0-1.9,1.9-1.9c0.5,0,0.7,0.2,0.7,0.7C20.8,13.3,20.8,14,20.8,14.8z"/>
|
||||
<path id="XMLID_655_" class="st0" d="M18,2.1l-6.8,3.9V2.7c0-0.3,0.3-0.6,0.6-0.6H18z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1196
api/public/swagger.yaml
Normal file
1196
api/public/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
35
api/scripts/compileSysInit.ts
Normal file
35
api/scripts/compileSysInit.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
createFile,
|
||||
loadDependenciesFile,
|
||||
readFile,
|
||||
SASJsFileType
|
||||
} from '@sasjs/utils'
|
||||
import { apiRoot, sysInitCompiledPath } from '../src/utils'
|
||||
|
||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||
|
||||
const compiledSystemInit = async (systemInit: string) =>
|
||||
'options ps=max;\n' +
|
||||
(await loadDependenciesFile({
|
||||
fileContent: systemInit,
|
||||
type: SASJsFileType.job,
|
||||
programFolders: [],
|
||||
macroFolders: [],
|
||||
buildSourceFolder: '',
|
||||
binaryFolders: [],
|
||||
macroCorePath
|
||||
}))
|
||||
|
||||
const createSysInitFile = async () => {
|
||||
const systemInitContent = await readFile(
|
||||
path.join(__dirname, 'systemInit.sas')
|
||||
)
|
||||
|
||||
await createFile(
|
||||
path.join(sysInitCompiledPath),
|
||||
await compiledSystemInit(systemInitContent)
|
||||
)
|
||||
}
|
||||
|
||||
createSysInitFile()
|
||||
21
api/scripts/copySASjsCore.ts
Normal file
21
api/scripts/copySASjsCore.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import path from 'path'
|
||||
import { asyncForEach, copy, createFolder, deleteFolder } from '@sasjs/utils'
|
||||
|
||||
import { apiRoot, sasJSCoreMacros } from '../src/utils'
|
||||
|
||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||
|
||||
export const copySASjsCore = async () => {
|
||||
await deleteFolder(sasJSCoreMacros)
|
||||
await createFolder(sasJSCoreMacros)
|
||||
|
||||
const foldersToCopy = ['base', 'ddl', 'fcmp', 'lua', 'server']
|
||||
|
||||
await asyncForEach(foldersToCopy, async (coreSubFolder) => {
|
||||
const coreSubFolderPath = path.join(macroCorePath, coreSubFolder)
|
||||
|
||||
await copy(coreSubFolderPath, sasJSCoreMacros)
|
||||
})
|
||||
}
|
||||
|
||||
copySASjsCore()
|
||||
27
api/scripts/systemInit.sas
Normal file
27
api/scripts/systemInit.sas
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
@file
|
||||
@brief The systemInit program
|
||||
@details This program is inserted into every sasjs/server program invocation,
|
||||
_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".
|
||||
|
||||
<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
|
||||
|
||||
**/
|
||||
|
||||
65
api/src/app.ts
Normal file
65
api/src/app.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import path from 'path'
|
||||
import express, { ErrorRequestHandler } from 'express'
|
||||
import morgan from 'morgan'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import dotenv from 'dotenv'
|
||||
import cors from 'cors'
|
||||
|
||||
import {
|
||||
connectDB,
|
||||
copySASjsCore,
|
||||
getWebBuildFolderPath,
|
||||
loadAppStreamConfig,
|
||||
sasJSCoreMacros,
|
||||
setProcessVariables
|
||||
} from './utils'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const app = express()
|
||||
|
||||
const { MODE, CORS, WHITELIST } = process.env
|
||||
|
||||
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(/\/$/, ''))
|
||||
})
|
||||
|
||||
console.log('All CORS Requests are enabled for:', whiteList)
|
||||
app.use(cors({ credentials: true, origin: whiteList }))
|
||||
}
|
||||
|
||||
app.use(cookieParser())
|
||||
app.use(morgan('tiny'))
|
||||
app.use(express.json({ limit: '50mb' }))
|
||||
app.use(express.static(path.join(__dirname, '../public')))
|
||||
|
||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
console.error(err.stack)
|
||||
res.status(500).send('Something broke!')
|
||||
}
|
||||
|
||||
export default setProcessVariables().then(async () => {
|
||||
await copySASjsCore()
|
||||
|
||||
// loading these modules after setting up variables due to
|
||||
// multer's usage of process var process.driveLoc
|
||||
const { setupRoutes } = await import('./routes/setupRoutes')
|
||||
setupRoutes(app)
|
||||
|
||||
await loadAppStreamConfig()
|
||||
|
||||
// should be served after setting up web route
|
||||
// index.html needs to be injected with some js script.
|
||||
app.use(express.static(getWebBuildFolderPath()))
|
||||
|
||||
console.log('sasJSCoreMacros', sasJSCoreMacros)
|
||||
|
||||
app.use(onError)
|
||||
|
||||
await connectDB()
|
||||
return app
|
||||
})
|
||||
212
api/src/controllers/auth.ts
Normal file
212
api/src/controllers/auth.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
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
|
||||
} from '../utils'
|
||||
|
||||
@Route('SASjsApi/auth')
|
||||
@Tags('Auth')
|
||||
export class AuthController {
|
||||
static authCodes: { [key: string]: { [key: string]: string } } = {}
|
||||
static saveCode = (userId: number, clientId: string, code: string) => {
|
||||
if (AuthController.authCodes[userId])
|
||||
return (AuthController.authCodes[userId][clientId] = code)
|
||||
|
||||
AuthController.authCodes[userId] = { [clientId]: code }
|
||||
return AuthController.authCodes[userId][clientId]
|
||||
}
|
||||
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
|
||||
*
|
||||
*/
|
||||
@Example<TokenResponse>({
|
||||
accessToken: 'someRandomCryptoString',
|
||||
refreshToken: 'someRandomCryptoString'
|
||||
})
|
||||
@Post('/token')
|
||||
public async token(@Body() body: TokenPayload): Promise<TokenResponse> {
|
||||
return token(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Returns new access/refresh tokens
|
||||
*
|
||||
*/
|
||||
@Example<TokenResponse>({
|
||||
accessToken: 'someRandomCryptoString',
|
||||
refreshToken: 'someRandomCryptoString'
|
||||
})
|
||||
@Security('bearerAuth')
|
||||
@Post('/refresh')
|
||||
public async refresh(
|
||||
@Query() @Hidden() data?: InfoJWT
|
||||
): Promise<TokenResponse> {
|
||||
return refresh(data!)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Logout terminate access/refresh tokens and returns nothing
|
||||
*
|
||||
*/
|
||||
@Security('bearerAuth')
|
||||
@Post('/logout')
|
||||
public async logout(@Query() @Hidden() data?: InfoJWT) {
|
||||
return logout(data!)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const userInfo = await verifyAuthCode(clientId, code)
|
||||
if (!userInfo) throw new Error('Invalid Auth Code')
|
||||
|
||||
if (AuthController.authCodes[userInfo.userId][clientId] !== code)
|
||||
throw new Error('Invalid Auth Code')
|
||||
|
||||
AuthController.deleteCode(userInfo.userId, clientId)
|
||||
|
||||
const accessToken = generateAccessToken(userInfo)
|
||||
const refreshToken = generateRefreshToken(userInfo)
|
||||
|
||||
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
|
||||
|
||||
return { accessToken, refreshToken }
|
||||
}
|
||||
|
||||
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
|
||||
const accessToken = generateAccessToken(userInfo)
|
||||
const refreshToken = generateRefreshToken(userInfo)
|
||||
|
||||
await saveTokensInDB(
|
||||
userInfo.userId,
|
||||
userInfo.clientId,
|
||||
accessToken,
|
||||
refreshToken
|
||||
)
|
||||
|
||||
return { accessToken, refreshToken }
|
||||
}
|
||||
|
||||
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
|
||||
* @example "clientID1"
|
||||
*/
|
||||
clientId: string
|
||||
/**
|
||||
* Authorization code
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
code: string
|
||||
}
|
||||
|
||||
interface TokenResponse {
|
||||
/**
|
||||
* Access Token
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
accessToken: string
|
||||
/**
|
||||
* Refresh Token
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
const verifyAuthCode = async (
|
||||
clientId: string,
|
||||
code: string
|
||||
): Promise<InfoJWT | undefined> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
|
||||
if (err) return resolve(undefined)
|
||||
|
||||
const clientInfo: InfoJWT = {
|
||||
clientId: data?.clientId,
|
||||
userId: data?.userId
|
||||
}
|
||||
if (clientInfo.clientId === clientId) {
|
||||
return resolve(clientInfo)
|
||||
}
|
||||
return resolve(undefined)
|
||||
})
|
||||
})
|
||||
}
|
||||
44
api/src/controllers/client.ts
Normal file
44
api/src/controllers/client.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
|
||||
|
||||
import Client, { ClientPayload } from '../model/Client'
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/client')
|
||||
@Tags('Client')
|
||||
export class ClientController {
|
||||
/**
|
||||
* @summary Create client with the following attributes: ClientId, ClientSecret. Admin only task.
|
||||
*
|
||||
*/
|
||||
@Example<ClientPayload>({
|
||||
clientId: 'someFormattedClientID1234',
|
||||
clientSecret: 'someRandomCryptoString'
|
||||
})
|
||||
@Post('/')
|
||||
public async createClient(
|
||||
@Body() body: ClientPayload
|
||||
): Promise<ClientPayload> {
|
||||
return createClient(body)
|
||||
}
|
||||
}
|
||||
|
||||
const createClient = async (data: any): Promise<ClientPayload> => {
|
||||
const { clientId, clientSecret } = data
|
||||
|
||||
// Checking if client is already in the database
|
||||
const clientExist = await Client.findOne({ clientId })
|
||||
if (clientExist) throw new Error('Client ID already exists.')
|
||||
|
||||
// Create a new client
|
||||
const client = new Client({
|
||||
clientId,
|
||||
clientSecret
|
||||
})
|
||||
|
||||
const savedClient = await client.save()
|
||||
|
||||
return {
|
||||
clientId: savedClient.clientId,
|
||||
clientSecret: savedClient.clientSecret
|
||||
}
|
||||
}
|
||||
71
api/src/controllers/code.ts
Normal file
71
api/src/controllers/code.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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'
|
||||
|
||||
interface ExecuteSASCodePayload {
|
||||
/**
|
||||
* Code of SAS program
|
||||
* @example "* SAS Code HERE;"
|
||||
*/
|
||||
code: string
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/code')
|
||||
@Tags('CODE')
|
||||
export class CodeController {
|
||||
/**
|
||||
* Execute SAS code.
|
||||
* @summary Run SAS Code and returns log
|
||||
*/
|
||||
@Post('/execute')
|
||||
public async executeSASCode(
|
||||
@Request() request: express.Request,
|
||||
@Body() body: ExecuteSASCodePayload
|
||||
): Promise<ExecuteReturnJsonResponse> {
|
||||
return executeSASCode(request, body)
|
||||
}
|
||||
}
|
||||
|
||||
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
||||
try {
|
||||
const { webout, log, httpHeaders } =
|
||||
(await new ExecutionController().executeProgram(
|
||||
code,
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, _debug: 131 },
|
||||
undefined,
|
||||
true
|
||||
)) as ExecuteReturnJson
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
_webout: webout as string,
|
||||
log: parseLogToArray(log),
|
||||
httpHeaders
|
||||
}
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'failure',
|
||||
message: 'Job execution failed.',
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
313
api/src/controllers/drive.ts
Normal file
313
api/src/controllers/drive.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import path from 'path'
|
||||
import express, { Express } from 'express'
|
||||
import {
|
||||
Security,
|
||||
Request,
|
||||
Route,
|
||||
Tags,
|
||||
Example,
|
||||
Post,
|
||||
Body,
|
||||
Response,
|
||||
Query,
|
||||
Get,
|
||||
Patch,
|
||||
UploadedFile,
|
||||
FormField,
|
||||
Delete
|
||||
} from 'tsoa'
|
||||
import {
|
||||
fileExists,
|
||||
moveFile,
|
||||
createFolder,
|
||||
deleteFile as deleteFileOnSystem
|
||||
} from '@sasjs/utils'
|
||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||
|
||||
import { FileTree, isFileTree, TreeNode } from '../types'
|
||||
import { getTmpFilesFolderPath } from '../utils'
|
||||
|
||||
interface DeployPayload {
|
||||
appLoc: string
|
||||
streamWebFolder?: string
|
||||
fileTree: FileTree
|
||||
}
|
||||
|
||||
interface DeployResponse {
|
||||
status: string
|
||||
message: string
|
||||
streamServiceName?: string
|
||||
example?: FileTree
|
||||
}
|
||||
|
||||
interface GetFileResponse {
|
||||
status: string
|
||||
fileContent?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
interface GetFileTreeResponse {
|
||||
status: string
|
||||
tree: TreeNode
|
||||
}
|
||||
|
||||
interface UpdateFileResponse {
|
||||
status: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
const fileTreeExample = getTreeExample()
|
||||
|
||||
const successDeployResponse: DeployResponse = {
|
||||
status: 'success',
|
||||
message: 'Files deployed successfully to @sasjs/server.'
|
||||
}
|
||||
const invalidDeployFormatResponse: DeployResponse = {
|
||||
status: 'failure',
|
||||
message: 'Provided not supported data format.',
|
||||
example: fileTreeExample
|
||||
}
|
||||
const execDeployErrorResponse: DeployResponse = {
|
||||
status: 'failure',
|
||||
message: 'Deployment failed!'
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/drive')
|
||||
@Tags('Drive')
|
||||
export class DriveController {
|
||||
/**
|
||||
* @summary Creates/updates files within SASjs Drive using provided payload.
|
||||
*
|
||||
*/
|
||||
@Example<DeployResponse>(successDeployResponse)
|
||||
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
|
||||
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
|
||||
@Post('/deploy')
|
||||
public async deploy(@Body() body: DeployPayload): Promise<DeployResponse> {
|
||||
return deploy(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Get file from SASjs Drive
|
||||
* @query _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file"
|
||||
*/
|
||||
@Get('/file')
|
||||
public async getFile(
|
||||
@Request() request: express.Request,
|
||||
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
) {
|
||||
return getFile(request, (_filePath ?? filePath)!)
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Delete file from SASjs Drive
|
||||
* @query _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file"
|
||||
*/
|
||||
@Delete('/file')
|
||||
public async deleteFile(
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
) {
|
||||
return deleteFile((_filePath ?? filePath)!)
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Create a file in SASjs Drive
|
||||
* @param _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||
*
|
||||
*/
|
||||
@Example<UpdateFileResponse>({
|
||||
status: 'success'
|
||||
})
|
||||
@Response<UpdateFileResponse>(403, 'File already exists', {
|
||||
status: 'failure',
|
||||
message: 'File request failed.'
|
||||
})
|
||||
@Post('/file')
|
||||
public async saveFile(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
): Promise<UpdateFileResponse> {
|
||||
return saveFile((_filePath ?? filePath)!, file)
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Modify a file in SASjs Drive
|
||||
* @param _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||
*
|
||||
*/
|
||||
@Example<UpdateFileResponse>({
|
||||
status: 'success'
|
||||
})
|
||||
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
||||
status: 'failure',
|
||||
message: 'File request failed.'
|
||||
})
|
||||
@Patch('/file')
|
||||
public async updateFile(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
): Promise<UpdateFileResponse> {
|
||||
return updateFile((_filePath ?? filePath)!, file)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Fetch file tree within SASjs Drive.
|
||||
*
|
||||
*/
|
||||
@Get('/filetree')
|
||||
public async getFileTree(): Promise<GetFileTreeResponse> {
|
||||
return getFileTree()
|
||||
}
|
||||
}
|
||||
|
||||
const getFileTree = () => {
|
||||
const tree = new ExecutionController().buildDirectoryTree()
|
||||
return { status: 'success', tree }
|
||||
}
|
||||
|
||||
const deploy = async (data: DeployPayload) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
||||
|
||||
const appLocPath = path
|
||||
.join(getTmpFilesFolderPath(), ...appLocParts)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!appLocPath.includes(driveFilesPath)) {
|
||||
throw new Error('appLoc cannot be outside drive.')
|
||||
}
|
||||
|
||||
if (!isFileTree(data.fileTree)) {
|
||||
throw { code: 400, ...invalidDeployFormatResponse }
|
||||
}
|
||||
|
||||
await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
|
||||
throw { code: 500, ...execDeployErrorResponse, ...err }
|
||||
})
|
||||
|
||||
return successDeployResponse
|
||||
}
|
||||
|
||||
const getFile = async (req: express.Request, filePath: string) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
const filePathFull = path
|
||||
.join(getTmpFilesFolderPath(), filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot get file outside drive.')
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error('File does not exist.')
|
||||
}
|
||||
|
||||
const extension = path.extname(filePathFull).toLowerCase()
|
||||
if (extension === '.sas') {
|
||||
req.res?.setHeader('Content-type', 'text/plain')
|
||||
}
|
||||
|
||||
req.res?.sendFile(path.resolve(filePathFull))
|
||||
}
|
||||
|
||||
const deleteFile = async (filePath: string) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
const filePathFull = path
|
||||
.join(getTmpFilesFolderPath(), filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot delete file outside drive.')
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error('File does not exist.')
|
||||
}
|
||||
|
||||
await deleteFileOnSystem(filePathFull)
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const saveFile = async (
|
||||
filePath: string,
|
||||
multerFile: Express.Multer.File
|
||||
): Promise<GetFileResponse> => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
const filePathFull = path
|
||||
.join(driveFilesPath, filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot put file outside drive.')
|
||||
}
|
||||
|
||||
if (await fileExists(filePathFull)) {
|
||||
throw new Error('File already exists.')
|
||||
}
|
||||
|
||||
const folderPath = path.dirname(filePathFull)
|
||||
await createFolder(folderPath)
|
||||
await moveFile(multerFile.path, filePathFull)
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const updateFile = async (
|
||||
filePath: string,
|
||||
multerFile: Express.Multer.File
|
||||
): Promise<GetFileResponse> => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
const filePathFull = path
|
||||
.join(driveFilesPath, filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot modify file outside drive.')
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error(`File doesn't exist.`)
|
||||
}
|
||||
|
||||
await moveFile(multerFile.path, filePathFull)
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const validateFilePath = async (filePath: string) => {
|
||||
if (!(await fileExists(filePath))) {
|
||||
throw 'DriveController: File does not exists.'
|
||||
}
|
||||
}
|
||||
220
api/src/controllers/group.ts
Normal file
220
api/src/controllers/group.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
Security,
|
||||
Route,
|
||||
Tags,
|
||||
Path,
|
||||
Example,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body
|
||||
} from 'tsoa'
|
||||
|
||||
import Group, { GroupPayload } from '../model/Group'
|
||||
import User from '../model/User'
|
||||
import { UserResponse } from './user'
|
||||
|
||||
interface GroupResponse {
|
||||
groupId: number
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface GroupDetailsResponse {
|
||||
groupId: number
|
||||
name: string
|
||||
description: string
|
||||
isActive: boolean
|
||||
users: UserResponse[]
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/group')
|
||||
@Tags('Group')
|
||||
export class GroupController {
|
||||
/**
|
||||
* @summary Get list of all groups (groupName and groupDescription). All users can request this.
|
||||
*
|
||||
*/
|
||||
@Example<GroupResponse[]>([
|
||||
{
|
||||
groupId: 123,
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users'
|
||||
}
|
||||
])
|
||||
@Get('/')
|
||||
public async getAllGroups(): Promise<GroupResponse[]> {
|
||||
return getAllGroups()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create a new group. Admin only.
|
||||
*
|
||||
*/
|
||||
@Example<GroupDetailsResponse>({
|
||||
groupId: 123,
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users',
|
||||
isActive: true,
|
||||
users: []
|
||||
})
|
||||
@Post('/')
|
||||
public async createGroup(
|
||||
@Body() body: GroupPayload
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return createGroup(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get list of members of a group (userName). All users can request this.
|
||||
* @param groupId The group's identifier
|
||||
* @example groupId 1234
|
||||
*/
|
||||
@Get('{groupId}')
|
||||
public async getGroup(
|
||||
@Path() groupId: number
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return getGroup(groupId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Add a user to a group. Admin task only.
|
||||
* @param groupId The group's identifier
|
||||
* @example groupId "1234"
|
||||
* @param userId The user's identifier
|
||||
* @example userId "6789"
|
||||
*/
|
||||
@Example<GroupDetailsResponse>({
|
||||
groupId: 123,
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users',
|
||||
isActive: true,
|
||||
users: []
|
||||
})
|
||||
@Post('{groupId}/{userId}')
|
||||
public async addUserToGroup(
|
||||
@Path() groupId: number,
|
||||
@Path() userId: number
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return addUserToGroup(groupId, userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Remove a user to a group. Admin task only.
|
||||
* @param groupId The group's identifier
|
||||
* @example groupId "1234"
|
||||
* @param userId The user's identifier
|
||||
* @example userId "6789"
|
||||
*/
|
||||
@Example<GroupDetailsResponse>({
|
||||
groupId: 123,
|
||||
name: 'DCGroup',
|
||||
description: 'This group represents Data Controller Users',
|
||||
isActive: true,
|
||||
users: []
|
||||
})
|
||||
@Delete('{groupId}/{userId}')
|
||||
public async removeUserFromGroup(
|
||||
@Path() groupId: number,
|
||||
@Path() userId: number
|
||||
): Promise<GroupDetailsResponse> {
|
||||
return removeUserFromGroup(groupId, userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Delete a group. Admin task only.
|
||||
* @param groupId The group's identifier
|
||||
* @example groupId 1234
|
||||
*/
|
||||
@Delete('{groupId}')
|
||||
public async deleteGroup(@Path() groupId: number) {
|
||||
const { deletedCount } = await Group.deleteOne({ groupId })
|
||||
if (deletedCount) return
|
||||
throw new Error('No Group deleted!')
|
||||
}
|
||||
}
|
||||
|
||||
const getAllGroups = async (): Promise<GroupResponse[]> =>
|
||||
await Group.find({})
|
||||
.select({ _id: 0, groupId: 1, name: 1, description: 1 })
|
||||
.exec()
|
||||
|
||||
const createGroup = async ({
|
||||
name,
|
||||
description,
|
||||
isActive
|
||||
}: GroupPayload): Promise<GroupDetailsResponse> => {
|
||||
const group = new Group({
|
||||
name,
|
||||
description,
|
||||
isActive
|
||||
})
|
||||
|
||||
const savedGroup = await group.save()
|
||||
|
||||
return {
|
||||
groupId: savedGroup.groupId,
|
||||
name: savedGroup.name,
|
||||
description: savedGroup.description,
|
||||
isActive: savedGroup.isActive,
|
||||
users: []
|
||||
}
|
||||
}
|
||||
|
||||
const getGroup = async (groupId: number): Promise<GroupDetailsResponse> => {
|
||||
const group = (await Group.findOne(
|
||||
{ groupId },
|
||||
'groupId name description isActive users -_id'
|
||||
).populate(
|
||||
'users',
|
||||
'id username displayName -_id'
|
||||
)) as unknown as GroupDetailsResponse
|
||||
if (!group) throw new Error('Group not found.')
|
||||
|
||||
return {
|
||||
groupId: group.groupId,
|
||||
name: group.name,
|
||||
description: group.description,
|
||||
isActive: group.isActive,
|
||||
users: group.users
|
||||
}
|
||||
}
|
||||
|
||||
const addUserToGroup = async (
|
||||
groupId: number,
|
||||
userId: number
|
||||
): Promise<GroupDetailsResponse> =>
|
||||
updateUsersListInGroup(groupId, userId, 'addUser')
|
||||
|
||||
const removeUserFromGroup = async (
|
||||
groupId: number,
|
||||
userId: number
|
||||
): Promise<GroupDetailsResponse> =>
|
||||
updateUsersListInGroup(groupId, userId, 'removeUser')
|
||||
|
||||
const updateUsersListInGroup = async (
|
||||
groupId: number,
|
||||
userId: number,
|
||||
action: 'addUser' | 'removeUser'
|
||||
): Promise<GroupDetailsResponse> => {
|
||||
const group = await Group.findOne({ groupId })
|
||||
if (!group) throw new Error('Group not found.')
|
||||
|
||||
const user = await User.findOne({ id: userId })
|
||||
if (!user) throw new Error('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')
|
||||
|
||||
return {
|
||||
groupId: updatedGroup.groupId,
|
||||
name: updatedGroup.name,
|
||||
description: updatedGroup.description,
|
||||
isActive: updatedGroup.isActive,
|
||||
users: updatedGroup.users
|
||||
}
|
||||
}
|
||||
8
api/src/controllers/index.ts
Normal file
8
api/src/controllers/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './auth'
|
||||
export * from './client'
|
||||
export * from './code'
|
||||
export * from './drive'
|
||||
export * from './group'
|
||||
export * from './session'
|
||||
export * from './stp'
|
||||
export * from './user'
|
||||
223
api/src/controllers/internal/Execution.ts
Normal file
223
api/src/controllers/internal/Execution.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { getSessionController } from './'
|
||||
import {
|
||||
readFile,
|
||||
fileExists,
|
||||
createFile,
|
||||
moveFile,
|
||||
readFileBinary
|
||||
} from '@sasjs/utils'
|
||||
import { PreProgramVars, TreeNode } from '../../types'
|
||||
import {
|
||||
extractHeaders,
|
||||
generateFileUploadSasCode,
|
||||
getTmpFilesFolderPath,
|
||||
getTmpMacrosPath,
|
||||
HTTPHeaders,
|
||||
isDebugOn
|
||||
} from '../../utils'
|
||||
|
||||
export interface ExecutionVars {
|
||||
[key: string]: string | number | undefined
|
||||
}
|
||||
|
||||
export interface ExecuteReturnRaw {
|
||||
httpHeaders: HTTPHeaders
|
||||
result: string | Buffer
|
||||
}
|
||||
|
||||
export interface ExecuteReturnJson {
|
||||
httpHeaders: HTTPHeaders
|
||||
webout: string | Buffer
|
||||
log?: string
|
||||
}
|
||||
|
||||
export class ExecutionController {
|
||||
async executeFile(
|
||||
programPath: string,
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
) {
|
||||
if (!(await fileExists(programPath)))
|
||||
throw 'ExecutionController: SAS file does not exist.'
|
||||
|
||||
const program = await readFile(programPath)
|
||||
|
||||
return this.executeProgram(
|
||||
program,
|
||||
preProgramVariables,
|
||||
vars,
|
||||
otherArgs,
|
||||
returnJson
|
||||
)
|
||||
}
|
||||
|
||||
async executeProgram(
|
||||
program: string,
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||
const sessionController = getSessionController()
|
||||
|
||||
const session = await sessionController.getSession()
|
||||
session.inUse = true
|
||||
session.consumed = true
|
||||
|
||||
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')
|
||||
|
||||
await createFile(weboutPath, '')
|
||||
await createFile(
|
||||
tokenFile,
|
||||
preProgramVariables?.accessToken ?? 'accessToken'
|
||||
)
|
||||
|
||||
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="${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
|
||||
|
||||
const webout = (await fileExists(weboutPath))
|
||||
? fileResponse
|
||||
? await readFileBinary(weboutPath)
|
||||
: await readFile(weboutPath)
|
||||
: ''
|
||||
|
||||
// it should be deleted by scheduleSessionDestroy
|
||||
session.inUse = false
|
||||
|
||||
if (returnJson) {
|
||||
return {
|
||||
httpHeaders,
|
||||
webout,
|
||||
log: isDebugOn(vars) || session.crashed ? log : undefined
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
buildDirectoryTree() {
|
||||
const root: TreeNode = {
|
||||
name: 'files',
|
||||
relativePath: '',
|
||||
absolutePath: getTmpFilesFolderPath(),
|
||||
children: []
|
||||
}
|
||||
|
||||
const stack = [root]
|
||||
|
||||
while (stack.length) {
|
||||
const currentNode = stack.pop()
|
||||
|
||||
if (currentNode) {
|
||||
const children = fs.readdirSync(currentNode.absolutePath)
|
||||
|
||||
for (let child of children) {
|
||||
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
|
||||
const relativeChildPath = `${currentNode.relativePath}/${child}`
|
||||
const childNode: TreeNode = {
|
||||
name: child,
|
||||
relativePath: relativeChildPath,
|
||||
absolutePath: absoluteChildPath,
|
||||
children: []
|
||||
}
|
||||
currentNode.children.push(childNode)
|
||||
|
||||
if (fs.statSync(childNode.absolutePath).isDirectory()) {
|
||||
stack.push(childNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
36
api/src/controllers/internal/FileUploadController.ts
Normal file
36
api/src/controllers/internal/FileUploadController.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import multer from 'multer'
|
||||
import { uuidv4 } from '@sasjs/utils'
|
||||
import { getSessionController } from '.'
|
||||
|
||||
export class FileUploadController {
|
||||
private storage = multer.diskStorage({
|
||||
destination: function (req: any, file: any, cb: any) {
|
||||
//Sending the intercepted files to the sessions subfolder
|
||||
cb(null, req.sasSession.path)
|
||||
},
|
||||
filename: function (req: any, file: any, cb: any) {
|
||||
//req_file prefix + unique hash added to sas request files
|
||||
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
|
||||
}
|
||||
})
|
||||
|
||||
private upload = multer({ storage: this.storage })
|
||||
|
||||
//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
|
||||
|
||||
const sessionController = getSessionController()
|
||||
session = await sessionController.getSession()
|
||||
session.inUse = true
|
||||
|
||||
req.sasSession = session
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
public getMulterUploadObject() {
|
||||
return this.upload
|
||||
}
|
||||
}
|
||||
174
api/src/controllers/internal/Session.ts
Normal file
174
api/src/controllers/internal/Session.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import path from 'path'
|
||||
import { Session } from '../../types'
|
||||
import { promisify } from 'util'
|
||||
import { execFile } from 'child_process'
|
||||
import {
|
||||
getTmpSessionsFolderPath,
|
||||
generateUniqueFileName,
|
||||
sysInitCompiledPath
|
||||
} from '../../utils'
|
||||
import {
|
||||
deleteFolder,
|
||||
createFile,
|
||||
fileExists,
|
||||
generateTimestamp,
|
||||
readFile,
|
||||
moveFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
|
||||
export class SessionController {
|
||||
private sessions: Session[] = []
|
||||
|
||||
private getReadySessions = (): Session[] =>
|
||||
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
||||
|
||||
public async getSession() {
|
||||
const readySessions = this.getReadySessions()
|
||||
|
||||
const session = readySessions.length
|
||||
? readySessions[0]
|
||||
: await this.createSession()
|
||||
|
||||
if (readySessions.length < 2) this.createSession()
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
private async createSession(): Promise<Session> {
|
||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||
|
||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||
const deathTimeStamp = (
|
||||
parseInt(creationTimeStamp) +
|
||||
15 * 60 * 1000 -
|
||||
1000
|
||||
).toString()
|
||||
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
ready: false,
|
||||
inUse: false,
|
||||
consumed: false,
|
||||
completed: false,
|
||||
creationTimeStamp,
|
||||
deathTimeStamp,
|
||||
path: sessionFolder
|
||||
}
|
||||
|
||||
// we do not want to leave sessions running forever
|
||||
// we clean them up after a predefined period, if unused
|
||||
this.scheduleSessionDestroy(session)
|
||||
|
||||
// Place compiled system init code to autoexec
|
||||
const compiledSystemInitContent = await readFile(sysInitCompiledPath)
|
||||
|
||||
// the autoexec file is executed on SAS startup
|
||||
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
||||
const contentForAutoExec = `/* compiled systemInit */
|
||||
${compiledSystemInitContent}
|
||||
/* autoexec */
|
||||
${autoExecContent}`
|
||||
await createFile(autoExecPath, contentForAutoExec)
|
||||
|
||||
// create empty code.sas as SAS will not start without a SYSIN
|
||||
const codePath = path.join(session.path, 'code.sas')
|
||||
await createFile(codePath, '')
|
||||
|
||||
// trigger SAS but don't wait for completion - we need to
|
||||
// update the session array to say that it is currently running
|
||||
// however we also need a promise so that we can update the
|
||||
// session array to say that it has (eventually) finished.
|
||||
|
||||
execFilePromise(process.sasLoc, [
|
||||
'-SYSIN',
|
||||
codePath,
|
||||
'-LOG',
|
||||
path.join(session.path, 'log.log'),
|
||||
'-WORK',
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
autoExecPath,
|
||||
process.platform === 'win32' ? '-nosplash' : ''
|
||||
])
|
||||
.then(() => {
|
||||
session.completed = true
|
||||
console.log('session completed', session)
|
||||
})
|
||||
.catch((err) => {
|
||||
session.completed = true
|
||||
session.crashed = err.toString()
|
||||
console.log('session crashed', session.id, session.crashed)
|
||||
})
|
||||
|
||||
// we have a triggered session - add to array
|
||||
this.sessions.push(session)
|
||||
|
||||
// SAS has been triggered but we can't use it until
|
||||
// the autoexec deletes the code.sas file
|
||||
await this.waitForSession(session)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
private async waitForSession(session: Session) {
|
||||
const codeFilePath = path.join(session.path, 'code.sas')
|
||||
|
||||
// TODO: don't wait forever
|
||||
while ((await fileExists(codeFilePath)) && !session.crashed) {}
|
||||
|
||||
if (session.crashed)
|
||||
console.log('session crashed! while waiting to be ready', session.crashed)
|
||||
|
||||
session.ready = true
|
||||
}
|
||||
|
||||
public async deleteSession(session: Session) {
|
||||
// remove the temporary files, to avoid buildup
|
||||
await deleteFolder(session.path)
|
||||
|
||||
// remove the session from the session array
|
||||
this.sessions = this.sessions.filter(
|
||||
(sess: Session) => sess.id !== session.id
|
||||
)
|
||||
}
|
||||
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
setTimeout(async () => {
|
||||
if (session.inUse) {
|
||||
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
await this.deleteSession(session)
|
||||
}
|
||||
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
|
||||
}
|
||||
}
|
||||
|
||||
export const getSessionController = (): SessionController => {
|
||||
if (process.sessionController) return process.sessionController
|
||||
|
||||
process.sessionController = new SessionController()
|
||||
|
||||
return process.sessionController
|
||||
}
|
||||
|
||||
const autoExecContent = `
|
||||
data _null_;
|
||||
/* remove the dummy SYSIN */
|
||||
length fname $8;
|
||||
call missing(fname);
|
||||
rc=filename(fname,getoption('SYSIN') );
|
||||
if rc = 0 and fexist(fname) then rc=fdelete(fname);
|
||||
rc=filename(fname);
|
||||
/* now wait for the real SYSIN */
|
||||
slept=0;
|
||||
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
|
||||
slept=slept+sleep(0.01,1);
|
||||
end;
|
||||
stop;
|
||||
run;
|
||||
`
|
||||
@@ -1,10 +1,11 @@
|
||||
import { MemberType, FolderMember, ServiceMember } from '../types'
|
||||
import { getTmpFilesFolderPath } from '../utils/file'
|
||||
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
||||
import path from 'path'
|
||||
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types'
|
||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
||||
|
||||
// REFACTOR: export FileTreeCpntroller
|
||||
export const createFileTree = async (
|
||||
members: [FolderMember, ServiceMember],
|
||||
members: (FolderMember | ServiceMember)[],
|
||||
parentFolders: string[] = []
|
||||
) => {
|
||||
const destinationPath = path.join(
|
||||
@@ -13,7 +14,9 @@ export const createFileTree = async (
|
||||
)
|
||||
|
||||
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
|
||||
const name = member.name
|
||||
let name = member.name
|
||||
|
||||
if (member.type === MemberType.service) name += '.sas'
|
||||
|
||||
if (member.type === MemberType.folder) {
|
||||
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
||||
@@ -24,28 +27,32 @@ export const createFileTree = async (
|
||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
} else {
|
||||
await createFile(path.join(destinationPath, name), member.code).catch(
|
||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
const encoding = member.type === MemberType.file ? 'base64' : undefined
|
||||
|
||||
await createFile(
|
||||
path.join(destinationPath, name),
|
||||
member.code,
|
||||
encoding
|
||||
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export const getTreeExample = () => ({
|
||||
export const getTreeExample = (): FileTree => ({
|
||||
members: [
|
||||
{
|
||||
name: 'jobs',
|
||||
type: 'folder',
|
||||
type: MemberType.folder,
|
||||
members: [
|
||||
{
|
||||
name: 'extract',
|
||||
type: 'folder',
|
||||
type: MemberType.folder,
|
||||
members: [
|
||||
{
|
||||
name: 'makedata1',
|
||||
type: 'service',
|
||||
type: MemberType.service,
|
||||
code: '%put Hello World!;'
|
||||
}
|
||||
]
|
||||
4
api/src/controllers/internal/index.ts
Normal file
4
api/src/controllers/internal/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './deploy'
|
||||
export * from './Session'
|
||||
export * from './Execution'
|
||||
export * from './FileUploadController'
|
||||
30
api/src/controllers/session.ts
Normal file
30
api/src/controllers/session.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import express from 'express'
|
||||
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
||||
import { UserResponse } from './user'
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/session')
|
||||
@Tags('Session')
|
||||
export class SessionController {
|
||||
/**
|
||||
* @summary Get session info (username).
|
||||
*
|
||||
*/
|
||||
@Example<UserResponse>({
|
||||
id: 123,
|
||||
username: 'johnusername',
|
||||
displayName: 'John'
|
||||
})
|
||||
@Get('/')
|
||||
public async session(
|
||||
@Request() request: express.Request
|
||||
): Promise<UserResponse> {
|
||||
return session(request)
|
||||
}
|
||||
}
|
||||
|
||||
const session = (req: any) => ({
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
displayName: req.user.displayName
|
||||
})
|
||||
225
api/src/controllers/stp.ts
Normal file
225
api/src/controllers/stp.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import {
|
||||
Request,
|
||||
Security,
|
||||
Route,
|
||||
Tags,
|
||||
Post,
|
||||
Body,
|
||||
Get,
|
||||
Query,
|
||||
Example
|
||||
} from 'tsoa'
|
||||
import {
|
||||
ExecuteReturnJson,
|
||||
ExecuteReturnRaw,
|
||||
ExecutionController,
|
||||
ExecutionVars
|
||||
} from './internal'
|
||||
import { PreProgramVars } from '../types'
|
||||
import {
|
||||
getTmpFilesFolderPath,
|
||||
HTTPHeaders,
|
||||
isDebugOn,
|
||||
LogLine,
|
||||
makeFilesNamesMap,
|
||||
parseLogToArray
|
||||
} from '../utils'
|
||||
|
||||
interface ExecuteReturnJsonPayload {
|
||||
/**
|
||||
* Location of SAS program
|
||||
* @example "/Public/somefolder/some.file"
|
||||
*/
|
||||
_program?: string
|
||||
}
|
||||
|
||||
interface IRecordOfAny {
|
||||
[key: string]: any
|
||||
}
|
||||
export interface ExecuteReturnJsonResponse {
|
||||
status: string
|
||||
_webout: string | IRecordOfAny
|
||||
log: LogLine[]
|
||||
message?: string
|
||||
httpHeaders: HTTPHeaders
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/stp')
|
||||
@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.
|
||||
*
|
||||
* Additional URL parameters are turned into SAS macro variables.
|
||||
*
|
||||
* Any files provided in the request body are placed into the SAS session with
|
||||
* corresponding _WEBIN_XXX variables created.
|
||||
*
|
||||
* 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"
|
||||
*/
|
||||
@Get('/execute')
|
||||
public async executeReturnRaw(
|
||||
@Request() request: express.Request,
|
||||
@Query() _program: string
|
||||
): Promise<string | Buffer> {
|
||||
return executeReturnRaw(request, _program)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Additional URL parameters are turned into SAS macro variables.
|
||||
*
|
||||
* Any files provided in the request body are placed into the SAS session with
|
||||
* corresponding _WEBIN_XXX variables created.
|
||||
*
|
||||
* 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
|
||||
* 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"
|
||||
*/
|
||||
@Example<ExecuteReturnJsonResponse>({
|
||||
status: 'success',
|
||||
_webout: 'webout content',
|
||||
log: [],
|
||||
httpHeaders: {
|
||||
'Content-type': 'application/zip',
|
||||
'Cache-Control': 'public, max-age=1000'
|
||||
}
|
||||
})
|
||||
@Post('/execute')
|
||||
public async executeReturnJson(
|
||||
@Request() request: express.Request,
|
||||
@Body() body?: ExecuteReturnJsonPayload,
|
||||
@Query() _program?: string
|
||||
): Promise<ExecuteReturnJsonResponse> {
|
||||
const program = _program ?? body?._program
|
||||
return executeReturnJson(request, program!)
|
||||
}
|
||||
}
|
||||
|
||||
const executeReturnRaw = async (
|
||||
req: express.Request,
|
||||
_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 { result, httpHeaders } =
|
||||
(await new ExecutionController().executeFile(
|
||||
sasCodePath,
|
||||
getPreProgramVariables(req),
|
||||
query
|
||||
)) as ExecuteReturnRaw
|
||||
|
||||
// Should over-ride response header for
|
||||
// debug on GET request to see entire log
|
||||
// rendering on browser.
|
||||
if (isDebugOn(query)) {
|
||||
httpHeaders['content-type'] = 'text/plain'
|
||||
}
|
||||
|
||||
req.res?.set(httpHeaders)
|
||||
|
||||
if (result instanceof Buffer) {
|
||||
;(req as any).sasHeaders = httpHeaders
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'failure',
|
||||
message: 'Job execution failed.',
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const executeReturnJson = async (
|
||||
req: any,
|
||||
_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
|
||||
|
||||
try {
|
||||
const { webout, log, httpHeaders } =
|
||||
(await new ExecutionController().executeFile(
|
||||
sasCodePath,
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, ...req.body },
|
||||
{ filesNamesMap: filesNamesMap },
|
||||
true
|
||||
)) as ExecuteReturnJson
|
||||
|
||||
let weboutRes: string | IRecordOfAny = webout
|
||||
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
|
||||
try {
|
||||
weboutRes = JSON.parse(webout as string)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
_webout: weboutRes,
|
||||
log: parseLogToArray(log),
|
||||
httpHeaders
|
||||
}
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'failure',
|
||||
message: 'Job execution failed.',
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
220
api/src/controllers/user.ts
Normal file
220
api/src/controllers/user.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
Security,
|
||||
Route,
|
||||
Tags,
|
||||
Path,
|
||||
Query,
|
||||
Example,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Hidden
|
||||
} from 'tsoa'
|
||||
|
||||
import User, { UserPayload } from '../model/User'
|
||||
|
||||
export interface UserResponse {
|
||||
id: number
|
||||
username: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
interface UserDetailsResponse {
|
||||
id: number
|
||||
displayName: string
|
||||
username: string
|
||||
isActive: boolean
|
||||
isAdmin: boolean
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@Route('SASjsApi/user')
|
||||
@Tags('User')
|
||||
export class UserController {
|
||||
/**
|
||||
* @summary Get list of all users (username, displayname). All users can request this.
|
||||
*
|
||||
*/
|
||||
@Example<UserResponse[]>([
|
||||
{
|
||||
id: 123,
|
||||
username: 'johnusername',
|
||||
displayName: 'John'
|
||||
},
|
||||
{
|
||||
id: 456,
|
||||
username: 'starkusername',
|
||||
displayName: 'Stark'
|
||||
}
|
||||
])
|
||||
@Get('/')
|
||||
public async getAllUsers(): Promise<UserResponse[]> {
|
||||
return getAllUsers()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.
|
||||
*
|
||||
*/
|
||||
@Example<UserDetailsResponse>({
|
||||
id: 1234,
|
||||
displayName: 'John Snow',
|
||||
username: 'johnSnow01',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
})
|
||||
@Post('/')
|
||||
public async createUser(
|
||||
@Body() body: UserPayload
|
||||
): Promise<UserDetailsResponse> {
|
||||
return createUser(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @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)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Update user properties - such as displayName. Can be performed either by admins, or the user in question.
|
||||
* @param userId The user's identifier
|
||||
* @example userId "1234"
|
||||
*/
|
||||
@Example<UserDetailsResponse>({
|
||||
id: 1234,
|
||||
displayName: 'John Snow',
|
||||
username: 'johnSnow01',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
})
|
||||
@Patch('{userId}')
|
||||
public async updateUser(
|
||||
@Path() userId: number,
|
||||
@Body() body: UserPayload
|
||||
): Promise<UserDetailsResponse> {
|
||||
return updateUser(userId, body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Delete a user. Can be performed either by admins, or the user in question.
|
||||
* @param userId The user's identifier
|
||||
* @example userId 1234
|
||||
*/
|
||||
@Delete('{userId}')
|
||||
public async deleteUser(
|
||||
@Path() userId: number,
|
||||
@Body() body: { password?: string },
|
||||
@Query() @Hidden() isAdmin: boolean = false
|
||||
) {
|
||||
return deleteUser(userId, isAdmin, body)
|
||||
}
|
||||
}
|
||||
|
||||
const getAllUsers = async (): Promise<UserResponse[]> =>
|
||||
await User.find({})
|
||||
.select({ _id: 0, id: 1, username: 1, displayName: 1 })
|
||||
.exec()
|
||||
|
||||
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
const { displayName, username, password, isAdmin, isActive } = data
|
||||
|
||||
// Checking if user is already in the database
|
||||
const usernameExist = await User.findOne({ username })
|
||||
if (usernameExist) throw new Error('Username already exists.')
|
||||
|
||||
// Hash passwords
|
||||
const hashPassword = User.hashPassword(password)
|
||||
|
||||
// Create a new user
|
||||
const user = new User({
|
||||
displayName,
|
||||
username,
|
||||
password: hashPassword,
|
||||
isAdmin,
|
||||
isActive
|
||||
})
|
||||
|
||||
const savedUser = await user.save()
|
||||
|
||||
return {
|
||||
id: savedUser.id,
|
||||
displayName: savedUser.displayName,
|
||||
username: savedUser.username,
|
||||
isActive: savedUser.isActive,
|
||||
isAdmin: savedUser.isAdmin
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
if (!user) throw new Error('User is not found.')
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
const updateUser = async (
|
||||
id: number,
|
||||
data: UserPayload
|
||||
): Promise<UserDetailsResponse> => {
|
||||
const { displayName, username, password, isAdmin, isActive } = data
|
||||
|
||||
const params: any = { displayName, isAdmin, isActive }
|
||||
|
||||
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.')
|
||||
params.username = username
|
||||
}
|
||||
|
||||
if (password) {
|
||||
// Hash passwords
|
||||
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')
|
||||
|
||||
return updatedUser
|
||||
}
|
||||
|
||||
const deleteUser = async (
|
||||
id: number,
|
||||
isAdmin: boolean,
|
||||
{ password }: { password?: string }
|
||||
) => {
|
||||
const user = await User.findOne({ id })
|
||||
if (!user) throw new Error('User is not found.')
|
||||
|
||||
if (!isAdmin) {
|
||||
const validPass = user.comparePassword(password!)
|
||||
if (!validPass) throw new Error('Invalid password.')
|
||||
}
|
||||
|
||||
await User.deleteOne({ id })
|
||||
}
|
||||
71
api/src/middlewares/authenticateToken.ts
Normal file
71
api/src/middlewares/authenticateToken.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { verifyTokenInDB } from '../utils'
|
||||
|
||||
export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
||||
authenticateToken(
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
process.env.ACCESS_TOKEN_SECRET as string,
|
||||
'accessToken'
|
||||
)
|
||||
}
|
||||
|
||||
export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
||||
authenticateToken(
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
process.env.REFRESH_TOKEN_SECRET as string,
|
||||
'refreshToken'
|
||||
)
|
||||
}
|
||||
|
||||
const authenticateToken = (
|
||||
req: any,
|
||||
res: any,
|
||||
next: any,
|
||||
key: string,
|
||||
tokenType: 'accessToken' | 'refreshToken'
|
||||
) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') {
|
||||
req.user = {
|
||||
userId: '1234',
|
||||
clientId: 'desktopModeClientId',
|
||||
username: 'desktopModeUsername',
|
||||
displayName: 'desktopModeDisplayName',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
req.accessToken = 'desktopModeAccessToken'
|
||||
return next()
|
||||
}
|
||||
|
||||
const authHeader = req.headers['authorization']
|
||||
const token =
|
||||
authHeader?.split(' ')[1] ??
|
||||
(tokenType === 'accessToken' ? req.cookies.accessToken : '')
|
||||
if (!token) return res.sendStatus(401)
|
||||
|
||||
jwt.verify(token, key, async (err: any, data: any) => {
|
||||
if (err) return res.sendStatus(401)
|
||||
|
||||
// verify this valid token's entry in DB
|
||||
const user = await verifyTokenInDB(
|
||||
data?.userId,
|
||||
data?.clientId,
|
||||
token,
|
||||
tokenType
|
||||
)
|
||||
|
||||
if (user) {
|
||||
if (user.isActive) {
|
||||
req.user = user
|
||||
if (tokenType === 'accessToken') req.accessToken = token
|
||||
return next()
|
||||
} else return res.sendStatus(401)
|
||||
}
|
||||
return res.sendStatus(401)
|
||||
})
|
||||
}
|
||||
18
api/src/middlewares/desktop.ts
Normal file
18
api/src/middlewares/desktop.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const desktopRestrict = (req: any, res: any, next: any) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server')
|
||||
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()
|
||||
}
|
||||
4
api/src/middlewares/index.ts
Normal file
4
api/src/middlewares/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './authenticateToken'
|
||||
export * from './desktop'
|
||||
export * from './verifyAdmin'
|
||||
export * from './verifyAdminIfNeeded'
|
||||
72
api/src/middlewares/multer.ts
Normal file
72
api/src/middlewares/multer.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import path from 'path'
|
||||
import { Request } from 'express'
|
||||
import multer, { FileFilterCallback, Options } from 'multer'
|
||||
import { blockFileRegex, getTmpUploadsPath } from '../utils'
|
||||
|
||||
const fieldNameSize = 300
|
||||
const fileSize = 10485760 // 10 MB
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: getTmpUploadsPath(),
|
||||
filename: function (
|
||||
_req: Request,
|
||||
file: Express.Multer.File,
|
||||
callback: (error: Error | null, filename: string) => void
|
||||
) {
|
||||
callback(
|
||||
null,
|
||||
file.fieldname + path.extname(file.originalname) + '-' + Date.now()
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const limits: Options['limits'] = {
|
||||
fieldNameSize,
|
||||
fileSize
|
||||
}
|
||||
|
||||
const fileFilter: Options['fileFilter'] = (
|
||||
req: Request,
|
||||
file: Express.Multer.File,
|
||||
callback: FileFilterCallback
|
||||
) => {
|
||||
const fileExtension = path.extname(file.originalname)
|
||||
const shouldBlockUpload = blockFileRegex.test(file.originalname)
|
||||
if (shouldBlockUpload) {
|
||||
return callback(
|
||||
new Error(`File extension '${fileExtension}' not acceptable.`)
|
||||
)
|
||||
}
|
||||
|
||||
const uploadFileSize = parseInt(req.headers['content-length'] ?? '')
|
||||
if (uploadFileSize > fileSize) {
|
||||
return callback(
|
||||
new Error(
|
||||
`File size is over limit. File limit is: ${fileSize / 1024 / 1024} MB`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
callback(null, true)
|
||||
}
|
||||
|
||||
const options: Options = { storage, limits, fileFilter }
|
||||
|
||||
const multerInstance = multer(options)
|
||||
|
||||
export const multerSingle = (fileName: string, arg: any) => {
|
||||
const [req, res, next] = arg
|
||||
const upload = multerInstance.single(fileName)
|
||||
|
||||
upload(req, res, function (err) {
|
||||
if (err instanceof multer.MulterError) {
|
||||
return res.status(500).send(err.message)
|
||||
} else if (err) {
|
||||
return res.status(400).send(err.message)
|
||||
}
|
||||
// Everything went fine.
|
||||
next()
|
||||
})
|
||||
}
|
||||
|
||||
export default multerInstance
|
||||
8
api/src/middlewares/verifyAdmin.ts
Normal file
8
api/src/middlewares/verifyAdmin.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const verifyAdmin = (req: any, res: any, next: any) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') return next()
|
||||
|
||||
const { user } = req
|
||||
if (!user?.isAdmin) return res.status(401).send('Admin account required')
|
||||
next()
|
||||
}
|
||||
9
api/src/middlewares/verifyAdminIfNeeded.ts
Normal file
9
api/src/middlewares/verifyAdminIfNeeded.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => {
|
||||
const { user } = req
|
||||
const userId = parseInt(req.params.userId)
|
||||
|
||||
if (!user.isAdmin && user.userId !== userId) {
|
||||
return res.status(401).send('Admin account required')
|
||||
}
|
||||
next()
|
||||
}
|
||||
27
api/src/model/Client.ts
Normal file
27
api/src/model/Client.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import mongoose, { Schema } from 'mongoose'
|
||||
|
||||
export interface ClientPayload {
|
||||
/**
|
||||
* Client ID
|
||||
* @example "someFormattedClientID1234"
|
||||
*/
|
||||
clientId: string
|
||||
/**
|
||||
* Client Secret
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
clientSecret: string
|
||||
}
|
||||
|
||||
const ClientSchema = new Schema<ClientPayload>({
|
||||
clientId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
clientSecret: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
export default mongoose.model('Client', ClientSchema)
|
||||
87
api/src/model/Group.ts
Normal file
87
api/src/model/Group.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
|
||||
export interface GroupPayload {
|
||||
/**
|
||||
* Name of the group
|
||||
* @example "DCGroup"
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* Description of the group
|
||||
* @example "This group represents Data Controller Users"
|
||||
*/
|
||||
description: string
|
||||
/**
|
||||
* Group should be active or not, defaults to true
|
||||
* @example "true"
|
||||
*/
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
interface IGroupDocument extends GroupPayload, Document {
|
||||
groupId: number
|
||||
isActive: boolean
|
||||
users: Schema.Types.ObjectId[]
|
||||
}
|
||||
|
||||
interface IGroup extends IGroupDocument {
|
||||
addUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
||||
removeUser(userObjectId: Schema.Types.ObjectId): Promise<IGroup>
|
||||
}
|
||||
interface IGroupModel extends Model<IGroup> {}
|
||||
|
||||
const groupSchema = new Schema<IGroupDocument>({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'Group description.'
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
|
||||
})
|
||||
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
|
||||
|
||||
// Hooks
|
||||
groupSchema.post('save', function (group: IGroup, next: Function) {
|
||||
group.populate('users', 'id username displayName -_id').then(function () {
|
||||
next()
|
||||
})
|
||||
})
|
||||
|
||||
// Instance Methods
|
||||
groupSchema.method(
|
||||
'addUser',
|
||||
async function (userObjectId: Schema.Types.ObjectId) {
|
||||
const userIdIndex = this.users.indexOf(userObjectId)
|
||||
if (userIdIndex === -1) {
|
||||
this.users.push(userObjectId)
|
||||
}
|
||||
this.markModified('users')
|
||||
return this.save()
|
||||
}
|
||||
)
|
||||
groupSchema.method(
|
||||
'removeUser',
|
||||
async function (userObjectId: Schema.Types.ObjectId) {
|
||||
const userIdIndex = this.users.indexOf(userObjectId)
|
||||
if (userIdIndex > -1) {
|
||||
this.users.splice(userIdIndex, 1)
|
||||
}
|
||||
this.markModified('users')
|
||||
return this.save()
|
||||
}
|
||||
)
|
||||
|
||||
export const Group: IGroupModel = model<IGroup, IGroupModel>(
|
||||
'Group',
|
||||
groupSchema
|
||||
)
|
||||
|
||||
export default Group
|
||||
103
api/src/model/User.ts
Normal file
103
api/src/model/User.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
||||
export interface UserPayload {
|
||||
/**
|
||||
* Display name for user
|
||||
* @example "John Snow"
|
||||
*/
|
||||
displayName: string
|
||||
/**
|
||||
* Username for user
|
||||
* @example "johnSnow01"
|
||||
*/
|
||||
username: string
|
||||
/**
|
||||
* Password for user
|
||||
*/
|
||||
password: string
|
||||
/**
|
||||
* Account should be admin or not, defaults to false
|
||||
* @example "false"
|
||||
*/
|
||||
isAdmin?: boolean
|
||||
/**
|
||||
* Account should be active or not, defaults to true
|
||||
* @example "true"
|
||||
*/
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
interface IUserDocument extends UserPayload, Document {
|
||||
id: number
|
||||
isAdmin: boolean
|
||||
isActive: boolean
|
||||
groups: Schema.Types.ObjectId[]
|
||||
tokens: [{ [key: string]: string }]
|
||||
}
|
||||
|
||||
interface IUser extends IUserDocument {
|
||||
comparePassword(password: string): boolean
|
||||
}
|
||||
interface IUserModel extends Model<IUser> {
|
||||
hashPassword(password: string): string
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUserDocument>({
|
||||
displayName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
||||
tokens: [
|
||||
{
|
||||
clientId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
accessToken: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
refreshToken: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
userSchema.plugin(AutoIncrement, { inc_field: 'id' })
|
||||
|
||||
// Static Methods
|
||||
userSchema.static('hashPassword', (password: string): string => {
|
||||
const salt = bcrypt.genSaltSync(10)
|
||||
return bcrypt.hashSync(password, salt)
|
||||
})
|
||||
|
||||
// Instance Methods
|
||||
userSchema.method('comparePassword', function (password: string): boolean {
|
||||
if (bcrypt.compareSync(password, this.password)) return true
|
||||
return false
|
||||
})
|
||||
|
||||
export const User: IUserModel = model<IUser, IUserModel>('User', userSchema)
|
||||
|
||||
export default User
|
||||
90
api/src/routes/api/auth.ts
Normal file
90
api/src/routes/api/auth.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
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 { InfoJWT } from '../../types'
|
||||
|
||||
const authRouter = express.Router()
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
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: any, res) => {
|
||||
const userInfo: InfoJWT = req.user
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.refresh(userInfo)
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
|
||||
const userInfo: InfoJWT = req.user
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
await controller.logout(userInfo)
|
||||
} catch (e) {}
|
||||
|
||||
res.sendStatus(204)
|
||||
})
|
||||
|
||||
export default authRouter
|
||||
20
api/src/routes/api/client.ts
Normal file
20
api/src/routes/api/client.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import express from 'express'
|
||||
import { ClientController } from '../../controllers'
|
||||
import { registerClientValidation } from '../../utils'
|
||||
|
||||
const clientRouter = express.Router()
|
||||
|
||||
clientRouter.post('/', async (req, res) => {
|
||||
const { error, value: body } = registerClientValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const controller = new ClientController()
|
||||
try {
|
||||
const response = await controller.createClient(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default clientRouter
|
||||
31
api/src/routes/api/code.ts
Normal file
31
api/src/routes/api/code.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import express from 'express'
|
||||
import { runSASValidation } from '../../utils'
|
||||
import { CodeController } from '../../controllers/'
|
||||
|
||||
const runRouter = express.Router()
|
||||
|
||||
const controller = new CodeController()
|
||||
|
||||
runRouter.post('/execute', async (req, res) => {
|
||||
const { error, value: body } = runSASValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.executeSASCode(req, body)
|
||||
|
||||
if (response instanceof Buffer) {
|
||||
res.writeHead(200, (req as any).sasHeaders)
|
||||
return res.end(response)
|
||||
}
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default runRouter
|
||||
137
api/src/routes/api/drive.ts
Normal file
137
api/src/routes/api/drive.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import express from 'express'
|
||||
import { deleteFile } from '@sasjs/utils'
|
||||
|
||||
import { publishAppStream } from '../appStream'
|
||||
|
||||
import { multerSingle } from '../../middlewares/multer'
|
||||
import { DriveController } from '../../controllers/'
|
||||
import {
|
||||
deployValidation,
|
||||
fileBodyValidation,
|
||||
fileParamValidation
|
||||
} from '../../utils'
|
||||
|
||||
const controller = new DriveController()
|
||||
|
||||
const driveRouter = express.Router()
|
||||
|
||||
driveRouter.post('/deploy', async (req, res) => {
|
||||
const { error, value: body } = deployValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.deploy(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)
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.get('/file', async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
await controller.getFile(req, query._filePath, body.filePath)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.delete('/file', async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.deleteFile(query._filePath, body.filePath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.post(
|
||||
'/file',
|
||||
(...arg) => multerSingle('file', arg),
|
||||
async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) {
|
||||
if (req.file) await deleteFile(req.file.path)
|
||||
return res.status(400).send(errQ.details[0].message)
|
||||
}
|
||||
|
||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||
|
||||
try {
|
||||
const response = await controller.saveFile(
|
||||
req.file,
|
||||
query._filePath,
|
||||
body.filePath
|
||||
)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
await deleteFile(req.file.path)
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
driveRouter.patch(
|
||||
'/file',
|
||||
(...arg) => multerSingle('file', arg),
|
||||
async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) {
|
||||
if (req.file) await deleteFile(req.file.path)
|
||||
return res.status(400).send(errQ.details[0].message)
|
||||
}
|
||||
|
||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||
|
||||
try {
|
||||
const response = await controller.updateFile(
|
||||
req.file,
|
||||
query._filePath,
|
||||
body.filePath
|
||||
)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
await deleteFile(req.file.path)
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
driveRouter.get('/fileTree', async (req, res) => {
|
||||
try {
|
||||
const response = await controller.getFileTree()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default driveRouter
|
||||
99
api/src/routes/api/group.ts
Normal file
99
api/src/routes/api/group.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import express from 'express'
|
||||
import { GroupController } from '../../controllers/'
|
||||
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
||||
import { registerGroupValidation } from '../../utils'
|
||||
|
||||
const groupRouter = express.Router()
|
||||
|
||||
groupRouter.post(
|
||||
'/',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req, res) => {
|
||||
const { error, value: body } = registerGroupValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.createGroup(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
groupRouter.get('/', authenticateAccessToken, async (req, res) => {
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.getAllGroups()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
groupRouter.get('/:groupId', authenticateAccessToken, async (req: any, res) => {
|
||||
const { groupId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.getGroup(groupId)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
groupRouter.post(
|
||||
'/:groupId/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
const { groupId, userId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.addUserToGroup(groupId, userId)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
groupRouter.delete(
|
||||
'/:groupId/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
const { groupId, userId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.removeUserFromGroup(groupId, userId)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
groupRouter.delete(
|
||||
'/:groupId',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
const { groupId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
await controller.deleteGroup(groupId)
|
||||
res.status(200).send('Group Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default groupRouter
|
||||
47
api/src/routes/api/index.ts
Normal file
47
api/src/routes/api/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import express from 'express'
|
||||
|
||||
import swaggerUi from 'swagger-ui-express'
|
||||
|
||||
import {
|
||||
authenticateAccessToken,
|
||||
desktopRestrict,
|
||||
desktopUsername,
|
||||
verifyAdmin
|
||||
} from '../../middlewares'
|
||||
|
||||
import driveRouter from './drive'
|
||||
import stpRouter from './stp'
|
||||
import codeRouter from './code'
|
||||
import userRouter from './user'
|
||||
import groupRouter from './group'
|
||||
import clientRouter from './client'
|
||||
import authRouter from './auth'
|
||||
import sessionRouter from './session'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||
router.use('/auth', desktopRestrict, authRouter)
|
||||
router.use(
|
||||
'/client',
|
||||
desktopRestrict,
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
clientRouter
|
||||
)
|
||||
router.use('/drive', authenticateAccessToken, driveRouter)
|
||||
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'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export default router
|
||||
16
api/src/routes/api/session.ts
Normal file
16
api/src/routes/api/session.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import express from 'express'
|
||||
import { SessionController } from '../../controllers'
|
||||
|
||||
const sessionRouter = express.Router()
|
||||
|
||||
sessionRouter.get('/', async (req, res) => {
|
||||
const controller = new SessionController()
|
||||
try {
|
||||
const response = await controller.session(req)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default sessionRouter
|
||||
359
api/src/routes/api/spec/auth.spec.ts
Normal file
359
api/src/routes/api/spec/auth.spec.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
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,
|
||||
AuthController
|
||||
} from '../../../controllers/'
|
||||
import { populateClients } from '../auth'
|
||||
import { InfoJWT } from '../../../types'
|
||||
import {
|
||||
generateAccessToken,
|
||||
generateAuthCode,
|
||||
generateRefreshToken,
|
||||
saveTokensInDB,
|
||||
verifyTokenInDB
|
||||
} from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const clientSecret = 'someclientSecret'
|
||||
const user = {
|
||||
id: 1234,
|
||||
displayName: 'Test User',
|
||||
username: 'testUsername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
describe('auth', () => {
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
await clientController.createClient({ clientId, clientSecret })
|
||||
await populateClients()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await con.connection.dropDatabase()
|
||||
await con.connection.close()
|
||||
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,
|
||||
userId: user.id
|
||||
}
|
||||
beforeAll(async () => {
|
||||
await userController.createUser(user)
|
||||
})
|
||||
afterAll(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond with access and refresh tokens', async () => {
|
||||
const code = AuthController.saveCode(
|
||||
userInfo.userId,
|
||||
userInfo.clientId,
|
||||
generateAuthCode(userInfo)
|
||||
)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/token')
|
||||
.send({
|
||||
clientId,
|
||||
code
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toHaveProperty('accessToken')
|
||||
expect(res.body).toHaveProperty('refreshToken')
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if code is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/token')
|
||||
.send({
|
||||
clientId
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"code" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if clientId is missing', async () => {
|
||||
const code = AuthController.saveCode(
|
||||
userInfo.userId,
|
||||
userInfo.clientId,
|
||||
generateAuthCode(userInfo)
|
||||
)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/token')
|
||||
.send({
|
||||
code
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"clientId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if code is invalid', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/token')
|
||||
.send({
|
||||
clientId,
|
||||
code: 'InvalidCode'
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if clientId is invalid', async () => {
|
||||
const code = AuthController.saveCode(
|
||||
userInfo.userId,
|
||||
userInfo.clientId,
|
||||
generateAuthCode(userInfo)
|
||||
)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/token')
|
||||
.send({
|
||||
clientId: 'WrongClientID',
|
||||
code
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('refresh', () => {
|
||||
let refreshToken: string
|
||||
let currentUser: any
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUser = await userController.createUser(user)
|
||||
refreshToken = generateRefreshToken({
|
||||
clientId,
|
||||
userId: currentUser.id
|
||||
})
|
||||
await saveTokensInDB(
|
||||
currentUser.id,
|
||||
clientId,
|
||||
'accessToken',
|
||||
refreshToken
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond with new access and refresh tokens', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/refresh')
|
||||
.auth(refreshToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toHaveProperty('accessToken')
|
||||
expect(res.body).toHaveProperty('refreshToken')
|
||||
|
||||
// cannot use same refresh again
|
||||
const resWithError = await request(app)
|
||||
.post('/SASjsApi/auth/refresh')
|
||||
.auth(refreshToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(resWithError.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('logout', () => {
|
||||
let accessToken: string
|
||||
let currentUser: any
|
||||
|
||||
beforeEach(async () => {
|
||||
currentUser = await userController.createUser(user)
|
||||
accessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId: currentUser.id
|
||||
})
|
||||
|
||||
await saveTokensInDB(
|
||||
currentUser.id,
|
||||
clientId,
|
||||
accessToken,
|
||||
'refreshToken'
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond no content and remove access/refresh tokens from DB', async () => {
|
||||
const res = await request(app)
|
||||
.delete('/SASjsApi/auth/logout')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(204)
|
||||
|
||||
expect(res.body).toEqual({})
|
||||
|
||||
expect(
|
||||
await verifyTokenInDB(
|
||||
currentUser.id,
|
||||
clientId,
|
||||
accessToken,
|
||||
'accessToken'
|
||||
)
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
162
api/src/routes/api/spec/client.spec.ts
Normal file
162
api/src/routes/api/spec/client.spec.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
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/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const client = {
|
||||
clientId: 'someclientID',
|
||||
clientSecret: 'someclientSecret'
|
||||
}
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
username: 'testAdminUsername',
|
||||
password: '12345678',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
const newClient = {
|
||||
clientId: 'newClientID',
|
||||
clientSecret: 'newClientSecret'
|
||||
}
|
||||
|
||||
describe('client', () => {
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await con.connection.dropDatabase()
|
||||
await con.connection.close()
|
||||
await mongoServer.stop()
|
||||
})
|
||||
|
||||
describe('create', () => {
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
const dbUser = await userController.createUser(adminUser)
|
||||
adminAccessToken = generateAccessToken({
|
||||
clientId: client.clientId,
|
||||
userId: dbUser.id
|
||||
})
|
||||
await saveTokensInDB(
|
||||
dbUser.id,
|
||||
client.clientId,
|
||||
adminAccessToken,
|
||||
'refreshToken'
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['clients']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond with new client', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/client')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send(newClient)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.clientId).toEqual(newClient.clientId)
|
||||
expect(res.body.clientSecret).toEqual(newClient.clientSecret)
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/client')
|
||||
.send(newClient)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbideen if access token is not of an admin account', async () => {
|
||||
const user = {
|
||||
displayName: 'User 1',
|
||||
username: 'username1',
|
||||
password: '12345678',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
}
|
||||
const dbUser = await userController.createUser(user)
|
||||
const accessToken = generateAccessToken({
|
||||
clientId: client.clientId,
|
||||
userId: dbUser.id
|
||||
})
|
||||
await saveTokensInDB(
|
||||
dbUser.id,
|
||||
client.clientId,
|
||||
accessToken,
|
||||
'refreshToken'
|
||||
)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/client')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(newClient)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Admin account required')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if clientId is already present', async () => {
|
||||
await clientController.createClient(newClient)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/client')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send(newClient)
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Client ID already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if clientId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/client')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...newClient,
|
||||
clientId: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"clientId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if clientSecret is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/client')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...newClient,
|
||||
clientSecret: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"clientSecret" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
491
api/src/routes/api/spec/drive.spec.ts
Normal file
491
api/src/routes/api/spec/drive.spec.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
import path from 'path'
|
||||
import { Express } from 'express'
|
||||
import mongoose, { Mongoose } from 'mongoose'
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||
import request from 'supertest'
|
||||
|
||||
import {
|
||||
folderExists,
|
||||
fileExists,
|
||||
readFile,
|
||||
deleteFolder,
|
||||
generateTimestamp,
|
||||
copy
|
||||
} from '@sasjs/utils'
|
||||
import * as fileUtilModules from '../../../utils/file'
|
||||
|
||||
const timestamp = generateTimestamp()
|
||||
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
||||
jest
|
||||
.spyOn(fileUtilModules, 'getTmpFolderPath')
|
||||
.mockImplementation(() => tmpFolder)
|
||||
jest
|
||||
.spyOn(fileUtilModules, 'getTmpUploadsPath')
|
||||
.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 clientId = 'someclientID'
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
username: 'testUsername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
describe('files', () => {
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const controller = new UserController()
|
||||
|
||||
let accessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
const dbUser = await controller.createUser(user)
|
||||
accessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId: dbUser.id
|
||||
})
|
||||
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await con.connection.dropDatabase()
|
||||
await con.connection.close()
|
||||
await mongoServer.stop()
|
||||
await deleteFolder(tmpFolder)
|
||||
})
|
||||
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 })
|
||||
|
||||
expect(res.statusCode).toEqual(400)
|
||||
|
||||
if (payload === undefined) {
|
||||
expect(res.text).toEqual('"fileTree" is required')
|
||||
} else {
|
||||
expect(res.body).toEqual({
|
||||
status: 'failure',
|
||||
message: 'Provided not supported data format.',
|
||||
example: getTreeExample()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
it('should respond with payload example if valid payload was not provided', async () => {
|
||||
await shouldFailAssertion(null)
|
||||
await shouldFailAssertion(undefined)
|
||||
await shouldFailAssertion('data')
|
||||
await shouldFailAssertion({})
|
||||
await shouldFailAssertion({
|
||||
userId: 1,
|
||||
title: 'test is cool'
|
||||
})
|
||||
await shouldFailAssertion({
|
||||
membersWRONG: []
|
||||
})
|
||||
await shouldFailAssertion({
|
||||
members: {}
|
||||
})
|
||||
await shouldFailAssertion({
|
||||
members: [
|
||||
{
|
||||
nameWRONG: 'jobs',
|
||||
type: 'folder',
|
||||
members: []
|
||||
}
|
||||
]
|
||||
})
|
||||
await shouldFailAssertion({
|
||||
members: [
|
||||
{
|
||||
name: 'jobs',
|
||||
type: 'WRONG',
|
||||
members: []
|
||||
}
|
||||
]
|
||||
})
|
||||
await shouldFailAssertion({
|
||||
members: [
|
||||
{
|
||||
name: 'jobs',
|
||||
type: 'folder',
|
||||
members: [
|
||||
{
|
||||
name: 'extract',
|
||||
type: 'folder',
|
||||
members: [
|
||||
{
|
||||
name: 'makedata1',
|
||||
type: 'service',
|
||||
codeWRONG: '%put Hello World!;'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('should successfully deploy if valid payload was provided', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/deploy')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ appLoc: '/public', fileTree: getTreeExample() })
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.text).toEqual(
|
||||
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
||||
)
|
||||
await expect(folderExists(getTmpFilesFolderPath())).resolves.toEqual(true)
|
||||
|
||||
const testJobFolder = path.join(
|
||||
getTmpFilesFolderPath(),
|
||||
'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(getTmpFilesFolderPath())
|
||||
})
|
||||
})
|
||||
|
||||
describe('file', () => {
|
||||
describe('create', () => {
|
||||
it('should create a SAS file on drive having filePath as form field', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', '/my/path/code.sas')
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should create a SAS file on drive having _filePath as query param', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: '/my/path/code1.sas' })
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.field('filePath', '/my/path/code.sas')
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if file is already present', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
pathToUpload
|
||||
)
|
||||
await copy(fileToAttachPath, pathToCopy)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: File already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/../path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Cannot put file outside drive.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if filePath is missing', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"_filePath" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.exe'
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
// .field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Invalid file extension')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if file is missing', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('"file" is not present.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
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 res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', attachedFile, 'another.sas')
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 10 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a SAS file on drive having filePath as form field', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
pathToUpload
|
||||
)
|
||||
await copy(fileToAttachPath, pathToCopy)
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should update a SAS file on drive having _filePath as query param', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
pathToUpload
|
||||
)
|
||||
await copy(fileToAttachPath, pathToCopy)
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.field('filePath', '/my/path/code.sas')
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if file is not present', async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', `/my/path/code-${generateTimestamp()}.sas`)
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/../path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if filePath is missing', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"_filePath" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.exe'
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
// .field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Invalid file extension')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if file is missing', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('"file" is not present.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
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 res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', attachedFile, 'another.sas')
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 10 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const getExampleService = (): ServiceMember =>
|
||||
((getTreeExample().members[0] as FolderMember).members[0] as FolderMember)
|
||||
.members[0] as ServiceMember
|
||||
1
api/src/routes/api/spec/files/sample.exe
Normal file
1
api/src/routes/api/spec/files/sample.exe
Normal file
@@ -0,0 +1 @@
|
||||
some code of sas
|
||||
1
api/src/routes/api/spec/files/sample.sas
Normal file
1
api/src/routes/api/spec/files/sample.sas
Normal file
@@ -0,0 +1 @@
|
||||
some code of sas
|
||||
489
api/src/routes/api/spec/group.spec.ts
Normal file
489
api/src/routes/api/spec/group.spec.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
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, 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',
|
||||
password: '12345678',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
username: 'testUsername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
const group = {
|
||||
name: 'DCGroup1',
|
||||
description: 'DC group for testing purposes.'
|
||||
}
|
||||
|
||||
const userController = new UserController()
|
||||
const groupController = new GroupController()
|
||||
|
||||
describe('group', () => {
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
adminAccessToken = await generateSaveTokenAndCreateUser()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await con.connection.dropDatabase()
|
||||
await con.connection.close()
|
||||
await mongoServer.stop()
|
||||
})
|
||||
|
||||
describe('create', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllGroups()
|
||||
})
|
||||
|
||||
it('should respond with new group', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/group')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send(group)
|
||||
.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).post('/SASjsApi/group').send().expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||
const accessToken = await generateSaveTokenAndCreateUser({
|
||||
...user,
|
||||
username: 'create' + user.username
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/group')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Admin account required')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if name is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/group')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...group,
|
||||
name: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"name" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllGroups()
|
||||
})
|
||||
|
||||
it('should respond with OK when admin user requests', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/1234`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: No Group deleted!')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized when access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.delete('/SASjsApi/group/1234')
|
||||
.send()
|
||||
.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', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const accessToken = await generateSaveTokenAndCreateUser({
|
||||
...user,
|
||||
username: 'delete' + user.username
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Admin account required')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('get', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllGroups()
|
||||
})
|
||||
|
||||
it('should respond with group', async () => {
|
||||
const { groupId } = await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/group/${groupId}`)
|
||||
.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: 'get' + user.username
|
||||
})
|
||||
|
||||
const { groupId } = await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/group/${groupId}`)
|
||||
.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/1234')
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/group/1234')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Group not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAll', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllGroups()
|
||||
})
|
||||
|
||||
it('should respond with all groups', async () => {
|
||||
await groupController.createGroup(group)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/group')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
groupId: expect.anything(),
|
||||
name: 'DCGroup1',
|
||||
description: 'DC group for testing purposes.'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should respond with all groups when access token is not of an admin account', async () => {
|
||||
await groupController.createGroup(group)
|
||||
const accessToken = await generateSaveTokenAndCreateUser({
|
||||
...user,
|
||||
username: 'getAllrandomUser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/group')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(user)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
groupId: expect.anything(),
|
||||
name: 'DCGroup1',
|
||||
description: 'DC group for testing purposes.'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app).get('/SASjsApi/group').send().expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('AddUser', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllGroups()
|
||||
})
|
||||
|
||||
it('should respond with group having new user in it', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const dbUser = await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.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([
|
||||
{
|
||||
id: expect.anything(),
|
||||
username: user.username,
|
||||
displayName: user.displayName
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should respond with group without duplicating user', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const dbUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'addUserRandomUser'
|
||||
})
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.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([
|
||||
{
|
||||
id: expect.anything(),
|
||||
username: 'addUserRandomUser',
|
||||
displayName: user.displayName
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/group/123/123')
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||
const accessToken = await generateSaveTokenAndCreateUser({
|
||||
...user,
|
||||
username: 'addUser' + user.username
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/group/123/123')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Admin account required')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/group/123/123')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Group not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden 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(res.text).toEqual('Error: User not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('RemoveUser', () => {
|
||||
afterEach(async () => {
|
||||
await deleteAllGroups()
|
||||
})
|
||||
|
||||
it('should respond with group having user removed from it', async () => {
|
||||
const dbGroup = await groupController.createGroup(group)
|
||||
const dbUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'removeUserRandomUser'
|
||||
})
|
||||
await groupController.addUserToGroup(dbGroup.groupId, dbUser.id)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.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 Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.delete('/SASjsApi/group/123/123')
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||
const accessToken = await generateSaveTokenAndCreateUser({
|
||||
...user,
|
||||
username: 'removeUser' + user.username
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.delete('/SASjsApi/group/123/123')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Admin account required')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if groupId is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.delete('/SASjsApi/group/123/123')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Group not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden 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(res.text).toEqual('Error: User not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const generateSaveTokenAndCreateUser = async (
|
||||
someUser?: any
|
||||
): Promise<string> => {
|
||||
const dbUser = await userController.createUser(someUser ?? adminUser)
|
||||
|
||||
return generateAndSaveToken(dbUser.id)
|
||||
}
|
||||
|
||||
const generateAndSaveToken = async (userId: number) => {
|
||||
const adminAccessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId
|
||||
})
|
||||
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
||||
return adminAccessToken
|
||||
}
|
||||
|
||||
const deleteAllGroups = async () => {
|
||||
const { collections } = mongoose.connection
|
||||
const collection = collections['groups']
|
||||
await collection.deleteMany({})
|
||||
}
|
||||
516
api/src/routes/api/spec/user.spec.ts
Normal file
516
api/src/routes/api/spec/user.spec.ts
Normal file
@@ -0,0 +1,516 @@
|
||||
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 } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
username: 'testAdminUsername',
|
||||
password: '12345678',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
username: 'testUsername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
const controller = new UserController()
|
||||
|
||||
describe('user', () => {
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await con.connection.dropDatabase()
|
||||
await con.connection.close()
|
||||
await mongoServer.stop()
|
||||
})
|
||||
|
||||
describe('create', () => {
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeEach(async () => {
|
||||
adminAccessToken = await generateSaveTokenAndCreateUser()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllUsers()
|
||||
})
|
||||
|
||||
it('should respond with new user', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send(user)
|
||||
.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)
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
.send(user)
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not of an admin account', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const accessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId: dbUser.id
|
||||
})
|
||||
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
.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 () => {
|
||||
await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send(user)
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Username already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if username is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...user,
|
||||
username: undefined
|
||||
})
|
||||
.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/user')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...user,
|
||||
password: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"password" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if displayName is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({
|
||||
...user,
|
||||
displayName: undefined
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"displayName" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update', () => {
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeEach(async () => {
|
||||
adminAccessToken = await generateSaveTokenAndCreateUser()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllUsers()
|
||||
})
|
||||
|
||||
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/${dbUser.id}`)
|
||||
.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/${dbUser.id}`)
|
||||
.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/${dbUser.id}`)
|
||||
.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/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/${dbUser1.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ username: dbUser2.username })
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Username already exists.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete', () => {
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeEach(async () => {
|
||||
adminAccessToken = await generateSaveTokenAndCreateUser()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllUsers()
|
||||
})
|
||||
|
||||
it('should respond with OK when admin user requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.delete(`/SASjsApi/user/${dbUser.id}`)
|
||||
.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/${dbUser.id}`)
|
||||
.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/${dbUser.id}`)
|
||||
.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/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)
|
||||
.delete(`/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 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/${dbUser.id}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ password: 'incorrectpassword' })
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Invalid password.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('get', () => {
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeEach(async () => {
|
||||
adminAccessToken = await generateSaveTokenAndCreateUser()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllUsers()
|
||||
})
|
||||
|
||||
it('should respond with user', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const userId = dbUser.id
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/${userId}`)
|
||||
.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)
|
||||
})
|
||||
|
||||
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 userId = 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)
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/user/1234')
|
||||
.send()
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if userId is incorrect', async () => {
|
||||
await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/user/1234')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: User is not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAll', () => {
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeEach(async () => {
|
||||
adminAccessToken = await generateSaveTokenAndCreateUser()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteAllUsers()
|
||||
})
|
||||
|
||||
it('should respond with all users', async () => {
|
||||
await controller.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/user')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
id: expect.anything(),
|
||||
username: adminUser.username,
|
||||
displayName: adminUser.displayName
|
||||
},
|
||||
{
|
||||
id: expect.anything(),
|
||||
username: user.username,
|
||||
displayName: user.displayName
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should respond with all users when access token is not of an admin account', async () => {
|
||||
const accessToken = await generateSaveTokenAndCreateUser({
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/user')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual([
|
||||
{
|
||||
id: expect.anything(),
|
||||
username: adminUser.username,
|
||||
displayName: adminUser.displayName
|
||||
},
|
||||
{
|
||||
id: expect.anything(),
|
||||
username: 'randomUser',
|
||||
displayName: user.displayName
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app).get('/SASjsApi/user').send().expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const generateSaveTokenAndCreateUser = async (
|
||||
someUser?: any
|
||||
): Promise<string> => {
|
||||
const dbUser = await controller.createUser(someUser ?? adminUser)
|
||||
|
||||
return generateAndSaveToken(dbUser.id)
|
||||
}
|
||||
|
||||
const generateAndSaveToken = async (userId: number) => {
|
||||
const adminAccessToken = generateAccessToken({
|
||||
clientId,
|
||||
userId
|
||||
})
|
||||
await saveTokensInDB(userId, clientId, adminAccessToken, 'refreshToken')
|
||||
return adminAccessToken
|
||||
}
|
||||
|
||||
const deleteAllUsers = async () => {
|
||||
const { collections } = mongoose.connection
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
}
|
||||
66
api/src/routes/api/stp.ts
Normal file
66
api/src/routes/api/stp.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import express from 'express'
|
||||
import { executeProgramRawValidation } from '../../utils'
|
||||
import { STPController } from '../../controllers/'
|
||||
import { FileUploadController } from '../../controllers/internal'
|
||||
|
||||
const stpRouter = express.Router()
|
||||
|
||||
const fileUploadController = new FileUploadController()
|
||||
const controller = new STPController()
|
||||
|
||||
stpRouter.get('/execute', async (req, res) => {
|
||||
const { error, value: query } = executeProgramRawValidation(req.query)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.executeReturnRaw(req, query._program)
|
||||
|
||||
if (response instanceof Buffer) {
|
||||
res.writeHead(200, (req as any).sasHeaders)
|
||||
return res.end(response)
|
||||
}
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err)
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.executeReturnJson(
|
||||
req,
|
||||
body,
|
||||
query?._program
|
||||
)
|
||||
|
||||
if (response instanceof Buffer) {
|
||||
res.writeHead(200, (req as any).sasHeaders)
|
||||
return res.end(response)
|
||||
}
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default stpRouter
|
||||
95
api/src/routes/api/user.ts
Normal file
95
api/src/routes/api/user.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import express from 'express'
|
||||
import { UserController } from '../../controllers/'
|
||||
import {
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
verifyAdminIfNeeded
|
||||
} from '../../middlewares'
|
||||
import {
|
||||
deleteUserValidation,
|
||||
registerUserValidation,
|
||||
updateUserValidation
|
||||
} from '../../utils'
|
||||
|
||||
const userRouter = express.Router()
|
||||
|
||||
userRouter.post('/', authenticateAccessToken, verifyAdmin, async (req, res) => {
|
||||
const { error, value: body } = registerUserValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
const response = await controller.createUser(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
userRouter.get('/', authenticateAccessToken, async (req, res) => {
|
||||
const controller = new UserController()
|
||||
try {
|
||||
const response = await controller.getAllUsers()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
|
||||
const { userId } = req.params
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
const response = await controller.getUser(userId)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
userRouter.patch(
|
||||
'/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdminIfNeeded,
|
||||
async (req: any, 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(userId, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
userRouter.delete(
|
||||
'/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdminIfNeeded,
|
||||
async (req: any, 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)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
await controller.deleteUser(userId, data, user.isAdmin)
|
||||
res.status(200).send('Account Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default userRouter
|
||||
54
api/src/routes/appStream/appStreamHtml.ts
Normal file
54
api/src/routes/appStream/appStreamHtml.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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;
|
||||
border-radius: 10px 10px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
.app-container .app img{
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>`
|
||||
|
||||
const defaultAppLogo = '/sasjs-logo.svg'
|
||||
|
||||
const singleAppStreamHtml = (
|
||||
streamServiceName: string,
|
||||
appLoc: string,
|
||||
logo?: string
|
||||
) =>
|
||||
` <a class="app" href="${streamServiceName}" title="${appLoc}">
|
||||
<img src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}" />
|
||||
${streamServiceName}
|
||||
</a>`
|
||||
|
||||
export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
|
||||
<html>
|
||||
<head>
|
||||
<base href="/AppStream/">
|
||||
${style}
|
||||
</head>
|
||||
<body>
|
||||
<h1>App Stream</h1>
|
||||
<div class="app-container">
|
||||
${Object.entries(appStreamConfig)
|
||||
.map(([streamServiceName, entry]) =>
|
||||
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
76
api/src/routes/appStream/index.ts
Normal file
76
api/src/routes/appStream/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import { folderExists } from '@sasjs/utils'
|
||||
|
||||
import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
|
||||
import { appStreamHtml } from './appStreamHtml'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', async (_, res) => {
|
||||
const content = appStreamHtml(process.appStreamConfig)
|
||||
|
||||
return res.send(content)
|
||||
})
|
||||
|
||||
export const publishAppStream = async (
|
||||
appLoc: string,
|
||||
streamWebFolder: string,
|
||||
streamServiceName?: string,
|
||||
streamLogo?: string,
|
||||
addEntryToFile: boolean = true
|
||||
) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
||||
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
||||
if (!appLocPath.includes(driveFilesPath)) {
|
||||
throw new Error('appLoc cannot be outside drive.')
|
||||
}
|
||||
|
||||
const pathToDeployment = path.join(appLocPath, 'services', streamWebFolder)
|
||||
if (!pathToDeployment.includes(appLocPath)) {
|
||||
throw new Error('streamWebFolder cannot be outside appLoc.')
|
||||
}
|
||||
|
||||
if (await folderExists(pathToDeployment)) {
|
||||
const appCount = process.appStreamConfig
|
||||
? Object.keys(process.appStreamConfig).length
|
||||
: 0
|
||||
|
||||
if (!streamServiceName) {
|
||||
streamServiceName = `AppStreamName${appCount + 1}`
|
||||
} else {
|
||||
const alreadyDeployed = process.appStreamConfig[streamServiceName]
|
||||
if (alreadyDeployed) {
|
||||
if (alreadyDeployed.appLoc === appLoc) {
|
||||
// redeploying to same streamServiceName
|
||||
} else {
|
||||
// trying to deploy to another existing streamServiceName
|
||||
// assign new streamServiceName
|
||||
streamServiceName = `${streamServiceName}-${appCount + 1}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
|
||||
|
||||
addEntryToAppStreamConfig(
|
||||
streamServiceName,
|
||||
appLoc,
|
||||
streamWebFolder,
|
||||
streamLogo,
|
||||
addEntryToFile
|
||||
)
|
||||
|
||||
const sasJsPort = process.env.PORT ?? 5000
|
||||
console.log(
|
||||
'Serving Stream App: ',
|
||||
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
||||
)
|
||||
return { streamServiceName }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export default router
|
||||
16
api/src/routes/setupRoutes.ts
Normal file
16
api/src/routes/setupRoutes.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Express } from 'express'
|
||||
|
||||
import webRouter from './web'
|
||||
import apiRouter from './api'
|
||||
import appStreamRouter from './appStream'
|
||||
|
||||
export const setupRoutes = (app: Express) => {
|
||||
app.use('/', webRouter)
|
||||
app.use('/SASjsApi', apiRouter)
|
||||
|
||||
app.use('/AppStream', function (req, res, next) {
|
||||
// this needs to be a function to hook on
|
||||
// whatever the current router is
|
||||
appStreamRouter(req, res, next)
|
||||
})
|
||||
}
|
||||
8
api/src/routes/web/index.ts
Normal file
8
api/src/routes/web/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import express from 'express'
|
||||
import webRouter from './web'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/', webRouter)
|
||||
|
||||
export default router
|
||||
34
api/src/routes/web/web.ts
Normal file
34
api/src/routes/web/web.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { readFile } from '@sasjs/utils'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import { getWebBuildFolderPath } from '../../utils'
|
||||
|
||||
const webRouter = express.Router()
|
||||
|
||||
const codeToInject = `
|
||||
<script>
|
||||
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
|
||||
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
|
||||
</script>`
|
||||
|
||||
webRouter.get('/', async (_, res) => {
|
||||
let content: string
|
||||
try {
|
||||
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
||||
content = await readFile(indexHtmlPath)
|
||||
} catch (_) {
|
||||
return res.send('Web Build is not present')
|
||||
}
|
||||
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') {
|
||||
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
|
||||
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
return res.send(injectedContent)
|
||||
}
|
||||
|
||||
return res.send(content)
|
||||
})
|
||||
|
||||
export default webRouter
|
||||
28
api/src/server.ts
Normal file
28
api/src/server.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createServer } from 'https'
|
||||
|
||||
import appPromise from './app'
|
||||
import { getCertificates } from './utils'
|
||||
|
||||
appPromise.then(async (app) => {
|
||||
const protocol = process.env.PROTOCOL ?? 'http'
|
||||
const sasJsPort = process.env.PORT ?? 5000
|
||||
|
||||
console.log('PROTOCOL: ', protocol)
|
||||
|
||||
if (protocol !== 'https') {
|
||||
app.listen(sasJsPort, () => {
|
||||
console.log(
|
||||
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
|
||||
)
|
||||
})
|
||||
} else {
|
||||
const { key, cert } = await getCertificates()
|
||||
|
||||
const httpsServer = createServer({ key, cert }, app)
|
||||
httpsServer.listen(sasJsPort, () => {
|
||||
console.log(
|
||||
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
7
api/src/types/AppStreamConfig.ts
Normal file
7
api/src/types/AppStreamConfig.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface AppStreamConfig {
|
||||
[key: string]: {
|
||||
appLoc: string
|
||||
streamWebFolder: string
|
||||
streamLogo?: string
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
export interface FileTree {
|
||||
members: [FolderMember, ServiceMember]
|
||||
members: (FolderMember | ServiceMember)[]
|
||||
}
|
||||
|
||||
export enum MemberType {
|
||||
folder = 'folder',
|
||||
service = 'service'
|
||||
service = 'service',
|
||||
file = 'file'
|
||||
}
|
||||
|
||||
export interface FolderMember {
|
||||
name: string
|
||||
type: MemberType.folder
|
||||
members: [FolderMember, ServiceMember]
|
||||
members: (FolderMember | ServiceMember)[]
|
||||
}
|
||||
|
||||
export interface ServiceMember {
|
||||
name: string
|
||||
type: MemberType.service
|
||||
type: MemberType.service | MemberType.file
|
||||
code: string
|
||||
}
|
||||
|
||||
@@ -36,7 +37,9 @@ const isFolderMember = (arg: any): arg is FolderMember =>
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isFolderMember(member) && !isServiceMember(member)
|
||||
!isFolderMember(member) &&
|
||||
!isServiceMember(member) &&
|
||||
!isFileMember(member)
|
||||
).length === 0
|
||||
|
||||
const isServiceMember = (arg: any): arg is ServiceMember =>
|
||||
@@ -45,3 +48,10 @@ const isServiceMember = (arg: any): arg is ServiceMember =>
|
||||
arg.type === MemberType.service &&
|
||||
arg.code &&
|
||||
typeof arg.code === 'string'
|
||||
|
||||
const isFileMember = (arg: any): arg is ServiceMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.file &&
|
||||
arg.code &&
|
||||
typeof arg.code === 'string'
|
||||
4
api/src/types/InfoJWT.ts
Normal file
4
api/src/types/InfoJWT.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface InfoJWT {
|
||||
clientId: string
|
||||
userId: number
|
||||
}
|
||||
7
api/src/types/PreProgramVars.ts
Normal file
7
api/src/types/PreProgramVars.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface PreProgramVars {
|
||||
username: string
|
||||
userId: number
|
||||
displayName: string
|
||||
serverUrl: string
|
||||
accessToken: string
|
||||
}
|
||||
8
api/src/types/Process.d.ts
vendored
Normal file
8
api/src/types/Process.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
sasLoc: string
|
||||
driveLoc: string
|
||||
sessionController?: import('../controllers/internal').SessionController
|
||||
appStreamConfig: import('./').AppStreamConfig
|
||||
}
|
||||
}
|
||||
17
api/src/types/Request.ts
Normal file
17
api/src/types/Request.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
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'
|
||||
11
api/src/types/Session.ts
Normal file
11
api/src/types/Session.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface Session {
|
||||
id: string
|
||||
ready: boolean
|
||||
creationTimeStamp: string
|
||||
deathTimeStamp: string
|
||||
path: string
|
||||
inUse: boolean
|
||||
consumed: boolean
|
||||
completed: boolean
|
||||
crashed?: string
|
||||
}
|
||||
6
api/src/types/TreeNode.ts
Normal file
6
api/src/types/TreeNode.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface TreeNode {
|
||||
name: string
|
||||
relativePath: string
|
||||
absolutePath: string
|
||||
children: Array<TreeNode>
|
||||
}
|
||||
10
api/src/types/Upload.ts
Normal file
10
api/src/types/Upload.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface MulterFile {
|
||||
fieldname: string
|
||||
originalname: string
|
||||
encoding: string
|
||||
mimetype: string
|
||||
destination: string
|
||||
filename: string
|
||||
path: string
|
||||
size: number
|
||||
}
|
||||
9
api/src/types/index.ts
Normal file
9
api/src/types/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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'
|
||||
87
api/src/utils/appStreamConfig.ts
Normal file
87
api/src/utils/appStreamConfig.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { createFile, fileExists, readFile } from '@sasjs/utils'
|
||||
import { publishAppStream } from '../routes/appStream'
|
||||
import { AppStreamConfig } from '../types'
|
||||
|
||||
import { getTmpAppStreamConfigPath } from './file'
|
||||
|
||||
export const loadAppStreamConfig = async () => {
|
||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
||||
|
||||
const content = (await fileExists(appStreamConfigPath))
|
||||
? await readFile(appStreamConfigPath)
|
||||
: '{}'
|
||||
|
||||
let appStreamConfig: AppStreamConfig
|
||||
try {
|
||||
appStreamConfig = JSON.parse(content)
|
||||
|
||||
if (!isValidAppStreamConfig(appStreamConfig)) throw 'invalid type'
|
||||
} catch (_) {
|
||||
appStreamConfig = {}
|
||||
}
|
||||
process.appStreamConfig = {}
|
||||
|
||||
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
|
||||
const { appLoc, streamWebFolder, streamLogo } = entry
|
||||
|
||||
publishAppStream(
|
||||
appLoc,
|
||||
streamWebFolder,
|
||||
streamServiceName,
|
||||
streamLogo,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
console.log('App Stream Config loaded!')
|
||||
}
|
||||
|
||||
export const addEntryToAppStreamConfig = (
|
||||
streamServiceName: string,
|
||||
appLoc: string,
|
||||
streamWebFolder: string,
|
||||
streamLogo?: string,
|
||||
addEntryToFile: boolean = true
|
||||
) => {
|
||||
if (streamServiceName && appLoc && streamWebFolder) {
|
||||
process.appStreamConfig[streamServiceName] = {
|
||||
appLoc,
|
||||
streamWebFolder,
|
||||
streamLogo
|
||||
}
|
||||
if (addEntryToFile) saveAppStreamConfig()
|
||||
}
|
||||
}
|
||||
|
||||
export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
|
||||
if (streamServiceName) {
|
||||
delete process.appStreamConfig[streamServiceName]
|
||||
saveAppStreamConfig()
|
||||
}
|
||||
}
|
||||
|
||||
const saveAppStreamConfig = async () => {
|
||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
||||
|
||||
try {
|
||||
await createFile(
|
||||
appStreamConfigPath,
|
||||
JSON.stringify(process.appStreamConfig, null, 2)
|
||||
)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const isValidAppStreamConfig = (config: any) => {
|
||||
if (config) {
|
||||
return !Object.entries(config).some(([streamServiceName, entry]) => {
|
||||
const { appLoc, streamWebFolder, streamLogo } = entry as any
|
||||
|
||||
return (
|
||||
typeof streamServiceName !== 'string' ||
|
||||
typeof appLoc !== 'string' ||
|
||||
typeof streamWebFolder !== 'string'
|
||||
)
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
25
api/src/utils/connectDB.ts
Normal file
25
api/src/utils/connectDB.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { populateClients } from '../routes/api/auth'
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
8
api/src/utils/copySASjsCore.ts
Normal file
8
api/src/utils/copySASjsCore.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { copy } from '@sasjs/utils'
|
||||
|
||||
import { getTmpMacrosPath, sasJSCoreMacros } from '.'
|
||||
|
||||
export const copySASjsCore = async () => {
|
||||
const macrosDrivePath = getTmpMacrosPath()
|
||||
await copy(sasJSCoreMacros, macrosDrivePath)
|
||||
}
|
||||
25
api/src/utils/extractHeaders.ts
Normal file
25
api/src/utils/extractHeaders.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
const headerUtils = require('http-headers-validation')
|
||||
|
||||
export interface HTTPHeaders {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export const extractHeaders = (content?: string): HTTPHeaders => {
|
||||
const headersObj: HTTPHeaders = {}
|
||||
const headersArr = content
|
||||
?.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => !!line)
|
||||
|
||||
headersArr?.forEach((headerStr) => {
|
||||
const [key, value] = headerStr.split(':').map((data) => data.trim())
|
||||
|
||||
if (value && headerUtils.validateHeader(key, value)) {
|
||||
headersObj[key.toLowerCase()] = value
|
||||
} else {
|
||||
delete headersObj[key.toLowerCase()]
|
||||
}
|
||||
})
|
||||
|
||||
return headersObj
|
||||
}
|
||||
44
api/src/utils/file.ts
Normal file
44
api/src/utils/file.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import path from 'path'
|
||||
|
||||
export const apiRoot = path.join(__dirname, '..', '..')
|
||||
export const codebaseRoot = path.join(apiRoot, '..')
|
||||
export const sysInitCompiledPath = path.join(
|
||||
apiRoot,
|
||||
'sasjsbuild',
|
||||
'systemInitCompiled.sas'
|
||||
)
|
||||
|
||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||
|
||||
export const getWebBuildFolderPath = () =>
|
||||
path.join(codebaseRoot, 'web', 'build')
|
||||
|
||||
export const getTmpFolderPath = () => process.driveLoc
|
||||
|
||||
export const getTmpAppStreamConfigPath = () =>
|
||||
path.join(getTmpFolderPath(), 'appStreamConfig.json')
|
||||
|
||||
export const getTmpMacrosPath = () => path.join(getTmpFolderPath(), 'sasjscore')
|
||||
|
||||
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
|
||||
|
||||
export const getTmpFilesFolderPath = () =>
|
||||
path.join(getTmpFolderPath(), 'files')
|
||||
|
||||
export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs')
|
||||
|
||||
export const getTmpWeboutFolderPath = () =>
|
||||
path.join(getTmpFolderPath(), 'webouts')
|
||||
|
||||
export const getTmpSessionsFolderPath = () =>
|
||||
path.join(getTmpFolderPath(), 'sessions')
|
||||
|
||||
export const generateUniqueFileName = (fileName: string, extension = '') =>
|
||||
[
|
||||
fileName,
|
||||
'-',
|
||||
Math.round(Math.random() * 100000),
|
||||
'-',
|
||||
new Date().getTime(),
|
||||
extension
|
||||
].join('')
|
||||
7
api/src/utils/generateAccessToken.ts
Normal file
7
api/src/utils/generateAccessToken.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { InfoJWT } from '../types'
|
||||
|
||||
export const generateAccessToken = (data: InfoJWT) =>
|
||||
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
|
||||
expiresIn: '1day'
|
||||
})
|
||||
7
api/src/utils/generateAuthCode.ts
Normal file
7
api/src/utils/generateAuthCode.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { InfoJWT } from '../types'
|
||||
|
||||
export const generateAuthCode = (data: InfoJWT) =>
|
||||
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, {
|
||||
expiresIn: '30s'
|
||||
})
|
||||
7
api/src/utils/generateRefreshToken.ts
Normal file
7
api/src/utils/generateRefreshToken.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { InfoJWT } from '../types'
|
||||
|
||||
export const generateRefreshToken = (data: InfoJWT) =>
|
||||
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
|
||||
expiresIn: '30 days'
|
||||
})
|
||||
36
api/src/utils/getCertificates.ts
Normal file
36
api/src/utils/getCertificates.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import path from 'path'
|
||||
import { fileExists, getString, readFile } from '@sasjs/utils'
|
||||
|
||||
export const getCertificates = async () => {
|
||||
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
||||
|
||||
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
|
||||
const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)'))
|
||||
|
||||
console.log('keyPath: ', keyPath)
|
||||
console.log('certPath: ', certPath)
|
||||
|
||||
const key = await readFile(keyPath)
|
||||
const cert = await readFile(certPath)
|
||||
|
||||
return { key, cert }
|
||||
}
|
||||
|
||||
const getFileInput = async (filename: string): Promise<string> => {
|
||||
const validator = async (filePath: string) => {
|
||||
if (!filePath) return `Path to ${filename} is required.`
|
||||
|
||||
if (!(await fileExists(path.join(process.cwd(), filePath)))) {
|
||||
return 'No file found at provided path.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const targetName = await getString(
|
||||
`Please enter path to ${filename} (relative path): `,
|
||||
validator
|
||||
)
|
||||
|
||||
return targetName
|
||||
}
|
||||
63
api/src/utils/getDesktopFields.ts
Normal file
63
api/src/utils/getDesktopFields.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import path from 'path'
|
||||
import { getString } from '@sasjs/utils/input'
|
||||
import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
||||
|
||||
const isWindows = () => process.platform === 'win32'
|
||||
|
||||
export const getDesktopFields = async () => {
|
||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||
|
||||
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
||||
|
||||
return { sasLoc, driveLoc }
|
||||
}
|
||||
|
||||
const getDriveLocation = async (): Promise<string> => {
|
||||
const validator = async (filePath: string) => {
|
||||
if (!filePath) return 'Path to files/drive is required.'
|
||||
|
||||
const drivePath = path.join(process.cwd(), filePath)
|
||||
|
||||
if (!(await folderExists(drivePath))) {
|
||||
await createFolder(drivePath)
|
||||
await createFolder(path.join(drivePath, 'files'))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const defaultLocation = isWindows() ? '.\\tmp\\' : './tmp/'
|
||||
|
||||
const targetName = await getString(
|
||||
'Please enter path to file/drive (relative to executable): ',
|
||||
validator,
|
||||
defaultLocation
|
||||
)
|
||||
|
||||
return targetName
|
||||
}
|
||||
|
||||
const getSASLocation = async (): Promise<string> => {
|
||||
const validator = async (filePath: string) => {
|
||||
if (!filePath) return 'Path to SAS executable is required.'
|
||||
|
||||
if (!(await fileExists(filePath))) {
|
||||
return 'No file found at provided path.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const defaultLocation = isWindows()
|
||||
? 'C:\\Program Files\\SASHome\\SASFoundation\\9.4\\sas.exe'
|
||||
: '/opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas'
|
||||
|
||||
const targetName = await getString(
|
||||
'Please enter path to SAS executable (absolute path): ',
|
||||
validator,
|
||||
defaultLocation
|
||||
)
|
||||
|
||||
return targetName
|
||||
}
|
||||
19
api/src/utils/index.ts
Normal file
19
api/src/utils/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export * from './appStreamConfig'
|
||||
export * from './connectDB'
|
||||
export * from './copySASjsCore'
|
||||
export * from './extractHeaders'
|
||||
export * from './file'
|
||||
export * from './generateAccessToken'
|
||||
export * from './generateAuthCode'
|
||||
export * from './generateRefreshToken'
|
||||
export * from './getCertificates'
|
||||
export * from './getDesktopFields'
|
||||
export * from './isDebugOn'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
export * from './saveTokensInDB'
|
||||
export * from './setProcessVariables'
|
||||
export * from './sleep'
|
||||
export * from './upload'
|
||||
export * from './validation'
|
||||
export * from './verifyTokenInDB'
|
||||
8
api/src/utils/isDebugOn.ts
Normal file
8
api/src/utils/isDebugOn.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ExecutionVars } from '../controllers/internal'
|
||||
|
||||
export const isDebugOn = (vars: ExecutionVars) => {
|
||||
const debugValue =
|
||||
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
||||
|
||||
return !!(debugValue && debugValue >= 131)
|
||||
}
|
||||
9
api/src/utils/parseLogToArray.ts
Normal file
9
api/src/utils/parseLogToArray.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface LogLine {
|
||||
line: string
|
||||
}
|
||||
|
||||
export const parseLogToArray = (content?: string): LogLine[] => {
|
||||
if (!content) return []
|
||||
|
||||
return content.split('\n').map((line) => ({ line: line }))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user