mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
255 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e7f28a6f8 | ||
|
|
5689169ce4 | ||
|
|
6139e7bff6 | ||
|
|
2c77317bb9 | ||
|
|
57b63db9cb | ||
|
|
60a2a4fe32 | ||
|
|
09611cb416 | ||
|
|
2a9bb6e6b1 | ||
|
|
b4b60c69cf | ||
|
|
b060ad1b8e | ||
|
|
d47ed6d0e8 | ||
|
|
a6993ef5ae | ||
|
|
2571fc2ca8 | ||
|
|
992f39b63a | ||
|
|
1ea3f6d8b3 | ||
|
|
e462aebdc0 | ||
|
|
13403517a4 | ||
|
|
c3c2048e75 | ||
|
|
1d8acc36eb | ||
|
|
4c7ad56326 | ||
|
|
e57443f1ed | ||
|
|
5da93f318a | ||
|
|
a30fb1a241 | ||
|
|
4ae8f35e9a | ||
|
|
ebb46f51b6 | ||
|
|
fe24f51ca2 | ||
|
|
fd15f3fb41 | ||
|
|
7d31ee7696 | ||
|
|
667e26b080 | ||
|
|
d09876c05f | ||
|
|
fb8e18be75 | ||
|
|
7ac7a4e083 | ||
|
|
8e23786dd4 | ||
|
|
4bd01bcf29 | ||
|
|
4ad8c81e49 | ||
|
|
51f6aa34a1 | ||
|
|
486207128d | ||
|
|
1e4b0b9171 | ||
|
|
1ff820605a | ||
|
|
9c1a781b3a | ||
| 36628551ae | |||
| 23cf8fa06f | |||
| 84ee743eae | |||
|
|
19e5bd7d2d | ||
|
|
e251747302 | ||
|
|
7e7558d4cf | ||
|
|
f02996facf | ||
|
|
803c51f400 | ||
|
|
c35b2b3f59 | ||
|
|
fe0866ace7 | ||
|
|
1513c3623d | ||
|
|
7fe43ae0b7 | ||
|
|
c4cea4a12b | ||
|
|
9fc7a132ba | ||
|
|
d55a619d64 | ||
|
|
737d2a24c2 | ||
|
|
2e63831b90 | ||
|
|
c7ffde1a3b | ||
|
|
db70b1ce55 | ||
|
|
8a3fe8b217 | ||
| 9dca552e82 | |||
|
|
505f2089c7 | ||
|
|
3344c400a8 | ||
| fa6248e3ef | |||
| 9fb5f1f8e7 | |||
|
|
92e0b8a088 | ||
|
|
b484306ed8 | ||
| 5e08aacc51 | |||
| a9e4eb685d | |||
| 31b09f27cc | |||
| 9f3ec92f8e | |||
| 6c9e449614 | |||
| 68e84b0994 | |||
| f0bb51a0d5 | |||
| b93a0da3a3 | |||
|
|
e5facbf54c | ||
|
|
cb2bebbe76 | ||
|
|
9e1e0ce8cc | ||
|
|
29928753b7 | ||
|
|
edd69ecaae | ||
|
|
74ba65f9f3 | ||
|
|
f257602834 | ||
|
|
61080d4694 | ||
|
|
82633adbc4 | ||
|
|
23db7e7b7d | ||
|
|
cbaa687c9b | ||
|
|
527f70e90d | ||
|
|
122faad55f | ||
|
|
3ff6f5e865 | ||
|
|
7d5128c0d6 | ||
|
|
e1ebbfd087 | ||
|
|
e430bdb0d4 | ||
|
|
9d9769eef3 | ||
|
|
9d167abe2a | ||
|
|
18d0604bdd | ||
|
|
7b7bc6b778 | ||
|
|
fb4f3442d5 | ||
|
|
09d1b7d5d4 | ||
|
|
99839ae62f | ||
|
|
f700561e1a | ||
|
|
8b4b4b91ab | ||
|
|
acb3ae0493 | ||
|
|
f48aeb1b0b | ||
|
|
5c0e8e5344 | ||
|
|
0ac9e4af7d | ||
|
|
ee80f3f968 | ||
|
|
7f4201ba85 | ||
|
|
f830bbc058 | ||
|
|
f8e1522a5a | ||
|
|
0a5aeceab5 | ||
|
|
6dc39c0d91 | ||
|
|
117a53ceea | ||
|
|
dd56a95314 | ||
|
|
c5117abe71 | ||
|
|
84c632a861 | ||
|
|
3ddd09eba0 | ||
|
|
0c0301433c | ||
|
|
954b2e3e2e | ||
|
|
5655311b96 | ||
|
|
9ace33d783 | ||
|
|
adc5aca0f0 | ||
|
|
71c6be6b84 | ||
|
|
9c751877d1 | ||
|
|
2204d54cd6 | ||
|
|
f4eb75ff34 | ||
|
|
a3cde343b7 | ||
|
|
7a70d40dbf | ||
|
|
d27e070fc8 | ||
|
|
27e260e6a4 | ||
|
|
2796db8ead | ||
|
|
84f7c2ab89 | ||
|
|
e68090181a | ||
|
|
d2956fc641 | ||
|
|
a701bb25e7 | ||
|
|
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 |
@@ -1,4 +1,5 @@
|
|||||||
SAS_EXEC=<path to folder containing SAS executable 'sas'>
|
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_API=<port for sasjs server (api)>
|
||||||
PORT_WEB=<port for sasjs web component(react)>
|
PORT_WEB=<port for sasjs web component(react)>
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
|
|||||||
114
.github/CONTRIBUTING.md
vendored
Normal file
114
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# CONTRIBUTING
|
||||||
|
|
||||||
|
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
|
||||||
|
|
||||||
|
The app can be deployed using Docker or NodeJS.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration is made using `.env` files (per [README.md](https://github.com/sasjs/server#env-var-configuration) settings), _except_ for one case, when running in NodeJS in production - in which case the path to the SAS executable is made in the `configuration` section of `package.json`.
|
||||||
|
|
||||||
|
The `.env` file should be created in the location(s) below. Each folder contains a `.env.example` file that may be adjusted and renamed.
|
||||||
|
|
||||||
|
* `.env` - the root .env file is used only for Docker deploys.
|
||||||
|
* `api/.env` - this is the primary file used in NodeJS deploys
|
||||||
|
* `web/.env` - this file is only necessary in NodeJS when running `web` and `api` seperately (on different ports).
|
||||||
|
|
||||||
|
|
||||||
|
## Using Docker
|
||||||
|
|
||||||
|
### Docker Development Mode
|
||||||
|
|
||||||
|
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_
|
||||||
|
|
||||||
|
|
||||||
|
### Docker Production Mode
|
||||||
|
|
||||||
|
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 NodeJS:
|
||||||
|
|
||||||
|
Be sure to use v16 or above, and to set your environment variables in the relevant `.env` file(s) - else defaults will be used.
|
||||||
|
|
||||||
|
### NodeJS Development Mode
|
||||||
|
|
||||||
|
SASjs Server is split between an API server (serving REST requests) and a WEB Server (everything else). These can be run together, or on seperate ports.
|
||||||
|
|
||||||
|
### NodeJS Dev - Single Port
|
||||||
|
|
||||||
|
Here the environment variables should be configured under `api.env`. Then:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd ./web && npm i && npm build
|
||||||
|
cd ../api && npm i && npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### NodeJS Dev - Seperate Ports
|
||||||
|
|
||||||
|
Set the backend variables in `api/.env` and the frontend variables in `web/.env`. Then:
|
||||||
|
|
||||||
|
#### API server
|
||||||
|
```
|
||||||
|
cd api
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Web Server
|
||||||
|
|
||||||
|
```
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### NodeJS Production Mode
|
||||||
|
|
||||||
|
Update the `.env` file in the *api* folder. Then:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run server
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install/build `web` and install `api`, then start prod server.
|
||||||
|
|
||||||
|
|
||||||
|
## Executables
|
||||||
|
|
||||||
|
In order to generate the final 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`
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
To cut a release, run `npm run release` on the main branch, then push the tags (per the console log link)
|
||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -8,10 +8,20 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [lts/*]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
- name: Install Dependencies WEB
|
- name: Install Dependencies WEB
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ sas/
|
|||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
|
sasjscore/
|
||||||
certificates/
|
certificates/
|
||||||
executables/
|
executables/
|
||||||
.env
|
.env
|
||||||
|
|||||||
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
|
||||||
385
CHANGELOG.md
385
CHANGELOG.md
@@ -2,6 +2,391 @@
|
|||||||
|
|
||||||
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.
|
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.63](https://github.com/sasjs/server/compare/v0.0.62...v0.0.63) (2022-04-30)
|
||||||
|
|
||||||
|
### [0.0.62](https://github.com/sasjs/server/compare/v0.0.61...v0.0.62) (2022-04-30)
|
||||||
|
|
||||||
|
### [0.0.61](https://github.com/sasjs/server/compare/v0.0.59...v0.0.61) (2022-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||||
|
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||||
|
|
||||||
|
### [0.0.60](https://github.com/sasjs/server/compare/v0.0.59...v0.0.60) (2022-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||||
|
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||||
|
|
||||||
|
### [0.0.59](https://github.com/sasjs/server/compare/v0.0.58...v0.0.59) (2022-04-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* enabled csrf tokens for web component ([e462aeb](https://github.com/sasjs/server/commit/e462aebdc01f3c0068ed0074473a2063412dcf45))
|
||||||
|
* enabled session based authentication for web ([5da93f3](https://github.com/sasjs/server/commit/5da93f318aad10b1c67032a467191e4dbb99f411))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fetch client from DB for each request ([4ad8c81](https://github.com/sasjs/server/commit/4ad8c81e4927c1a82220ec015a781b095c8e859e))
|
||||||
|
* **web:** show display name instead of username ([e57443f](https://github.com/sasjs/server/commit/e57443f1ed662a022494bb93d79c3d2f10a2d082))
|
||||||
|
|
||||||
|
### [0.0.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bumping core library to get latest user management macros ([4862071](https://github.com/sasjs/server/commit/486207128da58fc4866bd0919c1bed2bd98097ea))
|
||||||
|
* missing dependency ([d09876c](https://github.com/sasjs/server/commit/d09876c05f89166eec20064f7aa7ed5b867be081))
|
||||||
|
|
||||||
|
### [0.0.57](https://github.com/sasjs/server/compare/v0.0.56...v0.0.57) (2022-04-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* create AppContext ([84ee743](https://github.com/sasjs/server/commit/84ee743eae16e87eaa91969393bebf01e2d15a44))
|
||||||
|
|
||||||
|
### [0.0.56](https://github.com/sasjs/server/compare/v0.0.55...v0.0.56) (2022-04-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* shortening min length of username. Closes [#61](https://github.com/sasjs/server/issues/61) ([f02996f](https://github.com/sasjs/server/commit/f02996facf1019ec4022ccfbc99c1d0137074e1b))
|
||||||
|
|
||||||
|
### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||||
|
* drive path in server mode ([c4cea4a](https://github.com/sasjs/server/commit/c4cea4a12b7eda4daeed995f41c0b10bcea79871))
|
||||||
|
|
||||||
|
### [0.0.54](https://github.com/sasjs/server/compare/v0.0.53...v0.0.54) (2022-04-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||||
|
|
||||||
|
### [0.0.53](https://github.com/sasjs/server/compare/v0.0.49...v0.0.53) (2022-04-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||||
|
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||||
|
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||||
|
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* provide clientId to web component ([db70b1c](https://github.com/sasjs/server/commit/db70b1ce555df6b29fb09c0c960d38b911c97b1b))
|
||||||
|
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||||
|
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||||
|
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||||
|
|
||||||
|
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||||
|
|
||||||
|
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||||
|
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||||
|
|
||||||
|
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||||
|
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||||
|
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||||
|
|
||||||
|
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
|
||||||
|
|
||||||
|
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **deploy:** new route added for deploy with build.json ([18d0604](https://github.com/sasjs/server/commit/18d0604bdd0b20ad468f9345474b4de034ee3a67))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* remove uploaded build.json from temp folder in all cases ([9d167ab](https://github.com/sasjs/server/commit/9d167abe2adb743bca161862b4561bf573182c00))
|
||||||
|
* **stp:** return log+webout for debug on ([3ff6f5e](https://github.com/sasjs/server/commit/3ff6f5e86581cd2ac23bbe0b8e2c367fbea890ed))
|
||||||
|
|
||||||
|
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** updated STUDIO log and webout ([f700561](https://github.com/sasjs/server/commit/f700561e1a8d06c18ca2bdbe4605d7ab34f7a761))
|
||||||
|
|
||||||
|
### [0.0.46](https://github.com/sasjs/server/compare/v0.0.45...v0.0.46) (2022-03-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **drive:** GET folder contents API added ([0ac9e4a](https://github.com/sasjs/server/commit/0ac9e4af7d67c4431053e80eb2384bf5bdc3f8b3))
|
||||||
|
|
||||||
|
### [0.0.45](https://github.com/sasjs/server/compare/v0.0.43...v0.0.45) (2022-03-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||||
|
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||||
|
* proving a PRINT destination during SAS invocation. ([7f4201b](https://github.com/sasjs/server/commit/7f4201ba855743144fa6d3efac2b11e816d4696e)), closes [#111](https://github.com/sasjs/server/issues/111)
|
||||||
|
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||||
|
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||||
|
|
||||||
|
### [0.0.44](https://github.com/sasjs/server/compare/v0.0.43...v0.0.44) (2022-03-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||||
|
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||||
|
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||||
|
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||||
|
|
||||||
|
### [0.0.43](https://github.com/sasjs/server/compare/v0.0.42...v0.0.43) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deploy:** user can deploy to same appName with different/same appLoc ([9ace33d](https://github.com/sasjs/server/commit/9ace33d7830a9def42d741c23b46090afe0c5510))
|
||||||
|
* fallback logo on AppStream ([5655311](https://github.com/sasjs/server/commit/5655311b9663225823c192b39a03f39d17dda730))
|
||||||
|
|
||||||
|
### [0.0.42](https://github.com/sasjs/server/compare/v0.0.41...v0.0.42) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* execute api, webout as raw ([9c75187](https://github.com/sasjs/server/commit/9c751877d1ed0d0677aff816169a1df7c34c6bf5))
|
||||||
|
|
||||||
|
### [0.0.41](https://github.com/sasjs/server/compare/v0.0.40...v0.0.41) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **scroll:** closes [#100](https://github.com/sasjs/server/issues/100) ([f4eb75f](https://github.com/sasjs/server/commit/f4eb75ff347e78ac334e55ee26fbdd247bb8eaa2))
|
||||||
|
|
||||||
|
### [0.0.40](https://github.com/sasjs/server/compare/v0.0.39...v0.0.40) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deploy:** validating empty file or service in filetree ([27e260e](https://github.com/sasjs/server/commit/27e260e6a453e9978830db63ab669bd48c029897))
|
||||||
|
* macros available for SAS ([7a70d40](https://github.com/sasjs/server/commit/7a70d40dbf0cd91cb3af156755f10006b860f917))
|
||||||
|
* moved macros from codebase to drive ([d27e070](https://github.com/sasjs/server/commit/d27e070fc83894854278df22a8223b8016a1f5f7))
|
||||||
|
|
||||||
|
### [0.0.39](https://github.com/sasjs/server/compare/v0.0.38...v0.0.39) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* included sasjs core macros at compile time ([e680901](https://github.com/sasjs/server/commit/e68090181acd844f86f3e81153cb5a4e3f4a307f))
|
||||||
|
|
||||||
|
### [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)
|
### [0.0.22](https://github.com/sasjs/server/compare/v0.0.17...v0.0.22) (2022-02-08)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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.
|
||||||
197
README.md
197
README.md
@@ -1,134 +1,153 @@
|
|||||||
# SASjs Server
|
# 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
|
- Virtual filesystem for storing SAS programs and other content
|
||||||
- Ability to execute Stored Programs from a URL
|
- Ability to execute Stored Programs from a URL
|
||||||
- Ability to create web apps using simple Desktop SAS
|
- 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).
|
||||||
|
|
||||||
|
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentication, and a database)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
First, download the relevant package from the [releases](https://github.com/sasjs/server/releases) page - either manually, or with commandline, eg as follow:
|
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
|
```bash
|
||||||
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||||
unzip linux.zip
|
unzip linux.zip
|
||||||
./api-linux
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Second, trigger by double clicking (windows) or executing from commandline.
|
The app can then be launched with `./api-linux` and prompts followed (if ENV vars not set).
|
||||||
|
|
||||||
You are presented with two prompts:
|
### Manual
|
||||||
|
|
||||||
* Location of your `sas.exe` / `sas.sh` executable
|
1. Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
|
||||||
* Path to a filesystem location for Stored Programs and temporary files
|
2. Trigger by double clicking (windows) or executing from commandline.
|
||||||
|
|
||||||
## Configuration
|
You are presented with two prompts (if not set as ENV vars):
|
||||||
|
|
||||||
Configuration is made in the `configuration` section of `package.json`:
|
- Location of your `sas.exe` / `sas.sh` executable
|
||||||
|
- Path to a filesystem location for Stored Programs and temporary files
|
||||||
|
|
||||||
- Provide path to SAS9 executable.
|
## ENV Var configuration
|
||||||
|
|
||||||
### Using dockers:
|
When launching the app, it will make use of specific environment variables. These can be set in the following places:
|
||||||
|
|
||||||
There is `.env.example` file present at root of the project. [for Production]
|
- 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
|
||||||
|
|
||||||
There is `.env.example` file present at `./api` of the project. [for Development]
|
Example contents of a `.env` file:
|
||||||
|
|
||||||
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
|
# options: [desktop|server] default: `desktop`
|
||||||
```
|
MODE=
|
||||||
|
|
||||||
It uses default docker compose file i.e. `docker-compose.yml` present at root.
|
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||||
It will build following images if running first time:
|
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
||||||
|
CORS=
|
||||||
|
|
||||||
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
|
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||||
- `sasjs_server_web` - image for sasjs web component app based on _ReactJS_
|
WHITELIST=
|
||||||
- `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
|
# options: [http|https] default: http
|
||||||
|
PROTOCOL=
|
||||||
|
|
||||||
Command to run docker for production:
|
# default: 5000
|
||||||
|
PORT=
|
||||||
|
|
||||||
|
|
||||||
|
# optional
|
||||||
|
# for MODE: `desktop`, prompts user
|
||||||
|
# for MODE: `server` gets value from api/package.json `configuration.sasPath`
|
||||||
|
SAS_PATH=/path/to/sas/executable.exe
|
||||||
|
|
||||||
|
|
||||||
|
# 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>
|
||||||
|
SESSION_SECRET=<secret>
|
||||||
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
|
# SAS Options
|
||||||
|
# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
|
||||||
|
# Any options set here are automatically applied in the SAS session
|
||||||
|
# See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm
|
||||||
|
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
|
||||||
|
SAS_OPTIONS= -NOXCMD
|
||||||
|
SASV9_OPTIONS= -NOXCMD
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
|
## Persisting the Session
|
||||||
|
|
||||||
|
Normally the server process will stop when your terminal dies. To keep it going you can use the following suggested approaches:
|
||||||
|
|
||||||
|
1. Linux Background Job
|
||||||
|
2. NPM package `pm2`
|
||||||
|
|
||||||
|
### Background Job
|
||||||
|
|
||||||
|
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
|
||||||
|
|
||||||
|
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
|
||||||
|
|
||||||
|
### PM2
|
||||||
|
|
||||||
|
Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) and execute, eg as follows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||||
|
export PORT=5001
|
||||||
|
export DRIVE_PATH=./tmp
|
||||||
|
|
||||||
|
pm2 start api-linux
|
||||||
```
|
```
|
||||||
|
|
||||||
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
|
To get the logs (and some useful commands):
|
||||||
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 `/`
|
```bash
|
||||||
- `mongodb` - image for mongo database
|
pm2 [list|ls|status]
|
||||||
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
|
pm2 logs
|
||||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
pm2 logs --lines 200
|
||||||
|
|
||||||
### 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
|
Managing processes:
|
||||||
|
|
||||||
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
|
pm2 restart app_name
|
||||||
npm start
|
pm2 reload app_name
|
||||||
|
pm2 stop app_name
|
||||||
|
pm2 delete app_name
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Development (running only api server and have web build served):
|
Instead of `app_name` you can pass:
|
||||||
|
|
||||||
##### API server also serving Web build files
|
- `all` to act on all processes
|
||||||
|
- `id` to act on a specific process id
|
||||||
|
|
||||||
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
|
## Server Version
|
||||||
Command to install and run api server.
|
|
||||||
|
|
||||||
```
|
The following credentials can be used for the initial connection to SASjs/server. It is highly recommended to change these on first use.
|
||||||
cd ./web && npm i && npm build && cd ../
|
|
||||||
cd ./api && npm i && npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Production
|
- CLIENTID: `clientID1`
|
||||||
|
- USERNAME: `secretuser`
|
||||||
##### API & WEB
|
- PASSWORD: `secretpassword`
|
||||||
|
|
||||||
```
|
|
||||||
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`
|
|
||||||
|
|||||||
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>
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
MODE=[desktop|server] default considered as desktop
|
MODE=[desktop|server] default considered as desktop
|
||||||
CORS=[disable|enable] default considered as disable
|
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
|
PORT=[5000] default value is 5000
|
||||||
PORT_WEB=[port for sasjs web component(react)] default value is 3000
|
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
REFRESH_TOKEN_SECRET=<secret>
|
||||||
AUTH_CODE_SECRET=<secret>
|
AUTH_CODE_SECRET=<secret>
|
||||||
|
SESSION_SECRET=<secret>
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9885
api/package-lock.json
generated
9885
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,32 +4,33 @@
|
|||||||
"description": "Api of SASjs server",
|
"description": "Api of SASjs server",
|
||||||
"main": "./src/server.ts",
|
"main": "./src/server.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"initial": "npm run swagger && npm run compileSysInit",
|
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
|
||||||
"prestart": "npm run initial",
|
"prestart": "npm run initial",
|
||||||
"prestart:prod": "npm run initial",
|
|
||||||
"prebuild": "npm run initial",
|
"prebuild": "npm run initial",
|
||||||
"start": "nodemon ./src/server.ts",
|
"start": "nodemon ./src/server.ts",
|
||||||
"start:prod": "nodemon ./src/prod-server.ts",
|
"start:prod": "node ./build/src/server.js",
|
||||||
"build": "rimraf build && tsc",
|
"build": "rimraf build && tsc",
|
||||||
|
"postbuild": "npm run copy:files",
|
||||||
"swagger": "tsoa spec",
|
"swagger": "tsoa spec",
|
||||||
"semantic-release": "semantic-release -d",
|
|
||||||
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
|
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
|
||||||
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --silent --coverage",
|
"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: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}\"",
|
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
|
"exe": "npm run build && pkg .",
|
||||||
"exe": "npm run build && npm run exe:copy && pkg .",
|
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
||||||
"exe:copy": "npm run public:copy && npm run sasjsbuild:copy && npm run web:copy",
|
|
||||||
"public:copy": "cp -r ./public/ ./build/public/",
|
"public:copy": "cp -r ./public/ ./build/public/",
|
||||||
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
"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/",
|
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
|
||||||
"compileSysInit": "ts-node ./scripts/compileSysInit.ts"
|
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
|
||||||
|
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
|
||||||
},
|
},
|
||||||
"bin": "./build/src/server.js",
|
"bin": "./build/src/server.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"assets": [
|
"assets": [
|
||||||
"./build/public/**/*",
|
"./build/public/**/*",
|
||||||
"./build/sasjsbuild/**/*",
|
"./build/sasjsbuild/**/*",
|
||||||
|
"./build/sasjscore/**/*",
|
||||||
"./web/build/**/*"
|
"./web/build/**/*"
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
@@ -46,24 +47,31 @@
|
|||||||
},
|
},
|
||||||
"author": "4GL Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "3.11.1",
|
"@sasjs/core": "^4.19.0",
|
||||||
"@sasjs/utils": "2.34.1",
|
"@sasjs/utils": "2.42.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"connect-mongo": "^4.6.0",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"csurf": "^1.11.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-session": "^1.17.2",
|
||||||
|
"helmet": "^5.0.2",
|
||||||
"joi": "^17.4.2",
|
"joi": "^17.4.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mongoose": "^6.0.12",
|
"mongoose": "^6.0.12",
|
||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
"swagger-ui-express": "^4.1.6",
|
"swagger-ui-express": "^4.1.6"
|
||||||
"tsoa": "^3.14.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/csurf": "^1.11.2",
|
||||||
"@types/express": "^4.17.12",
|
"@types/express": "^4.17.12",
|
||||||
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/jsonwebtoken": "^8.5.5",
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
"@types/mongoose-sequence": "^3.0.6",
|
"@types/mongoose-sequence": "^3.0.6",
|
||||||
@@ -73,19 +81,25 @@
|
|||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
|
"http-headers-validation": "^0.0.1",
|
||||||
"jest": "^27.0.6",
|
"jest": "^27.0.6",
|
||||||
"mongodb-memory-server": "^8.0.0",
|
"mongodb-memory-server": "^8.0.0",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"pkg": "^5.4.1",
|
"pkg": "5.6.0",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"semantic-release": "^17.4.3",
|
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^27.0.3",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
|
"tsoa": "3.14.1",
|
||||||
"typescript": "^4.3.2"
|
"typescript": "^4.3.2"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
||||||
|
},
|
||||||
|
"nodemonConfig": {
|
||||||
|
"ignore": [
|
||||||
|
"tmp/**/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
api/public/plus.png
Normal file
BIN
api/public/plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 899 B |
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 |
@@ -5,6 +5,21 @@ components:
|
|||||||
requestBodies: {}
|
requestBodies: {}
|
||||||
responses: {}
|
responses: {}
|
||||||
schemas:
|
schemas:
|
||||||
|
LoginPayload:
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: 'Username for user'
|
||||||
|
example: secretuser
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: 'Password for user'
|
||||||
|
example: secretpassword
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
AuthorizeResponse:
|
AuthorizeResponse:
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
@@ -92,6 +107,48 @@ components:
|
|||||||
- clientSecret
|
- clientSecret
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
IRecordOfAny:
|
||||||
|
properties: {}
|
||||||
|
type: object
|
||||||
|
additionalProperties: {}
|
||||||
|
LogLine:
|
||||||
|
properties:
|
||||||
|
line:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- line
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
HTTPHeaders:
|
||||||
|
properties: {}
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
ExecuteReturnJsonResponse:
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
_webout:
|
||||||
|
anyOf:
|
||||||
|
-
|
||||||
|
type: string
|
||||||
|
-
|
||||||
|
$ref: '#/components/schemas/IRecordOfAny'
|
||||||
|
log:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/LogLine'
|
||||||
|
type: array
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
httpHeaders:
|
||||||
|
$ref: '#/components/schemas/HTTPHeaders'
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- _webout
|
||||||
|
- log
|
||||||
|
- httpHeaders
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
ExecuteSASCodePayload:
|
ExecuteSASCodePayload:
|
||||||
properties:
|
properties:
|
||||||
code:
|
code:
|
||||||
@@ -119,6 +176,8 @@ components:
|
|||||||
$ref: '#/components/schemas/FolderMember'
|
$ref: '#/components/schemas/FolderMember'
|
||||||
-
|
-
|
||||||
$ref: '#/components/schemas/ServiceMember'
|
$ref: '#/components/schemas/ServiceMember'
|
||||||
|
-
|
||||||
|
$ref: '#/components/schemas/FileMember'
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
@@ -144,6 +203,24 @@ components:
|
|||||||
- code
|
- code
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
MemberType.file:
|
||||||
|
enum:
|
||||||
|
- file
|
||||||
|
type: string
|
||||||
|
FileMember:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/MemberType.file'
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- type
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
FileTree:
|
FileTree:
|
||||||
properties:
|
properties:
|
||||||
members:
|
members:
|
||||||
@@ -153,6 +230,8 @@ components:
|
|||||||
$ref: '#/components/schemas/FolderMember'
|
$ref: '#/components/schemas/FolderMember'
|
||||||
-
|
-
|
||||||
$ref: '#/components/schemas/ServiceMember'
|
$ref: '#/components/schemas/ServiceMember'
|
||||||
|
-
|
||||||
|
$ref: '#/components/schemas/FileMember'
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- members
|
- members
|
||||||
@@ -164,6 +243,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
|
streamServiceName:
|
||||||
|
type: string
|
||||||
example:
|
example:
|
||||||
$ref: '#/components/schemas/FileTree'
|
$ref: '#/components/schemas/FileTree'
|
||||||
required:
|
required:
|
||||||
@@ -175,24 +256,15 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
appLoc:
|
appLoc:
|
||||||
type: string
|
type: string
|
||||||
|
streamWebFolder:
|
||||||
|
type: string
|
||||||
fileTree:
|
fileTree:
|
||||||
$ref: '#/components/schemas/FileTree'
|
$ref: '#/components/schemas/FileTree'
|
||||||
required:
|
required:
|
||||||
|
- appLoc
|
||||||
- fileTree
|
- fileTree
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
GetFileResponse:
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
fileContent:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
UpdateFileResponse:
|
UpdateFileResponse:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
@@ -203,21 +275,6 @@ components:
|
|||||||
- status
|
- status
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
FilePayload:
|
|
||||||
properties:
|
|
||||||
filePath:
|
|
||||||
type: string
|
|
||||||
description: 'Path of the file'
|
|
||||||
example: /Public/somefolder/some.file
|
|
||||||
fileContent:
|
|
||||||
type: string
|
|
||||||
description: 'Contents of the file'
|
|
||||||
example: 'Contents of the File'
|
|
||||||
required:
|
|
||||||
- filePath
|
|
||||||
- fileContent
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
TreeNode:
|
TreeNode:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
@@ -368,21 +425,6 @@ components:
|
|||||||
- description
|
- description
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
ExecuteReturnJsonResponse:
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
_webout:
|
|
||||||
type: string
|
|
||||||
log:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
- _webout
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
ExecuteReturnJsonPayload:
|
ExecuteReturnJsonPayload:
|
||||||
properties:
|
properties:
|
||||||
_program:
|
_program:
|
||||||
@@ -391,6 +433,25 @@ components:
|
|||||||
example: /Public/somefolder/some.file
|
example: /Public/somefolder/some.file
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
InfoResponse:
|
||||||
|
properties:
|
||||||
|
mode:
|
||||||
|
type: string
|
||||||
|
cors:
|
||||||
|
type: string
|
||||||
|
whiteList:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
protocol:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- mode
|
||||||
|
- cors
|
||||||
|
- whiteList
|
||||||
|
- protocol
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@@ -398,12 +459,53 @@ components:
|
|||||||
bearerFormat: JWT
|
bearerFormat: JWT
|
||||||
info:
|
info:
|
||||||
title: api
|
title: api
|
||||||
version: 0.0.1
|
version: 0.0.2
|
||||||
description: 'Api of SASjs server'
|
description: 'Api of SASjs server'
|
||||||
contact:
|
contact:
|
||||||
name: '4GL Ltd'
|
name: '4GL Ltd'
|
||||||
openapi: 3.0.0
|
openapi: 3.0.0
|
||||||
paths:
|
paths:
|
||||||
|
/login:
|
||||||
|
post:
|
||||||
|
operationId: Login
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
user: {properties: {displayName: {type: string}, username: {type: string}}, required: [displayName, username], type: object}
|
||||||
|
loggedIn: {type: boolean}
|
||||||
|
required:
|
||||||
|
- user
|
||||||
|
- loggedIn
|
||||||
|
type: object
|
||||||
|
summary: 'Accept a valid username/password'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/LoginPayload'
|
||||||
|
/logout:
|
||||||
|
get:
|
||||||
|
operationId: Logout
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
summary: 'Accept a valid username/password'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
/SASjsApi/auth/authorize:
|
/SASjsApi/auth/authorize:
|
||||||
post:
|
post:
|
||||||
operationId: Authorize
|
operationId: Authorize
|
||||||
@@ -520,7 +622,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
||||||
description: 'Execute SAS code.'
|
description: 'Execute SAS code.'
|
||||||
summary: 'Run SAS Code and returns log'
|
summary: 'Run SAS Code and returns log'
|
||||||
tags:
|
tags:
|
||||||
@@ -579,28 +681,62 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/DeployPayload'
|
$ref: '#/components/schemas/DeployPayload'
|
||||||
/SASjsApi/drive/file:
|
/SASjsApi/drive/deploy/upload:
|
||||||
get:
|
post:
|
||||||
operationId: GetFile
|
operationId: DeployUpload
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Ok
|
description: Ok
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GetFileResponse'
|
$ref: '#/components/schemas/DeployResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: success, fileContent: 'Contents of the File'}
|
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'}
|
||||||
'400':
|
'400':
|
||||||
description: 'Unable to get File'
|
description: 'Invalid Format'
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/GetFileResponse'
|
$ref: '#/components/schemas/DeployResponse'
|
||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: failure, message: 'File request failed.'}
|
value: {status: failure, message: 'Provided not supported data format.'}
|
||||||
|
'500':
|
||||||
|
description: 'Execution Error'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DeployResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {status: failure, message: 'Deployment failed!'}
|
||||||
|
summary: 'Creates/updates files within SASjs Drive using uploaded JSON file.'
|
||||||
|
tags:
|
||||||
|
- Drive
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
required:
|
||||||
|
- file
|
||||||
|
/SASjsApi/drive/file:
|
||||||
|
get:
|
||||||
|
operationId: GetFile
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: 'No content'
|
||||||
summary: 'Get file from SASjs Drive'
|
summary: 'Get file from SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
@@ -610,7 +746,34 @@ paths:
|
|||||||
parameters:
|
parameters:
|
||||||
-
|
-
|
||||||
in: query
|
in: query
|
||||||
name: filePath
|
name: _filePath
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/some.file
|
||||||
|
delete:
|
||||||
|
operationId: DeleteFile
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
status: {type: string}
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
type: object
|
||||||
|
summary: 'Delete file from SASjs Drive'
|
||||||
|
tags:
|
||||||
|
- Drive
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
in: query
|
||||||
|
name: _filePath
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
@@ -627,7 +790,7 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: success}
|
value: {status: success}
|
||||||
'400':
|
'403':
|
||||||
description: 'File already exists'
|
description: 'File already exists'
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@@ -636,19 +799,36 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: failure, message: 'File request failed.'}
|
value: {status: failure, message: 'File request failed.'}
|
||||||
|
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
|
||||||
summary: 'Create a file in SASjs Drive'
|
summary: 'Create a file in SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'Location of SAS program'
|
||||||
|
in: query
|
||||||
|
name: _filePath
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/some.file.sas
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/FilePayload'
|
type: object
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
filePath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- file
|
||||||
patch:
|
patch:
|
||||||
operationId: UpdateFile
|
operationId: UpdateFile
|
||||||
responses:
|
responses:
|
||||||
@@ -661,8 +841,8 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: success}
|
value: {status: success}
|
||||||
'400':
|
'403':
|
||||||
description: 'Unable to get File'
|
description: ""
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@@ -670,19 +850,66 @@ paths:
|
|||||||
examples:
|
examples:
|
||||||
'Example 1':
|
'Example 1':
|
||||||
value: {status: failure, message: 'File request failed.'}
|
value: {status: failure, message: 'File request failed.'}
|
||||||
|
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
|
||||||
summary: 'Modify a file in SASjs Drive'
|
summary: 'Modify a file in SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters:
|
||||||
|
-
|
||||||
|
description: 'Location of SAS program'
|
||||||
|
in: query
|
||||||
|
name: _filePath
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder/some.file.sas
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/FilePayload'
|
type: object
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
filePath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- file
|
||||||
|
/SASjsApi/drive/folder:
|
||||||
|
get:
|
||||||
|
operationId: GetFolder
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
folders: {items: {type: string}, type: array}
|
||||||
|
files: {items: {type: string}, type: array}
|
||||||
|
required:
|
||||||
|
- folders
|
||||||
|
- files
|
||||||
|
type: object
|
||||||
|
summary: 'Get folder contents from SASjs Drive'
|
||||||
|
tags:
|
||||||
|
- Drive
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
in: query
|
||||||
|
name: _folderPath
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder
|
||||||
/SASjsApi/drive/filetree:
|
/SASjsApi/drive/filetree:
|
||||||
get:
|
get:
|
||||||
operationId: GetFileTree
|
operationId: GetFileTree
|
||||||
@@ -1035,9 +1262,11 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
anyOf:
|
||||||
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
- {type: string}
|
||||||
summary: 'Execute Stored Program, return raw content'
|
- {type: string, format: byte}
|
||||||
|
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. Setting _debug=131 will\ncause the log to be streamed in the output.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response headers can be adjusted using the mfs_httpheader() macro. Any\nfile type can be returned, including binary files such as zip or xls.\n\nIf _debug is >= 131, response headers will contain Content-Type: 'text/plain'\n\nThis behaviour differs for POST requests, in which case the response is\nalways JSON."
|
||||||
|
summary: 'Execute Stored Program, return raw _webout content.'
|
||||||
tags:
|
tags:
|
||||||
- STP
|
- STP
|
||||||
security:
|
security:
|
||||||
@@ -1045,6 +1274,7 @@ paths:
|
|||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
-
|
-
|
||||||
|
description: 'Location of SAS program'
|
||||||
in: query
|
in: query
|
||||||
name: _program
|
name: _program
|
||||||
required: true
|
required: true
|
||||||
@@ -1060,7 +1290,10 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
|
||||||
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}}
|
||||||
|
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. In any case, the log is\nalways returned in the log object.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response will be a JSON object with the following root attributes: log,\nwebout, headers.\n\nThe webout will be a nested JSON object ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content.\n\nResponse headers from the mfs_httpheader macro are simply listed in the\nheaders object, for POST requests they have no effect on the actual\nresponse header."
|
||||||
summary: 'Execute Stored Program, return JSON'
|
summary: 'Execute Stored Program, return JSON'
|
||||||
tags:
|
tags:
|
||||||
- STP
|
- STP
|
||||||
@@ -1069,6 +1302,7 @@ paths:
|
|||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
-
|
-
|
||||||
|
description: 'Location of SAS program'
|
||||||
in: query
|
in: query
|
||||||
name: _program
|
name: _program
|
||||||
required: false
|
required: false
|
||||||
@@ -1081,10 +1315,31 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
||||||
|
/SASjsApi/info:
|
||||||
|
get:
|
||||||
|
operationId: Info
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/InfoResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http}
|
||||||
|
summary: 'Get server info (mode, cors, whiteList, protocol).'
|
||||||
|
tags:
|
||||||
|
- Info
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
servers:
|
servers:
|
||||||
-
|
-
|
||||||
url: /
|
url: /
|
||||||
tags:
|
tags:
|
||||||
|
-
|
||||||
|
name: Info
|
||||||
|
description: 'Get Server Info'
|
||||||
-
|
-
|
||||||
name: Session
|
name: Session
|
||||||
description: 'Get Session information'
|
description: 'Get Session information'
|
||||||
@@ -1109,3 +1364,6 @@ tags:
|
|||||||
-
|
-
|
||||||
name: CODE
|
name: CODE
|
||||||
description: 'Operations on SAS code'
|
description: 'Operations on SAS code'
|
||||||
|
-
|
||||||
|
name: Web
|
||||||
|
description: 'Operations on Web'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import {
|
import {
|
||||||
|
CompileTree,
|
||||||
createFile,
|
createFile,
|
||||||
loadDependenciesFile,
|
loadDependenciesFile,
|
||||||
readFile,
|
readFile,
|
||||||
@@ -17,11 +18,12 @@ const compiledSystemInit = async (systemInit: string) =>
|
|||||||
programFolders: [],
|
programFolders: [],
|
||||||
macroFolders: [],
|
macroFolders: [],
|
||||||
buildSourceFolder: '',
|
buildSourceFolder: '',
|
||||||
macroCorePath
|
binaryFolders: [],
|
||||||
|
macroCorePath,
|
||||||
|
compileTree: new CompileTree('') // dummy compileTree
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const createSysInitFile = async () => {
|
const createSysInitFile = async () => {
|
||||||
console.log('macroCorePath', macroCorePath)
|
|
||||||
const systemInitContent = await readFile(
|
const systemInitContent = await readFile(
|
||||||
path.join(__dirname, 'systemInit.sas')
|
path.join(__dirname, 'systemInit.sas')
|
||||||
)
|
)
|
||||||
|
|||||||
32
api/scripts/copySASjsCore.ts
Normal file
32
api/scripts/copySASjsCore.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import {
|
||||||
|
asyncForEach,
|
||||||
|
copy,
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
listFilesInFolder
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
const fileNames = await listFilesInFolder(sasJSCoreMacros)
|
||||||
|
|
||||||
|
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
copySASjsCore()
|
||||||
@@ -4,10 +4,13 @@
|
|||||||
@details This program is inserted into every sasjs/server program invocation,
|
@details This program is inserted into every sasjs/server program invocation,
|
||||||
_before_ any user-provided content.
|
_before_ any user-provided content.
|
||||||
|
|
||||||
<h4> SAS Macros </h4>
|
A number of useful CORE macros are also compiled below, so that they can be
|
||||||
@li mcf_stpsrv_header.sas
|
available by default for Stored Programs.
|
||||||
|
|
||||||
|
Note that the full CORE library is available to sessions in SASjs Studio.
|
||||||
|
|
||||||
|
<h4> SAS Macros </h4>
|
||||||
|
@li mfs_httpheader.sas
|
||||||
|
@li ms_webout.sas
|
||||||
**/
|
**/
|
||||||
|
|
||||||
|
|
||||||
%mcf_stpsrv_header(wrap=YES, insert_cmplib=YES)
|
|
||||||
|
|||||||
112
api/src/app.ts
112
api/src/app.ts
@@ -1,36 +1,114 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express from 'express'
|
import express, { ErrorRequestHandler } from 'express'
|
||||||
|
import csrf from 'csurf'
|
||||||
|
import session from 'express-session'
|
||||||
|
import MongoStore from 'connect-mongo'
|
||||||
import morgan from 'morgan'
|
import morgan from 'morgan'
|
||||||
|
import cookieParser from 'cookie-parser'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
|
import helmet from 'helmet'
|
||||||
|
|
||||||
import webRouter from './routes/web'
|
import {
|
||||||
import apiRouter from './routes/api'
|
connectDB,
|
||||||
import { connectDB, getWebBuildFolderPath } from './utils'
|
copySASjsCore,
|
||||||
|
getWebBuildFolderPath,
|
||||||
|
loadAppStreamConfig,
|
||||||
|
setProcessVariables,
|
||||||
|
setupFolders
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { MODE, CORS, PORT_WEB } = process.env
|
app.use(cookieParser())
|
||||||
const whiteList = [
|
app.use(morgan('tiny'))
|
||||||
`http://localhost:${PORT_WEB ?? 3000}`,
|
|
||||||
'https://sas.analytium.co.uk:8343'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
const { MODE, CORS, WHITELIST, PROTOCOL } = process.env
|
||||||
|
|
||||||
|
export const cookieOptions = {
|
||||||
|
secure: PROTOCOL === 'https',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
|
}
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* CSRF Protection *
|
||||||
|
***********************************/
|
||||||
|
export const csrfProtection = csrf({ cookie: cookieOptions })
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Handle security and origin *
|
||||||
|
***********************************/
|
||||||
|
app.use(helmet())
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Enabling CORS *
|
||||||
|
***********************************/
|
||||||
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
||||||
console.log('All CORS Requests are enabled')
|
const whiteList: string[] = []
|
||||||
|
WHITELIST?.split(' ')
|
||||||
|
?.filter((url) => !!url)
|
||||||
|
.forEach((url) => {
|
||||||
|
if (url.startsWith('http'))
|
||||||
|
// removing trailing slash of URLs listing for CORS
|
||||||
|
whiteList.push(url.replace(/\/$/, ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('All CORS Requests are enabled for:', whiteList)
|
||||||
app.use(cors({ credentials: true, origin: whiteList }))
|
app.use(cors({ credentials: true, origin: whiteList }))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.json({ limit: '50mb' }))
|
/***********************************
|
||||||
app.use(morgan('tiny'))
|
* DB Connection & *
|
||||||
|
* Express Sessions *
|
||||||
|
* With Mongo Store *
|
||||||
|
***********************************/
|
||||||
|
if (MODE?.trim() === 'server') {
|
||||||
|
// NOTE: when exporting app.js as agent for supertest
|
||||||
|
// we should exclude connecting to the real database
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: process.env.SESSION_SECRET as string,
|
||||||
|
saveUninitialized: false, // don't create session until something stored
|
||||||
|
resave: false, //don't save session if unmodified
|
||||||
|
store: MongoStore.create({ clientPromise, collectionName: 'sessions' }),
|
||||||
|
cookie: cookieOptions
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.use(express.json({ limit: '100mb' }))
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
app.use('/', webRouter)
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
app.use('/SASjsApi', apiRouter)
|
if (err.code === 'EBADCSRFTOKEN')
|
||||||
app.use(express.json({ limit: '50mb' }))
|
return res.status(400).send('Invalid CSRF token!')
|
||||||
|
|
||||||
app.use(express.static(getWebBuildFolderPath()))
|
console.error(err.stack)
|
||||||
|
res.status(500).send('Something broke!')
|
||||||
|
}
|
||||||
|
|
||||||
export default connectDB().then(() => app)
|
export default setProcessVariables().then(async () => {
|
||||||
|
await setupFolders()
|
||||||
|
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()))
|
||||||
|
|
||||||
|
app.use(onError)
|
||||||
|
|
||||||
|
return app
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
|
import Client from '../model/Client'
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
@@ -81,6 +82,9 @@ export class AuthController {
|
|||||||
const authorize = async (data: any): Promise<AuthorizeResponse> => {
|
const authorize = async (data: any): Promise<AuthorizeResponse> => {
|
||||||
const { username, password, clientId } = data
|
const { username, password, clientId } = data
|
||||||
|
|
||||||
|
const client = await Client.findOne({ clientId })
|
||||||
|
if (!client) throw new Error('Invalid clientId.')
|
||||||
|
|
||||||
// Authenticate User
|
// Authenticate User
|
||||||
const user = await User.findOne({ username })
|
const user = await User.findOne({ username })
|
||||||
if (!user) throw new Error('Username is not found.')
|
if (!user) throw new Error('Username is not found.')
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
import { ExecutionController } from './internal'
|
import { ExecuteReturnJson, ExecutionController } from './internal'
|
||||||
import { PreProgramVars } from '../types'
|
import { PreProgramVars } from '../types'
|
||||||
|
import { ExecuteReturnJsonResponse } from '.'
|
||||||
|
import { parseLogToArray } from '../utils'
|
||||||
|
|
||||||
interface ExecuteSASCodePayload {
|
interface ExecuteSASCodePayload {
|
||||||
/**
|
/**
|
||||||
@@ -23,22 +25,28 @@ export class CodeController {
|
|||||||
public async executeSASCode(
|
public async executeSASCode(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body: ExecuteSASCodePayload
|
@Body() body: ExecuteSASCodePayload
|
||||||
): Promise<string> {
|
): Promise<ExecuteReturnJsonResponse> {
|
||||||
return executeSASCode(request, body)
|
return executeSASCode(request, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
||||||
try {
|
try {
|
||||||
const result = await new ExecutionController().executeProgram(
|
const { webout, log, httpHeaders } =
|
||||||
code,
|
(await new ExecutionController().executeProgram(
|
||||||
getPreProgramVariables(req),
|
code,
|
||||||
{ ...req.query, _debug: 131 },
|
getPreProgramVariables(req),
|
||||||
undefined,
|
{ ...req.query, _debug: 131 },
|
||||||
true
|
undefined,
|
||||||
)
|
true
|
||||||
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
return result as string
|
return {
|
||||||
|
status: 'success',
|
||||||
|
_webout: webout as string,
|
||||||
|
log: parseLogToArray(log),
|
||||||
|
httpHeaders
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import express, { Express } from 'express'
|
||||||
import {
|
import {
|
||||||
Security,
|
Security,
|
||||||
|
Request,
|
||||||
Route,
|
Route,
|
||||||
Tags,
|
Tags,
|
||||||
Example,
|
Example,
|
||||||
@@ -8,35 +11,39 @@ import {
|
|||||||
Response,
|
Response,
|
||||||
Query,
|
Query,
|
||||||
Get,
|
Get,
|
||||||
Patch
|
Patch,
|
||||||
|
UploadedFile,
|
||||||
|
FormField,
|
||||||
|
Delete,
|
||||||
|
Hidden
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
import { fileExists, readFile, createFile } from '@sasjs/utils'
|
import {
|
||||||
|
fileExists,
|
||||||
|
moveFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFile as deleteFileOnSystem,
|
||||||
|
folderExists,
|
||||||
|
listFilesInFolder,
|
||||||
|
listSubFoldersInFolder,
|
||||||
|
isFolder,
|
||||||
|
FileTree,
|
||||||
|
isFileTree
|
||||||
|
} from '@sasjs/utils'
|
||||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||||
|
|
||||||
import { FileTree, isFileTree, TreeNode } from '../types'
|
import { TreeNode } from '../types'
|
||||||
import path from 'path'
|
|
||||||
import { getTmpFilesFolderPath } from '../utils'
|
import { getTmpFilesFolderPath } from '../utils'
|
||||||
|
|
||||||
interface DeployPayload {
|
interface DeployPayload {
|
||||||
appLoc?: string
|
appLoc: string
|
||||||
|
streamWebFolder?: string
|
||||||
fileTree: FileTree
|
fileTree: FileTree
|
||||||
}
|
}
|
||||||
interface FilePayload {
|
|
||||||
/**
|
|
||||||
* Path of the file
|
|
||||||
* @example "/Public/somefolder/some.file"
|
|
||||||
*/
|
|
||||||
filePath: string
|
|
||||||
/**
|
|
||||||
* Contents of the file
|
|
||||||
* @example "Contents of the File"
|
|
||||||
*/
|
|
||||||
fileContent: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeployResponse {
|
interface DeployResponse {
|
||||||
status: string
|
status: string
|
||||||
message: string
|
message: string
|
||||||
|
streamServiceName?: string
|
||||||
example?: FileTree
|
example?: FileTree
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,57 +96,106 @@ export class DriveController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Get file from SASjs Drive
|
* @summary Creates/updates files within SASjs Drive using uploaded JSON file.
|
||||||
* @query filePath Location of SAS program
|
*
|
||||||
* @example filePath "/Public/somefolder/some.file"
|
|
||||||
*/
|
*/
|
||||||
@Example<GetFileResponse>({
|
@Example<DeployResponse>(successDeployResponse)
|
||||||
status: 'success',
|
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
|
||||||
fileContent: 'Contents of the File'
|
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
|
||||||
})
|
@Post('/deploy/upload')
|
||||||
@Response<GetFileResponse>(400, 'Unable to get File', {
|
public async deployUpload(
|
||||||
status: 'failure',
|
@UploadedFile() file: Express.Multer.File, // passing here for API docs
|
||||||
message: 'File request failed.'
|
@Query() @Hidden() body?: DeployPayload // Hidden decorator has be optional
|
||||||
})
|
): Promise<DeployResponse> {
|
||||||
@Get('/file')
|
return deploy(body!)
|
||||||
public async getFile(@Query() filePath: string): Promise<GetFileResponse> {
|
|
||||||
return getFile(filePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
) {
|
||||||
|
return getFile(request, _filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @summary Get folder contents from SASjs Drive
|
||||||
|
* @query _folderPath Location of SAS program
|
||||||
|
* @example _folderPath "/Public/somefolder"
|
||||||
|
*/
|
||||||
|
@Get('/folder')
|
||||||
|
public async getFolder(@Query() _folderPath?: string) {
|
||||||
|
return getFolder(_folderPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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) {
|
||||||
|
return deleteFile(_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
|
* @summary Create a file in SASjs Drive
|
||||||
|
* @param _filePath Location of SAS program
|
||||||
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<UpdateFileResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(400, 'File already exists', {
|
@Response<UpdateFileResponse>(403, 'File already exists', {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@Post('/file')
|
@Post('/file')
|
||||||
public async saveFile(
|
public async saveFile(
|
||||||
@Body() body: FilePayload
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Query() _filePath?: string,
|
||||||
|
@FormField() filePath?: string
|
||||||
): Promise<UpdateFileResponse> {
|
): Promise<UpdateFileResponse> {
|
||||||
return saveFile(body)
|
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
|
* @summary Modify a file in SASjs Drive
|
||||||
|
* @param _filePath Location of SAS program
|
||||||
|
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@Example<UpdateFileResponse>({
|
@Example<UpdateFileResponse>({
|
||||||
status: 'success'
|
status: 'success'
|
||||||
})
|
})
|
||||||
@Response<UpdateFileResponse>(400, 'Unable to get File', {
|
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
||||||
status: 'failure',
|
status: 'failure',
|
||||||
message: 'File request failed.'
|
message: 'File request failed.'
|
||||||
})
|
})
|
||||||
@Patch('/file')
|
@Patch('/file')
|
||||||
public async updateFile(
|
public async updateFile(
|
||||||
@Body() body: FilePayload
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Query() _filePath?: string,
|
||||||
|
@FormField() filePath?: string
|
||||||
): Promise<UpdateFileResponse> {
|
): Promise<UpdateFileResponse> {
|
||||||
return updateFile(body)
|
return updateFile((_filePath ?? filePath)!, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,91 +209,151 @@ export class DriveController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFileTree = () => {
|
const getFileTree = () => {
|
||||||
const tree = new ExecutionController().buildDirectorytree()
|
const tree = new ExecutionController().buildDirectoryTree()
|
||||||
return { status: 'success', tree }
|
return { status: 'success', tree }
|
||||||
}
|
}
|
||||||
|
|
||||||
const deploy = async (data: DeployPayload) => {
|
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)) {
|
if (!isFileTree(data.fileTree)) {
|
||||||
throw { code: 400, ...invalidDeployFormatResponse }
|
throw { code: 400, ...invalidDeployFormatResponse }
|
||||||
}
|
}
|
||||||
|
|
||||||
await createFileTree(
|
await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
|
||||||
data.fileTree.members,
|
|
||||||
data.appLoc ? data.appLoc.replace(/^\//, '').split('/') : []
|
|
||||||
).catch((err) => {
|
|
||||||
throw { code: 500, ...execDeployErrorResponse, ...err }
|
throw { code: 500, ...execDeployErrorResponse, ...err }
|
||||||
})
|
})
|
||||||
|
|
||||||
return successDeployResponse
|
return successDeployResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFile = async (filePath: string): Promise<GetFileResponse> => {
|
const getFile = async (req: express.Request, filePath: string) => {
|
||||||
try {
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
const filePathFull = path
|
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
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 doesn't exist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = path.extname(filePathFull).toLowerCase()
|
||||||
|
if (extension === '.sas') {
|
||||||
|
req.res?.setHeader('Content-type', 'text/plain')
|
||||||
|
}
|
||||||
|
|
||||||
|
req.res?.sendFile(path.resolve(filePathFull))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFolder = async (folderPath?: string) => {
|
||||||
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
|
|
||||||
|
if (folderPath) {
|
||||||
|
const folderPathFull = path
|
||||||
|
.join(getTmpFilesFolderPath(), folderPath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
await validateFilePath(filePathFull)
|
if (!folderPathFull.includes(driveFilesPath)) {
|
||||||
const fileContent = await readFile(filePathFull)
|
throw new Error('Cannot get folder outside drive.')
|
||||||
|
|
||||||
return { status: 'success', fileContent: fileContent }
|
|
||||||
} catch (err: any) {
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'File request failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await folderExists(folderPathFull))) {
|
||||||
|
throw new Error("Folder doesn't exist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await isFolder(folderPathFull))) {
|
||||||
|
throw new Error('Not a Folder.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: string[] = await listFilesInFolder(folderPathFull)
|
||||||
|
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
||||||
|
return { files, folders }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const files: string[] = await listFilesInFolder(driveFilesPath)
|
||||||
|
const folders: string[] = await listSubFoldersInFolder(driveFilesPath)
|
||||||
|
return { files, folders }
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveFile = async (body: FilePayload): Promise<GetFileResponse> => {
|
const deleteFile = async (filePath: string) => {
|
||||||
const { filePath, fileContent } = body
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
try {
|
|
||||||
const filePathFull = path
|
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
if (await fileExists(filePathFull)) {
|
const filePathFull = path
|
||||||
throw 'DriveController: File already exists.'
|
.join(getTmpFilesFolderPath(), filePath)
|
||||||
}
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
await createFile(filePathFull, fileContent)
|
|
||||||
|
|
||||||
return { status: 'success' }
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
} catch (err: any) {
|
throw new Error('Cannot delete file outside drive.')
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'File request failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(await fileExists(filePathFull))) {
|
||||||
|
throw new Error('File does not exist.')
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteFileOnSystem(filePathFull)
|
||||||
|
|
||||||
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFile = async (body: FilePayload): Promise<GetFileResponse> => {
|
const saveFile = async (
|
||||||
const { filePath, fileContent } = body
|
filePath: string,
|
||||||
try {
|
multerFile: Express.Multer.File
|
||||||
const filePathFull = path
|
): Promise<GetFileResponse> => {
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
|
||||||
|
|
||||||
await validateFilePath(filePathFull)
|
const filePathFull = path
|
||||||
await createFile(filePathFull, fileContent)
|
.join(driveFilesPath, filePath)
|
||||||
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
return { status: 'success' }
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
} catch (err: any) {
|
throw new Error('Cannot put file outside drive.')
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'File request failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 validateFilePath = async (filePath: string) => {
|
const updateFile = async (
|
||||||
if (!(await fileExists(filePath))) {
|
filePath: string,
|
||||||
throw 'DriveController: File does not exists.'
|
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' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from './group'
|
|||||||
export * from './session'
|
export * from './session'
|
||||||
export * from './stp'
|
export * from './stp'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
|
export * from './info'
|
||||||
|
|||||||
36
api/src/controllers/info.ts
Normal file
36
api/src/controllers/info.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Route, Tags, Example, Get } from 'tsoa'
|
||||||
|
|
||||||
|
export interface InfoResponse {
|
||||||
|
mode: string
|
||||||
|
cors: string
|
||||||
|
whiteList: string[]
|
||||||
|
protocol: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route('SASjsApi/info')
|
||||||
|
@Tags('Info')
|
||||||
|
export class InfoController {
|
||||||
|
/**
|
||||||
|
* @summary Get server info (mode, cors, whiteList, protocol).
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<InfoResponse>({
|
||||||
|
mode: 'desktop',
|
||||||
|
cors: 'enable',
|
||||||
|
whiteList: ['http://example.com', 'http://example2.com'],
|
||||||
|
protocol: 'http'
|
||||||
|
})
|
||||||
|
@Get('/')
|
||||||
|
public info(): InfoResponse {
|
||||||
|
const response = {
|
||||||
|
mode: process.env.MODE ?? 'desktop',
|
||||||
|
cors:
|
||||||
|
process.env.CORS ||
|
||||||
|
(process.env.MODE === 'server' ? 'disable' : 'enable'),
|
||||||
|
whiteList:
|
||||||
|
process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [],
|
||||||
|
protocol: process.env.PROTOCOL ?? 'http'
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,46 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { getSessionController } from './'
|
import { getSessionController } from './'
|
||||||
import { readFile, fileExists, createFile, moveFile } from '@sasjs/utils'
|
import {
|
||||||
import { PreProgramVars, TreeNode } from '../../types'
|
readFile,
|
||||||
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
|
fileExists,
|
||||||
|
createFile,
|
||||||
|
moveFile,
|
||||||
|
readFileBinary
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
import { PreProgramVars, Session, 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 {
|
export class ExecutionController {
|
||||||
async executeFile(
|
async executeFile(
|
||||||
programPath: string,
|
programPath: string,
|
||||||
preProgramVariables: PreProgramVars,
|
preProgramVariables: PreProgramVars,
|
||||||
vars: { [key: string]: string | number | undefined },
|
vars: ExecutionVars,
|
||||||
otherArgs?: any,
|
otherArgs?: any,
|
||||||
returnJson?: boolean
|
returnJson?: boolean,
|
||||||
|
session?: Session
|
||||||
) {
|
) {
|
||||||
if (!(await fileExists(programPath)))
|
if (!(await fileExists(programPath)))
|
||||||
throw 'ExecutionController: SAS file does not exist.'
|
throw 'ExecutionController: SAS file does not exist.'
|
||||||
@@ -23,28 +52,32 @@ export class ExecutionController {
|
|||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson
|
returnJson,
|
||||||
|
session
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeProgram(
|
async executeProgram(
|
||||||
program: string,
|
program: string,
|
||||||
preProgramVariables: PreProgramVars,
|
preProgramVariables: PreProgramVars,
|
||||||
vars: { [key: string]: string | number | undefined },
|
vars: ExecutionVars,
|
||||||
otherArgs?: any,
|
otherArgs?: any,
|
||||||
returnJson?: boolean
|
returnJson?: boolean,
|
||||||
) {
|
sessionByFileUpload?: Session
|
||||||
|
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||||
const sessionController = getSessionController()
|
const sessionController = getSessionController()
|
||||||
|
|
||||||
const session = await sessionController.getSession()
|
const session =
|
||||||
|
sessionByFileUpload ?? (await sessionController.getSession())
|
||||||
session.inUse = true
|
session.inUse = true
|
||||||
session.consumed = true
|
session.consumed = true
|
||||||
|
|
||||||
const logPath = path.join(session.path, 'log.log')
|
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 weboutPath = path.join(session.path, 'webout.txt')
|
||||||
await createFile(weboutPath, '')
|
|
||||||
|
|
||||||
const tokenFile = path.join(session.path, 'accessToken.txt')
|
const tokenFile = path.join(session.path, 'accessToken.txt')
|
||||||
|
|
||||||
|
await createFile(weboutPath, '')
|
||||||
await createFile(
|
await createFile(
|
||||||
tokenFile,
|
tokenFile,
|
||||||
preProgramVariables?.accessToken ?? 'accessToken'
|
preProgramVariables?.accessToken ?? 'accessToken'
|
||||||
@@ -55,6 +88,7 @@ export class ExecutionController {
|
|||||||
`${computed}%let ${key}=${vars[key]};\n`,
|
`${computed}%let ${key}=${vars[key]};\n`,
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
|
|
||||||
const preProgramVarStatments = `
|
const preProgramVarStatments = `
|
||||||
%let _sasjs_tokenfile=${tokenFile};
|
%let _sasjs_tokenfile=${tokenFile};
|
||||||
%let _sasjs_username=${preProgramVariables?.username};
|
%let _sasjs_username=${preProgramVariables?.username};
|
||||||
@@ -65,17 +99,19 @@ export class ExecutionController {
|
|||||||
%let _metaperson=&_sasjs_displayname;
|
%let _metaperson=&_sasjs_displayname;
|
||||||
%let _metauser=&_sasjs_username;
|
%let _metauser=&_sasjs_username;
|
||||||
%let sasjsprocessmode=Stored Program;
|
%let sasjsprocessmode=Stored Program;
|
||||||
|
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
||||||
|
|
||||||
%global SYSPROCESSMODE SYSTCPIPHOSTNAME;
|
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
||||||
%macro _sasjs_server_init();
|
%macro _sasjs_server_init();
|
||||||
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
|
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
|
||||||
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
||||||
%mend;
|
%mend;
|
||||||
%_sasjs_server_init()
|
%_sasjs_server_init()
|
||||||
%sysmacdelete _sasjs_server_init;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
|
options insert=(SASAUTOS="${getTmpMacrosPath()}");
|
||||||
|
|
||||||
/* runtime vars */
|
/* runtime vars */
|
||||||
${varStatments}
|
${varStatments}
|
||||||
filename _webout "${weboutPath}" mod;
|
filename _webout "${weboutPath}" mod;
|
||||||
@@ -87,7 +123,7 @@ ${preProgramVarStatments}
|
|||||||
${program}`
|
${program}`
|
||||||
|
|
||||||
// if no files are uploaded filesNamesMap will be undefined
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
if (otherArgs && otherArgs.filesNamesMap) {
|
if (otherArgs?.filesNamesMap) {
|
||||||
const uploadSasCode = await generateFileUploadSasCode(
|
const uploadSasCode = await generateFileUploadSasCode(
|
||||||
otherArgs.filesNamesMap,
|
otherArgs.filesNamesMap,
|
||||||
session.path
|
session.path
|
||||||
@@ -116,30 +152,42 @@ ${program}`
|
|||||||
}
|
}
|
||||||
|
|
||||||
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
|
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
|
||||||
const webout = (await fileExists(weboutPath))
|
const headersContent = (await fileExists(headersPath))
|
||||||
? await readFile(weboutPath)
|
? await readFile(headersPath)
|
||||||
: ''
|
: ''
|
||||||
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
|
const fileResponse: boolean =
|
||||||
|
httpHeaders.hasOwnProperty('content-type') &&
|
||||||
|
!returnJson && // not a POST Request
|
||||||
|
!isDebugOn(vars) // Debug is not enabled
|
||||||
|
|
||||||
const debugValue =
|
const webout = (await fileExists(weboutPath))
|
||||||
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
? fileResponse
|
||||||
|
? await readFileBinary(weboutPath)
|
||||||
|
: await readFile(weboutPath)
|
||||||
|
: ''
|
||||||
|
|
||||||
// it should be deleted by scheduleSessionDestroy
|
// it should be deleted by scheduleSessionDestroy
|
||||||
session.inUse = false
|
session.inUse = false
|
||||||
|
|
||||||
if (returnJson) {
|
if (returnJson) {
|
||||||
return {
|
return {
|
||||||
|
httpHeaders,
|
||||||
webout,
|
webout,
|
||||||
log:
|
log: isDebugOn(vars) || session.crashed ? log : undefined
|
||||||
(debugValue && debugValue >= 131) || session.crashed ? log : undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (debugValue && debugValue >= 131) || session.crashed
|
return {
|
||||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
httpHeaders,
|
||||||
: webout
|
result:
|
||||||
|
isDebugOn(vars) || session.crashed
|
||||||
|
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||||
|
: webout
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDirectorytree() {
|
buildDirectoryTree() {
|
||||||
const root: TreeNode = {
|
const root: TreeNode = {
|
||||||
name: 'files',
|
name: 'files',
|
||||||
relativePath: '',
|
relativePath: '',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import multer from 'multer'
|
||||||
import { uuidv4 } from '@sasjs/utils'
|
import { uuidv4 } from '@sasjs/utils'
|
||||||
import { getSessionController } from '.'
|
import { getSessionController } from '.'
|
||||||
const multer = require('multer')
|
|
||||||
|
|
||||||
export class FileUploadController {
|
export class FileUploadController {
|
||||||
private storage = multer.diskStorage({
|
private storage = multer.diskStorage({
|
||||||
@@ -18,12 +18,14 @@ export class FileUploadController {
|
|||||||
|
|
||||||
//It will intercept request and generate unique uuid to be used as a subfolder name
|
//It will intercept request and generate unique uuid to be used as a subfolder name
|
||||||
//that will store the files uploaded
|
//that will store the files uploaded
|
||||||
public preuploadMiddleware = async (req: any, res: any, next: any) => {
|
public preUploadMiddleware = async (req: any, res: any, next: any) => {
|
||||||
let session
|
let session
|
||||||
|
|
||||||
const sessionController = getSessionController()
|
const sessionController = getSessionController()
|
||||||
session = await sessionController.getSession()
|
session = await sessionController.getSession()
|
||||||
session.inUse = true
|
// marking consumed true, so that it's not available
|
||||||
|
// as readySession for any other request
|
||||||
|
session.consumed = true
|
||||||
|
|
||||||
req.sasSession = session
|
req.sasSession = session
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
createFile,
|
createFile,
|
||||||
fileExists,
|
fileExists,
|
||||||
generateTimestamp,
|
generateTimestamp,
|
||||||
readFile,
|
readFile
|
||||||
moveFile
|
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
@@ -31,17 +30,17 @@ export class SessionController {
|
|||||||
? readySessions[0]
|
? readySessions[0]
|
||||||
: await this.createSession()
|
: await this.createSession()
|
||||||
|
|
||||||
if (readySessions.length < 2) this.createSession()
|
if (readySessions.length < 3) this.createSession()
|
||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createSession(): Promise<Session> {
|
private async createSession(): Promise<Session> {
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
console.log('creating session', sessionId)
|
|
||||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
|
// death time of session is 15 mins from creation
|
||||||
const deathTimeStamp = (
|
const deathTimeStamp = (
|
||||||
parseInt(creationTimeStamp) +
|
parseInt(creationTimeStamp) +
|
||||||
15 * 60 * 1000 -
|
15 * 60 * 1000 -
|
||||||
@@ -68,7 +67,10 @@ export class SessionController {
|
|||||||
|
|
||||||
// the autoexec file is executed on SAS startup
|
// the autoexec file is executed on SAS startup
|
||||||
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
|
||||||
const contentForAutoExec = `/* compiled systemInit */\n${compiledSystemInitContent}\n/* autoexec */\n${autoExecContent}`
|
const contentForAutoExec = `/* compiled systemInit */
|
||||||
|
${compiledSystemInitContent}
|
||||||
|
/* autoexec */
|
||||||
|
${autoExecContent}`
|
||||||
await createFile(autoExecPath, contentForAutoExec)
|
await createFile(autoExecPath, contentForAutoExec)
|
||||||
|
|
||||||
// create empty code.sas as SAS will not start without a SYSIN
|
// create empty code.sas as SAS will not start without a SYSIN
|
||||||
@@ -85,6 +87,8 @@ export class SessionController {
|
|||||||
codePath,
|
codePath,
|
||||||
'-LOG',
|
'-LOG',
|
||||||
path.join(session.path, 'log.log'),
|
path.join(session.path, 'log.log'),
|
||||||
|
'-PRINT',
|
||||||
|
path.join(session.path, 'output.lst'),
|
||||||
'-WORK',
|
'-WORK',
|
||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
@@ -136,7 +140,9 @@ export class SessionController {
|
|||||||
private scheduleSessionDestroy(session: Session) {
|
private scheduleSessionDestroy(session: Session) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (session.inUse) {
|
if (session.inUse) {
|
||||||
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
|
// adding 10 more minutes
|
||||||
|
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||||
|
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||||
|
|
||||||
this.scheduleSessionDestroy(session)
|
this.scheduleSessionDestroy(session)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types'
|
|
||||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
|
||||||
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||||
|
import {
|
||||||
|
createFolder,
|
||||||
|
createFile,
|
||||||
|
asyncForEach,
|
||||||
|
FolderMember,
|
||||||
|
ServiceMember,
|
||||||
|
FileMember,
|
||||||
|
MemberType,
|
||||||
|
FileTree
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
// REFACTOR: export FileTreeCpntroller
|
// REFACTOR: export FileTreeCpntroller
|
||||||
export const createFileTree = async (
|
export const createFileTree = async (
|
||||||
members: (FolderMember | ServiceMember)[],
|
members: (FolderMember | ServiceMember | FileMember)[],
|
||||||
parentFolders: string[] = []
|
parentFolders: string[] = []
|
||||||
) => {
|
) => {
|
||||||
const destinationPath = path.join(
|
const destinationPath = path.join(
|
||||||
@@ -13,25 +21,32 @@ export const createFileTree = async (
|
|||||||
path.join(...parentFolders)
|
path.join(...parentFolders)
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
|
await asyncForEach(
|
||||||
let name = member.name
|
members,
|
||||||
|
async (member: FolderMember | ServiceMember | FileMember) => {
|
||||||
|
let name = member.name
|
||||||
|
|
||||||
if (member.type === MemberType.service) name += '.sas'
|
if (member.type === MemberType.service) name += '.sas'
|
||||||
|
|
||||||
if (member.type === MemberType.folder) {
|
if (member.type === MemberType.folder) {
|
||||||
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
||||||
Promise.reject({ error: err, failedToCreate: name })
|
Promise.reject({ error: err, failedToCreate: name })
|
||||||
)
|
)
|
||||||
|
|
||||||
await createFileTree(member.members, [...parentFolders, name]).catch(
|
await createFileTree(member.members, [...parentFolders, name]).catch(
|
||||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
await createFile(path.join(destinationPath, name), member.code).catch(
|
const encoding = member.type === MemberType.file ? 'base64' : undefined
|
||||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
|
||||||
)
|
await createFile(
|
||||||
|
path.join(destinationPath, name),
|
||||||
|
member.code,
|
||||||
|
encoding
|
||||||
|
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = (req: any) => ({
|
const session = (req: any) => ({
|
||||||
id: req.user.id,
|
id: req.user.userId,
|
||||||
username: req.user.username,
|
username: req.user.username,
|
||||||
displayName: req.user.displayName
|
displayName: req.user.displayName
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
import {
|
||||||
import { ExecutionController } from './internal'
|
Request,
|
||||||
|
Security,
|
||||||
|
Route,
|
||||||
|
Tags,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Get,
|
||||||
|
Query,
|
||||||
|
Example
|
||||||
|
} from 'tsoa'
|
||||||
|
import {
|
||||||
|
ExecuteReturnJson,
|
||||||
|
ExecuteReturnRaw,
|
||||||
|
ExecutionController,
|
||||||
|
ExecutionVars
|
||||||
|
} from './internal'
|
||||||
import { PreProgramVars } from '../types'
|
import { PreProgramVars } from '../types'
|
||||||
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
|
import {
|
||||||
|
getTmpFilesFolderPath,
|
||||||
|
HTTPHeaders,
|
||||||
|
isDebugOn,
|
||||||
|
LogLine,
|
||||||
|
makeFilesNamesMap,
|
||||||
|
parseLogToArray
|
||||||
|
} from '../utils'
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecuteReturnJsonPayload {
|
||||||
/**
|
/**
|
||||||
@@ -12,11 +34,16 @@ interface ExecuteReturnJsonPayload {
|
|||||||
*/
|
*/
|
||||||
_program?: string
|
_program?: string
|
||||||
}
|
}
|
||||||
interface ExecuteReturnJsonResponse {
|
|
||||||
|
interface IRecordOfAny {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
export interface ExecuteReturnJsonResponse {
|
||||||
status: string
|
status: string
|
||||||
_webout: string
|
_webout: string | IRecordOfAny
|
||||||
log?: string
|
log: LogLine[]
|
||||||
message?: string
|
message?: string
|
||||||
|
httpHeaders: HTTPHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@@ -24,33 +51,69 @@ interface ExecuteReturnJsonResponse {
|
|||||||
@Tags('STP')
|
@Tags('STP')
|
||||||
export class STPController {
|
export class STPController {
|
||||||
/**
|
/**
|
||||||
* Trigger a SAS program using it's location in the _program parameter.
|
* Trigger a SAS program using it's location in the _program URL parameter.
|
||||||
* Enable debugging using the _debug 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.
|
* Additional URL parameters are turned into SAS macro variables.
|
||||||
* Any files provided are placed into the session and
|
*
|
||||||
* corresponding _WEBIN_XXX variables are created.
|
* Any files provided in the request body are placed into the SAS session with
|
||||||
* @summary Execute Stored Program, return raw content
|
* corresponding _WEBIN_XXX variables created.
|
||||||
* @query _program Location of SAS program
|
*
|
||||||
|
* 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"
|
* @example _program "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
@Get('/execute')
|
@Get('/execute')
|
||||||
public async executeReturnRaw(
|
public async executeReturnRaw(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Query() _program: string
|
@Query() _program: string
|
||||||
): Promise<string> {
|
): Promise<string | Buffer> {
|
||||||
return executeReturnRaw(request, _program)
|
return executeReturnRaw(request, _program)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a SAS program using it's location in the _program parameter.
|
* Trigger a SAS program using it's location in the _program URL parameter.
|
||||||
* Enable debugging using the _debug 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.
|
* Additional URL parameters are turned into SAS macro variables.
|
||||||
* Any files provided are placed into the session and
|
*
|
||||||
* corresponding _WEBIN_XXX variables are created.
|
* 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
|
* @summary Execute Stored Program, return JSON
|
||||||
* @query _program Location of SAS program
|
* @param _program Location of SAS program
|
||||||
* @example _program "/Public/somefolder/some.file"
|
* @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')
|
@Post('/execute')
|
||||||
public async executeReturnJson(
|
public async executeReturnJson(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@@ -65,21 +128,34 @@ export class STPController {
|
|||||||
const executeReturnRaw = async (
|
const executeReturnRaw = async (
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
_program: string
|
_program: string
|
||||||
): Promise<string> => {
|
): Promise<string | Buffer> => {
|
||||||
const query = req.query as { [key: string]: string | number | undefined }
|
const query = req.query as ExecutionVars
|
||||||
const sasCodePath =
|
const sasCodePath =
|
||||||
path
|
path
|
||||||
.join(getTmpFilesFolderPath(), _program)
|
.join(getTmpFilesFolderPath(), _program)
|
||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await new ExecutionController().executeFile(
|
const { result, httpHeaders } =
|
||||||
sasCodePath,
|
(await new ExecutionController().executeFile(
|
||||||
getPreProgramVariables(req),
|
sasCodePath,
|
||||||
query
|
getPreProgramVariables(req),
|
||||||
)
|
query
|
||||||
|
)) as ExecuteReturnRaw
|
||||||
|
|
||||||
return result as string
|
// 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) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
@@ -102,17 +178,28 @@ const executeReturnJson = async (
|
|||||||
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log } = (await new ExecutionController().executeFile(
|
const { webout, log, httpHeaders } =
|
||||||
sasCodePath,
|
(await new ExecutionController().executeFile(
|
||||||
getPreProgramVariables(req),
|
sasCodePath,
|
||||||
{ ...req.query, ...req.body },
|
getPreProgramVariables(req),
|
||||||
{ filesNamesMap: filesNamesMap },
|
{ ...req.query, ...req.body },
|
||||||
true
|
{ filesNamesMap: filesNamesMap },
|
||||||
)) as { webout: string; log: string }
|
true,
|
||||||
|
req.sasSession
|
||||||
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
|
let weboutRes: string | IRecordOfAny = webout
|
||||||
|
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
|
||||||
|
try {
|
||||||
|
weboutRes = JSON.parse(webout as string)
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
_webout: webout,
|
_webout: weboutRes,
|
||||||
log
|
log: parseLogToArray(log),
|
||||||
|
httpHeaders
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
|
|||||||
75
api/src/controllers/web.ts
Normal file
75
api/src/controllers/web.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { Request, Route, Tags, Post, Body, Get } from 'tsoa'
|
||||||
|
import User from '../model/User'
|
||||||
|
|
||||||
|
@Route('/')
|
||||||
|
@Tags('Web')
|
||||||
|
export class WebController {
|
||||||
|
/**
|
||||||
|
* @summary Accept a valid username/password
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Post('/login')
|
||||||
|
public async login(
|
||||||
|
@Request() req: express.Request,
|
||||||
|
@Body() body: LoginPayload
|
||||||
|
) {
|
||||||
|
return login(req, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Accept a valid username/password
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Get('/logout')
|
||||||
|
public async logout(@Request() req: express.Request) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
req.session.destroy(() => {
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async (
|
||||||
|
req: express.Request,
|
||||||
|
{ username, password }: LoginPayload
|
||||||
|
) => {
|
||||||
|
// Authenticate User
|
||||||
|
const user = await User.findOne({ username })
|
||||||
|
if (!user) throw new Error('Username is not found.')
|
||||||
|
|
||||||
|
const validPass = user.comparePassword(password)
|
||||||
|
if (!validPass) throw new Error('Invalid password.')
|
||||||
|
|
||||||
|
req.session.loggedIn = true
|
||||||
|
req.session.user = {
|
||||||
|
userId: user.id,
|
||||||
|
clientId: 'web_app',
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
isActive: user.isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loggedIn: true,
|
||||||
|
user: {
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginPayload {
|
||||||
|
/**
|
||||||
|
* Username for user
|
||||||
|
* @example "secretuser"
|
||||||
|
*/
|
||||||
|
username: string
|
||||||
|
/**
|
||||||
|
* Password for user
|
||||||
|
* @example "secretpassword"
|
||||||
|
*/
|
||||||
|
password: string
|
||||||
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { csrfProtection } from '../app'
|
||||||
import { verifyTokenInDB } from '../utils'
|
import { verifyTokenInDB } from '../utils'
|
||||||
|
|
||||||
export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
||||||
|
// if request is coming from web and has valid session
|
||||||
|
// we can validate the request and check for CSRF Token
|
||||||
|
if (req.session?.loggedIn) {
|
||||||
|
req.user = req.session.user
|
||||||
|
|
||||||
|
return csrfProtection(req, res, next)
|
||||||
|
}
|
||||||
|
|
||||||
authenticateToken(
|
authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
|
|||||||
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 = 104857600 // 100 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
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
import * as https from 'https'
|
|
||||||
import appPromise from './app'
|
|
||||||
|
|
||||||
const keyPath = path.join('..', 'certificates', 'privkey.pem')
|
|
||||||
const certPath = path.join('..', 'certificates', 'fullchain.pem')
|
|
||||||
|
|
||||||
const key = readFileSync(keyPath)
|
|
||||||
const cert = readFileSync(certPath)
|
|
||||||
|
|
||||||
appPromise.then((app) => {
|
|
||||||
const httpsServer = https.createServer({ key, cert }, app)
|
|
||||||
|
|
||||||
const sasJsPort = process.env.PORT ?? 5000
|
|
||||||
httpsServer.listen(sasJsPort, () => {
|
|
||||||
console.log(
|
|
||||||
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,44 +1,22 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
|
||||||
import { AuthController } from '../../controllers/'
|
import { AuthController } from '../../controllers/'
|
||||||
import Client from '../../model/Client'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
authenticateRefreshToken
|
authenticateRefreshToken
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
import {
|
import { authorizeValidation, tokenValidation } from '../../utils'
|
||||||
authorizeValidation,
|
|
||||||
getDesktopFields,
|
|
||||||
tokenValidation
|
|
||||||
} from '../../utils'
|
|
||||||
import { InfoJWT } from '../../types'
|
import { InfoJWT } from '../../types'
|
||||||
|
|
||||||
const authRouter = express.Router()
|
const authRouter = express.Router()
|
||||||
|
const controller = new AuthController()
|
||||||
const clientIDs = new Set()
|
|
||||||
|
|
||||||
export const populateClients = async () => {
|
|
||||||
const result = await Client.find()
|
|
||||||
clientIDs.clear()
|
|
||||||
result.forEach((r) => {
|
|
||||||
clientIDs.add(r.clientId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
authRouter.post('/authorize', async (req, res) => {
|
authRouter.post('/authorize', async (req, res) => {
|
||||||
const { error, value: body } = authorizeValidation(req.body)
|
const { error, value: body } = authorizeValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
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 {
|
try {
|
||||||
const response = await controller.authorize(body)
|
const response = await controller.authorize(body)
|
||||||
|
|
||||||
@@ -52,7 +30,6 @@ authRouter.post('/token', async (req, res) => {
|
|||||||
const { error, value: body } = tokenValidation(req.body)
|
const { error, value: body } = tokenValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
const controller = new AuthController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.token(body)
|
const response = await controller.token(body)
|
||||||
|
|
||||||
@@ -65,7 +42,6 @@ authRouter.post('/token', async (req, res) => {
|
|||||||
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
||||||
const userInfo: InfoJWT = req.user
|
const userInfo: InfoJWT = req.user
|
||||||
|
|
||||||
const controller = new AuthController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.refresh(userInfo)
|
const response = await controller.refresh(userInfo)
|
||||||
|
|
||||||
@@ -78,7 +54,6 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
|||||||
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
|
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
|
||||||
const userInfo: InfoJWT = req.user
|
const userInfo: InfoJWT = req.user
|
||||||
|
|
||||||
const controller = new AuthController()
|
|
||||||
try {
|
try {
|
||||||
await controller.logout(userInfo)
|
await controller.logout(userInfo)
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ runRouter.post('/execute', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeSASCode(req, body)
|
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)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
const statusCode = err.code
|
||||||
|
|||||||
@@ -1,13 +1,38 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import { deleteFile, readFile } from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { publishAppStream } from '../appStream'
|
||||||
|
|
||||||
|
import { multerSingle } from '../../middlewares/multer'
|
||||||
import { DriveController } from '../../controllers/'
|
import { DriveController } from '../../controllers/'
|
||||||
import { getFileDriveValidation, updateFileDriveValidation } from '../../utils'
|
import {
|
||||||
|
deployValidation,
|
||||||
|
fileBodyValidation,
|
||||||
|
fileParamValidation,
|
||||||
|
folderParamValidation
|
||||||
|
} from '../../utils'
|
||||||
|
|
||||||
|
const controller = new DriveController()
|
||||||
|
|
||||||
const driveRouter = express.Router()
|
const driveRouter = express.Router()
|
||||||
|
|
||||||
driveRouter.post('/deploy', async (req, res) => {
|
driveRouter.post('/deploy', async (req, res) => {
|
||||||
const controller = new DriveController()
|
const { error, value: body } = deployValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.deploy(req.body)
|
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)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
const statusCode = err.code
|
||||||
@@ -18,59 +43,149 @@ driveRouter.post('/deploy', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
driveRouter.post(
|
||||||
|
'/deploy/upload',
|
||||||
|
(...arg) => multerSingle('file', arg),
|
||||||
|
async (req, res) => {
|
||||||
|
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||||
|
|
||||||
|
const fileContent = await readFile(req.file.path)
|
||||||
|
|
||||||
|
let jsonContent
|
||||||
|
try {
|
||||||
|
jsonContent = JSON.parse(fileContent)
|
||||||
|
} catch (err) {
|
||||||
|
deleteFile(req.file.path)
|
||||||
|
return res.status(400).send('File containing invalid JSON content.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error, value: body } = deployValidation(jsonContent)
|
||||||
|
if (error) {
|
||||||
|
deleteFile(req.file.path)
|
||||||
|
return res.status(400).send(error.details[0].message)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.deployUpload(req.file, body)
|
||||||
|
|
||||||
|
if (body.streamWebFolder) {
|
||||||
|
const { streamServiceName } = await publishAppStream(
|
||||||
|
body.appLoc,
|
||||||
|
body.streamWebFolder,
|
||||||
|
body.streamServiceName,
|
||||||
|
body.streamLogo
|
||||||
|
)
|
||||||
|
response.streamServiceName = streamServiceName
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
const statusCode = err.code
|
||||||
|
|
||||||
|
delete err.code
|
||||||
|
|
||||||
|
res.status(statusCode).send(err)
|
||||||
|
} finally {
|
||||||
|
deleteFile(req.file.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
driveRouter.get('/file', async (req, res) => {
|
driveRouter.get('/file', async (req, res) => {
|
||||||
const { error, value: query } = getFileDriveValidation(req.query)
|
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||||
|
|
||||||
const controller = new DriveController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.getFile(query.filePath)
|
await controller.getFile(req, query._filePath)
|
||||||
res.send(response)
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
driveRouter.post('/file', async (req, res) => {
|
driveRouter.get('/folder', async (req, res) => {
|
||||||
const { error, value: body } = updateFileDriveValidation(req.body)
|
const { error: errQ, value: query } = folderParamValidation(req.query)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||||
|
|
||||||
const controller = new DriveController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.saveFile(body)
|
const response = await controller.getFolder(query._folderPath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
driveRouter.patch('/file', async (req, res) => {
|
driveRouter.delete('/file', async (req, res) => {
|
||||||
const { error, value: body } = updateFileDriveValidation(req.body)
|
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
|
||||||
|
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||||
|
|
||||||
const controller = new DriveController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.updateFile(body)
|
const response = await controller.deleteFile(query._filePath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
res.status(403).send(err.toString())
|
||||||
|
|
||||||
delete err.code
|
|
||||||
|
|
||||||
res.status(statusCode).send(err)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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) => {
|
driveRouter.get('/fileTree', async (req, res) => {
|
||||||
const controller = new DriveController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.getFileTree()
|
const response = await controller.getFileTree()
|
||||||
res.send(response)
|
res.send(response)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
verifyAdmin
|
verifyAdmin
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
|
import infoRouter from './info'
|
||||||
import driveRouter from './drive'
|
import driveRouter from './drive'
|
||||||
import stpRouter from './stp'
|
import stpRouter from './stp'
|
||||||
import codeRouter from './code'
|
import codeRouter from './code'
|
||||||
@@ -20,6 +21,7 @@ import sessionRouter from './session'
|
|||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.use('/info', infoRouter)
|
||||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||||
router.use('/auth', desktopRestrict, authRouter)
|
router.use('/auth', desktopRestrict, authRouter)
|
||||||
router.use(
|
router.use(
|
||||||
|
|||||||
16
api/src/routes/api/info.ts
Normal file
16
api/src/routes/api/info.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { InfoController } from '../../controllers'
|
||||||
|
|
||||||
|
const infoRouter = express.Router()
|
||||||
|
|
||||||
|
infoRouter.get('/', async (req, res) => {
|
||||||
|
const controller = new InfoController()
|
||||||
|
try {
|
||||||
|
const response = controller.info()
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default infoRouter
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { SessionController } from '../../controllers'
|
import { SessionController } from '../../controllers'
|
||||||
import { authenticateAccessToken } from '../../middlewares'
|
|
||||||
|
|
||||||
const sessionRouter = express.Router()
|
const sessionRouter = express.Router()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
ClientController,
|
ClientController,
|
||||||
AuthController
|
AuthController
|
||||||
} from '../../../controllers/'
|
} from '../../../controllers/'
|
||||||
import { populateClients } from '../auth'
|
|
||||||
import { InfoJWT } from '../../../types'
|
import { InfoJWT } from '../../../types'
|
||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
@@ -18,11 +17,6 @@ import {
|
|||||||
verifyTokenInDB
|
verifyTokenInDB
|
||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const clientSecret = 'someclientSecret'
|
const clientSecret = 'someclientSecret'
|
||||||
const user = {
|
const user = {
|
||||||
@@ -35,16 +29,18 @@ const user = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('auth', () => {
|
describe('auth', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const clientController = new ClientController()
|
const clientController = new ClientController()
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
await clientController.createClient({ clientId, clientSecret })
|
await clientController.createClient({ clientId, clientSecret })
|
||||||
await populateClients()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -156,7 +152,7 @@ describe('auth', () => {
|
|||||||
})
|
})
|
||||||
.expect(403)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Invalid clientId.')
|
expect(res.text).toEqual('Error: Invalid clientId.')
|
||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
|||||||
import { UserController, ClientController } from '../../../controllers/'
|
import { UserController, ClientController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
clientId: 'someclientID',
|
clientId: 'someclientID',
|
||||||
clientSecret: 'someclientSecret'
|
clientSecret: 'someclientSecret'
|
||||||
@@ -28,12 +23,15 @@ const newClient = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('client', () => {
|
describe('client', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const clientController = new ClientController()
|
const clientController = new ClientController()
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +1,37 @@
|
|||||||
|
import path from 'path'
|
||||||
import { Express } from 'express'
|
import { Express } from 'express'
|
||||||
import mongoose, { Mongoose } from 'mongoose'
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
import { MongoMemoryServer } from 'mongodb-memory-server'
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
|
|
||||||
|
import {
|
||||||
|
folderExists,
|
||||||
|
fileExists,
|
||||||
|
readFile,
|
||||||
|
deleteFolder,
|
||||||
|
generateTimestamp,
|
||||||
|
copy,
|
||||||
|
createFolder,
|
||||||
|
createFile,
|
||||||
|
ServiceMember,
|
||||||
|
FolderMember
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
import * as fileUtilModules from '../../../utils/file'
|
||||||
|
|
||||||
|
const timestamp = generateTimestamp()
|
||||||
|
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
||||||
|
jest
|
||||||
|
.spyOn(fileUtilModules, 'getTmpFolderPath')
|
||||||
|
.mockImplementation(() => tmpFolder)
|
||||||
|
jest
|
||||||
|
.spyOn(fileUtilModules, 'getTmpUploadsPath')
|
||||||
|
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||||
|
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
import { getTmpFilesFolderPath } from '../../../utils/file'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||||
import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils'
|
const { getTmpFilesFolderPath } = fileUtilModules
|
||||||
import path from 'path'
|
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
|
||||||
import { FolderMember, ServiceMember } from '../../../types'
|
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const user = {
|
const user = {
|
||||||
@@ -25,45 +42,53 @@ const user = {
|
|||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('files', () => {
|
describe('drive', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
|
|
||||||
|
let accessToken: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
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 () => {
|
afterAll(async () => {
|
||||||
await con.connection.dropDatabase()
|
await con.connection.dropDatabase()
|
||||||
await con.connection.close()
|
await con.connection.close()
|
||||||
await mongoServer.stop()
|
await mongoServer.stop()
|
||||||
|
await deleteFolder(tmpFolder)
|
||||||
})
|
})
|
||||||
describe('deploy', () => {
|
|
||||||
let accessToken: string
|
|
||||||
let dbUser: any
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
describe('deploy', () => {
|
||||||
dbUser = await controller.createUser(user)
|
|
||||||
accessToken = generateAccessToken({
|
|
||||||
clientId,
|
|
||||||
userId: dbUser.id
|
|
||||||
})
|
|
||||||
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
|
|
||||||
})
|
|
||||||
const shouldFailAssertion = async (payload: any) => {
|
const shouldFailAssertion = async (payload: any) => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/drive/deploy')
|
.post('/SASjsApi/drive/deploy')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send(payload)
|
.send({ appLoc: '/Public', fileTree: payload })
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(400)
|
expect(res.statusCode).toEqual(400)
|
||||||
expect(res.body).toEqual({
|
|
||||||
status: 'failure',
|
if (payload === undefined) {
|
||||||
message: 'Provided not supported data format.',
|
expect(res.text).toEqual('"fileTree" is required')
|
||||||
example: getTreeExample()
|
} 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 () => {
|
it('should respond with payload example if valid payload was not provided', async () => {
|
||||||
@@ -122,11 +147,11 @@ describe('files', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with payload example if valid payload was not provided', async () => {
|
it('should successfully deploy if valid payload was provided', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/drive/deploy')
|
.post('/SASjsApi/drive/deploy')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send({ fileTree: getTreeExample() })
|
.send({ appLoc: '/public', fileTree: getTreeExample() })
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
expect(res.statusCode).toEqual(200)
|
||||||
expect(res.text).toEqual(
|
expect(res.text).toEqual(
|
||||||
@@ -136,6 +161,7 @@ describe('files', () => {
|
|||||||
|
|
||||||
const testJobFolder = path.join(
|
const testJobFolder = path.join(
|
||||||
getTmpFilesFolderPath(),
|
getTmpFilesFolderPath(),
|
||||||
|
'public',
|
||||||
'jobs',
|
'jobs',
|
||||||
'extract'
|
'extract'
|
||||||
)
|
)
|
||||||
@@ -144,13 +170,504 @@ describe('files', () => {
|
|||||||
const exampleService = getExampleService()
|
const exampleService = getExampleService()
|
||||||
const testJobFile = path.join(testJobFolder, exampleService.name) + '.sas'
|
const testJobFile = path.join(testJobFolder, exampleService.name) + '.sas'
|
||||||
|
|
||||||
console.log(`[testJobFile]`, testJobFile)
|
|
||||||
|
|
||||||
await expect(fileExists(testJobFile)).resolves.toEqual(true)
|
await expect(fileExists(testJobFile)).resolves.toEqual(true)
|
||||||
|
|
||||||
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
||||||
|
|
||||||
await deleteFolder(getTmpFilesFolderPath())
|
await deleteFolder(path.join(getTmpFilesFolderPath(), 'public'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('folder', () => {
|
||||||
|
describe('get', () => {
|
||||||
|
const getFolderApi = '/SASjsApi/drive/folder'
|
||||||
|
|
||||||
|
it('should get root SAS folder on drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get(getFolderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({ files: [], folders: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get a SAS folder on drive having _folderPath as query param', async () => {
|
||||||
|
const pathToDrive = fileUtilModules.getTmpFilesFolderPath()
|
||||||
|
|
||||||
|
const dirLevel1 = 'level1'
|
||||||
|
const dirLevel2 = 'level2'
|
||||||
|
const fileLevel1 = 'file1'
|
||||||
|
const fileLevel2 = 'file2'
|
||||||
|
|
||||||
|
await createFolder(path.join(pathToDrive, dirLevel1, dirLevel2))
|
||||||
|
await createFile(
|
||||||
|
path.join(pathToDrive, dirLevel1, fileLevel1),
|
||||||
|
'some file content'
|
||||||
|
)
|
||||||
|
await createFile(
|
||||||
|
path.join(pathToDrive, dirLevel1, dirLevel2, fileLevel2),
|
||||||
|
'some file content'
|
||||||
|
)
|
||||||
|
|
||||||
|
const res1 = await request(app)
|
||||||
|
.get(getFolderApi)
|
||||||
|
.query({ _folderPath: '/' })
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
|
||||||
|
expect(res1.statusCode).toEqual(200)
|
||||||
|
expect(res1.body).toEqual({ files: [], folders: [dirLevel1] })
|
||||||
|
|
||||||
|
const res2 = await request(app)
|
||||||
|
.get(getFolderApi)
|
||||||
|
.query({ _folderPath: dirLevel1 })
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
|
||||||
|
expect(res2.statusCode).toEqual(200)
|
||||||
|
expect(res2.body).toEqual({ files: [fileLevel1], folders: [dirLevel2] })
|
||||||
|
|
||||||
|
const res3 = await request(app)
|
||||||
|
.get(getFolderApi)
|
||||||
|
.query({ _folderPath: `${dirLevel1}/${dirLevel2}` })
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
|
||||||
|
expect(res3.statusCode).toEqual(200)
|
||||||
|
expect(res3.body).toEqual({ files: [fileLevel2], folders: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app).get(getFolderApi).expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if folder is not present', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get(getFolderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if folderPath outside Drive', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get(getFolderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _folderPath: '/../path/code.sas' })
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if folderPath is of a file', async () => {
|
||||||
|
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const filePath = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const pathToCopy = path.join(
|
||||||
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
|
filePath
|
||||||
|
)
|
||||||
|
await copy(fileToCopyPath, pathToCopy)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get(getFolderApi)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _folderPath: filePath })
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Not a Folder.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('file', () => {
|
||||||
|
describe('create', () => {
|
||||||
|
it('should create a SAS file on drive having filePath as form field', async () => {
|
||||||
|
const pathToUpload = `/my/path/code-1.sas`
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.field('filePath', pathToUpload)
|
||||||
|
.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 pathToUpload = `/my/path/code-2.sas`
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _filePath: pathToUpload })
|
||||||
|
.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-${generateTimestamp()}.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(110 * 1024 * 1024)) // 110mb
|
||||||
|
|
||||||
|
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: 100 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-3.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')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _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(110 * 1024 * 1024)) // 110mb
|
||||||
|
|
||||||
|
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: 100 MB'
|
||||||
|
)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
it('should get a SAS file on drive having _filePath as query param', async () => {
|
||||||
|
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
|
const fileToCopyContent = await readFile(fileToCopyPath)
|
||||||
|
const filePath = '/my/path/code.sas'
|
||||||
|
|
||||||
|
const pathToCopy = path.join(
|
||||||
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
|
filePath
|
||||||
|
)
|
||||||
|
await copy(fileToCopyPath, pathToCopy)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _filePath: filePath })
|
||||||
|
|
||||||
|
expect(res.statusCode).toEqual(200)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
expect(res.text).toEqual(fileToCopyContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app).get('/SASjsApi/drive/file').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)
|
||||||
|
.get('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _filePath: `/my/path/code-4.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 res = await request(app)
|
||||||
|
.get('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _filePath: '/../path/code.sas' })
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Cannot get file outside drive.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.patch('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.query({ _filePath: '/my/path/code.exe' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Invalid file extension')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if filePath is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/drive/file')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"_filePath" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
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
|
||||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
|||||||
import { UserController, GroupController } from '../../../controllers/'
|
import { UserController, GroupController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
displayName: 'Test Admin',
|
displayName: 'Test Admin',
|
||||||
@@ -36,11 +31,14 @@ const userController = new UserController()
|
|||||||
const groupController = new GroupController()
|
const groupController = new GroupController()
|
||||||
|
|
||||||
describe('group', () => {
|
describe('group', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
let adminAccessToken: string
|
let adminAccessToken: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
|
|||||||
20
api/src/routes/api/spec/info.spec.ts
Normal file
20
api/src/routes/api/spec/info.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import request from 'supertest'
|
||||||
|
import appPromise from '../../../app'
|
||||||
|
|
||||||
|
describe('Info', () => {
|
||||||
|
let app: Express
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should should return configured information of the server instance', async () => {
|
||||||
|
const res = await request(app).get('/SASjsApi/info').expect(200)
|
||||||
|
|
||||||
|
expect(res.body.mode).toEqual('server')
|
||||||
|
expect(res.body.cors).toEqual('disable')
|
||||||
|
expect(res.body.whiteList).toEqual([])
|
||||||
|
expect(res.body.protocol).toEqual('http')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
|||||||
import { UserController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
displayName: 'Test Admin',
|
displayName: 'Test Admin',
|
||||||
@@ -30,10 +25,13 @@ const user = {
|
|||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
|
|
||||||
describe('user', () => {
|
describe('user', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { executeProgramRawValidation, runSASValidation } from '../../utils'
|
import { executeProgramRawValidation } from '../../utils'
|
||||||
import { STPController } from '../../controllers/'
|
import { STPController } from '../../controllers/'
|
||||||
import { FileUploadController } from '../../controllers/internal'
|
import { FileUploadController } from '../../controllers/internal'
|
||||||
|
|
||||||
@@ -14,6 +14,12 @@ stpRouter.get('/execute', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnRaw(req, query._program)
|
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)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
const statusCode = err.code
|
||||||
@@ -26,7 +32,7 @@ stpRouter.get('/execute', async (req, res) => {
|
|||||||
|
|
||||||
stpRouter.post(
|
stpRouter.post(
|
||||||
'/execute',
|
'/execute',
|
||||||
fileUploadController.preuploadMiddleware,
|
fileUploadController.preUploadMiddleware,
|
||||||
fileUploadController.getMulterUploadObject().any(),
|
fileUploadController.getMulterUploadObject().any(),
|
||||||
async (req: any, res: any) => {
|
async (req: any, res: any) => {
|
||||||
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
||||||
@@ -40,6 +46,12 @@ stpRouter.post(
|
|||||||
body,
|
body,
|
||||||
query?._program
|
query?._program
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (response instanceof Buffer) {
|
||||||
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
|
return res.end(response)
|
||||||
|
}
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const statusCode = err.code
|
const statusCode = err.code
|
||||||
|
|||||||
44
api/src/routes/appStream/appStreamHtml.ts
Normal file
44
api/src/routes/appStream/appStreamHtml.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { AppStreamConfig } from '../../types'
|
||||||
|
import { script } from './script'
|
||||||
|
import { style } from './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}"
|
||||||
|
onerror="this.src = '${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('')}
|
||||||
|
<a class="app" title="Upload build.json">
|
||||||
|
<input id="fileId" type="file" hidden />
|
||||||
|
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
|
||||||
|
<img src="/plus.png" />
|
||||||
|
</button>
|
||||||
|
<span id="uploadMessage">Upload New App</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
${script}
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
65
api/src/routes/appStream/index.ts
Normal file
65
api/src/routes/appStream/index.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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}`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
58
api/src/routes/appStream/script.ts
Normal file
58
api/src/routes/appStream/script.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
export const script = `<script>
|
||||||
|
const inputElement = document.getElementById('fileId')
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('uploadButton')
|
||||||
|
.addEventListener('click', function () {
|
||||||
|
inputElement.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
inputElement.addEventListener(
|
||||||
|
'change',
|
||||||
|
function () {
|
||||||
|
const fileList = this.files /* now you can work with the file list */
|
||||||
|
|
||||||
|
updateFileUploadMessage('Requesting ...')
|
||||||
|
|
||||||
|
const file = fileList[0]
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('file', file)
|
||||||
|
fetch('/SASjsApi/drive/deploy/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
const { status, ok } = res
|
||||||
|
if (status === 200 && ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
return (
|
||||||
|
data.message +
|
||||||
|
'\\nstreamServiceName: ' +
|
||||||
|
data.streamServiceName +
|
||||||
|
'\\nrefreshing page once alert box closes.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw await res.text()
|
||||||
|
})
|
||||||
|
.then((message) => {
|
||||||
|
alert(message)
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert(error)
|
||||||
|
resetFileUpload()
|
||||||
|
updateFileUploadMessage('Upload New App')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
function updateFileUploadMessage(message) {
|
||||||
|
document.getElementById('uploadMessage').innerHTML = message
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFileUpload() {
|
||||||
|
inputElement.value = null
|
||||||
|
}
|
||||||
|
</script>`
|
||||||
22
api/src/routes/appStream/style.ts
Normal file
22
api/src/routes/appStream/style.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const style = `<style>
|
||||||
|
* {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.app-container .app {
|
||||||
|
width: 150px;
|
||||||
|
margin: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.app-container .app img{
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>`
|
||||||
17
api/src/routes/setupRoutes.ts
Normal file
17
api/src/routes/setupRoutes.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
|
||||||
|
import webRouter from './web'
|
||||||
|
import apiRouter from './api'
|
||||||
|
import appStreamRouter from './appStream'
|
||||||
|
|
||||||
|
export const setupRoutes = (app: Express) => {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/', webRouter)
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import { csrfProtection } from '../../app'
|
||||||
import webRouter from './web'
|
import webRouter from './web'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.use(csrfProtection)
|
||||||
|
|
||||||
router.use('/', webRouter)
|
router.use('/', webRouter)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1,34 +1,43 @@
|
|||||||
import { readFile } from '@sasjs/utils'
|
|
||||||
import express from 'express'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getWebBuildFolderPath } from '../../utils'
|
import express from 'express'
|
||||||
|
import { fileExists } from '@sasjs/utils'
|
||||||
|
import { WebController } from '../../controllers/web'
|
||||||
|
import { getWebBuildFolderPath, loginWebValidation } from '../../utils'
|
||||||
|
|
||||||
const webRouter = express.Router()
|
const webRouter = express.Router()
|
||||||
|
|
||||||
const codeToInject = `
|
webRouter.get('/', async (req, res) => {
|
||||||
<script>
|
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
||||||
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
|
|
||||||
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
|
|
||||||
</script>`
|
|
||||||
|
|
||||||
webRouter.get('/', async (_, res) => {
|
if (await fileExists(indexHtmlPath)) {
|
||||||
let content: string
|
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||||
|
return res.sendFile(indexHtmlPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send('Web Build is not present')
|
||||||
|
})
|
||||||
|
|
||||||
|
webRouter.post('/login', async (req, res) => {
|
||||||
|
const { error, value: body } = loginWebValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
const controller = new WebController()
|
||||||
try {
|
try {
|
||||||
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
const response = await controller.login(req, body)
|
||||||
content = await readFile(indexHtmlPath)
|
res.send(response)
|
||||||
} catch (_) {
|
} catch (err: any) {
|
||||||
return res.send('Web Build is not present')
|
res.status(400).send(err.toString())
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const { MODE } = process.env
|
webRouter.get('/logout', async (req, res) => {
|
||||||
if (MODE?.trim() !== 'server') {
|
const controller = new WebController()
|
||||||
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
|
try {
|
||||||
|
await controller.logout(req)
|
||||||
res.setHeader('Content-Type', 'text/html')
|
res.status(200).send()
|
||||||
return res.send(injectedContent)
|
} catch (err: any) {
|
||||||
|
res.status(400).send(err.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.send(content)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default webRouter
|
export default webRouter
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
import appPromise from './app'
|
import { createServer } from 'https'
|
||||||
|
|
||||||
appPromise.then((app) => {
|
import appPromise from './app'
|
||||||
const sasJsPort = process.env.PORT ?? 5000
|
import { getCertificates } from './utils'
|
||||||
app.listen(sasJsPort, () => {
|
|
||||||
console.log(
|
appPromise.then(async (app) => {
|
||||||
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
|
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,47 +0,0 @@
|
|||||||
export interface FileTree {
|
|
||||||
members: (FolderMember | ServiceMember)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MemberType {
|
|
||||||
folder = 'folder',
|
|
||||||
service = 'service'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FolderMember {
|
|
||||||
name: string
|
|
||||||
type: MemberType.folder
|
|
||||||
members: (FolderMember | ServiceMember)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServiceMember {
|
|
||||||
name: string
|
|
||||||
type: MemberType.service
|
|
||||||
code: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isFileTree = (arg: any): arg is FileTree =>
|
|
||||||
arg &&
|
|
||||||
arg.members &&
|
|
||||||
Array.isArray(arg.members) &&
|
|
||||||
arg.members.filter(
|
|
||||||
(member: FolderMember | ServiceMember) =>
|
|
||||||
!isFolderMember(member) && !isServiceMember(member)
|
|
||||||
).length === 0
|
|
||||||
|
|
||||||
const isFolderMember = (arg: any): arg is FolderMember =>
|
|
||||||
arg &&
|
|
||||||
typeof arg.name === 'string' &&
|
|
||||||
arg.type === MemberType.folder &&
|
|
||||||
arg.members &&
|
|
||||||
Array.isArray(arg.members) &&
|
|
||||||
arg.members.filter(
|
|
||||||
(member: FolderMember | ServiceMember) =>
|
|
||||||
!isFolderMember(member) && !isServiceMember(member)
|
|
||||||
).length === 0
|
|
||||||
|
|
||||||
const isServiceMember = (arg: any): arg is ServiceMember =>
|
|
||||||
arg &&
|
|
||||||
typeof arg.name === 'string' &&
|
|
||||||
arg.type === MemberType.service &&
|
|
||||||
arg.code &&
|
|
||||||
typeof arg.code === 'string'
|
|
||||||
7
api/src/types/Process.d.ts
vendored
7
api/src/types/Process.d.ts
vendored
@@ -1,7 +0,0 @@
|
|||||||
declare namespace NodeJS {
|
|
||||||
export interface Process {
|
|
||||||
sasLoc: string
|
|
||||||
driveLoc: string
|
|
||||||
sessionController?: import('../controllers/internal').SessionController
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { MacroVars } from '@sasjs/utils'
|
|
||||||
|
|
||||||
export interface ExecutionQuery {
|
|
||||||
_program: string
|
|
||||||
macroVars?: MacroVars
|
|
||||||
_debug?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileQuery {
|
|
||||||
filePath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isExecutionQuery = (arg: any): arg is ExecutionQuery =>
|
|
||||||
arg && !Array.isArray(arg) && typeof arg._program === 'string'
|
|
||||||
|
|
||||||
export const isFileQuery = (arg: any): arg is FileQuery =>
|
|
||||||
arg && !Array.isArray(arg) && typeof arg.filePath === 'string'
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
// TODO: uppercase types
|
// TODO: uppercase types
|
||||||
|
export * from './AppStreamConfig'
|
||||||
export * from './Execution'
|
export * from './Execution'
|
||||||
export * from './FileTree'
|
|
||||||
export * from './InfoJWT'
|
export * from './InfoJWT'
|
||||||
export * from './PreProgramVars'
|
export * from './PreProgramVars'
|
||||||
export * from './Request'
|
|
||||||
export * from './Session'
|
export * from './Session'
|
||||||
export * from './TreeNode'
|
export * from './TreeNode'
|
||||||
|
|||||||
14
api/src/types/system/express-session.d.ts
vendored
Normal file
14
api/src/types/system/express-session.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import express from 'express'
|
||||||
|
declare module 'express-session' {
|
||||||
|
interface SessionData {
|
||||||
|
loggedIn: boolean
|
||||||
|
user: {
|
||||||
|
userId: number
|
||||||
|
clientId: string
|
||||||
|
username: string
|
||||||
|
displayName: string
|
||||||
|
isAdmin: boolean
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
api/src/types/system/global.d.ts
vendored
Normal file
1
api/src/types/system/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import 'jest-extended'
|
||||||
8
api/src/types/system/process.d.ts
vendored
Normal file
8
api/src/types/system/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
|
||||||
|
}
|
||||||
|
}
|
||||||
89
api/src/utils/appStreamConfig.ts
Normal file
89
api/src/utils/appStreamConfig.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { createFile, fileExists, readFile } from '@sasjs/utils'
|
||||||
|
import { publishAppStream } from '../routes/appStream'
|
||||||
|
import { AppStreamConfig } from '../types'
|
||||||
|
|
||||||
|
import { getTmpAppStreamConfigPath } from './file'
|
||||||
|
|
||||||
|
export const loadAppStreamConfig = async () => {
|
||||||
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,46 +1,15 @@
|
|||||||
import path from 'path'
|
|
||||||
import mongoose from 'mongoose'
|
import mongoose from 'mongoose'
|
||||||
import { configuration } from '../../package.json'
|
import { seedDB } from './seedDB'
|
||||||
import { getDesktopFields } from '.'
|
|
||||||
import { populateClients } from '../routes/api/auth'
|
|
||||||
import { getRealPath } from '@sasjs/utils'
|
|
||||||
|
|
||||||
export const connectDB = async () => {
|
export const connectDB = async () => {
|
||||||
// NOTE: when exporting app.js as agent for supertest
|
try {
|
||||||
// we should exlcude connecting to the real database
|
await mongoose.connect(process.env.DB_CONNECT as string)
|
||||||
if (process.env.NODE_ENV === 'test') {
|
} catch (err) {
|
||||||
process.driveLoc = path.join(process.cwd(), 'tmp')
|
throw new Error('Unable to connect to DB!')
|
||||||
return
|
|
||||||
} else {
|
|
||||||
const { MODE } = process.env
|
|
||||||
|
|
||||||
if (MODE?.trim() !== 'server') {
|
|
||||||
console.log('Running in Destop Mode, no DB to connect.')
|
|
||||||
|
|
||||||
const { sasLoc, driveLoc } = await getDesktopFields()
|
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
|
||||||
process.driveLoc = driveLoc
|
|
||||||
} else {
|
|
||||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
|
||||||
|
|
||||||
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
|
||||||
process.driveLoc = getRealPath(
|
|
||||||
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
|
||||||
|
|
||||||
if (MODE?.trim() !== 'server') return
|
|
||||||
|
|
||||||
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
|
|
||||||
if (err) throw err
|
|
||||||
|
|
||||||
console.log('Connected to db!')
|
|
||||||
|
|
||||||
await populateClients()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Connected to DB!')
|
||||||
|
await seedDB()
|
||||||
|
|
||||||
|
return mongoose.connection
|
||||||
}
|
}
|
||||||
|
|||||||
34
api/src/utils/copySASjsCore.ts
Normal file
34
api/src/utils/copySASjsCore.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import {
|
||||||
|
asyncForEach,
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
readFile
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||||
|
|
||||||
|
export const copySASjsCore = async () => {
|
||||||
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
|
console.log('Copying Macros from container to drive(tmp).')
|
||||||
|
|
||||||
|
const macrosDrivePath = getTmpMacrosPath()
|
||||||
|
|
||||||
|
await deleteFolder(macrosDrivePath)
|
||||||
|
await createFolder(macrosDrivePath)
|
||||||
|
|
||||||
|
const macros = await readFile(sasJSCoreMacrosInfo)
|
||||||
|
|
||||||
|
await asyncForEach(macros.split('\n'), async (macroName) => {
|
||||||
|
const macroFileSourcePath = path.join(sasJSCoreMacros, macroName)
|
||||||
|
const macroContent = await readFile(macroFileSourcePath)
|
||||||
|
|
||||||
|
const macroFileDestPath = path.join(macrosDrivePath, macroName)
|
||||||
|
|
||||||
|
await createFile(macroFileDestPath, macroContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Macros Drive Path:', 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
|
||||||
|
}
|
||||||
@@ -8,11 +8,21 @@ export const sysInitCompiledPath = path.join(
|
|||||||
'systemInitCompiled.sas'
|
'systemInitCompiled.sas'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||||
|
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||||
|
|
||||||
export const getWebBuildFolderPath = () =>
|
export const getWebBuildFolderPath = () =>
|
||||||
path.join(codebaseRoot, 'web', 'build')
|
path.join(codebaseRoot, 'web', 'build')
|
||||||
|
|
||||||
export const getTmpFolderPath = () => process.driveLoc
|
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 = () =>
|
export const getTmpFilesFolderPath = () =>
|
||||||
path.join(getTmpFolderPath(), 'files')
|
path.join(getTmpFolderPath(), 'files')
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { InfoJWT } from '../types'
|
|||||||
|
|
||||||
export const generateAccessToken = (data: InfoJWT) =>
|
export const generateAccessToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
|
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
|
||||||
expiresIn: '1h'
|
expiresIn: '1day'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { InfoJWT } from '../types'
|
|||||||
|
|
||||||
export const generateRefreshToken = (data: InfoJWT) =>
|
export const generateRefreshToken = (data: InfoJWT) =>
|
||||||
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
|
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
|
||||||
expiresIn: '1day'
|
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
|
||||||
|
}
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
|
export * from './appStreamConfig'
|
||||||
export * from './connectDB'
|
export * from './connectDB'
|
||||||
|
export * from './copySASjsCore'
|
||||||
|
export * from './extractHeaders'
|
||||||
export * from './file'
|
export * from './file'
|
||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
export * from './generateAuthCode'
|
export * from './generateAuthCode'
|
||||||
export * from './generateRefreshToken'
|
export * from './generateRefreshToken'
|
||||||
|
export * from './getCertificates'
|
||||||
export * from './getDesktopFields'
|
export * from './getDesktopFields'
|
||||||
|
export * from './isDebugOn'
|
||||||
|
export * from './parseLogToArray'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
export * from './saveTokensInDB'
|
export * from './saveTokensInDB'
|
||||||
export * from './sleep'
|
export * from './seedDB'
|
||||||
|
export * from './setProcessVariables'
|
||||||
|
export * from './setupFolders'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
export * from './verifyTokenInDB'
|
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 }))
|
||||||
|
}
|
||||||
35
api/src/utils/seedDB.ts
Normal file
35
api/src/utils/seedDB.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Client from '../model/Client'
|
||||||
|
import User from '../model/User'
|
||||||
|
|
||||||
|
const CLIENT = {
|
||||||
|
clientId: 'clientID1',
|
||||||
|
clientSecret: 'clientSecret'
|
||||||
|
}
|
||||||
|
const ADMIN_USER = {
|
||||||
|
id: 1,
|
||||||
|
displayName: 'Super Admin',
|
||||||
|
username: 'secretuser',
|
||||||
|
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
|
||||||
|
isAdmin: true,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const seedDB = async () => {
|
||||||
|
// Checking if client is already in the database
|
||||||
|
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
||||||
|
if (!clientExist) {
|
||||||
|
const client = new Client(CLIENT)
|
||||||
|
await client.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if user is already in the database
|
||||||
|
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||||
|
if (!usernameExist) {
|
||||||
|
const user = new User(ADMIN_USER)
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
api/src/utils/setProcessVariables.ts
Normal file
30
api/src/utils/setProcessVariables.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { configuration } from '../../package.json'
|
||||||
|
import { getDesktopFields } from '.'
|
||||||
|
|
||||||
|
export const setProcessVariables = async () => {
|
||||||
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
process.driveLoc = path.join(process.cwd(), 'tmp')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { MODE } = process.env
|
||||||
|
|
||||||
|
if (MODE?.trim() === 'server') {
|
||||||
|
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||||
|
|
||||||
|
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
||||||
|
const absPath = getAbsolutePath(DRIVE_PATH ?? 'tmp', process.cwd())
|
||||||
|
process.driveLoc = getRealPath(absPath)
|
||||||
|
} else {
|
||||||
|
const { sasLoc, driveLoc } = await getDesktopFields()
|
||||||
|
|
||||||
|
process.sasLoc = sasLoc
|
||||||
|
process.driveLoc = driveLoc
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
|
}
|
||||||
7
api/src/utils/setupFolders.ts
Normal file
7
api/src/utils/setupFolders.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFolder } from '@sasjs/utils'
|
||||||
|
import { getTmpFilesFolderPath } from './file'
|
||||||
|
|
||||||
|
export const setupFolders = async () => {
|
||||||
|
const drivePath = getTmpFilesFolderPath()
|
||||||
|
await createFolder(drivePath)
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export const sleep = async (delay: number) => {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
||||||
}
|
|
||||||
52
api/src/utils/specs/extractHeaders.spec.ts
Normal file
52
api/src/utils/specs/extractHeaders.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { extractHeaders } from '..'
|
||||||
|
|
||||||
|
describe('extractHeaders', () => {
|
||||||
|
it('should return valid http headers', () => {
|
||||||
|
const headers = extractHeaders(`
|
||||||
|
Content-type: application/csv
|
||||||
|
Cache-Control: public, max-age=2000
|
||||||
|
Content-type: application/text
|
||||||
|
Cache-Control: public, max-age=1500
|
||||||
|
Content-type: application/zip
|
||||||
|
Cache-Control: public, max-age=1000
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(headers).toEqual({
|
||||||
|
'content-type': 'application/zip',
|
||||||
|
'cache-control': 'public, max-age=1000'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not return http headers if last occurrence is blank', () => {
|
||||||
|
const headers = extractHeaders(`
|
||||||
|
Content-type: application/csv
|
||||||
|
Cache-Control: public, max-age=1000
|
||||||
|
Content-type: application/text
|
||||||
|
Content-type:
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(headers).toEqual({ 'cache-control': 'public, max-age=1000' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return only valid http headers', () => {
|
||||||
|
const headers = extractHeaders(`
|
||||||
|
Content-type[]: application/csv
|
||||||
|
Content//-type: application/text
|
||||||
|
Content()-type: application/zip
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(headers).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return http headers if empty', () => {
|
||||||
|
const headers = extractHeaders('')
|
||||||
|
|
||||||
|
expect(headers).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return http headers if not provided', () => {
|
||||||
|
const headers = extractHeaders()
|
||||||
|
|
||||||
|
expect(headers).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
33
api/src/utils/specs/parseLogToArray.spec.ts
Normal file
33
api/src/utils/specs/parseLogToArray.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { parseLogToArray } from '..'
|
||||||
|
|
||||||
|
describe('parseLogToArray', () => {
|
||||||
|
it('should parse log to array type', () => {
|
||||||
|
const log = parseLogToArray(`
|
||||||
|
line 1 of log content
|
||||||
|
line 2 of log content
|
||||||
|
line 3 of log content
|
||||||
|
line 4 of log content
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(log).toEqual([
|
||||||
|
{ line: '' },
|
||||||
|
{ line: 'line 1 of log content' },
|
||||||
|
{ line: 'line 2 of log content' },
|
||||||
|
{ line: 'line 3 of log content' },
|
||||||
|
{ line: 'line 4 of log content' },
|
||||||
|
{ line: ' ' }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse log to array type if empty', () => {
|
||||||
|
const log = parseLogToArray('')
|
||||||
|
|
||||||
|
expect(log).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse log to array type if not provided', () => {
|
||||||
|
const log = parseLogToArray()
|
||||||
|
|
||||||
|
expect(log).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
import path from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { getTmpSessionsFolderPath } from '.'
|
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
import { listFilesInFolder } from '@sasjs/utils'
|
import { listFilesInFolder } from '@sasjs/utils'
|
||||||
|
|
||||||
|
interface FilenameMapSingle {
|
||||||
|
fieldName: string
|
||||||
|
originalName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilenamesMap {
|
||||||
|
[key: string]: FilenameMapSingle
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadedFiles extends FilenameMapSingle {
|
||||||
|
fileref: string
|
||||||
|
filepath: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It will create an object that maps hashed file names to the original names
|
* It will create an object that maps hashed file names to the original names
|
||||||
* @param files array of files to be mapped
|
* @param files array of files to be mapped
|
||||||
@@ -12,10 +24,13 @@ import { listFilesInFolder } from '@sasjs/utils'
|
|||||||
export const makeFilesNamesMap = (files: MulterFile[]) => {
|
export const makeFilesNamesMap = (files: MulterFile[]) => {
|
||||||
if (!files) return null
|
if (!files) return null
|
||||||
|
|
||||||
const filesNamesMap: { [key: string]: string } = {}
|
const filesNamesMap: FilenamesMap = {}
|
||||||
|
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
filesNamesMap[file.filename] = file.fieldname
|
filesNamesMap[file.filename] = {
|
||||||
|
fieldName: file.fieldname,
|
||||||
|
originalName: file.originalname
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filesNamesMap
|
return filesNamesMap
|
||||||
@@ -28,17 +43,12 @@ export const makeFilesNamesMap = (files: MulterFile[]) => {
|
|||||||
* @returns generated sas code
|
* @returns generated sas code
|
||||||
*/
|
*/
|
||||||
export const generateFileUploadSasCode = async (
|
export const generateFileUploadSasCode = async (
|
||||||
filesNamesMap: any,
|
filesNamesMap: FilenamesMap,
|
||||||
sasSessionFolder: string
|
sasSessionFolder: string
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
let uploadSasCode = ''
|
let uploadSasCode = ''
|
||||||
let fileCount = 0
|
let fileCount = 0
|
||||||
let uploadedFilesMap: {
|
const uploadedFiles: UploadedFiles[] = []
|
||||||
fileref: string
|
|
||||||
filepath: string
|
|
||||||
filename: string
|
|
||||||
count: number
|
|
||||||
}[] = []
|
|
||||||
|
|
||||||
const sasSessionFolderList: string[] = await listFilesInFolder(
|
const sasSessionFolderList: string[] = await listFilesInFolder(
|
||||||
sasSessionFolder
|
sasSessionFolder
|
||||||
@@ -50,31 +60,32 @@ export const generateFileUploadSasCode = async (
|
|||||||
if (fileName.includes('req_file')) {
|
if (fileName.includes('req_file')) {
|
||||||
fileCount++
|
fileCount++
|
||||||
|
|
||||||
uploadedFilesMap.push({
|
uploadedFiles.push({
|
||||||
fileref: `_sjs${fileCountString}`,
|
fileref: `_sjs${fileCountString}`,
|
||||||
filepath: `${sasSessionFolder}/${fileName}`,
|
filepath: `${sasSessionFolder}/${fileName}`,
|
||||||
filename: filesNamesMap[fileName],
|
originalName: filesNamesMap[fileName].originalName,
|
||||||
|
fieldName: filesNamesMap[fileName].fieldName,
|
||||||
count: fileCount
|
count: fileCount
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
for (let uploadedMap of uploadedFilesMap) {
|
for (const uploadedFile of uploadedFiles) {
|
||||||
uploadSasCode += `\nfilename ${uploadedMap.fileref} "${uploadedMap.filepath}";`
|
uploadSasCode += `\nfilename ${uploadedFile.fileref} "${uploadedFile.filepath}";`
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
|
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
|
||||||
|
|
||||||
for (let uploadedMap of uploadedFilesMap) {
|
for (const uploadedFile of uploadedFiles) {
|
||||||
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedMap.count}=${uploadedMap.filepath};`
|
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedFile.count}=${uploadedFile.originalName};`
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let uploadedMap of uploadedFilesMap) {
|
for (const uploadedFile of uploadedFiles) {
|
||||||
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedMap.count}=${uploadedMap.fileref};`
|
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedFile.count}=${uploadedFile.fileref};`
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let uploadedMap of uploadedFilesMap) {
|
for (const uploadedFile of uploadedFiles) {
|
||||||
uploadSasCode += `\n%let _WEBIN_NAME${uploadedMap.count}=${uploadedMap.filename};`
|
uploadSasCode += `\n%let _WEBIN_NAME${uploadedFile.count}=${uploadedFile.fieldName};`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileCount > 0) {
|
if (fileCount > 0) {
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import Joi from 'joi'
|
import Joi from 'joi'
|
||||||
|
|
||||||
const usernameSchema = Joi.string().alphanum().min(6).max(20)
|
const usernameSchema = Joi.string().alphanum().min(3).max(16)
|
||||||
const passwordSchema = Joi.string().min(6).max(1024)
|
const passwordSchema = Joi.string().min(6).max(1024)
|
||||||
|
|
||||||
|
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
|
export const loginWebValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
username: usernameSchema.required(),
|
||||||
|
password: passwordSchema.required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
username: usernameSchema.required(),
|
username: usernameSchema.required(),
|
||||||
@@ -66,15 +74,39 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
|||||||
clientSecret: Joi.string().required()
|
clientSecret: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const getFileDriveValidation = (data: any): Joi.ValidationResult =>
|
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
filePath: Joi.string().required()
|
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
||||||
|
streamServiceName: Joi.string(),
|
||||||
|
streamWebFolder: Joi.string(),
|
||||||
|
streamLogo: Joi.string(),
|
||||||
|
fileTree: Joi.any().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
|
const filePathSchema = Joi.string()
|
||||||
|
.custom((value, helpers) => {
|
||||||
|
if (blockFileRegex.test(value)) return helpers.error('string.pattern.base')
|
||||||
|
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
.required()
|
||||||
|
.messages({
|
||||||
|
'string.pattern.base': `Invalid file extension`
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fileBodyValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
filePath: Joi.string().required(),
|
filePath: filePathSchema
|
||||||
fileContent: Joi.string().required()
|
}).validate(data)
|
||||||
|
|
||||||
|
export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
_filePath: filePathSchema
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
|
export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
_folderPath: Joi.string()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Info",
|
||||||
|
"description": "Get Server Info"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Session",
|
"name": "Session",
|
||||||
"description": "Get Session information"
|
"description": "Get Session information"
|
||||||
@@ -42,6 +46,10 @@
|
|||||||
{
|
{
|
||||||
"name": "CODE",
|
"name": "CODE",
|
||||||
"description": "Operations on SAS code"
|
"description": "Operations on SAS code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Web",
|
||||||
|
"description": "Operations on Web"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"yaml": true,
|
"yaml": true,
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ services:
|
|||||||
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
|
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
|
||||||
AUTH_CODE_SECRET: ${AUTH_CODE_SECRET}
|
AUTH_CODE_SECRET: ${AUTH_CODE_SECRET}
|
||||||
DB_CONNECT: mongodb://mongodb:27017/sasjs
|
DB_CONNECT: mongodb://mongodb:27017/sasjs
|
||||||
SAS_PATH: /usr/server/sasexe
|
SAS_PATH: /usr/server/sasexe/${SAS_EXEC_NAME}
|
||||||
expose:
|
expose:
|
||||||
- ${PORT_API}
|
- ${PORT_API}
|
||||||
ports:
|
ports:
|
||||||
- ${PORT_API}:${PORT_API}
|
- ${PORT_API}:${PORT_API}
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: ${SAS_EXEC}
|
source: ${SAS_EXEC_PATH}
|
||||||
target: /usr/server/sasexe
|
target: /usr/server/sasexe
|
||||||
read_only: true
|
read_only: true
|
||||||
links:
|
links:
|
||||||
|
|||||||
1595
package-lock.json
generated
1595
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.22",
|
"version": "0.0.63",
|
||||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||||
"repository": "https://github.com/sasjs/server",
|
"repository": "https://github.com/sasjs/server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "npm run server:prepare && npm run server:start",
|
"server": "npm run server:prepare && npm run server:start",
|
||||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..",
|
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
|
||||||
"server:start": "cd api && npm run start:prod",
|
"server:start": "cd api && npm run start:prod",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
|
|||||||
22
restClient/auth.rest
Normal file
22
restClient/auth.rest
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
### Get Auth Code
|
||||||
|
POST http://localhost:5000/SASjsApi/auth/authorize
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "secretuser",
|
||||||
|
"password": "secretpassword",
|
||||||
|
"client_id": "clientID1"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Exchange AuthCode with Access/Refresh Tokens
|
||||||
|
POST http://localhost:5000/SASjsApi/auth/token
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"client_id": "clientID1",
|
||||||
|
"client_secret": "clientID1secret",
|
||||||
|
"code": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDYxLCJleHAiOjE2MzU4MDQwOTF9.jV7DpBWG7XAGODs22zAW_kWOqVLZvOxmmYJGpSNQ-KM"
|
||||||
|
}
|
||||||
|
|
||||||
|
### Perform logout to deactivate access token instantly
|
||||||
|
DELETE http://localhost:5000/SASjsApi/auth/logout
|
||||||
48
restClient/drive.rest
Normal file
48
restClient/drive.rest
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
### Get contents of folder
|
||||||
|
GET http://localhost:5000/SASjsApi/drive/folder?_path=/Public/app/react-seed-app/services/web
|
||||||
|
|
||||||
|
###
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/deploy
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I
|
||||||
|
|
||||||
|
### multipart upload to sas server file
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/file
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="filePath"
|
||||||
|
|
||||||
|
/saad/files/new.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
|
||||||
|
< ./sample.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||||
|
|
||||||
|
### multipart upload to sas server file text
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/file
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW \n
|
||||||
|
Content-Disposition: form-data; name="filePath"
|
||||||
|
|
||||||
|
/saad/files/new2.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
SOME CONTENTS OF SAS FILE IN REQUEST
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||||
|
|
||||||
|
|
||||||
|
Users
|
||||||
|
"username": "username1",
|
||||||
|
"password": "some password",
|
||||||
|
|
||||||
|
"username": "username2",
|
||||||
|
"password": "some password",
|
||||||
|
Admins
|
||||||
|
"username": "secretuser",
|
||||||
|
"password": "secretpassword",
|
||||||
@@ -23,7 +23,7 @@ Content-Type: application/json
|
|||||||
"client_secret": "newClientSecret"
|
"client_secret": "newClientSecret"
|
||||||
}
|
}
|
||||||
###
|
###
|
||||||
POST https://sas.analytium.co.uk:5002/SASjsApi/auth/authorize
|
POST http://localhost:5000/SASjsApi/auth/authorize
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -45,6 +45,41 @@ Content-Type: application/json
|
|||||||
###
|
###
|
||||||
DELETE http://localhost:5000/SASjsApi/auth/logout
|
DELETE http://localhost:5000/SASjsApi/auth/logout
|
||||||
|
|
||||||
|
###
|
||||||
|
GET http://localhost:5000/SASjsApi/session
|
||||||
|
|
||||||
|
|
||||||
|
### multipart upload to sas server file
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/file
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="filePath"
|
||||||
|
|
||||||
|
/saad/files/new.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
|
||||||
|
Content-Type: application/octet-stream
|
||||||
|
|
||||||
|
< ./sample.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||||
|
|
||||||
|
### multipart upload to sas server file text
|
||||||
|
POST http://localhost:5000/SASjsApi/drive/file
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW \n
|
||||||
|
Content-Disposition: form-data; name="filePath"
|
||||||
|
|
||||||
|
/saad/files/new2.sas
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||||
|
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
SOME CONTENTS OF SAS FILE IN REQUEST
|
||||||
|
|
||||||
|
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||||
|
|
||||||
|
|
||||||
Users
|
Users
|
||||||
"username": "username1",
|
"username": "username1",
|
||||||
1
restClient/sample.sas
Normal file
1
restClient/sample.sas
Normal file
@@ -0,0 +1 @@
|
|||||||
|
some code of sas
|
||||||
2
restClient/session.rest
Normal file
2
restClient/session.rest
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
### Get current user's info via access token
|
||||||
|
GET http://localhost:5000/SASjsApi/session
|
||||||
25
restClient/stp.rest
Normal file
25
restClient/stp.rest
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
### testing upload file example
|
||||||
|
POST http://localhost:5000/SASjsApi/stp/execute/?_program=/Public/app/viya/services/editors/loadfile&table=DCCONFIG.MPE_X_TEST
|
||||||
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||||
|
|
||||||
|
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||||
|
Content-Disposition: form-data; name="fileSome11"; filename="DCCONFIG.MPE_X_TEST.xlsx"
|
||||||
|
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
|
||||||
|
|
||||||
|
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||||
|
Content-Disposition: form-data; name="fileSome22"; filename="DCCONFIG.MPE_X_TEST.xlsx.csv"
|
||||||
|
Content-Type: application/csv
|
||||||
|
|
||||||
|
_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM
|
||||||
|
,0,this is dummy data 321,Option 1,42,1960-02-12,1960-01-01 00:00:42,00:00:42,3,44
|
||||||
|
,1,more dummy data 123,Option 2,42,1960-02-12,1960-01-01 00:00:42,00:07:02,3,44
|
||||||
|
,1039,39 bottles of beer on the wall,Option 1,0.8716847965827607,1962-05-30,1960-01-01 00:05:21,00:01:30,89,6
|
||||||
|
,1045,45 bottles of beer on the wall,Option 1,0.7279699667021492,1960-03-24,1960-01-01 07:18:54,00:01:08,89,83
|
||||||
|
,1047,47 bottles of beer on the wall,Option 1,0.6224654082313484,1961-06-07,1960-01-01 09:45:23,00:01:33,76,98
|
||||||
|
,1048,48 bottles of beer on the wall,Option 1,0.0874847523344144,1962-03-01,1960-01-01 13:06:13,00:00:02,76,63
|
||||||
|
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||||
|
Content-Disposition: form-data; name="_debug"
|
||||||
|
|
||||||
|
131
|
||||||
|
------WebKitFormBoundarynkYOqevUMKZrXeAy--
|
||||||
10
restClient/users.rest
Normal file
10
restClient/users.rest
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
### Create User
|
||||||
|
POST http://localhost:5000/SASjsApi/user
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InNlY3JldHVzZXIiLCJpc2FkbWluIjp0cnVlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODAzOTc3LCJleHAiOjE2MzU4OTAzNzd9.f-FLgLwryKvB5XrihdzaGZajO3d5E5OHEEuJI_03GRI
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"displayname": "User 2",
|
||||||
|
"username": "username2",
|
||||||
|
"password": "some password"
|
||||||
|
}
|
||||||
4
web/.babelrc
Normal file
4
web/.babelrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"],
|
||||||
|
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||||
|
}
|
||||||
@@ -1,2 +1 @@
|
|||||||
REACT_APP_PORT_API=[place sasjs server port] default value is 5000
|
PORT_API=[place sasjs server port] default value is 5000
|
||||||
REACT_APP_CLIENT_ID=<place clientId here>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user