mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 11:24:35 +00:00
Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f29dec16f | ||
|
|
e2a97fcb7c | ||
|
|
6adeeefcf5 | ||
|
|
c9d66b8576 | ||
|
|
5aaac24080 | ||
|
|
6d34206bbc | ||
|
|
7b39cc06d3 | ||
|
|
6e7f28a6f8 | ||
|
|
5689169ce4 | ||
|
|
6139e7bff6 | ||
|
|
2c77317bb9 | ||
|
|
57b63db9cb | ||
|
|
60a2a4fe32 | ||
|
|
09611cb416 | ||
|
|
2a9bb6e6b1 | ||
|
|
b4b60c69cf | ||
|
|
b060ad1b8e | ||
|
|
d47ed6d0e8 | ||
|
|
a6993ef5ae | ||
|
|
2571fc2ca8 | ||
|
|
992f39b63a | ||
|
|
1ea3f6d8b3 | ||
|
|
e462aebdc0 | ||
|
|
13403517a4 | ||
|
|
c3c2048e75 | ||
|
|
1d8acc36eb | ||
|
|
4c7ad56326 | ||
|
|
e57443f1ed | ||
|
|
5da93f318a | ||
|
|
a30fb1a241 | ||
|
|
4ae8f35e9a | ||
|
|
ebb46f51b6 | ||
|
|
fe24f51ca2 | ||
|
|
fd15f3fb41 | ||
|
|
7d31ee7696 | ||
|
|
667e26b080 | ||
|
|
d09876c05f | ||
|
|
fb8e18be75 | ||
|
|
7ac7a4e083 | ||
|
|
8e23786dd4 | ||
|
|
4bd01bcf29 | ||
|
|
4ad8c81e49 | ||
|
|
51f6aa34a1 | ||
|
|
486207128d | ||
|
|
1e4b0b9171 | ||
|
|
1ff820605a | ||
|
|
9c1a781b3a | ||
| 36628551ae | |||
| 23cf8fa06f | |||
| 84ee743eae | |||
|
|
19e5bd7d2d | ||
|
|
e251747302 | ||
|
|
7e7558d4cf | ||
|
|
f02996facf | ||
|
|
803c51f400 | ||
|
|
c35b2b3f59 | ||
|
|
fe0866ace7 | ||
|
|
1513c3623d | ||
|
|
7fe43ae0b7 | ||
|
|
c4cea4a12b | ||
|
|
9fc7a132ba | ||
|
|
d55a619d64 | ||
|
|
737d2a24c2 | ||
|
|
2e63831b90 | ||
|
|
c7ffde1a3b | ||
|
|
db70b1ce55 | ||
|
|
8a3fe8b217 | ||
| 9dca552e82 | |||
|
|
505f2089c7 | ||
|
|
3344c400a8 | ||
| fa6248e3ef | |||
| 9fb5f1f8e7 | |||
|
|
92e0b8a088 | ||
|
|
b484306ed8 | ||
| 5e08aacc51 | |||
| a9e4eb685d | |||
| 31b09f27cc | |||
| 9f3ec92f8e | |||
| 6c9e449614 | |||
| 68e84b0994 | |||
| f0bb51a0d5 | |||
| b93a0da3a3 | |||
|
|
e5facbf54c | ||
|
|
cb2bebbe76 | ||
|
|
9e1e0ce8cc | ||
|
|
29928753b7 | ||
|
|
edd69ecaae | ||
|
|
74ba65f9f3 | ||
|
|
f257602834 | ||
|
|
61080d4694 | ||
|
|
82633adbc4 | ||
|
|
23db7e7b7d | ||
|
|
cbaa687c9b | ||
|
|
527f70e90d | ||
|
|
122faad55f | ||
|
|
3ff6f5e865 | ||
|
|
7d5128c0d6 | ||
|
|
e1ebbfd087 | ||
|
|
e430bdb0d4 | ||
|
|
9d9769eef3 | ||
|
|
9d167abe2a | ||
|
|
18d0604bdd | ||
|
|
7b7bc6b778 | ||
|
|
fb4f3442d5 | ||
|
|
09d1b7d5d4 | ||
|
|
99839ae62f | ||
|
|
f700561e1a | ||
|
|
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 |
77
.github/CONTRIBUTING.md
vendored
77
.github/CONTRIBUTING.md
vendored
@@ -2,25 +2,22 @@
|
||||
|
||||
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
|
||||
|
||||
The app can be deployed using Docker or NodeJS.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is made in the `configuration` section of `package.json`:
|
||||
Configuration is made using `.env` files (per [README.md](https://github.com/sasjs/server#env-var-configuration) settings), _except_ for one case, when running in NodeJS in production - in which case the path to the SAS executable is made in the `configuration` section of `package.json`.
|
||||
|
||||
- Provide path to SAS9 executable.
|
||||
The `.env` file should be created in the location(s) below. Each folder contains a `.env.example` file that may be adjusted and renamed.
|
||||
|
||||
* `.env` - the root .env file is used only for Docker deploys.
|
||||
* `api/.env` - this is the primary file used in NodeJS deploys
|
||||
* `web/.env` - this file is only necessary in NodeJS when running `web` and `api` seperately (on different ports).
|
||||
|
||||
|
||||
### Using dockers:
|
||||
## Using Docker
|
||||
|
||||
There is `.env.example` file present at root of the project. [for Production]
|
||||
|
||||
There is `.env.example` file present at `./api` of the project. [for Development]
|
||||
|
||||
There is `.env.example` file present at `./web` of the project. [for Development]
|
||||
|
||||
Remember to provide enviornment variables.
|
||||
|
||||
#### Development
|
||||
### Docker Development Mode
|
||||
|
||||
Command to run docker for development:
|
||||
|
||||
@@ -38,7 +35,7 @@ It will build following images if running first time:
|
||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||
|
||||
|
||||
#### Production
|
||||
### Docker Production Mode
|
||||
|
||||
Command to run docker for production:
|
||||
|
||||
@@ -54,47 +51,45 @@ It will build following images if running first time:
|
||||
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
|
||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||
|
||||
### Using node:
|
||||
## Using NodeJS:
|
||||
|
||||
#### Development (running api and web seperately):
|
||||
Be sure to use v16 or above, and to set your environment variables in the relevant `.env` file(s) - else defaults will be used.
|
||||
|
||||
##### API
|
||||
### NodeJS Development Mode
|
||||
|
||||
Navigate to `./api`
|
||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
SASjs Server is split between an API server (serving REST requests) and a WEB Server (everything else). These can be run together, or on seperate ports.
|
||||
|
||||
### NodeJS Dev - Single Port
|
||||
|
||||
Here the environment variables should be configured under `api.env`. Then:
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build
|
||||
cd ../api && npm i && npm start
|
||||
```
|
||||
|
||||
### NodeJS Dev - Seperate Ports
|
||||
|
||||
Set the backend variables in `api/.env` and the frontend variables in `web/.env`. Then:
|
||||
|
||||
#### API server
|
||||
```
|
||||
cd api
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
##### Web
|
||||
|
||||
Navigate to `./web`
|
||||
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
#### Web Server
|
||||
|
||||
```
|
||||
cd web
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
#### Development (running only api server and have web build served):
|
||||
#### NodeJS Production Mode
|
||||
|
||||
##### API server also serving Web build files
|
||||
|
||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build && cd ../
|
||||
cd ./api && npm i && npm start
|
||||
```
|
||||
|
||||
#### Production
|
||||
|
||||
##### API & WEB
|
||||
Update the `.env` file in the *api* folder. Then:
|
||||
|
||||
```
|
||||
npm run server
|
||||
@@ -105,7 +100,7 @@ This will install/build `web` and install `api`, then start prod server.
|
||||
|
||||
## Executables
|
||||
|
||||
Command to generate executables
|
||||
In order to generate the final executables:
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build && cd ../
|
||||
@@ -113,3 +108,7 @@ cd ./api && npm i && npm run exe
|
||||
```
|
||||
|
||||
This will install/build web app and install/create executables of sasjs server at root `./executables`
|
||||
|
||||
## Releases
|
||||
|
||||
To cut a release, run `npm run release` on the main branch, then push the tags (per the console log link)
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -8,10 +8,20 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/*]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install Dependencies WEB
|
||||
working-directory: ./web
|
||||
run: npm ci
|
||||
|
||||
217
CHANGELOG.md
217
CHANGELOG.md
@@ -2,6 +2,223 @@
|
||||
|
||||
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.66](https://github.com/sasjs/server/compare/v0.0.64...v0.0.66) (2022-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added swagger ui init file manually ([e2a97fc](https://github.com/sasjs/server/commit/e2a97fcb7c54a57a7ca118677cfce93fe9430d8f))
|
||||
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
|
||||
|
||||
### [0.0.65](https://github.com/sasjs/server/compare/v0.0.64...v0.0.65) (2022-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
|
||||
|
||||
### [0.0.64](https://github.com/sasjs/server/compare/v0.0.63...v0.0.64) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removed fileExists for serving web ([7b39cc0](https://github.com/sasjs/server/commit/7b39cc06d358f5ffecb87955040c4eb0fcc7469e))
|
||||
|
||||
### [0.0.63](https://github.com/sasjs/server/compare/v0.0.62...v0.0.63) (2022-04-30)
|
||||
|
||||
### [0.0.62](https://github.com/sasjs/server/compare/v0.0.61...v0.0.62) (2022-04-30)
|
||||
|
||||
### [0.0.61](https://github.com/sasjs/server/compare/v0.0.59...v0.0.61) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||
|
||||
### [0.0.60](https://github.com/sasjs/server/compare/v0.0.59...v0.0.60) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||
|
||||
### [0.0.59](https://github.com/sasjs/server/compare/v0.0.58...v0.0.59) (2022-04-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* enabled csrf tokens for web component ([e462aeb](https://github.com/sasjs/server/commit/e462aebdc01f3c0068ed0074473a2063412dcf45))
|
||||
* enabled session based authentication for web ([5da93f3](https://github.com/sasjs/server/commit/5da93f318aad10b1c67032a467191e4dbb99f411))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fetch client from DB for each request ([4ad8c81](https://github.com/sasjs/server/commit/4ad8c81e4927c1a82220ec015a781b095c8e859e))
|
||||
* **web:** show display name instead of username ([e57443f](https://github.com/sasjs/server/commit/e57443f1ed662a022494bb93d79c3d2f10a2d082))
|
||||
|
||||
### [0.0.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping core library to get latest user management macros ([4862071](https://github.com/sasjs/server/commit/486207128da58fc4866bd0919c1bed2bd98097ea))
|
||||
* missing dependency ([d09876c](https://github.com/sasjs/server/commit/d09876c05f89166eec20064f7aa7ed5b867be081))
|
||||
|
||||
### [0.0.57](https://github.com/sasjs/server/compare/v0.0.56...v0.0.57) (2022-04-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* create AppContext ([84ee743](https://github.com/sasjs/server/commit/84ee743eae16e87eaa91969393bebf01e2d15a44))
|
||||
|
||||
### [0.0.56](https://github.com/sasjs/server/compare/v0.0.55...v0.0.56) (2022-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* shortening min length of username. Closes [#61](https://github.com/sasjs/server/issues/61) ([f02996f](https://github.com/sasjs/server/commit/f02996facf1019ec4022ccfbc99c1d0137074e1b))
|
||||
|
||||
### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||
* drive path in server mode ([c4cea4a](https://github.com/sasjs/server/commit/c4cea4a12b7eda4daeed995f41c0b10bcea79871))
|
||||
|
||||
### [0.0.54](https://github.com/sasjs/server/compare/v0.0.53...v0.0.54) (2022-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||
|
||||
### [0.0.53](https://github.com/sasjs/server/compare/v0.0.49...v0.0.53) (2022-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* provide clientId to web component ([db70b1c](https://github.com/sasjs/server/commit/db70b1ce555df6b29fb09c0c960d38b911c97b1b))
|
||||
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||
|
||||
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||
|
||||
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||
|
||||
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||
|
||||
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
|
||||
|
||||
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deploy:** new route added for deploy with build.json ([18d0604](https://github.com/sasjs/server/commit/18d0604bdd0b20ad468f9345474b4de034ee3a67))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove uploaded build.json from temp folder in all cases ([9d167ab](https://github.com/sasjs/server/commit/9d167abe2adb743bca161862b4561bf573182c00))
|
||||
* **stp:** return log+webout for debug on ([3ff6f5e](https://github.com/sasjs/server/commit/3ff6f5e86581cd2ac23bbe0b8e2c367fbea890ed))
|
||||
|
||||
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** updated STUDIO log and webout ([f700561](https://github.com/sasjs/server/commit/f700561e1a8d06c18ca2bdbe4605d7ab34f7a761))
|
||||
|
||||
### [0.0.46](https://github.com/sasjs/server/compare/v0.0.45...v0.0.46) (2022-03-29)
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
|
||||
48
README.md
48
README.md
@@ -48,11 +48,22 @@ When launching the app, it will make use of specific environment variables. Thes
|
||||
Example contents of a `.env` file:
|
||||
|
||||
```
|
||||
MODE=desktop # options: [desktop|server] default: `desktop`
|
||||
CORS=disable # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||
WHITELIST= # options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||
PROTOCOL=http # options: [http|https] default: http
|
||||
PORT=5000 # default: 5000
|
||||
# options: [desktop|server] default: `desktop`
|
||||
MODE=
|
||||
|
||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
||||
CORS=
|
||||
|
||||
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||
WHITELIST=
|
||||
|
||||
# options: [http|https] default: http
|
||||
PROTOCOL=
|
||||
|
||||
# default: 5000
|
||||
PORT=
|
||||
|
||||
|
||||
# optional
|
||||
# for MODE: `desktop`, prompts user
|
||||
@@ -73,12 +84,35 @@ FULL_CHAIN=fullchain.pem
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
SESSION_SECRET=<secret>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
# 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
|
||||
|
||||
```
|
||||
|
||||
## Persisting the Session
|
||||
|
||||
Normally the server process will stop when your terminal dies. To keep it going you can use the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) as follows:
|
||||
Normally the server process will stop when your terminal dies. To keep it going you can use the following suggested approaches:
|
||||
|
||||
1. Linux Background Job
|
||||
2. NPM package `pm2`
|
||||
|
||||
### Background Job
|
||||
|
||||
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
|
||||
|
||||
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
|
||||
|
||||
### PM2
|
||||
|
||||
Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) and execute, eg as follows:
|
||||
|
||||
```bash
|
||||
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||
@@ -112,7 +146,7 @@ Instead of `app_name` you can pass:
|
||||
|
||||
## Server Version
|
||||
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is highly recommended to change these on first use.
|
||||
|
||||
- CLIENTID: `clientID1`
|
||||
- USERNAME: `secretuser`
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
MODE=[desktop|server] default considered as desktop
|
||||
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
|
||||
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
||||
|
||||
PROTOCOL=[http|https] default considered as http
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
|
||||
PORT=[5000] default value is 5000
|
||||
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
SESSION_SECRET=<secret>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||
|
||||
517
api/package-lock.json
generated
517
api/package-lock.json
generated
@@ -8,19 +8,23 @@
|
||||
"name": "api",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "4.9.0",
|
||||
"@sasjs/utils": "2.36.2",
|
||||
"@sasjs/core": "^4.19.0",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"csurf": "^1.11.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"helmet": "^5.0.2",
|
||||
"joi": "^17.4.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.3",
|
||||
"swagger-ui-express": "^4.1.6"
|
||||
"swagger-ui-express": "4.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"api": "build/src/server.js"
|
||||
@@ -29,7 +33,9 @@
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/csurf": "^1.11.2",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
@@ -43,7 +49,7 @@
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "^8.0.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"pkg": "5.5.2",
|
||||
"pkg": "5.6.0",
|
||||
"prettier": "^2.3.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"supertest": "^6.1.3",
|
||||
@@ -1379,14 +1385,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz",
|
||||
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
|
||||
"integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
|
||||
},
|
||||
"node_modules/@sasjs/utils": {
|
||||
"version": "2.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz",
|
||||
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==",
|
||||
"version": "2.42.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
|
||||
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "9.0.13",
|
||||
@@ -1833,6 +1839,15 @@
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/csurf": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
|
||||
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/express-serve-static-core": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
|
||||
@@ -1856,6 +1871,15 @@
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-session": {
|
||||
"version": "1.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.4.tgz",
|
||||
"integrity": "sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/fs-extra": {
|
||||
"version": "9.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||
@@ -2447,10 +2471,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||
"dependencies": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
@@ -2674,6 +2709,11 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||
@@ -3238,6 +3278,42 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/connect-mongo": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-4.6.0.tgz",
|
||||
"integrity": "sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.1",
|
||||
"kruptein": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mongodb": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/connect-mongo/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/connect-mongo/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
|
||||
@@ -3362,6 +3438,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/csrf": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
|
||||
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
|
||||
"dependencies": {
|
||||
"rndm": "1.2.0",
|
||||
"tsscmp": "1.0.6",
|
||||
"uid-safe": "2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cssom": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
||||
@@ -3386,6 +3475,40 @@
|
||||
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/csurf": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
|
||||
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
|
||||
"dependencies": {
|
||||
"cookie": "0.4.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"csrf": "3.1.0",
|
||||
"http-errors": "~1.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csurf/node_modules/http-errors": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
|
||||
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
|
||||
"dependencies": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/csurf/node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/csv-stringify": {
|
||||
"version": "5.6.5",
|
||||
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
||||
@@ -4027,6 +4150,59 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.17.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz",
|
||||
"integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==",
|
||||
"dependencies": {
|
||||
"cookie": "0.4.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.0.2",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||
@@ -4642,6 +4818,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz",
|
||||
"integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||
@@ -6828,6 +7012,17 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kruptein": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.4.tgz",
|
||||
"integrity": "sha512-614v+4fgOkcw98lI7rMO9HZ+Y2cK6MGYcR/NSVhRXcClUb72LTAf2NibAh8CKSjalY81rfrrjLQgb8TW9RP03Q==",
|
||||
"dependencies": {
|
||||
"asn1.js": "^5.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">8"
|
||||
}
|
||||
},
|
||||
"node_modules/latest-version": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
|
||||
@@ -7095,6 +7290,11 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
@@ -7107,9 +7307,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
@@ -7133,7 +7333,6 @@
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
||||
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bson": "^4.5.4",
|
||||
"denque": "^2.0.1",
|
||||
@@ -7971,9 +8170,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pkg": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.5.2.tgz",
|
||||
"integrity": "sha512-pD0UB2ud01C6pVv2wpGsTYJrXI/bnvGRYvMLd44wFzA1p+A2jrlTGFPAYa7YEYzmitXhx23PqalaG1eUEnSwcA==",
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.6.0.tgz",
|
||||
"integrity": "sha512-mHrAVSQWmHA41RnUmRpC7pK9lNnMfdA16CF3cqOI22a8LZxOQzF7M8YWtA2nfs+d7I0MTDXOtkDsAsFXeCpYjg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "7.16.2",
|
||||
@@ -7985,7 +8184,7 @@
|
||||
"into-stream": "^6.0.0",
|
||||
"minimist": "^1.2.5",
|
||||
"multistream": "^4.1.0",
|
||||
"pkg-fetch": "3.2.6",
|
||||
"pkg-fetch": "3.3.0",
|
||||
"prebuild-install": "6.1.4",
|
||||
"progress": "^2.0.3",
|
||||
"resolve": "^1.20.0",
|
||||
@@ -8017,9 +8216,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-fetch": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.2.6.tgz",
|
||||
"integrity": "sha512-Q8fx6SIT022g0cdSE4Axv/xpfHeltspo2gg1KsWRinLQZOTRRAtOOaEFghA1F3jJ8FVsh8hGrL/Pb6Ea5XHIFw==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.3.0.tgz",
|
||||
"integrity": "sha512-xJnIZ1KP+8rNN+VLafwu4tEeV4m8IkFBDdCFqmAJz9K1aiXEtbARmdbEe6HlXWGSVuShSHjFXpfkKRkDBQ5kiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
@@ -8076,9 +8275,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-fetch/node_modules/semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -8367,6 +8566,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -8547,6 +8754,11 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rndm": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
|
||||
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -9222,11 +9434,11 @@
|
||||
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
|
||||
},
|
||||
"node_modules/swagger-ui-express": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz",
|
||||
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz",
|
||||
"integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==",
|
||||
"dependencies": {
|
||||
"swagger-ui-dist": ">3.52.5"
|
||||
"swagger-ui-dist": ">=4.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= v0.10.32"
|
||||
@@ -9532,6 +9744,14 @@
|
||||
"yarn": ">=1.9.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tsscmp": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
|
||||
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
|
||||
"engines": {
|
||||
"node": ">=0.6.x"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -9626,6 +9846,17 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
|
||||
@@ -11127,14 +11358,14 @@
|
||||
}
|
||||
},
|
||||
"@sasjs/core": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz",
|
||||
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
|
||||
"integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
|
||||
},
|
||||
"@sasjs/utils": {
|
||||
"version": "2.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz",
|
||||
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==",
|
||||
"version": "2.42.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
|
||||
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
|
||||
"requires": {
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/prompts": "2.0.13",
|
||||
@@ -11525,6 +11756,15 @@
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/csurf": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
|
||||
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express-serve-static-core": "*"
|
||||
}
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
|
||||
@@ -11548,6 +11788,15 @@
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-session": {
|
||||
"version": "1.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.4.tgz",
|
||||
"integrity": "sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"@types/fs-extra": {
|
||||
"version": "9.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||
@@ -12059,10 +12308,21 @@
|
||||
"is-string": "^1.0.7"
|
||||
}
|
||||
},
|
||||
"asn1.js": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
|
||||
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
|
||||
"requires": {
|
||||
"bn.js": "^4.0.0",
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
@@ -12234,6 +12494,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"bn.js": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
|
||||
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||
@@ -12681,6 +12946,30 @@
|
||||
"xdg-basedir": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"connect-mongo": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-4.6.0.tgz",
|
||||
"integrity": "sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==",
|
||||
"requires": {
|
||||
"debug": "^4.3.1",
|
||||
"kruptein": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"consola": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
|
||||
@@ -12783,6 +13072,16 @@
|
||||
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
|
||||
"dev": true
|
||||
},
|
||||
"csrf": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
|
||||
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
|
||||
"requires": {
|
||||
"rndm": "1.2.0",
|
||||
"tsscmp": "1.0.6",
|
||||
"uid-safe": "2.1.5"
|
||||
}
|
||||
},
|
||||
"cssom": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
||||
@@ -12806,6 +13105,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"csurf": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
|
||||
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
|
||||
"requires": {
|
||||
"cookie": "0.4.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"csrf": "3.1.0",
|
||||
"http-errors": "~1.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"http-errors": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
|
||||
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
|
||||
"requires": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.1.1",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"csv-stringify": {
|
||||
"version": "5.6.5",
|
||||
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
||||
@@ -13303,6 +13632,38 @@
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"express-session": {
|
||||
"version": "1.17.2",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz",
|
||||
"integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==",
|
||||
"requires": {
|
||||
"cookie": "0.4.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.0.2",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
|
||||
},
|
||||
"depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||
@@ -13774,6 +14135,11 @@
|
||||
"integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
|
||||
"dev": true
|
||||
},
|
||||
"helmet": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz",
|
||||
"integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg=="
|
||||
},
|
||||
"html-encoding-sniffer": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||
@@ -15409,6 +15775,14 @@
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="
|
||||
},
|
||||
"kruptein": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.4.tgz",
|
||||
"integrity": "sha512-614v+4fgOkcw98lI7rMO9HZ+Y2cK6MGYcR/NSVhRXcClUb72LTAf2NibAh8CKSjalY81rfrrjLQgb8TW9RP03Q==",
|
||||
"requires": {
|
||||
"asn1.js": "^5.4.1"
|
||||
}
|
||||
},
|
||||
"latest-version": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
|
||||
@@ -15615,6 +15989,11 @@
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
@@ -15624,9 +16003,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
@@ -15644,7 +16023,6 @@
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
||||
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bson": "^4.5.4",
|
||||
"denque": "^2.0.1",
|
||||
@@ -16271,9 +16649,9 @@
|
||||
}
|
||||
},
|
||||
"pkg": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.5.2.tgz",
|
||||
"integrity": "sha512-pD0UB2ud01C6pVv2wpGsTYJrXI/bnvGRYvMLd44wFzA1p+A2jrlTGFPAYa7YEYzmitXhx23PqalaG1eUEnSwcA==",
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.6.0.tgz",
|
||||
"integrity": "sha512-mHrAVSQWmHA41RnUmRpC7pK9lNnMfdA16CF3cqOI22a8LZxOQzF7M8YWtA2nfs+d7I0MTDXOtkDsAsFXeCpYjg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "7.16.2",
|
||||
@@ -16285,7 +16663,7 @@
|
||||
"into-stream": "^6.0.0",
|
||||
"minimist": "^1.2.5",
|
||||
"multistream": "^4.1.0",
|
||||
"pkg-fetch": "3.2.6",
|
||||
"pkg-fetch": "3.3.0",
|
||||
"prebuild-install": "6.1.4",
|
||||
"progress": "^2.0.3",
|
||||
"resolve": "^1.20.0",
|
||||
@@ -16342,9 +16720,9 @@
|
||||
}
|
||||
},
|
||||
"pkg-fetch": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.2.6.tgz",
|
||||
"integrity": "sha512-Q8fx6SIT022g0cdSE4Axv/xpfHeltspo2gg1KsWRinLQZOTRRAtOOaEFghA1F3jJ8FVsh8hGrL/Pb6Ea5XHIFw==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.3.0.tgz",
|
||||
"integrity": "sha512-xJnIZ1KP+8rNN+VLafwu4tEeV4m8IkFBDdCFqmAJz9K1aiXEtbARmdbEe6HlXWGSVuShSHjFXpfkKRkDBQ5kiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.2",
|
||||
@@ -16386,9 +16764,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -16555,6 +16933,11 @@
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true
|
||||
},
|
||||
"random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -16692,6 +17075,11 @@
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"rndm": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
|
||||
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
|
||||
},
|
||||
"run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -17213,11 +17601,11 @@
|
||||
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
|
||||
},
|
||||
"swagger-ui-express": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz",
|
||||
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz",
|
||||
"integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==",
|
||||
"requires": {
|
||||
"swagger-ui-dist": ">3.52.5"
|
||||
"swagger-ui-dist": ">=4.1.3"
|
||||
}
|
||||
},
|
||||
"symbol-tree": {
|
||||
@@ -17429,6 +17817,11 @@
|
||||
"@tsoa/runtime": "^3.13.0"
|
||||
}
|
||||
},
|
||||
"tsscmp": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
|
||||
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -17495,6 +17888,14 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"requires": {
|
||||
"random-bytes": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"unbox-primitive": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"prestart": "npm run initial",
|
||||
"prebuild": "npm run initial",
|
||||
"start": "nodemon ./src/server.ts",
|
||||
"start:prod": "node ./build/src/server.js",
|
||||
"build": "rimraf build && tsc",
|
||||
"postbuild": "npm run copy:files",
|
||||
"swagger": "tsoa spec",
|
||||
@@ -46,25 +47,31 @@
|
||||
},
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "4.9.0",
|
||||
"@sasjs/utils": "2.36.2",
|
||||
"@sasjs/core": "^4.19.0",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"csurf": "^1.11.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"helmet": "^5.0.2",
|
||||
"joi": "^17.4.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.3",
|
||||
"swagger-ui-express": "^4.1.6"
|
||||
"swagger-ui-express": "4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/csurf": "^1.11.2",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
@@ -78,7 +85,7 @@
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "^8.0.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"pkg": "5.5.2",
|
||||
"pkg": "5.6.0",
|
||||
"prettier": "^2.3.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"supertest": "^6.1.3",
|
||||
|
||||
50
api/public/SASjsApi/swagger-ui-init.js
Normal file
50
api/public/SASjsApi/swagger-ui-init.js
Normal file
@@ -0,0 +1,50 @@
|
||||
window.onload = function () {
|
||||
// Build a system
|
||||
var url = window.location.search.match(/url=([^&]+)/)
|
||||
if (url && url.length > 1) {
|
||||
url = decodeURIComponent(url[1])
|
||||
} else {
|
||||
url = window.location.origin
|
||||
}
|
||||
var options = {
|
||||
customOptions: {
|
||||
url: '/swagger.yaml',
|
||||
requestInterceptor: function (request) {
|
||||
request.credentials = 'include'
|
||||
var cookie = document.cookie
|
||||
var startIndex = cookie.indexOf('XSRF-TOKEN')
|
||||
var csrf = cookie.slice(startIndex + 11).split('; ')[0]
|
||||
request.headers['X-XSRF-TOKEN'] = csrf
|
||||
return request
|
||||
}
|
||||
}
|
||||
}
|
||||
url = options.swaggerUrl || url
|
||||
var urls = options.swaggerUrls
|
||||
var customOptions = options.customOptions
|
||||
var spec1 = options.swaggerDoc
|
||||
var swaggerOptions = {
|
||||
spec: spec1,
|
||||
url: url,
|
||||
urls: urls,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||
layout: 'StandaloneLayout'
|
||||
}
|
||||
for (var attrname in customOptions) {
|
||||
swaggerOptions[attrname] = customOptions[attrname]
|
||||
}
|
||||
var ui = SwaggerUIBundle(swaggerOptions)
|
||||
|
||||
if (customOptions.oauth) {
|
||||
ui.initOAuth(customOptions.oauth)
|
||||
}
|
||||
|
||||
if (customOptions.authAction) {
|
||||
ui.authActions.authorize(customOptions.authAction)
|
||||
}
|
||||
|
||||
window.ui = ui
|
||||
}
|
||||
BIN
api/public/plus.png
Normal file
BIN
api/public/plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 899 B |
@@ -5,6 +5,21 @@ components:
|
||||
requestBodies: {}
|
||||
responses: {}
|
||||
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:
|
||||
properties:
|
||||
code:
|
||||
@@ -418,6 +433,25 @@ components:
|
||||
example: /Public/somefolder/some.file
|
||||
type: object
|
||||
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:
|
||||
bearerAuth:
|
||||
type: http
|
||||
@@ -431,6 +465,62 @@ info:
|
||||
name: '4GL Ltd'
|
||||
openapi: 3.0.0
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
operationId: Home
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
summary: 'Render index.html'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
/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:
|
||||
post:
|
||||
operationId: Authorize
|
||||
@@ -606,13 +696,62 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployPayload'
|
||||
/SASjsApi/drive/deploy/upload:
|
||||
post:
|
||||
operationId: DeployUpload
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'}
|
||||
'400':
|
||||
description: 'Invalid Format'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'Provided not supported data format.'}
|
||||
'500':
|
||||
description: 'Execution Error'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'Deployment failed!'}
|
||||
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'
|
||||
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: 'Get file from SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
@@ -623,19 +762,10 @@ paths:
|
||||
-
|
||||
in: query
|
||||
name: _filePath
|
||||
required: false
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder/some.file
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
filePath:
|
||||
type: string
|
||||
delete:
|
||||
operationId: DeleteFile
|
||||
responses:
|
||||
@@ -649,7 +779,6 @@ paths:
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
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: 'Delete file from SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
@@ -660,19 +789,10 @@ paths:
|
||||
-
|
||||
in: query
|
||||
name: _filePath
|
||||
required: false
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder/some.file
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
filePath:
|
||||
type: string
|
||||
post:
|
||||
operationId: SaveFile
|
||||
responses:
|
||||
@@ -775,6 +895,36 @@ paths:
|
||||
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:
|
||||
get:
|
||||
operationId: GetFileTree
|
||||
@@ -1180,10 +1330,31 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
-
|
||||
url: /
|
||||
tags:
|
||||
-
|
||||
name: Info
|
||||
description: 'Get Server Info'
|
||||
-
|
||||
name: Session
|
||||
description: 'Get Session information'
|
||||
@@ -1208,3 +1379,6 @@ tags:
|
||||
-
|
||||
name: CODE
|
||||
description: 'Operations on SAS code'
|
||||
-
|
||||
name: Web
|
||||
description: 'Operations on Web'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
CompileTree,
|
||||
createFile,
|
||||
loadDependenciesFile,
|
||||
readFile,
|
||||
@@ -18,7 +19,8 @@ const compiledSystemInit = async (systemInit: string) =>
|
||||
macroFolders: [],
|
||||
buildSourceFolder: '',
|
||||
binaryFolders: [],
|
||||
macroCorePath
|
||||
macroCorePath,
|
||||
compileTree: new CompileTree('') // dummy compileTree
|
||||
}))
|
||||
|
||||
const createSysInitFile = async () => {
|
||||
|
||||
@@ -5,23 +5,12 @@
|
||||
_before_ any user-provided content.
|
||||
|
||||
A number of useful CORE macros are also compiled below, so that they can be
|
||||
available "out of the box".
|
||||
available by default for Stored Programs.
|
||||
|
||||
Note that the full CORE library is available to sessions in SASjs Studio.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mcf_stpsrv_header.sas
|
||||
@li mf_getuser.sas
|
||||
@li mf_getvarlist.sas
|
||||
@li mf_mkdir.sas
|
||||
@li mf_nobs.sas
|
||||
@li mf_uid.sas
|
||||
@li mfs_httpheader.sas
|
||||
@li mp_dirlist.sas
|
||||
@li mp_ds2ddl.sas
|
||||
@li mp_ds2md.sas
|
||||
@li mp_getdbml.sas
|
||||
@li mp_init.sas
|
||||
@li mp_makedata.sas
|
||||
@li mp_zip.sas
|
||||
|
||||
@li ms_webout.sas
|
||||
**/
|
||||
|
||||
|
||||
@@ -1,48 +1,100 @@
|
||||
import path from 'path'
|
||||
import express, { ErrorRequestHandler } from 'express'
|
||||
import csrf from 'csurf'
|
||||
import session from 'express-session'
|
||||
import MongoStore from 'connect-mongo'
|
||||
import morgan from 'morgan'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import dotenv from 'dotenv'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import {
|
||||
connectDB,
|
||||
copySASjsCore,
|
||||
getWebBuildFolderPath,
|
||||
loadAppStreamConfig,
|
||||
sasJSCoreMacros,
|
||||
setProcessVariables
|
||||
setProcessVariables,
|
||||
setupFolders
|
||||
} from './utils'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const app = express()
|
||||
|
||||
const { MODE, CORS, WHITELIST } = process.env
|
||||
app.use(cookieParser())
|
||||
app.use(morgan('tiny'))
|
||||
|
||||
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') {
|
||||
const whiteList: string[] = []
|
||||
WHITELIST?.split(' ')?.forEach((url) => {
|
||||
if (url.startsWith('http'))
|
||||
// removing trailing slash of URLs listing for CORS
|
||||
whiteList.push(url.replace(/\/$/, ''))
|
||||
})
|
||||
WHITELIST?.split(' ')
|
||||
?.filter((url) => !!url)
|
||||
.forEach((url) => {
|
||||
if (url.startsWith('http'))
|
||||
// removing trailing slash of URLs listing for CORS
|
||||
whiteList.push(url.replace(/\/$/, ''))
|
||||
})
|
||||
|
||||
console.log('All CORS Requests are enabled for:', whiteList)
|
||||
app.use(cors({ credentials: true, origin: whiteList }))
|
||||
}
|
||||
|
||||
app.use(cookieParser())
|
||||
app.use(morgan('tiny'))
|
||||
app.use(express.json({ limit: '50mb' }))
|
||||
/***********************************
|
||||
* 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')))
|
||||
|
||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
if (err.code === 'EBADCSRFTOKEN')
|
||||
return res.status(400).send('Invalid CSRF token!')
|
||||
|
||||
console.error(err.stack)
|
||||
res.status(500).send('Something broke!')
|
||||
}
|
||||
|
||||
export default setProcessVariables().then(async () => {
|
||||
await setupFolders()
|
||||
await copySASjsCore()
|
||||
|
||||
// loading these modules after setting up variables due to
|
||||
@@ -58,6 +110,5 @@ export default setProcessVariables().then(async () => {
|
||||
|
||||
app.use(onError)
|
||||
|
||||
await connectDB()
|
||||
return app
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import User from '../model/User'
|
||||
import Client from '../model/Client'
|
||||
import { InfoJWT } from '../types'
|
||||
import {
|
||||
generateAccessToken,
|
||||
@@ -81,6 +82,9 @@ export class AuthController {
|
||||
const authorize = async (data: any): Promise<AuthorizeResponse> => {
|
||||
const { username, password, clientId } = data
|
||||
|
||||
const client = await Client.findOne({ clientId })
|
||||
if (!client) throw new Error('Invalid clientId.')
|
||||
|
||||
// Authenticate User
|
||||
const user = await User.findOne({ username })
|
||||
if (!user) throw new Error('Username is not found.')
|
||||
|
||||
@@ -14,17 +14,24 @@ import {
|
||||
Patch,
|
||||
UploadedFile,
|
||||
FormField,
|
||||
Delete
|
||||
Delete,
|
||||
Hidden
|
||||
} from 'tsoa'
|
||||
import {
|
||||
fileExists,
|
||||
moveFile,
|
||||
createFolder,
|
||||
deleteFile as deleteFileOnSystem
|
||||
deleteFile as deleteFileOnSystem,
|
||||
folderExists,
|
||||
listFilesInFolder,
|
||||
listSubFoldersInFolder,
|
||||
isFolder,
|
||||
FileTree,
|
||||
isFileTree
|
||||
} from '@sasjs/utils'
|
||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||
|
||||
import { FileTree, isFileTree, TreeNode } from '../types'
|
||||
import { TreeNode } from '../types'
|
||||
import { getTmpFilesFolderPath } from '../utils'
|
||||
|
||||
interface DeployPayload {
|
||||
@@ -89,9 +96,21 @@ export class DriveController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Creates/updates files within SASjs Drive using uploaded JSON file.
|
||||
*
|
||||
*/
|
||||
@Example<DeployResponse>(successDeployResponse)
|
||||
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
|
||||
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
|
||||
@Post('/deploy/upload')
|
||||
public async deployUpload(
|
||||
@UploadedFile() file: Express.Multer.File, // passing here for API docs
|
||||
@Query() @Hidden() body?: DeployPayload // Hidden decorator has be optional
|
||||
): Promise<DeployResponse> {
|
||||
return deploy(body!)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Get file from SASjs Drive
|
||||
* @query _filePath Location of SAS program
|
||||
@@ -100,28 +119,31 @@ export class DriveController {
|
||||
@Get('/file')
|
||||
public async getFile(
|
||||
@Request() request: express.Request,
|
||||
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
@Query() _filePath: string
|
||||
) {
|
||||
return getFile(request, (_filePath ?? filePath)!)
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Delete file from SASjs Drive
|
||||
* @query _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file"
|
||||
*/
|
||||
@Delete('/file')
|
||||
public async deleteFile(
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
) {
|
||||
return deleteFile((_filePath ?? filePath)!)
|
||||
public async deleteFile(@Query() _filePath: string) {
|
||||
return deleteFile(_filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +249,7 @@ const getFile = async (req: express.Request, filePath: string) => {
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error('File does not exist.')
|
||||
throw new Error("File doesn't exist.")
|
||||
}
|
||||
|
||||
const extension = path.extname(filePathFull).toLowerCase()
|
||||
@@ -238,6 +260,36 @@ const getFile = async (req: express.Request, filePath: string) => {
|
||||
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)
|
||||
|
||||
if (!folderPathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot get folder outside drive.')
|
||||
}
|
||||
|
||||
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 deleteFile = async (filePath: string) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
@@ -305,9 +357,3 @@ const updateFile = async (
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const validateFilePath = async (filePath: string) => {
|
||||
if (!(await fileExists(filePath))) {
|
||||
throw 'DriveController: File does not exists.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './group'
|
||||
export * from './session'
|
||||
export * from './stp'
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
moveFile,
|
||||
readFileBinary
|
||||
} from '@sasjs/utils'
|
||||
import { PreProgramVars, TreeNode } from '../../types'
|
||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||
import {
|
||||
extractHeaders,
|
||||
generateFileUploadSasCode,
|
||||
@@ -39,7 +39,8 @@ export class ExecutionController {
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
returnJson?: boolean,
|
||||
session?: Session
|
||||
) {
|
||||
if (!(await fileExists(programPath)))
|
||||
throw 'ExecutionController: SAS file does not exist.'
|
||||
@@ -51,7 +52,8 @@ export class ExecutionController {
|
||||
preProgramVariables,
|
||||
vars,
|
||||
otherArgs,
|
||||
returnJson
|
||||
returnJson,
|
||||
session
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,11 +62,13 @@ export class ExecutionController {
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
returnJson?: boolean,
|
||||
sessionByFileUpload?: Session
|
||||
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||
const sessionController = getSessionController()
|
||||
|
||||
const session = await sessionController.getSession()
|
||||
const session =
|
||||
sessionByFileUpload ?? (await sessionController.getSession())
|
||||
session.inUse = true
|
||||
session.consumed = true
|
||||
|
||||
@@ -153,7 +157,9 @@ ${program}`
|
||||
: ''
|
||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||
const fileResponse: boolean =
|
||||
httpHeaders.hasOwnProperty('content-type') && !returnJson
|
||||
httpHeaders.hasOwnProperty('content-type') &&
|
||||
!returnJson && // not a POST Request
|
||||
!isDebugOn(vars) // Debug is not enabled
|
||||
|
||||
const webout = (await fileExists(weboutPath))
|
||||
? fileResponse
|
||||
@@ -174,11 +180,10 @@ ${program}`
|
||||
|
||||
return {
|
||||
httpHeaders,
|
||||
result: fileResponse
|
||||
? webout
|
||||
: isDebugOn(vars) || session.crashed
|
||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||
: webout
|
||||
result:
|
||||
isDebugOn(vars) || session.crashed
|
||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||
: webout
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ export class FileUploadController {
|
||||
|
||||
const sessionController = getSessionController()
|
||||
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
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
createFile,
|
||||
fileExists,
|
||||
generateTimestamp,
|
||||
readFile,
|
||||
moveFile
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
@@ -31,7 +30,7 @@ export class SessionController {
|
||||
? readySessions[0]
|
||||
: await this.createSession()
|
||||
|
||||
if (readySessions.length < 2) this.createSession()
|
||||
if (readySessions.length < 3) this.createSession()
|
||||
|
||||
return session
|
||||
}
|
||||
@@ -41,6 +40,7 @@ export class SessionController {
|
||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||
|
||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||
// death time of session is 15 mins from creation
|
||||
const deathTimeStamp = (
|
||||
parseInt(creationTimeStamp) +
|
||||
15 * 60 * 1000 -
|
||||
@@ -87,6 +87,8 @@ ${autoExecContent}`
|
||||
codePath,
|
||||
'-LOG',
|
||||
path.join(session.path, 'log.log'),
|
||||
'-PRINT',
|
||||
path.join(session.path, 'output.lst'),
|
||||
'-WORK',
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
@@ -138,7 +140,9 @@ ${autoExecContent}`
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
setTimeout(async () => {
|
||||
if (session.inUse) {
|
||||
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import path from 'path'
|
||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||
import {
|
||||
MemberType,
|
||||
createFolder,
|
||||
createFile,
|
||||
asyncForEach,
|
||||
FolderMember,
|
||||
ServiceMember,
|
||||
FileTree,
|
||||
FileMember
|
||||
} from '../../types'
|
||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
||||
FileMember,
|
||||
MemberType,
|
||||
FileTree
|
||||
} from '@sasjs/utils'
|
||||
|
||||
// REFACTOR: export FileTreeCpntroller
|
||||
export const createFileTree = async (
|
||||
|
||||
@@ -24,7 +24,7 @@ export class SessionController {
|
||||
}
|
||||
|
||||
const session = (req: any) => ({
|
||||
id: req.user.id,
|
||||
id: req.user.userId,
|
||||
username: req.user.username,
|
||||
displayName: req.user.displayName
|
||||
})
|
||||
|
||||
@@ -143,9 +143,8 @@ const executeReturnRaw = async (
|
||||
query
|
||||
)) as ExecuteReturnRaw
|
||||
|
||||
// Should over-ride response header for
|
||||
// debug on GET request to see entire log
|
||||
// rendering on browser.
|
||||
// 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'
|
||||
}
|
||||
@@ -185,7 +184,8 @@ const executeReturnJson = async (
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, ...req.body },
|
||||
{ filesNamesMap: filesNamesMap },
|
||||
true
|
||||
true,
|
||||
req.sasSession
|
||||
)) as ExecuteReturnJson
|
||||
|
||||
let weboutRes: string | IRecordOfAny = webout
|
||||
|
||||
101
api/src/controllers/web.ts
Normal file
101
api/src/controllers/web.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import { Request, Route, Tags, Post, Body, Get } from 'tsoa'
|
||||
import { readFile } from '@sasjs/utils'
|
||||
|
||||
import User from '../model/User'
|
||||
import { getWebBuildFolderPath } from '../utils'
|
||||
|
||||
@Route('/')
|
||||
@Tags('Web')
|
||||
export class WebController {
|
||||
/**
|
||||
* @summary Render index.html
|
||||
*
|
||||
*/
|
||||
@Get('/')
|
||||
public async home(@Request() req: express.Request) {
|
||||
return home(req)
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 home = async (req: express.Request) => {
|
||||
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
||||
|
||||
// Attention! Cannot use fileExists here,
|
||||
// due to limitation after building executable
|
||||
const content = await readFile(indexHtmlPath)
|
||||
|
||||
req.res?.cookie('XSRF-TOKEN', req.csrfToken())
|
||||
req.res?.setHeader('Content-Type', 'text/html')
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
const login = async (
|
||||
req: express.Request,
|
||||
{ username, password }: LoginPayload
|
||||
) => {
|
||||
// Authenticate User
|
||||
const user = await User.findOne({ username })
|
||||
if (!user) throw new Error('Username is not found.')
|
||||
|
||||
const validPass = user.comparePassword(password)
|
||||
if (!validPass) throw new Error('Invalid password.')
|
||||
|
||||
req.session.loggedIn = true
|
||||
req.session.user = {
|
||||
userId: user.id,
|
||||
clientId: 'web_app',
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
isAdmin: user.isAdmin,
|
||||
isActive: user.isActive
|
||||
}
|
||||
|
||||
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 { csrfProtection } from '../app'
|
||||
import { verifyTokenInDB } from '../utils'
|
||||
|
||||
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(
|
||||
req,
|
||||
res,
|
||||
@@ -43,9 +52,7 @@ const authenticateToken = (
|
||||
}
|
||||
|
||||
const authHeader = req.headers['authorization']
|
||||
const token =
|
||||
authHeader?.split(' ')[1] ??
|
||||
(tokenType === 'accessToken' ? req.cookies.accessToken : '')
|
||||
const token = authHeader?.split(' ')[1]
|
||||
if (!token) return res.sendStatus(401)
|
||||
|
||||
jwt.verify(token, key, async (err: any, data: any) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import multer, { FileFilterCallback, Options } from 'multer'
|
||||
import { blockFileRegex, getTmpUploadsPath } from '../utils'
|
||||
|
||||
const fieldNameSize = 300
|
||||
const fileSize = 10485760 // 10 MB
|
||||
const fileSize = 104857600 // 100 MB
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: getTmpUploadsPath(),
|
||||
|
||||
@@ -1,44 +1,22 @@
|
||||
import express from 'express'
|
||||
|
||||
import { AuthController } from '../../controllers/'
|
||||
import Client from '../../model/Client'
|
||||
|
||||
import {
|
||||
authenticateAccessToken,
|
||||
authenticateRefreshToken
|
||||
} from '../../middlewares'
|
||||
|
||||
import {
|
||||
authorizeValidation,
|
||||
getDesktopFields,
|
||||
tokenValidation
|
||||
} from '../../utils'
|
||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
||||
import { InfoJWT } from '../../types'
|
||||
|
||||
const authRouter = express.Router()
|
||||
|
||||
const clientIDs = new Set()
|
||||
|
||||
export const populateClients = async () => {
|
||||
const result = await Client.find()
|
||||
clientIDs.clear()
|
||||
result.forEach((r) => {
|
||||
clientIDs.add(r.clientId)
|
||||
})
|
||||
}
|
||||
const controller = new AuthController()
|
||||
|
||||
authRouter.post('/authorize', async (req, res) => {
|
||||
const { error, value: body } = authorizeValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const { clientId } = body
|
||||
|
||||
// Verify client ID
|
||||
if (!clientIDs.has(clientId)) {
|
||||
return res.status(403).send('Invalid clientId.')
|
||||
}
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.authorize(body)
|
||||
|
||||
@@ -52,12 +30,10 @@ authRouter.post('/token', async (req, res) => {
|
||||
const { error, value: body } = tokenValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.token(body)
|
||||
const { accessToken } = response
|
||||
|
||||
res.cookie('accessToken', accessToken).send(response)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
@@ -66,7 +42,6 @@ authRouter.post('/token', async (req, res) => {
|
||||
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
||||
const userInfo: InfoJWT = req.user
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.refresh(userInfo)
|
||||
|
||||
@@ -79,7 +54,6 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
||||
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
|
||||
const userInfo: InfoJWT = req.user
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
await controller.logout(userInfo)
|
||||
} catch (e) {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import express from 'express'
|
||||
import { deleteFile } from '@sasjs/utils'
|
||||
import { deleteFile, readFile } from '@sasjs/utils'
|
||||
|
||||
import { publishAppStream } from '../appStream'
|
||||
|
||||
@@ -8,7 +8,8 @@ import { DriveController } from '../../controllers/'
|
||||
import {
|
||||
deployValidation,
|
||||
fileBodyValidation,
|
||||
fileParamValidation
|
||||
fileParamValidation,
|
||||
folderParamValidation
|
||||
} from '../../utils'
|
||||
|
||||
const controller = new DriveController()
|
||||
@@ -42,14 +43,74 @@ 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) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) return res.status(400).send(errQ.details[0].message)
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
await controller.getFile(req, query._filePath, body.filePath)
|
||||
await controller.getFile(req, query._filePath)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.get('/folder', async (req, res) => {
|
||||
const { error: errQ, value: query } = folderParamValidation(req.query)
|
||||
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.getFolder(query._folderPath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
@@ -57,12 +118,11 @@ driveRouter.get('/file', async (req, res) => {
|
||||
|
||||
driveRouter.delete('/file', async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) return res.status(400).send(errQ.details[0].message)
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.deleteFile(query._filePath, body.filePath)
|
||||
const response = await controller.deleteFile(query._filePath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
verifyAdmin
|
||||
} from '../../middlewares'
|
||||
|
||||
import infoRouter from './info'
|
||||
import driveRouter from './drive'
|
||||
import stpRouter from './stp'
|
||||
import codeRouter from './code'
|
||||
@@ -20,6 +21,7 @@ import sessionRouter from './session'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/info', infoRouter)
|
||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||
router.use('/auth', desktopRestrict, authRouter)
|
||||
router.use(
|
||||
@@ -34,12 +36,22 @@ router.use('/group', desktopRestrict, groupRouter)
|
||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||
router.use('/code', authenticateAccessToken, codeRouter)
|
||||
router.use('/user', desktopRestrict, userRouter)
|
||||
|
||||
router.use(
|
||||
'/',
|
||||
swaggerUi.serve,
|
||||
swaggerUi.setup(undefined, {
|
||||
swaggerOptions: {
|
||||
url: '/swagger.yaml'
|
||||
url: '/swagger.yaml',
|
||||
requestInterceptor: (request: any) => {
|
||||
request.credentials = 'include'
|
||||
|
||||
const cookie = document.cookie
|
||||
const startIndex = cookie.indexOf('XSRF-TOKEN')
|
||||
const csrf = cookie.slice(startIndex + 11).split('; ')[0]
|
||||
request.headers['X-XSRF-TOKEN'] = csrf
|
||||
return request
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
16
api/src/routes/api/info.ts
Normal file
16
api/src/routes/api/info.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import express from 'express'
|
||||
import { InfoController } from '../../controllers'
|
||||
|
||||
const infoRouter = express.Router()
|
||||
|
||||
infoRouter.get('/', async (req, res) => {
|
||||
const controller = new InfoController()
|
||||
try {
|
||||
const response = controller.info()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default infoRouter
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
ClientController,
|
||||
AuthController
|
||||
} from '../../../controllers/'
|
||||
import { populateClients } from '../auth'
|
||||
import { InfoJWT } from '../../../types'
|
||||
import {
|
||||
generateAccessToken,
|
||||
@@ -18,11 +17,6 @@ import {
|
||||
verifyTokenInDB
|
||||
} from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const clientSecret = 'someclientSecret'
|
||||
const user = {
|
||||
@@ -35,16 +29,18 @@ const user = {
|
||||
}
|
||||
|
||||
describe('auth', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
await clientController.createClient({ clientId, clientSecret })
|
||||
await populateClients()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -156,7 +152,7 @@ describe('auth', () => {
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Invalid clientId.')
|
||||
expect(res.text).toEqual('Error: Invalid clientId.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
||||
import { UserController, ClientController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const client = {
|
||||
clientId: 'someclientID',
|
||||
clientSecret: 'someclientSecret'
|
||||
@@ -28,12 +23,15 @@ const newClient = {
|
||||
}
|
||||
|
||||
describe('client', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
readFile,
|
||||
deleteFolder,
|
||||
generateTimestamp,
|
||||
copy
|
||||
copy,
|
||||
createFolder,
|
||||
createFile,
|
||||
ServiceMember,
|
||||
FolderMember
|
||||
} from '@sasjs/utils'
|
||||
import * as fileUtilModules from '../../../utils/file'
|
||||
|
||||
@@ -26,15 +30,9 @@ jest
|
||||
import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import { getTreeExample } from '../../../controllers/internal'
|
||||
import { FolderMember, ServiceMember } from '../../../types'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||
const { getTmpFilesFolderPath } = fileUtilModules
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
@@ -44,7 +42,8 @@ const user = {
|
||||
isActive: true
|
||||
}
|
||||
|
||||
describe('files', () => {
|
||||
describe('drive', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const controller = new UserController()
|
||||
@@ -52,6 +51,8 @@ describe('files', () => {
|
||||
let accessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
@@ -69,6 +70,7 @@ describe('files', () => {
|
||||
await mongoServer.stop()
|
||||
await deleteFolder(tmpFolder)
|
||||
})
|
||||
|
||||
describe('deploy', () => {
|
||||
const shouldFailAssertion = async (payload: any) => {
|
||||
const res = await request(app)
|
||||
@@ -172,17 +174,126 @@ describe('files', () => {
|
||||
|
||||
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', '/my/path/code.sas')
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
@@ -192,10 +303,12 @@ describe('files', () => {
|
||||
})
|
||||
|
||||
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: '/my/path/code1.sas' })
|
||||
.query({ _filePath: pathToUpload })
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
@@ -217,7 +330,7 @@ describe('files', () => {
|
||||
|
||||
it('should respond with Forbidden if file is already present', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
@@ -310,7 +423,7 @@ describe('files', () => {
|
||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
@@ -320,7 +433,7 @@ describe('files', () => {
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 10 MB'
|
||||
'File size is over limit. File limit is: 100 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
@@ -386,7 +499,7 @@ describe('files', () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', `/my/path/code-${generateTimestamp()}.sas`)
|
||||
.field('filePath', `/my/path/code-3.sas`)
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
.expect(403)
|
||||
|
||||
@@ -427,9 +540,9 @@ describe('files', () => {
|
||||
const pathToUpload = '/my/path/code.exe'
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
// .field('filePath', pathToUpload)
|
||||
.query({ _filePath: pathToUpload })
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
@@ -468,7 +581,7 @@ describe('files', () => {
|
||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
@@ -478,11 +591,84 @@ describe('files', () => {
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 10 MB'
|
||||
'File size is over limit. File limit is: 100 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
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({})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
||||
import { UserController, GroupController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
@@ -36,11 +31,14 @@ const userController = new UserController()
|
||||
const groupController = new GroupController()
|
||||
|
||||
describe('group', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
|
||||
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 { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
@@ -30,10 +25,13 @@ const user = {
|
||||
const controller = new UserController()
|
||||
|
||||
describe('user', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
|
||||
@@ -1,27 +1,6 @@
|
||||
import { AppStreamConfig } from '../../types'
|
||||
|
||||
const style = `<style>
|
||||
* {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
}
|
||||
.app-container .app {
|
||||
width: 150px;
|
||||
margin: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px 10px 0 0;
|
||||
text-align: center;
|
||||
}
|
||||
.app-container .app img{
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>`
|
||||
import { script } from './script'
|
||||
import { style } from './style'
|
||||
|
||||
const defaultAppLogo = '/sasjs-logo.svg'
|
||||
|
||||
@@ -31,7 +10,10 @@ const singleAppStreamHtml = (
|
||||
logo?: string
|
||||
) =>
|
||||
` <a class="app" href="${streamServiceName}" title="${appLoc}">
|
||||
<img src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}" />
|
||||
<img
|
||||
src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}"
|
||||
onerror="this.src = '${defaultAppLogo}';"
|
||||
/>
|
||||
${streamServiceName}
|
||||
</a>`
|
||||
|
||||
@@ -49,6 +31,14 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
|
||||
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>`
|
||||
|
||||
@@ -40,17 +40,6 @@ export const publishAppStream = async (
|
||||
|
||||
if (!streamServiceName) {
|
||||
streamServiceName = `AppStreamName${appCount + 1}`
|
||||
} else {
|
||||
const alreadyDeployed = process.appStreamConfig[streamServiceName]
|
||||
if (alreadyDeployed) {
|
||||
if (alreadyDeployed.appLoc === appLoc) {
|
||||
// redeploying to same streamServiceName
|
||||
} else {
|
||||
// trying to deploy to another existing streamServiceName
|
||||
// assign new streamServiceName
|
||||
streamServiceName = `${streamServiceName}-${appCount + 1}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
|
||||
@@ -63,7 +52,7 @@ export const publishAppStream = async (
|
||||
addEntryToFile
|
||||
)
|
||||
|
||||
const sasJsPort = process.env.PORT ?? 5000
|
||||
const sasJsPort = process.env.PORT || 5000
|
||||
console.log(
|
||||
'Serving Stream App: ',
|
||||
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
||||
|
||||
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>`
|
||||
@@ -5,7 +5,6 @@ import apiRouter from './api'
|
||||
import appStreamRouter from './appStream'
|
||||
|
||||
export const setupRoutes = (app: Express) => {
|
||||
app.use('/', webRouter)
|
||||
app.use('/SASjsApi', apiRouter)
|
||||
|
||||
app.use('/AppStream', function (req, res, next) {
|
||||
@@ -13,4 +12,6 @@ export const setupRoutes = (app: Express) => {
|
||||
// whatever the current router is
|
||||
appStreamRouter(req, res, next)
|
||||
})
|
||||
|
||||
app.use('/', webRouter)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import express from 'express'
|
||||
import { csrfProtection } from '../../app'
|
||||
import webRouter from './web'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use(csrfProtection)
|
||||
|
||||
router.use('/', webRouter)
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
import { readFile } from '@sasjs/utils'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import { getWebBuildFolderPath } from '../../utils'
|
||||
import { WebController } from '../../controllers/web'
|
||||
import { loginWebValidation } from '../../utils'
|
||||
|
||||
const webRouter = express.Router()
|
||||
const controller = new WebController()
|
||||
|
||||
const codeToInject = `
|
||||
<script>
|
||||
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
|
||||
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
|
||||
</script>`
|
||||
|
||||
webRouter.get('/', async (_, res) => {
|
||||
let content: string
|
||||
webRouter.get('/', async (req, res) => {
|
||||
try {
|
||||
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
||||
content = await readFile(indexHtmlPath)
|
||||
const response = await controller.home(req)
|
||||
return res.send(response)
|
||||
} catch (_) {
|
||||
return res.send('Web Build is not present')
|
||||
}
|
||||
})
|
||||
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') {
|
||||
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
|
||||
webRouter.post('/login', async (req, res) => {
|
||||
const { error, value: body } = loginWebValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
return res.send(injectedContent)
|
||||
try {
|
||||
const response = await controller.login(req, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(400).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
return res.send(content)
|
||||
webRouter.get('/logout', async (req, res) => {
|
||||
try {
|
||||
await controller.logout(req)
|
||||
res.status(200).send('OK!')
|
||||
} catch (err: any) {
|
||||
res.status(400).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default webRouter
|
||||
|
||||
@@ -4,8 +4,8 @@ import appPromise from './app'
|
||||
import { getCertificates } from './utils'
|
||||
|
||||
appPromise.then(async (app) => {
|
||||
const protocol = process.env.PROTOCOL ?? 'http'
|
||||
const sasJsPort = process.env.PORT ?? 5000
|
||||
const protocol = process.env.PROTOCOL || 'http'
|
||||
const sasJsPort = process.env.PORT || 5000
|
||||
|
||||
console.log('PROTOCOL: ', protocol)
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
export enum MemberType {
|
||||
service = 'service',
|
||||
file = 'file',
|
||||
folder = 'folder'
|
||||
}
|
||||
|
||||
export interface ServiceMember {
|
||||
name: string
|
||||
type: MemberType.service
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface FileMember {
|
||||
name: string
|
||||
type: MemberType.file
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface FolderMember {
|
||||
name: string
|
||||
type: MemberType.folder
|
||||
members: (FolderMember | ServiceMember | FileMember)[]
|
||||
}
|
||||
export interface FileTree {
|
||||
members: (FolderMember | ServiceMember | FileMember)[]
|
||||
}
|
||||
|
||||
export const isFileTree = (arg: any): arg is FileTree =>
|
||||
arg &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: ServiceMember | FileMember | FolderMember) =>
|
||||
!isServiceMember(member, '-') &&
|
||||
!isFileMember(member, '-') &&
|
||||
!isFolderMember(member, '-')
|
||||
).length === 0
|
||||
|
||||
const isServiceMember = (arg: any, pre: string): arg is ServiceMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.service &&
|
||||
typeof arg.code === 'string'
|
||||
|
||||
const isFileMember = (arg: any, pre: string): arg is ServiceMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.file &&
|
||||
typeof arg.code === 'string'
|
||||
|
||||
const isFolderMember = (arg: any, pre: string): arg is FolderMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.folder &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isServiceMember(member, pre + '-') &&
|
||||
!isFileMember(member, pre + '-') &&
|
||||
!isFolderMember(member, pre + '-')
|
||||
).length === 0
|
||||
8
api/src/types/Process.d.ts
vendored
8
api/src/types/Process.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
sasLoc: string
|
||||
driveLoc: string
|
||||
sessionController?: import('../controllers/internal').SessionController
|
||||
appStreamConfig: import('./').AppStreamConfig
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MacroVars } from '@sasjs/utils'
|
||||
|
||||
export interface ExecutionQuery {
|
||||
_program: string
|
||||
macroVars?: MacroVars
|
||||
_debug?: number
|
||||
}
|
||||
|
||||
export interface FileQuery {
|
||||
filePath: string
|
||||
}
|
||||
|
||||
export const isExecutionQuery = (arg: any): arg is ExecutionQuery =>
|
||||
arg && !Array.isArray(arg) && typeof arg._program === 'string'
|
||||
|
||||
export const isFileQuery = (arg: any): arg is FileQuery =>
|
||||
arg && !Array.isArray(arg) && typeof arg.filePath === 'string'
|
||||
@@ -1,9 +1,7 @@
|
||||
// TODO: uppercase types
|
||||
export * from './AppStreamConfig'
|
||||
export * from './Execution'
|
||||
export * from './FileTree'
|
||||
export * from './InfoJWT'
|
||||
export * from './PreProgramVars'
|
||||
export * from './Request'
|
||||
export * from './Session'
|
||||
export * from './TreeNode'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ 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))
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { populateClients } from '../routes/api/auth'
|
||||
import { seedDB } from './seedDB'
|
||||
|
||||
export const connectDB = async () => {
|
||||
// NOTE: when exporting app.js as agent for supertest
|
||||
// we should exclude connecting to the real database
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return
|
||||
} else {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server') {
|
||||
console.log('Running in Destop Mode, no DB to connect.')
|
||||
return
|
||||
}
|
||||
|
||||
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
|
||||
if (err) throw err
|
||||
|
||||
console.log('Connected to db!')
|
||||
|
||||
await populateClients()
|
||||
})
|
||||
try {
|
||||
await mongoose.connect(process.env.DB_CONNECT as string)
|
||||
} catch (err) {
|
||||
throw new Error('Unable to connect to DB!')
|
||||
}
|
||||
|
||||
console.log('Connected to DB!')
|
||||
await seedDB()
|
||||
|
||||
return mongoose.connection
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
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()
|
||||
|
||||
@@ -9,7 +9,7 @@ export const sysInitCompiledPath = path.join(
|
||||
)
|
||||
|
||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||
export const sasJSCoreMacrosInfo = path.join(apiRoot, 'sasjscore', '.macrolist')
|
||||
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||
|
||||
export const getWebBuildFolderPath = () =>
|
||||
path.join(codebaseRoot, 'web', 'build')
|
||||
|
||||
@@ -12,8 +12,9 @@ export * from './isDebugOn'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
export * from './saveTokensInDB'
|
||||
export * from './seedDB'
|
||||
export * from './setProcessVariables'
|
||||
export * from './sleep'
|
||||
export * from './setupFolders'
|
||||
export * from './upload'
|
||||
export * from './validation'
|
||||
export * from './verifyTokenInDB'
|
||||
|
||||
35
api/src/utils/seedDB.ts
Normal file
35
api/src/utils/seedDB.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import Client from '../model/Client'
|
||||
import User from '../model/User'
|
||||
|
||||
const CLIENT = {
|
||||
clientId: 'clientID1',
|
||||
clientSecret: 'clientSecret'
|
||||
}
|
||||
const ADMIN_USER = {
|
||||
id: 1,
|
||||
displayName: 'Super Admin',
|
||||
username: 'secretuser',
|
||||
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
export const seedDB = async () => {
|
||||
// Checking if client is already in the database
|
||||
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
||||
if (!clientExist) {
|
||||
const client = new Client(CLIENT)
|
||||
await client.save()
|
||||
|
||||
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||
}
|
||||
|
||||
// Checking if user is already in the database
|
||||
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||
if (!usernameExist) {
|
||||
const user = new User(ADMIN_USER)
|
||||
await user.save()
|
||||
|
||||
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path'
|
||||
import { getRealPath } from '@sasjs/utils'
|
||||
import { getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||
|
||||
import { configuration } from '../../package.json'
|
||||
import { getDesktopFields } from '.'
|
||||
@@ -12,18 +12,17 @@ export const setProcessVariables = async () => {
|
||||
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server') {
|
||||
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
|
||||
} 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)
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
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)
|
||||
|
||||
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 =>
|
||||
Joi.object({
|
||||
username: usernameSchema.required(),
|
||||
@@ -98,6 +104,11 @@ export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
||||
_filePath: filePathSchema
|
||||
}).validate(data)
|
||||
|
||||
export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_folderPath: Joi.string()
|
||||
}).validate(data)
|
||||
|
||||
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
code: Joi.string().required()
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Info",
|
||||
"description": "Get Server Info"
|
||||
},
|
||||
{
|
||||
"name": "Session",
|
||||
"description": "Get Session information"
|
||||
@@ -42,6 +46,10 @@
|
||||
{
|
||||
"name": "CODE",
|
||||
"description": "Operations on SAS code"
|
||||
},
|
||||
{
|
||||
"name": "Web",
|
||||
"description": "Operations on Web"
|
||||
}
|
||||
],
|
||||
"yaml": true,
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.40",
|
||||
"version": "0.0.66",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "server",
|
||||
"version": "0.0.40",
|
||||
"version": "0.0.66",
|
||||
"devDependencies": {
|
||||
"prettier": "^2.3.1",
|
||||
"standard-version": "^9.3.2"
|
||||
@@ -1350,9 +1350,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimist-options": {
|
||||
@@ -3158,9 +3158,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist-options": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.40",
|
||||
"version": "0.0.66",
|
||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||
"repository": "https://github.com/sasjs/server",
|
||||
"scripts": {
|
||||
"server": "npm run server:prepare && npm run server:start",
|
||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..",
|
||||
"server:start": "cd api && npm run start",
|
||||
"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",
|
||||
"release": "standard-version",
|
||||
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
### 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
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
PORT_API=[place sasjs server port] default value is 5000
|
||||
CLIENT_ID=<place clientId here>
|
||||
PORT_API=[place sasjs server port] default value is 5000
|
||||
36
web/package-lock.json
generated
36
web/package-lock.json
generated
@@ -4191,9 +4191,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
@@ -8472,9 +8472,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.5",
|
||||
@@ -8581,9 +8581,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
|
||||
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
@@ -14381,9 +14381,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
@@ -17622,9 +17622,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
@@ -17715,9 +17715,9 @@
|
||||
}
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
|
||||
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
|
||||
"dev": true
|
||||
},
|
||||
"node-releases": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
BIN
web/public/running-sas.png
Normal file
BIN
web/public/running-sas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { Route, HashRouter, Switch } from 'react-router-dom'
|
||||
import { ThemeProvider } from '@mui/material/styles'
|
||||
import { theme } from './theme'
|
||||
@@ -9,12 +9,12 @@ import Home from './components/home'
|
||||
import Drive from './containers/Drive'
|
||||
import Studio from './containers/Studio'
|
||||
|
||||
import useTokens from './components/useTokens'
|
||||
import { AppContext } from './context/appContext'
|
||||
|
||||
function App() {
|
||||
const { tokens, setTokens } = useTokens()
|
||||
const appContext = useContext(AppContext)
|
||||
|
||||
if (!tokens) {
|
||||
if (!appContext.loggedIn) {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<HashRouter>
|
||||
@@ -24,7 +24,7 @@ function App() {
|
||||
<Login getCodeOnly />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Login setTokens={setTokens} />
|
||||
<Login />
|
||||
</Route>
|
||||
</Switch>
|
||||
</HashRouter>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||
|
||||
import AppBar from '@mui/material/AppBar'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import Tabs from '@mui/material/Tabs'
|
||||
import Tab from '@mui/material/Tab'
|
||||
import Button from '@mui/material/Button'
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button,
|
||||
Menu,
|
||||
MenuItem
|
||||
} from '@mui/material'
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||
|
||||
import Username from './username'
|
||||
import { AppContext } from '../context/appContext'
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const PORT_API = process.env.PORT_API
|
||||
const baseUrl =
|
||||
@@ -16,11 +23,29 @@ const baseUrl =
|
||||
const Header = (props: any) => {
|
||||
const history = useHistory()
|
||||
const { pathname } = useLocation()
|
||||
const appContext = useContext(AppContext)
|
||||
const [tabValue, setTabValue] = useState(pathname)
|
||||
const [anchorEl, setAnchorEl] = useState<
|
||||
(EventTarget & HTMLButtonElement) | null
|
||||
>(null)
|
||||
|
||||
const handleMenu = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
||||
setTabValue(value)
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
if (appContext.logout) appContext.logout()
|
||||
}
|
||||
return (
|
||||
<AppBar
|
||||
position="fixed"
|
||||
@@ -28,10 +53,10 @@ const Header = (props: any) => {
|
||||
>
|
||||
<Toolbar variant="dense">
|
||||
<img
|
||||
src="logo-white.png"
|
||||
src="logo.png"
|
||||
alt="logo"
|
||||
style={{
|
||||
width: '50px',
|
||||
width: '35px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '25px'
|
||||
}}
|
||||
@@ -81,6 +106,39 @@ const Header = (props: any) => {
|
||||
>
|
||||
App Stream
|
||||
</Button>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
<Username
|
||||
username={appContext.displayName || appContext.username}
|
||||
onClickHandler={handleMenu}
|
||||
/>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
open={!!anchorEl}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||
<Button variant="contained" color="primary">
|
||||
Logout
|
||||
</Button>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)
|
||||
|
||||
@@ -1,41 +1,21 @@
|
||||
import React, { useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
|
||||
import { AppContext } from '../context/appContext'
|
||||
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const PORT_API = process.env.PORT_API
|
||||
const baseUrl =
|
||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||
const getAuthCode = async (credentials: any) =>
|
||||
axios.post('/SASjsApi/auth/authorize', credentials).then((res) => res.data)
|
||||
|
||||
const getAuthCode = async (credentials: any) => {
|
||||
return fetch(`${baseUrl}/SASjsApi/auth/authorize`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(credentials)
|
||||
}).then(async (response) => {
|
||||
const resText = await response.text()
|
||||
if (response.status !== 200) throw resText
|
||||
const login = async (payload: { username: string; password: string }) =>
|
||||
axios.post('/login', payload).then((res) => res.data)
|
||||
|
||||
return JSON.parse(resText)
|
||||
})
|
||||
}
|
||||
const getTokens = async (payload: any) => {
|
||||
return fetch(`${baseUrl}/SASjsApi/auth/token`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload)
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
const Login = ({ getCodeOnly }: any) => {
|
||||
const location = useLocation()
|
||||
const [username, setUserName] = useState('')
|
||||
const appContext = useContext(AppContext)
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
let error: boolean
|
||||
@@ -45,34 +25,40 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
error = false
|
||||
setErrorMessage('')
|
||||
e.preventDefault()
|
||||
let clientId = process.env.CLIENT_ID
|
||||
|
||||
if (getCodeOnly) {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const responseType = params.get('response_type')
|
||||
if (responseType === 'code')
|
||||
clientId = params.get('client_id') ?? undefined
|
||||
if (responseType === 'code') {
|
||||
const clientId = params.get('client_id')
|
||||
|
||||
const { code } = await getAuthCode({
|
||||
clientId,
|
||||
username,
|
||||
password
|
||||
}).catch((err: any) => {
|
||||
error = true
|
||||
setErrorMessage(err.response.data)
|
||||
return {}
|
||||
})
|
||||
if (!error) return setDisplayCode(code)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const { code } = await getAuthCode({
|
||||
clientId,
|
||||
const { loggedIn, user } = await login({
|
||||
username,
|
||||
password
|
||||
}).catch((err: string) => {
|
||||
}).catch((err: any) => {
|
||||
error = true
|
||||
setErrorMessage(err)
|
||||
setErrorMessage(err.response.data)
|
||||
return {}
|
||||
})
|
||||
|
||||
if (!error) {
|
||||
if (getCodeOnly) return setDisplayCode(code)
|
||||
|
||||
const { accessToken, refreshToken } = await getTokens({
|
||||
clientId,
|
||||
code
|
||||
})
|
||||
|
||||
setTokens(accessToken, refreshToken)
|
||||
if (loggedIn) {
|
||||
appContext.setLoggedIn?.(loggedIn)
|
||||
appContext.setUsername?.(user.username)
|
||||
appContext.setDisplayName?.(user.displayName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +101,7 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
label="Username"
|
||||
type="text"
|
||||
variant="outlined"
|
||||
onChange={(e: any) => setUserName(e.target.value)}
|
||||
onChange={(e: any) => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
@@ -127,7 +113,11 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
required
|
||||
/>
|
||||
{errorMessage && <span>{errorMessage}</span>}
|
||||
<Button type="submit" variant="outlined">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outlined"
|
||||
disabled={!appContext.setLoggedIn}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Box>
|
||||
@@ -135,7 +125,6 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
||||
}
|
||||
|
||||
Login.propTypes = {
|
||||
setTokens: PropTypes.func,
|
||||
getCodeOnly: PropTypes.bool
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function useTokens() {
|
||||
const getTokens = () => {
|
||||
const accessToken = localStorage.getItem('accessToken')
|
||||
const refreshToken = localStorage.getItem('refreshToken')
|
||||
|
||||
if (accessToken && refreshToken) {
|
||||
setAxiosRequestHeader(accessToken)
|
||||
return { accessToken, refreshToken }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const [tokens, setTokens] = useState(getTokens())
|
||||
|
||||
useEffect(() => {
|
||||
if (tokens === undefined) {
|
||||
localStorage.removeItem('accessToken')
|
||||
localStorage.removeItem('refreshToken')
|
||||
}
|
||||
}, [tokens])
|
||||
setAxiosResponse(setTokens)
|
||||
|
||||
const saveTokens = (accessToken: string, refreshToken: string) => {
|
||||
localStorage.setItem('accessToken', accessToken)
|
||||
localStorage.setItem('refreshToken', refreshToken)
|
||||
setAxiosRequestHeader(accessToken)
|
||||
setTokens({ accessToken, refreshToken })
|
||||
}
|
||||
|
||||
return {
|
||||
setTokens: saveTokens,
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const PORT_API = process.env.PORT_API
|
||||
const baseUrl =
|
||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||
|
||||
const isAbsoluteURLRegex = /^(?:\w+:)\/\//
|
||||
|
||||
const setAxiosRequestHeader = (accessToken: string) => {
|
||||
axios.interceptors.request.use(function (config) {
|
||||
if (baseUrl && !isAbsoluteURLRegex.test(config.url as string)) {
|
||||
config.url = baseUrl + config.url
|
||||
}
|
||||
config.headers!['Authorization'] = `Bearer ${accessToken}`
|
||||
config.withCredentials = true
|
||||
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
const setAxiosResponse = (setTokens: Function) => {
|
||||
// Add a response interceptor
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
// Any status code that lie within the range of 2xx cause this function to trigger
|
||||
return response
|
||||
},
|
||||
async function (error) {
|
||||
if (error.response?.status === 401) {
|
||||
// refresh token
|
||||
// const { accessToken, refreshToken: newRefresh } = await refreshMyToken(
|
||||
// refreshToken
|
||||
// )
|
||||
|
||||
// if (accessToken && newRefresh) {
|
||||
// setTokens(accessToken, newRefresh)
|
||||
// error.config.headers['Authorization'] = 'Bearer ' + accessToken
|
||||
// error.config.baseURL = undefined
|
||||
|
||||
// return axios.request(error.config)
|
||||
// }
|
||||
setTokens(undefined)
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// const refreshMyToken = async (refreshToken: string) => {
|
||||
// return fetch('http://localhost:5000/SASjsApi/auth/refresh', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${refreshToken}`
|
||||
// }
|
||||
// }).then((data) => data.json())
|
||||
// }
|
||||
30
web/src/components/username.tsx
Normal file
30
web/src/components/username.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { Typography, IconButton } from '@mui/material'
|
||||
import AccountCircle from '@mui/icons-material/AccountCircle'
|
||||
|
||||
const Username = (props: any) => {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={props.onClickHandler}
|
||||
color="inherit"
|
||||
>
|
||||
{props.avatarContent ? (
|
||||
<img
|
||||
src={props.avatarContent}
|
||||
alt="user-avatar"
|
||||
style={{ width: '25px' }}
|
||||
/>
|
||||
) : (
|
||||
<AccountCircle></AccountCircle>
|
||||
)}
|
||||
<Typography variant="h6" sx={{ color: 'white', padding: '0 8px' }}>
|
||||
{props.username}
|
||||
</Typography>
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default Username
|
||||
@@ -90,7 +90,11 @@ const Drive = () => {
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
<SideBar directoryData={directoryData} handleSelect={handleSelect} />
|
||||
<SideBar
|
||||
selectedFilePath={selectedFilePath}
|
||||
directoryData={directoryData}
|
||||
handleSelect={handleSelect}
|
||||
/>
|
||||
<Main
|
||||
selectedFilePath={selectedFilePath}
|
||||
removeFileFromTree={removeFileFromTree}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { makeStyles } from '@mui/styles'
|
||||
|
||||
@@ -30,13 +30,27 @@ const useStyles = makeStyles(() => ({
|
||||
const drawerWidth = 240
|
||||
|
||||
type Props = {
|
||||
selectedFilePath: string
|
||||
directoryData: TreeNode | null
|
||||
handleSelect: (node: TreeNode) => void
|
||||
}
|
||||
|
||||
const SideBar = ({ directoryData, handleSelect }: Props) => {
|
||||
const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const defaultExpanded = useMemo(() => {
|
||||
const splittedPath = selectedFilePath.split('/')
|
||||
const arr = ['']
|
||||
let nodeId = ''
|
||||
splittedPath.forEach((path) => {
|
||||
if (path !== '') {
|
||||
nodeId += '/' + path
|
||||
arr.push(nodeId)
|
||||
}
|
||||
})
|
||||
return arr
|
||||
}, [selectedFilePath])
|
||||
|
||||
const renderTree = (nodes: TreeNode) => (
|
||||
<TreeItem
|
||||
classes={{ root: classes.root }}
|
||||
@@ -72,7 +86,8 @@ const SideBar = ({ directoryData, handleSelect }: Props) => {
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
defaultExpanded={[directoryData.relativePath]}
|
||||
defaultExpanded={defaultExpanded}
|
||||
selected={defaultExpanded.slice(-1)}
|
||||
>
|
||||
{renderTree(directoryData)}
|
||||
</TreeView>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
import Box from '@mui/material/Box'
|
||||
import { Button, Paper, Stack, Tab } from '@mui/material'
|
||||
import { Button, Paper, Stack, Tab, Tooltip } from '@mui/material'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import Editor, { OnMount } from '@monaco-editor/react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
@@ -15,6 +15,17 @@ const useStyles = makeStyles(() => ({
|
||||
'&.Mui-selected': {
|
||||
color: 'black'
|
||||
}
|
||||
},
|
||||
subMenu: {
|
||||
marginTop: '25px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
runButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '5px 5px',
|
||||
minWidth: 'unset'
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -22,8 +33,10 @@ const Studio = () => {
|
||||
const location = useLocation()
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState('')
|
||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||
const [webout, setWebout] = useState('')
|
||||
const [tab, setTab] = React.useState('1')
|
||||
|
||||
const handleTabChange = (_e: any, newValue: string) => {
|
||||
setTab(newValue)
|
||||
}
|
||||
@@ -50,30 +63,32 @@ const Studio = () => {
|
||||
.map((logLine: any) => logLine.line)
|
||||
.join('\n')
|
||||
|
||||
setLog(`<div><h2>SAS Log</h2><pre>${parsedLog}</pre></div>`)
|
||||
setLog(parsedLog)
|
||||
|
||||
let weboutString: string
|
||||
try {
|
||||
weboutString = res.data.webout
|
||||
.split('>>weboutBEGIN<<')[1]
|
||||
.split('>>weboutEND<<')[0]
|
||||
} catch (_) {
|
||||
weboutString = res?.data?.webout ?? ''
|
||||
}
|
||||
|
||||
let webout: string
|
||||
try {
|
||||
webout = JSON.stringify(JSON.parse(weboutString), null, 4)
|
||||
} catch (_) {
|
||||
webout = weboutString
|
||||
}
|
||||
|
||||
setWebout(`<pre><code>${webout}</code></pre>`)
|
||||
setWebout(`${res.data?._webout}`)
|
||||
setTab('2')
|
||||
|
||||
// Scroll to bottom of log
|
||||
window.scrollTo(0, document.body.scrollHeight)
|
||||
})
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: any) => {
|
||||
if (event.ctrlKey) {
|
||||
if (event.key === 'v') {
|
||||
setCtrlPressed(false)
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') runCode(getSelection() || fileContent)
|
||||
if (!ctrlPressed) setCtrlPressed(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (event: any) => {
|
||||
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const content = localStorage.getItem('fileContent') ?? ''
|
||||
setFileContent(content)
|
||||
@@ -97,73 +112,89 @@ const Studio = () => {
|
||||
}, [location.search])
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<Box sx={{ width: '100%', typography: 'body1' }}>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<Tab className={classes.root} label="Code" value="1" />
|
||||
<Tab className={classes.root} label="Log" value="2" />
|
||||
<Box
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
|
||||
>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<Tab className={classes.root} label="Code" value="1" />
|
||||
<Tab className={classes.root} label="Log" value="2" />
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Tab className={classes.root} label="Webout" value="3" />
|
||||
</TabList>
|
||||
</Box>
|
||||
<TabPanel value="1">
|
||||
{/* <Toolbar /> */}
|
||||
<Paper
|
||||
sx={{
|
||||
height: '70vh',
|
||||
marginTop: '50px',
|
||||
padding: '10px',
|
||||
overflow: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
<Editor
|
||||
height="95%"
|
||||
value={fileContent}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={(val) => {
|
||||
if (val) setFileContent(val)
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
<Stack
|
||||
spacing={3}
|
||||
direction="row"
|
||||
sx={{ justifyContent: 'center', marginTop: '20px' }}
|
||||
>
|
||||
<Button variant="contained" onClick={handleRunBtnClick}>
|
||||
Run SAS Code
|
||||
</Tooltip>
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
<TabPanel style={{ paddingBottom: 0 }} value="1">
|
||||
<div className={classes.subMenu}>
|
||||
<Tooltip title="CTRL+ENTER will also run SAS code">
|
||||
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
||||
<img
|
||||
draggable="false"
|
||||
style={{ width: '25px' }}
|
||||
src="/running-sas.png"
|
||||
></img>
|
||||
<span style={{ fontSize: '12px' }}>RUN</span>
|
||||
</Button>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
<TabPanel value="2">
|
||||
<div
|
||||
id="sas_log"
|
||||
style={{ marginTop: '50px' }}
|
||||
dangerouslySetInnerHTML={{ __html: log }}
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* <Toolbar /> */}
|
||||
<Paper
|
||||
sx={{
|
||||
height: 'calc(100vh - 170px)',
|
||||
padding: '10px',
|
||||
overflow: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
<Editor
|
||||
height="98%"
|
||||
value={fileContent}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{ readOnly: ctrlPressed }}
|
||||
onChange={(val) => {
|
||||
if (val) setFileContent(val)
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value="3">
|
||||
<div
|
||||
style={{ marginTop: '50px' }}
|
||||
dangerouslySetInnerHTML={{ __html: webout }}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</Box>
|
||||
</>
|
||||
<p
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: -10,
|
||||
textAlign: 'center',
|
||||
fontSize: '13px'
|
||||
}}
|
||||
>
|
||||
Press CTRL + ENTER to run SAS code
|
||||
</p>
|
||||
</Paper>
|
||||
</TabPanel>
|
||||
<TabPanel value="2">
|
||||
<div style={{ marginTop: '50px' }}>
|
||||
<h2>SAS Log</h2>
|
||||
<pre>{log}</pre>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel value="3">
|
||||
<div style={{ marginTop: '50px' }}>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
85
web/src/context/appContext.tsx
Normal file
85
web/src/context/appContext.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, {
|
||||
createContext,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
ReactNode
|
||||
} from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
interface AppContextProps {
|
||||
checkingSession: boolean
|
||||
loggedIn: boolean
|
||||
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
||||
username: string
|
||||
setUsername: Dispatch<SetStateAction<string>> | null
|
||||
displayName: string
|
||||
setDisplayName: Dispatch<SetStateAction<string>> | null
|
||||
logout: (() => void) | null
|
||||
}
|
||||
|
||||
export const AppContext = createContext<AppContextProps>({
|
||||
checkingSession: false,
|
||||
loggedIn: false,
|
||||
setLoggedIn: null,
|
||||
username: '',
|
||||
setUsername: null,
|
||||
displayName: '',
|
||||
setDisplayName: null,
|
||||
logout: null
|
||||
})
|
||||
|
||||
const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
const { children } = props
|
||||
const [checkingSession, setCheckingSession] = useState(false)
|
||||
const [loggedIn, setLoggedIn] = useState(false)
|
||||
const [username, setUsername] = useState('')
|
||||
const [displayName, setDisplayName] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setCheckingSession(true)
|
||||
|
||||
axios
|
||||
.get('/SASjsApi/session')
|
||||
.then((res) => res.data)
|
||||
.then((data: any) => {
|
||||
setCheckingSession(false)
|
||||
setLoggedIn(true)
|
||||
setUsername(data.username)
|
||||
setDisplayName(data.displayName)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoggedIn(false)
|
||||
axios.get('/') // get CSRF TOKEN
|
||||
})
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(() => {
|
||||
axios.get('/logout').then(() => {
|
||||
setLoggedIn(false)
|
||||
setUsername('')
|
||||
setDisplayName('')
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
checkingSession,
|
||||
loggedIn,
|
||||
setLoggedIn,
|
||||
username,
|
||||
setUsername,
|
||||
displayName,
|
||||
setDisplayName,
|
||||
logout
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppContextProvider
|
||||
@@ -2,10 +2,25 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
import AppContextProvider from './context/appContext'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const PORT_API = process.env.PORT_API
|
||||
const baseUrl =
|
||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||
|
||||
axios.defaults = Object.assign(axios.defaults, {
|
||||
withCredentials: true,
|
||||
baseURL: baseUrl
|
||||
})
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<AppContextProvider>
|
||||
<App />
|
||||
</AppContextProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user