mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
209 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b066734398 | ||
|
|
3b698fce5f | ||
|
|
5ad6ee5e0f | ||
|
|
7d11cc7916 | ||
|
|
ff1def6436 | ||
|
|
c275db184e | ||
|
|
e4239fbcc3 | ||
|
|
c6fd8fdd70 | ||
|
|
79dc2dba23 | ||
|
|
2a7223ad7d | ||
|
|
1fed5ea6ac | ||
|
|
97f689f292 | ||
|
|
53bf68a6af | ||
|
|
f37f8e95d1 | ||
|
|
80b33c7a18 | ||
|
|
b1803fe385 | ||
|
|
7dd08c3b5b | ||
|
|
b780b59b66 | ||
|
|
7b457eaec5 | ||
|
|
c017d13061 | ||
|
|
c2b5e353a5 | ||
|
|
f89389bbc6 | ||
|
|
fadcc9bd29 | ||
|
|
182def2f3e | ||
|
|
06a5f39fea | ||
|
|
143b367a0e | ||
|
|
b5fd800300 | ||
|
|
a0b52d9982 | ||
|
|
c4212665c8 | ||
|
|
97d9bc191c | ||
|
|
dd2a403985 | ||
|
|
7cfa2398e1 | ||
|
|
5888f04e08 | ||
|
|
b40de8fa6a | ||
|
|
45a2a01532 | ||
|
|
c61fec47c4 | ||
| 24d7f00c02 | |||
| b0fdaaaa79 | |||
|
|
2467616296 | ||
|
|
ceefbe48e9 | ||
|
|
426e90471e | ||
|
|
c0b57b9e76 | ||
|
|
4a8e32dd20 | ||
|
|
636301e664 | ||
|
|
25dc5dd215 | ||
|
|
503994dbd2 | ||
|
|
0dceb5c3c3 | ||
|
|
1af04fa3b3 | ||
|
|
efa81fec77 | ||
|
|
10caf1918a | ||
|
|
4ed20a3b75 | ||
|
|
98b2c5fa25 | ||
|
|
3ad327b85f | ||
|
|
dd3acce393 | ||
|
|
8065727b9b | ||
|
|
e1223ec3f8 | ||
|
|
1f89279264 | ||
|
|
a07f47a1ba | ||
|
|
2548c82dfe | ||
|
|
238aa1006f | ||
|
|
35cba97611 | ||
|
|
5f29dec16f | ||
|
|
e2a97fcb7c | ||
|
|
6adeeefcf5 | ||
|
|
c9d66b8576 | ||
|
|
5aaac24080 | ||
|
|
6d34206bbc | ||
|
|
7b39cc06d3 | ||
|
|
6e7f28a6f8 | ||
|
|
5689169ce4 | ||
|
|
6139e7bff6 | ||
|
|
2c77317bb9 | ||
|
|
57b63db9cb | ||
|
|
60a2a4fe32 | ||
|
|
09611cb416 | ||
|
|
2a9bb6e6b1 | ||
|
|
b4b60c69cf | ||
|
|
b060ad1b8e | ||
|
|
d47ed6d0e8 | ||
|
|
a6993ef5ae | ||
|
|
2571fc2ca8 | ||
|
|
992f39b63a | ||
|
|
1ea3f6d8b3 | ||
|
|
e462aebdc0 | ||
|
|
13403517a4 | ||
|
|
c3c2048e75 | ||
|
|
1d8acc36eb | ||
|
|
4c7ad56326 | ||
|
|
e57443f1ed | ||
|
|
5da93f318a | ||
|
|
a30fb1a241 | ||
|
|
4ae8f35e9a | ||
|
|
ebb46f51b6 | ||
|
|
fe24f51ca2 | ||
|
|
fd15f3fb41 | ||
|
|
7d31ee7696 | ||
|
|
667e26b080 | ||
|
|
d09876c05f | ||
|
|
fb8e18be75 | ||
|
|
7ac7a4e083 | ||
|
|
8e23786dd4 | ||
|
|
4bd01bcf29 | ||
|
|
4ad8c81e49 | ||
|
|
51f6aa34a1 | ||
|
|
486207128d | ||
|
|
1e4b0b9171 | ||
|
|
1ff820605a | ||
|
|
9c1a781b3a | ||
| 36628551ae | |||
| 23cf8fa06f | |||
| 84ee743eae | |||
|
|
19e5bd7d2d | ||
|
|
e251747302 | ||
|
|
7e7558d4cf | ||
|
|
f02996facf | ||
|
|
803c51f400 | ||
|
|
c35b2b3f59 | ||
|
|
fe0866ace7 | ||
|
|
1513c3623d | ||
|
|
7fe43ae0b7 | ||
|
|
c4cea4a12b | ||
|
|
9fc7a132ba | ||
|
|
d55a619d64 | ||
|
|
737d2a24c2 | ||
|
|
2e63831b90 | ||
|
|
c7ffde1a3b | ||
|
|
db70b1ce55 | ||
|
|
8a3fe8b217 | ||
| 9dca552e82 | |||
|
|
505f2089c7 | ||
|
|
3344c400a8 | ||
| fa6248e3ef | |||
| 9fb5f1f8e7 | |||
|
|
92e0b8a088 | ||
|
|
b484306ed8 | ||
| 5e08aacc51 | |||
| a9e4eb685d | |||
| 31b09f27cc | |||
| 9f3ec92f8e | |||
| 6c9e449614 | |||
| 68e84b0994 | |||
| f0bb51a0d5 | |||
| b93a0da3a3 | |||
|
|
e5facbf54c | ||
|
|
cb2bebbe76 | ||
|
|
9e1e0ce8cc | ||
|
|
29928753b7 | ||
|
|
edd69ecaae | ||
|
|
74ba65f9f3 | ||
|
|
f257602834 | ||
|
|
61080d4694 | ||
|
|
82633adbc4 | ||
|
|
23db7e7b7d | ||
|
|
cbaa687c9b | ||
|
|
527f70e90d | ||
|
|
122faad55f | ||
|
|
3ff6f5e865 | ||
|
|
7d5128c0d6 | ||
|
|
e1ebbfd087 | ||
|
|
e430bdb0d4 | ||
|
|
9d9769eef3 | ||
|
|
9d167abe2a | ||
|
|
18d0604bdd | ||
|
|
7b7bc6b778 | ||
|
|
fb4f3442d5 | ||
|
|
09d1b7d5d4 | ||
|
|
99839ae62f | ||
|
|
f700561e1a | ||
|
|
8b4b4b91ab | ||
|
|
acb3ae0493 | ||
|
|
f48aeb1b0b | ||
|
|
5c0e8e5344 | ||
|
|
0ac9e4af7d | ||
|
|
ee80f3f968 | ||
|
|
7f4201ba85 | ||
|
|
f830bbc058 | ||
|
|
f8e1522a5a | ||
|
|
0a5aeceab5 | ||
|
|
6dc39c0d91 | ||
|
|
117a53ceea | ||
|
|
dd56a95314 | ||
|
|
c5117abe71 | ||
|
|
84c632a861 | ||
|
|
3ddd09eba0 | ||
|
|
0c0301433c | ||
|
|
954b2e3e2e | ||
|
|
5655311b96 | ||
|
|
9ace33d783 | ||
|
|
adc5aca0f0 | ||
|
|
71c6be6b84 | ||
|
|
9c751877d1 | ||
|
|
2204d54cd6 | ||
|
|
f4eb75ff34 | ||
|
|
a3cde343b7 | ||
|
|
7a70d40dbf | ||
|
|
d27e070fc8 | ||
|
|
27e260e6a4 | ||
|
|
2796db8ead | ||
|
|
84f7c2ab89 | ||
|
|
e68090181a | ||
|
|
d2956fc641 | ||
|
|
a701bb25e7 | ||
|
|
5758bcd392 | ||
|
|
9e53470947 | ||
|
|
81f6605249 | ||
|
|
0b45402946 | ||
|
|
9ac3191891 | ||
|
|
cd00aa2af8 | ||
|
|
0147bcb701 |
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.
|
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
|
||||||
|
|
||||||
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]
|
### Docker Development Mode
|
||||||
|
|
||||||
There is `.env.example` file present at `./api` of the project. [for Development]
|
|
||||||
|
|
||||||
There is `.env.example` file present at `./web` of the project. [for Development]
|
|
||||||
|
|
||||||
Remember to provide enviornment variables.
|
|
||||||
|
|
||||||
#### Development
|
|
||||||
|
|
||||||
Command to run docker for development:
|
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_
|
- `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:
|
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-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_
|
- `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`
|
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.
|
||||||
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.
|
### 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 install
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Web
|
#### Web Server
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
cd web
|
||||||
npm install
|
npm install
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Development (running only api server and have web build served):
|
#### NodeJS Production Mode
|
||||||
|
|
||||||
##### API server also serving Web build files
|
Update the `.env` file in the *api* folder. Then:
|
||||||
|
|
||||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
|
||||||
Command to install and run api server.
|
|
||||||
|
|
||||||
```
|
|
||||||
cd ./web && npm i && npm build && cd ../
|
|
||||||
cd ./api && npm i && npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Production
|
|
||||||
|
|
||||||
##### API & WEB
|
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run server
|
npm run server
|
||||||
@@ -105,7 +100,7 @@ This will install/build `web` and install `api`, then start prod server.
|
|||||||
|
|
||||||
## Executables
|
## Executables
|
||||||
|
|
||||||
Command to generate executables
|
In order to generate the final executables:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd ./web && npm i && npm build && cd ../
|
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`
|
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)
|
||||||
|
|||||||
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -54,6 +54,7 @@ jobs:
|
|||||||
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
||||||
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
||||||
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
||||||
|
SESSION_SECRET: ${{secrets.SESSION_SECRET}}
|
||||||
|
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
working-directory: ./api
|
working-directory: ./api
|
||||||
|
|||||||
27
.github/workflows/release.yml
vendored
27
.github/workflows/release.yml
vendored
@@ -2,16 +2,26 @@ name: SASjs Server Executable Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
branches:
|
||||||
- 'v*.*.*'
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [lts/*]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
- name: Install Dependencies WEB
|
- name: Install Dependencies WEB
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -39,10 +49,11 @@ jobs:
|
|||||||
zip macos.zip api-macos
|
zip macos.zip api-macos
|
||||||
zip windows.zip api-win.exe
|
zip windows.zip api-win.exe
|
||||||
|
|
||||||
|
- name: Install Semantic Release and plugins
|
||||||
|
run: |
|
||||||
|
npm i
|
||||||
|
npm i -g semantic-release
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
run: |
|
||||||
with:
|
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
|
||||||
files: |
|
|
||||||
./executables/linux.zip
|
|
||||||
./executables/macos.zip
|
|
||||||
./executables/windows.zip
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.env*
|
.env*
|
||||||
sas/
|
sas/
|
||||||
|
sasjs_root/
|
||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
@@ -11,3 +12,4 @@ sasjscore/
|
|||||||
certificates/
|
certificates/
|
||||||
executables/
|
executables/
|
||||||
.env
|
.env
|
||||||
|
api/csp.config.json
|
||||||
|
|||||||
43
.releaserc
Normal file
43
.releaserc
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"CHANGELOG.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/github",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"path": "./executables/linux.zip",
|
||||||
|
"label": "Linux Executable Binary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./executables/macos.zip",
|
||||||
|
"label": "Macos Executable Binary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./executables/windows.zip",
|
||||||
|
"label": "Windows Executable Binary"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"publishCmd": "echo 'publish command'"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
369
CHANGELOG.md
369
CHANGELOG.md
@@ -1,6 +1,371 @@
|
|||||||
# Changelog
|
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** username should be lowercase ([5ad6ee5](https://github.com/sasjs/server/commit/5ad6ee5e0f5d7d6faa45b72215f1d9d55cfc37db))
|
||||||
|
* **web:** reduced width for autoexec input ([7d11cc7](https://github.com/sasjs/server/commit/7d11cc79161e5a07f6c5392d742ef6b9d8658071))
|
||||||
|
|
||||||
|
# [0.3.0](https://github.com/sasjs/server/compare/v0.2.0...v0.3.0) (2022-05-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **web:** added profile + edit + autoexec changes ([c275db1](https://github.com/sasjs/server/commit/c275db184e874f0ee3a4f08f2592cfacf1e90742))
|
||||||
|
|
||||||
|
# [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **autoexec:** usage in case of desktop from file ([79dc2db](https://github.com/sasjs/server/commit/79dc2dba23dc48ec218a973119392a45cb3856b5))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** added autoexec + major type setting changes ([2a7223a](https://github.com/sasjs/server/commit/2a7223ad7d6b8f3d4682447fd25d9426a7c79ac3))
|
||||||
|
|
||||||
|
# [0.1.0](https://github.com/sasjs/server/compare/v0.0.77...v0.1.0) (2022-05-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* issue174 + issue175 + issue146 ([80b33c7](https://github.com/sasjs/server/commit/80b33c7a18c1b7727316ffeca71658346733e935))
|
||||||
|
* **web:** click to copy + notification ([f37f8e9](https://github.com/sasjs/server/commit/f37f8e95d1a85e00ceca2413dbb5e1f3f3f72255))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **env:** added new env variable LOG_FORMAT_MORGAN ([53bf68a](https://github.com/sasjs/server/commit/53bf68a6aff44bb7b2f40d40d6554809253a01a8))
|
||||||
|
|
||||||
|
## [0.0.77](https://github.com/sasjs/server/compare/v0.0.76...v0.0.77) (2022-05-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **release:** Github workflow without npm token ([c017d13](https://github.com/sasjs/server/commit/c017d13061d21aeacd0690367992d12ca57a115b))
|
||||||
|
|
||||||
|
### [0.0.76](https://github.com/sasjs/server/compare/v0.0.75...v0.0.76) (2022-05-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* get csrf token from cookie if not present in header ([f89389b](https://github.com/sasjs/server/commit/f89389bbc6f1f8f7060db2bdeb89746cbd60f533))
|
||||||
|
|
||||||
|
### [0.0.75](https://github.com/sasjs/server/compare/v0.0.69...v0.0.75) (2022-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
|
||||||
|
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
|
||||||
|
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
|
||||||
|
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
|
||||||
|
* moved getAuthCode from api to web routes ([b40de8f](https://github.com/sasjs/server/commit/b40de8fa6a5aa763ed25a6fe6a381e483e0ab824))
|
||||||
|
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
|
||||||
|
* **web:** seperate container for auth code ([5888f04](https://github.com/sasjs/server/commit/5888f04e08a32c6d2c7bcfcbc3a1d32425bff3b3))
|
||||||
|
|
||||||
|
### [0.0.74](https://github.com/sasjs/server/compare/v0.0.73...v0.0.74) (2022-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
|
||||||
|
|
||||||
|
### [0.0.73](https://github.com/sasjs/server/compare/v0.0.72...v0.0.73) (2022-05-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
|
||||||
|
|
||||||
|
### [0.0.72](https://github.com/sasjs/server/compare/v0.0.71...v0.0.72) (2022-05-09)
|
||||||
|
|
||||||
|
### [0.0.71](https://github.com/sasjs/server/compare/v0.0.70...v0.0.71) (2022-05-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
|
||||||
|
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
|
||||||
|
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
|
||||||
|
|
||||||
|
### [0.0.70](https://github.com/sasjs/server/compare/v0.0.69...v0.0.70) (2022-05-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
|
||||||
|
|
||||||
|
### [0.0.69](https://github.com/sasjs/server/compare/v0.0.68...v0.0.69) (2022-05-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **upload:** appStream uses CSRF + Session authentication ([1f89279](https://github.com/sasjs/server/commit/1f8927926405887f3d134c0a1dd6452ffa33876e))
|
||||||
|
|
||||||
|
### [0.0.68](https://github.com/sasjs/server/compare/v0.0.67...v0.0.68) (2022-05-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* using monaco editor locally ([2548c82](https://github.com/sasjs/server/commit/2548c82dfe1149e62a570a00546dddd9e30049b1))
|
||||||
|
|
||||||
|
### [0.0.67](https://github.com/sasjs/server/compare/v0.0.66...v0.0.67) (2022-05-01)
|
||||||
|
|
||||||
|
### [0.0.66](https://github.com/sasjs/server/compare/v0.0.64...v0.0.66) (2022-05-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added swagger ui init file manually ([e2a97fc](https://github.com/sasjs/server/commit/e2a97fcb7c54a57a7ca118677cfce93fe9430d8f))
|
||||||
|
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
|
||||||
|
|
||||||
|
### [0.0.65](https://github.com/sasjs/server/compare/v0.0.64...v0.0.65) (2022-05-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
|
||||||
|
|
||||||
|
### [0.0.64](https://github.com/sasjs/server/compare/v0.0.63...v0.0.64) (2022-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removed fileExists for serving web ([7b39cc0](https://github.com/sasjs/server/commit/7b39cc06d358f5ffecb87955040c4eb0fcc7469e))
|
||||||
|
|
||||||
|
### [0.0.63](https://github.com/sasjs/server/compare/v0.0.62...v0.0.63) (2022-04-30)
|
||||||
|
|
||||||
|
### [0.0.62](https://github.com/sasjs/server/compare/v0.0.61...v0.0.62) (2022-04-30)
|
||||||
|
|
||||||
|
### [0.0.61](https://github.com/sasjs/server/compare/v0.0.59...v0.0.61) (2022-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||||
|
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||||
|
|
||||||
|
### [0.0.60](https://github.com/sasjs/server/compare/v0.0.59...v0.0.60) (2022-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||||
|
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||||
|
|
||||||
|
### [0.0.59](https://github.com/sasjs/server/compare/v0.0.58...v0.0.59) (2022-04-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* enabled csrf tokens for web component ([e462aeb](https://github.com/sasjs/server/commit/e462aebdc01f3c0068ed0074473a2063412dcf45))
|
||||||
|
* enabled session based authentication for web ([5da93f3](https://github.com/sasjs/server/commit/5da93f318aad10b1c67032a467191e4dbb99f411))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fetch client from DB for each request ([4ad8c81](https://github.com/sasjs/server/commit/4ad8c81e4927c1a82220ec015a781b095c8e859e))
|
||||||
|
* **web:** show display name instead of username ([e57443f](https://github.com/sasjs/server/commit/e57443f1ed662a022494bb93d79c3d2f10a2d082))
|
||||||
|
|
||||||
|
### [0.0.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bumping core library to get latest user management macros ([4862071](https://github.com/sasjs/server/commit/486207128da58fc4866bd0919c1bed2bd98097ea))
|
||||||
|
* missing dependency ([d09876c](https://github.com/sasjs/server/commit/d09876c05f89166eec20064f7aa7ed5b867be081))
|
||||||
|
|
||||||
|
### [0.0.57](https://github.com/sasjs/server/compare/v0.0.56...v0.0.57) (2022-04-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* create AppContext ([84ee743](https://github.com/sasjs/server/commit/84ee743eae16e87eaa91969393bebf01e2d15a44))
|
||||||
|
|
||||||
|
### [0.0.56](https://github.com/sasjs/server/compare/v0.0.55...v0.0.56) (2022-04-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* shortening min length of username. Closes [#61](https://github.com/sasjs/server/issues/61) ([f02996f](https://github.com/sasjs/server/commit/f02996facf1019ec4022ccfbc99c1d0137074e1b))
|
||||||
|
|
||||||
|
### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||||
|
* drive path in server mode ([c4cea4a](https://github.com/sasjs/server/commit/c4cea4a12b7eda4daeed995f41c0b10bcea79871))
|
||||||
|
|
||||||
|
### [0.0.54](https://github.com/sasjs/server/compare/v0.0.53...v0.0.54) (2022-04-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||||
|
|
||||||
|
### [0.0.53](https://github.com/sasjs/server/compare/v0.0.49...v0.0.53) (2022-04-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||||
|
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||||
|
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||||
|
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* provide clientId to web component ([db70b1c](https://github.com/sasjs/server/commit/db70b1ce555df6b29fb09c0c960d38b911c97b1b))
|
||||||
|
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||||
|
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||||
|
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||||
|
|
||||||
|
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||||
|
|
||||||
|
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||||
|
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||||
|
|
||||||
|
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||||
|
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||||
|
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||||
|
|
||||||
|
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
|
||||||
|
|
||||||
|
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **deploy:** new route added for deploy with build.json ([18d0604](https://github.com/sasjs/server/commit/18d0604bdd0b20ad468f9345474b4de034ee3a67))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* remove uploaded build.json from temp folder in all cases ([9d167ab](https://github.com/sasjs/server/commit/9d167abe2adb743bca161862b4561bf573182c00))
|
||||||
|
* **stp:** return log+webout for debug on ([3ff6f5e](https://github.com/sasjs/server/commit/3ff6f5e86581cd2ac23bbe0b8e2c367fbea890ed))
|
||||||
|
|
||||||
|
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** updated STUDIO log and webout ([f700561](https://github.com/sasjs/server/commit/f700561e1a8d06c18ca2bdbe4605d7ab34f7a761))
|
||||||
|
|
||||||
|
### [0.0.46](https://github.com/sasjs/server/compare/v0.0.45...v0.0.46) (2022-03-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **drive:** GET folder contents API added ([0ac9e4a](https://github.com/sasjs/server/commit/0ac9e4af7d67c4431053e80eb2384bf5bdc3f8b3))
|
||||||
|
|
||||||
|
### [0.0.45](https://github.com/sasjs/server/compare/v0.0.43...v0.0.45) (2022-03-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||||
|
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||||
|
* proving a PRINT destination during SAS invocation. ([7f4201b](https://github.com/sasjs/server/commit/7f4201ba855743144fa6d3efac2b11e816d4696e)), closes [#111](https://github.com/sasjs/server/issues/111)
|
||||||
|
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||||
|
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||||
|
|
||||||
|
### [0.0.44](https://github.com/sasjs/server/compare/v0.0.43...v0.0.44) (2022-03-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||||
|
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||||
|
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||||
|
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||||
|
|
||||||
|
### [0.0.43](https://github.com/sasjs/server/compare/v0.0.42...v0.0.43) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deploy:** user can deploy to same appName with different/same appLoc ([9ace33d](https://github.com/sasjs/server/commit/9ace33d7830a9def42d741c23b46090afe0c5510))
|
||||||
|
* fallback logo on AppStream ([5655311](https://github.com/sasjs/server/commit/5655311b9663225823c192b39a03f39d17dda730))
|
||||||
|
|
||||||
|
### [0.0.42](https://github.com/sasjs/server/compare/v0.0.41...v0.0.42) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* execute api, webout as raw ([9c75187](https://github.com/sasjs/server/commit/9c751877d1ed0d0677aff816169a1df7c34c6bf5))
|
||||||
|
|
||||||
|
### [0.0.41](https://github.com/sasjs/server/compare/v0.0.40...v0.0.41) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **scroll:** closes [#100](https://github.com/sasjs/server/issues/100) ([f4eb75f](https://github.com/sasjs/server/commit/f4eb75ff347e78ac334e55ee26fbdd247bb8eaa2))
|
||||||
|
|
||||||
|
### [0.0.40](https://github.com/sasjs/server/compare/v0.0.39...v0.0.40) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deploy:** validating empty file or service in filetree ([27e260e](https://github.com/sasjs/server/commit/27e260e6a453e9978830db63ab669bd48c029897))
|
||||||
|
* macros available for SAS ([7a70d40](https://github.com/sasjs/server/commit/7a70d40dbf0cd91cb3af156755f10006b860f917))
|
||||||
|
* moved macros from codebase to drive ([d27e070](https://github.com/sasjs/server/commit/d27e070fc83894854278df22a8223b8016a1f5f7))
|
||||||
|
|
||||||
|
### [0.0.39](https://github.com/sasjs/server/compare/v0.0.38...v0.0.39) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* included sasjs core macros at compile time ([e680901](https://github.com/sasjs/server/commit/e68090181acd844f86f3e81153cb5a4e3f4a307f))
|
||||||
|
|
||||||
|
### [0.0.38](https://github.com/sasjs/server/compare/v0.0.37...v0.0.38) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* quick fix for executables ([9e53470](https://github.com/sasjs/server/commit/9e53470947350f4b8d835a2cb6b70e3dabf247c4))
|
||||||
|
|
||||||
|
### [0.0.37](https://github.com/sasjs/server/compare/v0.0.36...v0.0.37) (2022-03-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* appStream html view ([cd00aa2](https://github.com/sasjs/server/commit/cd00aa2af8c7e0df851050a02152dfeddaec7b0f))
|
||||||
|
* moved macros from codebase to drive ([9ac3191](https://github.com/sasjs/server/commit/9ac3191891bf53ff07135ccec6ddc83b34ea871a))
|
||||||
|
* **webin:** closes [#99](https://github.com/sasjs/server/issues/99) ([0147bcb](https://github.com/sasjs/server/commit/0147bcb701a209266144147a3746baf1eb1ccc63))
|
||||||
|
|
||||||
### [0.0.36](https://github.com/sasjs/server/compare/v0.0.35...v0.0.36) (2022-03-21)
|
### [0.0.36](https://github.com/sasjs/server/compare/v0.0.35...v0.0.36) (2022-03-21)
|
||||||
|
|
||||||
|
|||||||
98
README.md
98
README.md
@@ -48,22 +48,46 @@ When launching the app, it will make use of specific environment variables. Thes
|
|||||||
Example contents of a `.env` file:
|
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`
|
## Core Settings
|
||||||
WHITELIST= # options: <http://localhost:3000 https://abc.com ...> space separated urls
|
#
|
||||||
PROTOCOL=http # options: [http|https] default: http
|
|
||||||
PORT=5000 # default: 5000
|
|
||||||
|
|
||||||
# optional
|
|
||||||
# for MODE: `desktop`, prompts user
|
# MODE options: [desktop|server] default: `desktop`
|
||||||
# for MODE: `server` gets value from api/package.json `configuration.sasPath`
|
# Desktop mode is single user and designed for workstation use
|
||||||
|
# Server mode is multi-user and suitable for intranet / internet use
|
||||||
|
MODE=
|
||||||
|
|
||||||
|
# Path to SAS executable (sas.exe / sas.sh)
|
||||||
SAS_PATH=/path/to/sas/executable.exe
|
SAS_PATH=/path/to/sas/executable.exe
|
||||||
|
|
||||||
|
# Path to working directory
|
||||||
|
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
||||||
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
# optional
|
# options: [http|https] default: http
|
||||||
# for MODE: `desktop`, prompts user
|
PROTOCOL=
|
||||||
# for MODE: `server` defaults to /tmp
|
|
||||||
DRIVE_PATH=/tmp
|
# default: 5000
|
||||||
|
PORT=
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
## Additional 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
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
## Additional Web Server Options
|
||||||
|
#
|
||||||
|
|
||||||
# ENV variables required for PROTOCOL: `https`
|
# ENV variables required for PROTOCOL: `https`
|
||||||
PRIVATE_KEY=privkey.pem
|
PRIVATE_KEY=privkey.pem
|
||||||
@@ -73,17 +97,61 @@ FULL_CHAIN=fullchain.pem
|
|||||||
ACCESS_TOKEN_SECRET=<secret>
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
REFRESH_TOKEN_SECRET=<secret>
|
||||||
AUTH_CODE_SECRET=<secret>
|
AUTH_CODE_SECRET=<secret>
|
||||||
|
SESSION_SECRET=<secret>
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
|
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||||
|
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
||||||
|
CORS=
|
||||||
|
|
||||||
|
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||||
|
WHITELIST=
|
||||||
|
|
||||||
|
# HELMET Cross Origin Embedder Policy
|
||||||
|
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
|
||||||
|
# options: [true|false] default: true
|
||||||
|
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
|
||||||
|
HELMET_COEP=
|
||||||
|
|
||||||
|
# HELMET Content Security Policy
|
||||||
|
# Path to a json file containing HELMET `contentSecurityPolicy` directives
|
||||||
|
# Docs: https://helmetjs.github.io/#reference
|
||||||
|
#
|
||||||
|
# Example config:
|
||||||
|
# {
|
||||||
|
# "img-src": ["'self'", "data:"],
|
||||||
|
# "script-src": ["'self'", "'unsafe-inline'"],
|
||||||
|
# "script-src-attr": ["'self'", "'unsafe-inline'"]
|
||||||
|
# }
|
||||||
|
HELMET_CSP_CONFIG_PATH=./csp.config.json
|
||||||
|
|
||||||
|
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
||||||
|
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
||||||
|
LOG_FORMAT_MORGAN=
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Persisting the Session
|
## 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
|
```bash
|
||||||
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||||
export PORT=5001
|
export PORT=5001
|
||||||
export DRIVE_PATH=./tmp
|
export SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
pm2 start api-linux
|
pm2 start api-linux
|
||||||
```
|
```
|
||||||
@@ -112,7 +180,7 @@ Instead of `app_name` you can pass:
|
|||||||
|
|
||||||
## Server Version
|
## 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`
|
- CLIENTID: `clientID1`
|
||||||
- USERNAME: `secretuser`
|
- USERNAME: `secretuser`
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
MODE=[desktop|server] default considered as desktop
|
MODE=[desktop|server] default considered as desktop
|
||||||
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
|
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`>
|
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
||||||
|
|
||||||
PROTOCOL=[http|https] default considered as http
|
PROTOCOL=[http|https] default considered as http
|
||||||
PRIVATE_KEY=privkey.pem
|
PRIVATE_KEY=privkey.pem
|
||||||
FULL_CHAIN=fullchain.pem
|
FULL_CHAIN=fullchain.pem
|
||||||
|
|
||||||
PORT=[5000] default value is 5000
|
PORT=[5000] default value is 5000
|
||||||
|
|
||||||
|
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
||||||
|
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
||||||
|
|
||||||
ACCESS_TOKEN_SECRET=<secret>
|
ACCESS_TOKEN_SECRET=<secret>
|
||||||
REFRESH_TOKEN_SECRET=<secret>
|
REFRESH_TOKEN_SECRET=<secret>
|
||||||
AUTH_CODE_SECRET=<secret>
|
AUTH_CODE_SECRET=<secret>
|
||||||
|
SESSION_SECRET=<secret>
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
DRIVE_PATH=./tmp
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
|
LOG_FORMAT_MORGAN=common
|
||||||
5
api/csp.config.example.json
Normal file
5
api/csp.config.example.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"img-src": ["'self'", "data:"],
|
||||||
|
"script-src": ["'self'", "'unsafe-inline'"],
|
||||||
|
"script-src-attr": ["'self'", "'unsafe-inline'"]
|
||||||
|
}
|
||||||
543
api/package-lock.json
generated
543
api/package-lock.json
generated
@@ -8,19 +8,23 @@
|
|||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "4.9.0",
|
"@sasjs/core": "^4.23.1",
|
||||||
"@sasjs/utils": "2.36.2",
|
"@sasjs/utils": "2.42.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"connect-mongo": "^4.6.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"csurf": "^1.11.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-session": "^1.17.2",
|
||||||
|
"helmet": "^5.0.2",
|
||||||
"joi": "^17.4.2",
|
"joi": "^17.4.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mongoose": "^6.0.12",
|
"mongoose": "^6.0.12",
|
||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
"swagger-ui-express": "^4.1.6"
|
"swagger-ui-express": "4.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"api": "build/src/server.js"
|
"api": "build/src/server.js"
|
||||||
@@ -29,7 +33,9 @@
|
|||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/csurf": "^1.11.2",
|
||||||
"@types/express": "^4.17.12",
|
"@types/express": "^4.17.12",
|
||||||
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/jsonwebtoken": "^8.5.5",
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
"@types/mongoose-sequence": "^3.0.6",
|
"@types/mongoose-sequence": "^3.0.6",
|
||||||
@@ -43,7 +49,7 @@
|
|||||||
"jest": "^27.0.6",
|
"jest": "^27.0.6",
|
||||||
"mongodb-memory-server": "^8.0.0",
|
"mongodb-memory-server": "^8.0.0",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"pkg": "5.5.2",
|
"pkg": "5.6.0",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
@@ -1379,14 +1385,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sasjs/core": {
|
"node_modules/@sasjs/core": {
|
||||||
"version": "4.9.0",
|
"version": "4.23.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
|
||||||
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
|
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
|
||||||
},
|
},
|
||||||
"node_modules/@sasjs/utils": {
|
"node_modules/@sasjs/utils": {
|
||||||
"version": "2.36.2",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
|
||||||
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==",
|
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/fs-extra": "9.0.13",
|
"@types/fs-extra": "9.0.13",
|
||||||
@@ -1833,6 +1839,15 @@
|
|||||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/express": {
|
||||||
"version": "4.17.12",
|
"version": "4.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
|
||||||
@@ -1856,6 +1871,15 @@
|
|||||||
"@types/range-parser": "*"
|
"@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": {
|
"node_modules/@types/fs-extra": {
|
||||||
"version": "9.0.13",
|
"version": "9.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||||
@@ -2447,10 +2471,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/async": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "^4.17.14"
|
"lodash": "^4.17.14"
|
||||||
}
|
}
|
||||||
@@ -2674,6 +2709,11 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.19.0",
|
"version": "1.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||||
@@ -2955,14 +2995,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001243",
|
"version": "1.0.30001340",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz",
|
||||||
"integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
|
"integrity": "sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": {
|
"funding": [
|
||||||
"type": "opencollective",
|
{
|
||||||
"url": "https://opencollective.com/browserslist"
|
"type": "opencollective",
|
||||||
}
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -3238,6 +3284,42 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/consola": {
|
||||||
"version": "2.15.0",
|
"version": "2.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
|
||||||
@@ -3362,6 +3444,19 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/cssom": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
||||||
@@ -3386,6 +3481,40 @@
|
|||||||
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/csv-stringify": {
|
||||||
"version": "5.6.5",
|
"version": "5.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
||||||
@@ -4027,6 +4156,59 @@
|
|||||||
"node": ">= 0.10.0"
|
"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": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.2.11",
|
"version": "3.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||||
@@ -4642,6 +4824,14 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||||
@@ -6828,6 +7018,17 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/latest-version": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
|
||||||
@@ -7095,6 +7296,11 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
@@ -7107,9 +7313,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -7133,7 +7339,6 @@
|
|||||||
"version": "4.1.4",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
||||||
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bson": "^4.5.4",
|
"bson": "^4.5.4",
|
||||||
"denque": "^2.0.1",
|
"denque": "^2.0.1",
|
||||||
@@ -7971,9 +8176,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pkg": {
|
"node_modules/pkg": {
|
||||||
"version": "5.5.2",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.6.0.tgz",
|
||||||
"integrity": "sha512-pD0UB2ud01C6pVv2wpGsTYJrXI/bnvGRYvMLd44wFzA1p+A2jrlTGFPAYa7YEYzmitXhx23PqalaG1eUEnSwcA==",
|
"integrity": "sha512-mHrAVSQWmHA41RnUmRpC7pK9lNnMfdA16CF3cqOI22a8LZxOQzF7M8YWtA2nfs+d7I0MTDXOtkDsAsFXeCpYjg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "7.16.2",
|
"@babel/parser": "7.16.2",
|
||||||
@@ -7985,7 +8190,7 @@
|
|||||||
"into-stream": "^6.0.0",
|
"into-stream": "^6.0.0",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"multistream": "^4.1.0",
|
"multistream": "^4.1.0",
|
||||||
"pkg-fetch": "3.2.6",
|
"pkg-fetch": "3.3.0",
|
||||||
"prebuild-install": "6.1.4",
|
"prebuild-install": "6.1.4",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
"resolve": "^1.20.0",
|
"resolve": "^1.20.0",
|
||||||
@@ -8017,9 +8222,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pkg-fetch": {
|
"node_modules/pkg-fetch": {
|
||||||
"version": "3.2.6",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.3.0.tgz",
|
||||||
"integrity": "sha512-Q8fx6SIT022g0cdSE4Axv/xpfHeltspo2gg1KsWRinLQZOTRRAtOOaEFghA1F3jJ8FVsh8hGrL/Pb6Ea5XHIFw==",
|
"integrity": "sha512-xJnIZ1KP+8rNN+VLafwu4tEeV4m8IkFBDdCFqmAJz9K1aiXEtbARmdbEe6HlXWGSVuShSHjFXpfkKRkDBQ5kiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
@@ -8076,9 +8281,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pkg-fetch/node_modules/semver": {
|
"node_modules/pkg-fetch/node_modules/semver": {
|
||||||
"version": "7.3.5",
|
"version": "7.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
@@ -8367,6 +8572,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": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -8547,6 +8760,11 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -9222,11 +9440,11 @@
|
|||||||
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
|
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
|
||||||
},
|
},
|
||||||
"node_modules/swagger-ui-express": {
|
"node_modules/swagger-ui-express": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz",
|
||||||
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==",
|
"integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"swagger-ui-dist": ">3.52.5"
|
"swagger-ui-dist": ">=4.1.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= v0.10.32"
|
"node": ">= v0.10.32"
|
||||||
@@ -9532,6 +9750,14 @@
|
|||||||
"yarn": ">=1.9.4"
|
"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": {
|
"node_modules/tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
@@ -9626,6 +9852,17 @@
|
|||||||
"node": ">=0.8.0"
|
"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": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
|
||||||
@@ -11127,14 +11364,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sasjs/core": {
|
"@sasjs/core": {
|
||||||
"version": "4.9.0",
|
"version": "4.23.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
|
||||||
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
|
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
|
||||||
},
|
},
|
||||||
"@sasjs/utils": {
|
"@sasjs/utils": {
|
||||||
"version": "2.36.2",
|
"version": "2.42.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
|
||||||
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==",
|
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/fs-extra": "9.0.13",
|
"@types/fs-extra": "9.0.13",
|
||||||
"@types/prompts": "2.0.13",
|
"@types/prompts": "2.0.13",
|
||||||
@@ -11525,6 +11762,15 @@
|
|||||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
|
||||||
"dev": true
|
"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": {
|
"@types/express": {
|
||||||
"version": "4.17.12",
|
"version": "4.17.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
|
||||||
@@ -11548,6 +11794,15 @@
|
|||||||
"@types/range-parser": "*"
|
"@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": {
|
"@types/fs-extra": {
|
||||||
"version": "9.0.13",
|
"version": "9.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||||
@@ -12059,10 +12314,21 @@
|
|||||||
"is-string": "^1.0.7"
|
"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": {
|
"async": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"lodash": "^4.17.14"
|
"lodash": "^4.17.14"
|
||||||
}
|
}
|
||||||
@@ -12234,6 +12500,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": {
|
"body-parser": {
|
||||||
"version": "1.19.0",
|
"version": "1.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||||
@@ -12447,9 +12718,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001243",
|
"version": "1.0.30001340",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz",
|
||||||
"integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
|
"integrity": "sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
@@ -12681,6 +12952,30 @@
|
|||||||
"xdg-basedir": "^4.0.0"
|
"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": {
|
"consola": {
|
||||||
"version": "2.15.0",
|
"version": "2.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
|
||||||
@@ -12783,6 +13078,16 @@
|
|||||||
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
|
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
|
||||||
"dev": true
|
"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": {
|
"cssom": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
||||||
@@ -12806,6 +13111,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": {
|
"csv-stringify": {
|
||||||
"version": "5.6.5",
|
"version": "5.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
||||||
@@ -13303,6 +13638,38 @@
|
|||||||
"vary": "~1.1.2"
|
"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": {
|
"fast-glob": {
|
||||||
"version": "3.2.11",
|
"version": "3.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
|
||||||
@@ -13774,6 +14141,11 @@
|
|||||||
"integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
|
"integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
|
||||||
"dev": true
|
"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": {
|
"html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||||
@@ -15409,6 +15781,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||||
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="
|
"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": {
|
"latest-version": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
|
||||||
@@ -15615,6 +15995,11 @@
|
|||||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||||
"dev": true
|
"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": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
@@ -15624,9 +16009,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||||
},
|
},
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -15644,7 +16029,6 @@
|
|||||||
"version": "4.1.4",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
||||||
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"bson": "^4.5.4",
|
"bson": "^4.5.4",
|
||||||
"denque": "^2.0.1",
|
"denque": "^2.0.1",
|
||||||
@@ -16271,9 +16655,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pkg": {
|
"pkg": {
|
||||||
"version": "5.5.2",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.6.0.tgz",
|
||||||
"integrity": "sha512-pD0UB2ud01C6pVv2wpGsTYJrXI/bnvGRYvMLd44wFzA1p+A2jrlTGFPAYa7YEYzmitXhx23PqalaG1eUEnSwcA==",
|
"integrity": "sha512-mHrAVSQWmHA41RnUmRpC7pK9lNnMfdA16CF3cqOI22a8LZxOQzF7M8YWtA2nfs+d7I0MTDXOtkDsAsFXeCpYjg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/parser": "7.16.2",
|
"@babel/parser": "7.16.2",
|
||||||
@@ -16285,7 +16669,7 @@
|
|||||||
"into-stream": "^6.0.0",
|
"into-stream": "^6.0.0",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"multistream": "^4.1.0",
|
"multistream": "^4.1.0",
|
||||||
"pkg-fetch": "3.2.6",
|
"pkg-fetch": "3.3.0",
|
||||||
"prebuild-install": "6.1.4",
|
"prebuild-install": "6.1.4",
|
||||||
"progress": "^2.0.3",
|
"progress": "^2.0.3",
|
||||||
"resolve": "^1.20.0",
|
"resolve": "^1.20.0",
|
||||||
@@ -16342,9 +16726,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pkg-fetch": {
|
"pkg-fetch": {
|
||||||
"version": "3.2.6",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.3.0.tgz",
|
||||||
"integrity": "sha512-Q8fx6SIT022g0cdSE4Axv/xpfHeltspo2gg1KsWRinLQZOTRRAtOOaEFghA1F3jJ8FVsh8hGrL/Pb6Ea5XHIFw==",
|
"integrity": "sha512-xJnIZ1KP+8rNN+VLafwu4tEeV4m8IkFBDdCFqmAJz9K1aiXEtbARmdbEe6HlXWGSVuShSHjFXpfkKRkDBQ5kiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
@@ -16386,9 +16770,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "7.3.5",
|
"version": "7.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"lru-cache": "^6.0.0"
|
"lru-cache": "^6.0.0"
|
||||||
@@ -16555,6 +16939,11 @@
|
|||||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||||
"dev": true
|
"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": {
|
"range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -16692,6 +17081,11 @@
|
|||||||
"glob": "^7.1.3"
|
"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": {
|
"run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
@@ -17213,11 +17607,11 @@
|
|||||||
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
|
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
|
||||||
},
|
},
|
||||||
"swagger-ui-express": {
|
"swagger-ui-express": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz",
|
||||||
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==",
|
"integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"swagger-ui-dist": ">3.52.5"
|
"swagger-ui-dist": ">=4.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"symbol-tree": {
|
"symbol-tree": {
|
||||||
@@ -17429,6 +17823,11 @@
|
|||||||
"@tsoa/runtime": "^3.13.0"
|
"@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": {
|
"tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
@@ -17495,6 +17894,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": 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": {
|
"unbox-primitive": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"prestart": "npm run initial",
|
"prestart": "npm run initial",
|
||||||
"prebuild": "npm run initial",
|
"prebuild": "npm run initial",
|
||||||
"start": "nodemon ./src/server.ts",
|
"start": "nodemon ./src/server.ts",
|
||||||
|
"start:prod": "node ./build/src/server.js",
|
||||||
"build": "rimraf build && tsc",
|
"build": "rimraf build && tsc",
|
||||||
"postbuild": "npm run copy:files",
|
"postbuild": "npm run copy:files",
|
||||||
"swagger": "tsoa spec",
|
"swagger": "tsoa spec",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"assets": [
|
"assets": [
|
||||||
"./build/public/**/*",
|
"./build/public/**/*",
|
||||||
"./build/sasjsbuild/**/*",
|
"./build/sasjsbuild/**/*",
|
||||||
|
"./build/sasjscore/**/*",
|
||||||
"./web/build/**/*"
|
"./web/build/**/*"
|
||||||
],
|
],
|
||||||
"targets": [
|
"targets": [
|
||||||
@@ -45,25 +47,31 @@
|
|||||||
},
|
},
|
||||||
"author": "4GL Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "4.9.0",
|
"@sasjs/core": "^4.23.1",
|
||||||
"@sasjs/utils": "2.36.2",
|
"@sasjs/utils": "2.42.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"connect-mongo": "^4.6.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"csurf": "^1.11.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-session": "^1.17.2",
|
||||||
|
"helmet": "^5.0.2",
|
||||||
"joi": "^17.4.2",
|
"joi": "^17.4.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mongoose": "^6.0.12",
|
"mongoose": "^6.0.12",
|
||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
"swagger-ui-express": "^4.1.6"
|
"swagger-ui-express": "4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/csurf": "^1.11.2",
|
||||||
"@types/express": "^4.17.12",
|
"@types/express": "^4.17.12",
|
||||||
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/jsonwebtoken": "^8.5.5",
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
"@types/mongoose-sequence": "^3.0.6",
|
"@types/mongoose-sequence": "^3.0.6",
|
||||||
@@ -77,7 +85,7 @@
|
|||||||
"jest": "^27.0.6",
|
"jest": "^27.0.6",
|
||||||
"mongodb-memory-server": "^8.0.0",
|
"mongodb-memory-server": "^8.0.0",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"pkg": "5.5.2",
|
"pkg": "5.6.0",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"supertest": "^6.1.3",
|
"supertest": "^6.1.3",
|
||||||
@@ -86,12 +94,9 @@
|
|||||||
"tsoa": "3.14.1",
|
"tsoa": "3.14.1",
|
||||||
"typescript": "^4.3.2"
|
"typescript": "^4.3.2"
|
||||||
},
|
},
|
||||||
"configuration": {
|
|
||||||
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
|
||||||
},
|
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"tmp/appStreamConfig.json"
|
"tmp/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
api/public/SASjsApi/swagger-ui-init.js
Normal file
50
api/public/SASjsApi/swagger-ui-init.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
window.onload = function () {
|
||||||
|
// Build a system
|
||||||
|
var url = window.location.search.match(/url=([^&]+)/)
|
||||||
|
if (url && url.length > 1) {
|
||||||
|
url = decodeURIComponent(url[1])
|
||||||
|
} else {
|
||||||
|
url = window.location.origin
|
||||||
|
}
|
||||||
|
var options = {
|
||||||
|
customOptions: {
|
||||||
|
url: '/swagger.yaml',
|
||||||
|
requestInterceptor: function (request) {
|
||||||
|
request.credentials = 'include'
|
||||||
|
var cookie = document.cookie
|
||||||
|
var startIndex = cookie.indexOf('XSRF-TOKEN')
|
||||||
|
var csrf = cookie.slice(startIndex + 11).split('; ')[0]
|
||||||
|
request.headers['X-XSRF-TOKEN'] = csrf
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url = options.swaggerUrl || url
|
||||||
|
var urls = options.swaggerUrls
|
||||||
|
var customOptions = options.customOptions
|
||||||
|
var spec1 = options.swaggerDoc
|
||||||
|
var swaggerOptions = {
|
||||||
|
spec: spec1,
|
||||||
|
url: url,
|
||||||
|
urls: urls,
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||||
|
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||||
|
layout: 'StandaloneLayout'
|
||||||
|
}
|
||||||
|
for (var attrname in customOptions) {
|
||||||
|
swaggerOptions[attrname] = customOptions[attrname]
|
||||||
|
}
|
||||||
|
var ui = SwaggerUIBundle(swaggerOptions)
|
||||||
|
|
||||||
|
if (customOptions.oauth) {
|
||||||
|
ui.initOAuth(customOptions.oauth)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customOptions.authAction) {
|
||||||
|
ui.authActions.authorize(customOptions.authAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ui = ui
|
||||||
|
}
|
||||||
49
api/public/app-streams-script.js
Normal file
49
api/public/app-streams-script.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const inputElement = document.getElementById('fileId')
|
||||||
|
|
||||||
|
document.getElementById('uploadButton').addEventListener('click', function () {
|
||||||
|
inputElement.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
inputElement.addEventListener(
|
||||||
|
'change',
|
||||||
|
function () {
|
||||||
|
const fileList = this.files /* now you can work with the file list */
|
||||||
|
|
||||||
|
updateFileUploadMessage('Requesting ...')
|
||||||
|
|
||||||
|
const file = fileList[0]
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post('/SASjsApi/drive/deploy/upload', formData)
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((data) => {
|
||||||
|
return (
|
||||||
|
data.message +
|
||||||
|
'\nstreamServiceName: ' +
|
||||||
|
data.streamServiceName +
|
||||||
|
'\nrefreshing page once alert box closes.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.then((message) => {
|
||||||
|
alert(message)
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert(error.response.data)
|
||||||
|
resetFileUpload()
|
||||||
|
updateFileUploadMessage('Upload New App')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
function updateFileUploadMessage(message) {
|
||||||
|
document.getElementById('uploadMessage').innerHTML = message
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFileUpload() {
|
||||||
|
inputElement.value = null
|
||||||
|
}
|
||||||
3
api/public/axios.min.js
vendored
Normal file
3
api/public/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
api/public/plus.png
Normal file
BIN
api/public/plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 899 B |
@@ -5,36 +5,6 @@ components:
|
|||||||
requestBodies: {}
|
requestBodies: {}
|
||||||
responses: {}
|
responses: {}
|
||||||
schemas:
|
schemas:
|
||||||
AuthorizeResponse:
|
|
||||||
properties:
|
|
||||||
code:
|
|
||||||
type: string
|
|
||||||
description: 'Authorization code'
|
|
||||||
example: someRandomCryptoString
|
|
||||||
required:
|
|
||||||
- code
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
AuthorizePayload:
|
|
||||||
properties:
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
description: 'Username for user'
|
|
||||||
example: secretuser
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
description: 'Password for user'
|
|
||||||
example: secretpassword
|
|
||||||
clientId:
|
|
||||||
type: string
|
|
||||||
description: 'Client ID'
|
|
||||||
example: clientID1
|
|
||||||
required:
|
|
||||||
- username
|
|
||||||
- password
|
|
||||||
- clientId
|
|
||||||
type: object
|
|
||||||
additionalProperties: false
|
|
||||||
TokenResponse:
|
TokenResponse:
|
||||||
properties:
|
properties:
|
||||||
accessToken:
|
accessToken:
|
||||||
@@ -77,6 +47,41 @@ components:
|
|||||||
- userId
|
- userId
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
LoginPayload:
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: 'Username for user'
|
||||||
|
example: secretuser
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: 'Password for user'
|
||||||
|
example: secretpassword
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
- password
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
AuthorizeResponse:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: 'Authorization code'
|
||||||
|
example: someRandomCryptoString
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
AuthorizePayload:
|
||||||
|
properties:
|
||||||
|
clientId:
|
||||||
|
type: string
|
||||||
|
description: 'Client ID'
|
||||||
|
example: clientID1
|
||||||
|
required:
|
||||||
|
- clientId
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
ClientPayload:
|
ClientPayload:
|
||||||
properties:
|
properties:
|
||||||
clientId:
|
clientId:
|
||||||
@@ -161,6 +166,8 @@ components:
|
|||||||
$ref: '#/components/schemas/FolderMember'
|
$ref: '#/components/schemas/FolderMember'
|
||||||
-
|
-
|
||||||
$ref: '#/components/schemas/ServiceMember'
|
$ref: '#/components/schemas/ServiceMember'
|
||||||
|
-
|
||||||
|
$ref: '#/components/schemas/FileMember'
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
@@ -172,20 +179,30 @@ components:
|
|||||||
enum:
|
enum:
|
||||||
- service
|
- service
|
||||||
type: string
|
type: string
|
||||||
MemberType.file:
|
|
||||||
enum:
|
|
||||||
- file
|
|
||||||
type: string
|
|
||||||
ServiceMember:
|
ServiceMember:
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
type:
|
type:
|
||||||
anyOf:
|
$ref: '#/components/schemas/MemberType.service'
|
||||||
-
|
code:
|
||||||
$ref: '#/components/schemas/MemberType.service'
|
type: string
|
||||||
-
|
required:
|
||||||
$ref: '#/components/schemas/MemberType.file'
|
- name
|
||||||
|
- type
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
MemberType.file:
|
||||||
|
enum:
|
||||||
|
- file
|
||||||
|
type: string
|
||||||
|
FileMember:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/MemberType.file'
|
||||||
code:
|
code:
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
@@ -203,6 +220,8 @@ components:
|
|||||||
$ref: '#/components/schemas/FolderMember'
|
$ref: '#/components/schemas/FolderMember'
|
||||||
-
|
-
|
||||||
$ref: '#/components/schemas/ServiceMember'
|
$ref: '#/components/schemas/ServiceMember'
|
||||||
|
-
|
||||||
|
$ref: '#/components/schemas/FileMember'
|
||||||
type: array
|
type: array
|
||||||
required:
|
required:
|
||||||
- members
|
- members
|
||||||
@@ -304,6 +323,8 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
isAdmin:
|
isAdmin:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
autoExec:
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- displayName
|
- displayName
|
||||||
@@ -333,6 +354,10 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
description: 'Account should be active or not, defaults to true'
|
description: 'Account should be active or not, defaults to true'
|
||||||
example: 'true'
|
example: 'true'
|
||||||
|
autoExec:
|
||||||
|
type: string
|
||||||
|
description: 'User-specific auto-exec code'
|
||||||
|
example: ""
|
||||||
required:
|
required:
|
||||||
- displayName
|
- displayName
|
||||||
- username
|
- username
|
||||||
@@ -396,6 +421,25 @@ components:
|
|||||||
- description
|
- description
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
InfoResponse:
|
||||||
|
properties:
|
||||||
|
mode:
|
||||||
|
type: string
|
||||||
|
cors:
|
||||||
|
type: string
|
||||||
|
whiteList:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
protocol:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- mode
|
||||||
|
- cors
|
||||||
|
- whiteList
|
||||||
|
- protocol
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
ExecuteReturnJsonPayload:
|
ExecuteReturnJsonPayload:
|
||||||
properties:
|
properties:
|
||||||
_program:
|
_program:
|
||||||
@@ -417,30 +461,6 @@ info:
|
|||||||
name: '4GL Ltd'
|
name: '4GL Ltd'
|
||||||
openapi: 3.0.0
|
openapi: 3.0.0
|
||||||
paths:
|
paths:
|
||||||
/SASjsApi/auth/authorize:
|
|
||||||
post:
|
|
||||||
operationId: Authorize
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: Ok
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/AuthorizeResponse'
|
|
||||||
examples:
|
|
||||||
'Example 1':
|
|
||||||
value: {code: someRandomCryptoString}
|
|
||||||
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
|
||||||
tags:
|
|
||||||
- Auth
|
|
||||||
security: []
|
|
||||||
parameters: []
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/AuthorizePayload'
|
|
||||||
/SASjsApi/auth/token:
|
/SASjsApi/auth/token:
|
||||||
post:
|
post:
|
||||||
operationId: Token
|
operationId: Token
|
||||||
@@ -498,6 +518,86 @@ paths:
|
|||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
parameters: []
|
parameters: []
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
operationId: Home
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: 'Render index.html'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
/SASLogon/login:
|
||||||
|
post:
|
||||||
|
operationId: Login
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
|
||||||
|
loggedIn: {type: boolean}
|
||||||
|
required:
|
||||||
|
- user
|
||||||
|
- loggedIn
|
||||||
|
type: object
|
||||||
|
summary: 'Accept a valid username/password'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/LoginPayload'
|
||||||
|
/SASLogon/authorize:
|
||||||
|
post:
|
||||||
|
operationId: Authorize
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizeResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {code: someRandomCryptoString}
|
||||||
|
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthorizePayload'
|
||||||
|
/logout:
|
||||||
|
get:
|
||||||
|
operationId: Logout
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
summary: 'Accept a valid username/password'
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
/SASjsApi/client:
|
/SASjsApi/client:
|
||||||
post:
|
post:
|
||||||
operationId: CreateClient
|
operationId: CreateClient
|
||||||
@@ -592,13 +692,62 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/DeployPayload'
|
$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:
|
/SASjsApi/drive/file:
|
||||||
get:
|
get:
|
||||||
operationId: GetFile
|
operationId: GetFile
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: 'No content'
|
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'
|
summary: 'Get file from SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
@@ -609,19 +758,10 @@ paths:
|
|||||||
-
|
-
|
||||||
in: query
|
in: query
|
||||||
name: _filePath
|
name: _filePath
|
||||||
required: false
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: /Public/somefolder/some.file
|
example: /Public/somefolder/some.file
|
||||||
requestBody:
|
|
||||||
required: false
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
filePath:
|
|
||||||
type: string
|
|
||||||
delete:
|
delete:
|
||||||
operationId: DeleteFile
|
operationId: DeleteFile
|
||||||
responses:
|
responses:
|
||||||
@@ -635,7 +775,6 @@ paths:
|
|||||||
required:
|
required:
|
||||||
- status
|
- status
|
||||||
type: object
|
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'
|
summary: 'Delete file from SASjs Drive'
|
||||||
tags:
|
tags:
|
||||||
- Drive
|
- Drive
|
||||||
@@ -646,19 +785,10 @@ paths:
|
|||||||
-
|
-
|
||||||
in: query
|
in: query
|
||||||
name: _filePath
|
name: _filePath
|
||||||
required: false
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: /Public/somefolder/some.file
|
example: /Public/somefolder/some.file
|
||||||
requestBody:
|
|
||||||
required: false
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
filePath:
|
|
||||||
type: string
|
|
||||||
post:
|
post:
|
||||||
operationId: SaveFile
|
operationId: SaveFile
|
||||||
responses:
|
responses:
|
||||||
@@ -761,6 +891,36 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- file
|
- file
|
||||||
|
/SASjsApi/drive/folder:
|
||||||
|
get:
|
||||||
|
operationId: GetFolder
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
folders: {items: {type: string}, type: array}
|
||||||
|
files: {items: {type: string}, type: array}
|
||||||
|
required:
|
||||||
|
- folders
|
||||||
|
- files
|
||||||
|
type: object
|
||||||
|
summary: 'Get folder contents from SASjs Drive'
|
||||||
|
tags:
|
||||||
|
- Drive
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
in: query
|
||||||
|
name: _folderPath
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: /Public/somefolder
|
||||||
/SASjsApi/drive/filetree:
|
/SASjsApi/drive/filetree:
|
||||||
get:
|
get:
|
||||||
operationId: GetFileTree
|
operationId: GetFileTree
|
||||||
@@ -835,6 +995,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UserDetailsResponse'
|
$ref: '#/components/schemas/UserDetailsResponse'
|
||||||
|
description: 'Only Admin or user itself will get user autoExec code.'
|
||||||
summary: 'Get user properties - such as group memberships, userName, displayName.'
|
summary: 'Get user properties - such as group memberships, userName, displayName.'
|
||||||
tags:
|
tags:
|
||||||
- User
|
- User
|
||||||
@@ -1084,6 +1245,24 @@ paths:
|
|||||||
format: double
|
format: double
|
||||||
type: number
|
type: number
|
||||||
example: '6789'
|
example: '6789'
|
||||||
|
/SASjsApi/info:
|
||||||
|
get:
|
||||||
|
operationId: Info
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/InfoResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http}
|
||||||
|
summary: 'Get server info (mode, cors, whiteList, protocol).'
|
||||||
|
tags:
|
||||||
|
- Info
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
/SASjsApi/session:
|
/SASjsApi/session:
|
||||||
get:
|
get:
|
||||||
operationId: Session
|
operationId: Session
|
||||||
@@ -1170,6 +1349,9 @@ servers:
|
|||||||
-
|
-
|
||||||
url: /
|
url: /
|
||||||
tags:
|
tags:
|
||||||
|
-
|
||||||
|
name: Info
|
||||||
|
description: 'Get Server Info'
|
||||||
-
|
-
|
||||||
name: Session
|
name: Session
|
||||||
description: 'Get Session information'
|
description: 'Get Session information'
|
||||||
@@ -1194,3 +1376,6 @@ tags:
|
|||||||
-
|
-
|
||||||
name: CODE
|
name: CODE
|
||||||
description: 'Operations on SAS code'
|
description: 'Operations on SAS code'
|
||||||
|
-
|
||||||
|
name: Web
|
||||||
|
description: 'Operations on Web'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import {
|
import {
|
||||||
|
CompileTree,
|
||||||
createFile,
|
createFile,
|
||||||
loadDependenciesFile,
|
loadDependenciesFile,
|
||||||
readFile,
|
readFile,
|
||||||
@@ -18,7 +19,8 @@ const compiledSystemInit = async (systemInit: string) =>
|
|||||||
macroFolders: [],
|
macroFolders: [],
|
||||||
buildSourceFolder: '',
|
buildSourceFolder: '',
|
||||||
binaryFolders: [],
|
binaryFolders: [],
|
||||||
macroCorePath
|
macroCorePath,
|
||||||
|
compileTree: new CompileTree('') // dummy compileTree
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const createSysInitFile = async () => {
|
const createSysInitFile = async () => {
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { asyncForEach, copy, createFolder, deleteFolder } from '@sasjs/utils'
|
import {
|
||||||
|
asyncForEach,
|
||||||
|
copy,
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
listFilesInFolder
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
import { apiRoot, sasJSCoreMacros } from '../src/utils'
|
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
|
||||||
|
|
||||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||||
|
|
||||||
@@ -16,6 +23,10 @@ export const copySASjsCore = async () => {
|
|||||||
|
|
||||||
await copy(coreSubFolderPath, sasJSCoreMacros)
|
await copy(coreSubFolderPath, sasJSCoreMacros)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fileNames = await listFilesInFolder(sasJSCoreMacros)
|
||||||
|
|
||||||
|
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
|
||||||
}
|
}
|
||||||
|
|
||||||
copySASjsCore()
|
copySASjsCore()
|
||||||
|
|||||||
@@ -5,23 +5,12 @@
|
|||||||
_before_ any user-provided content.
|
_before_ any user-provided content.
|
||||||
|
|
||||||
A number of useful CORE macros are also compiled below, so that they can be
|
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>
|
<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 mfs_httpheader.sas
|
||||||
@li mp_dirlist.sas
|
@li ms_webout.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
|
|
||||||
|
|
||||||
**/
|
**/
|
||||||
|
|
||||||
|
|||||||
128
api/src/app.ts
128
api/src/app.ts
@@ -1,47 +1,144 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express, { ErrorRequestHandler } from 'express'
|
import express, { ErrorRequestHandler } from 'express'
|
||||||
|
import csrf from 'csurf'
|
||||||
|
import session from 'express-session'
|
||||||
|
import MongoStore from 'connect-mongo'
|
||||||
import morgan from 'morgan'
|
import morgan from 'morgan'
|
||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
|
import helmet from 'helmet'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
connectDB,
|
connectDB,
|
||||||
getWebBuildFolderPath,
|
copySASjsCore,
|
||||||
|
CorsType,
|
||||||
|
getWebBuildFolder,
|
||||||
|
HelmetCoepType,
|
||||||
|
instantiateLogger,
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
sasJSCoreMacros,
|
ModeType,
|
||||||
setProcessVariables
|
ProtocolType,
|
||||||
|
ReturnCode,
|
||||||
|
setProcessVariables,
|
||||||
|
setupFolders,
|
||||||
|
verifyEnvVariables
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
instantiateLogger()
|
||||||
|
|
||||||
|
if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
const { MODE, CORS, WHITELIST } = process.env
|
app.use(cookieParser())
|
||||||
|
|
||||||
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
const {
|
||||||
const whiteList: string[] = []
|
MODE,
|
||||||
WHITELIST?.split(' ')?.forEach((url) => {
|
CORS,
|
||||||
if (url.startsWith('http'))
|
WHITELIST,
|
||||||
// removing trailing slash of URLs listing for CORS
|
PROTOCOL,
|
||||||
whiteList.push(url.replace(/\/$/, ''))
|
HELMET_CSP_CONFIG_PATH,
|
||||||
|
HELMET_COEP,
|
||||||
|
LOG_FORMAT_MORGAN
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
app.use(morgan(LOG_FORMAT_MORGAN as string))
|
||||||
|
|
||||||
|
export const cookieOptions = {
|
||||||
|
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||||
|
}
|
||||||
|
|
||||||
|
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
||||||
|
HELMET_CSP_CONFIG_PATH
|
||||||
|
)
|
||||||
|
if (PROTOCOL === ProtocolType.HTTP)
|
||||||
|
cspConfigJson['upgrade-insecure-requests'] = null
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* CSRF Protection *
|
||||||
|
***********************************/
|
||||||
|
export const csrfProtection = csrf({ cookie: cookieOptions })
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Handle security and origin *
|
||||||
|
***********************************/
|
||||||
|
app.use(
|
||||||
|
helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
||||||
|
...cspConfigJson
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
* Enabling CORS *
|
||||||
|
***********************************/
|
||||||
|
if (CORS === CorsType.ENABLED) {
|
||||||
|
const whiteList: string[] = []
|
||||||
|
WHITELIST?.split(' ')
|
||||||
|
?.filter((url) => !!url)
|
||||||
|
.forEach((url) => {
|
||||||
|
if (url.startsWith('http'))
|
||||||
|
// removing trailing slash of URLs listing for CORS
|
||||||
|
whiteList.push(url.replace(/\/$/, ''))
|
||||||
|
})
|
||||||
|
|
||||||
console.log('All CORS Requests are enabled for:', whiteList)
|
console.log('All CORS Requests are enabled for:', whiteList)
|
||||||
app.use(cors({ credentials: true, origin: whiteList }))
|
app.use(cors({ credentials: true, origin: whiteList }))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(cookieParser())
|
/***********************************
|
||||||
app.use(morgan('tiny'))
|
* DB Connection & *
|
||||||
app.use(express.json({ limit: '50mb' }))
|
* Express Sessions *
|
||||||
|
* With Mongo Store *
|
||||||
|
***********************************/
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
let store: MongoStore | undefined
|
||||||
|
|
||||||
|
// NOTE: when exporting app.js as agent for supertest
|
||||||
|
// we should exclude connecting to the real database
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
||||||
|
|
||||||
|
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
session({
|
||||||
|
secret: process.env.SESSION_SECRET as string,
|
||||||
|
saveUninitialized: false, // don't create session until something stored
|
||||||
|
resave: false, //don't save session if unmodified
|
||||||
|
store,
|
||||||
|
cookie: cookieOptions
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(express.json({ limit: '100mb' }))
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||||
|
if (err.code === 'EBADCSRFTOKEN')
|
||||||
|
return res.status(400).send('Invalid CSRF token!')
|
||||||
|
|
||||||
console.error(err.stack)
|
console.error(err.stack)
|
||||||
res.status(500).send('Something broke!')
|
res.status(500).send('Something broke!')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default setProcessVariables().then(async () => {
|
export default setProcessVariables().then(async () => {
|
||||||
|
await setupFolders()
|
||||||
|
await copySASjsCore()
|
||||||
|
|
||||||
// loading these modules after setting up variables due to
|
// loading these modules after setting up variables due to
|
||||||
// multer's usage of process var process.driveLoc
|
// multer's usage of process var process.driveLoc
|
||||||
const { setupRoutes } = await import('./routes/setupRoutes')
|
const { setupRoutes } = await import('./routes/setupRoutes')
|
||||||
@@ -51,12 +148,9 @@ export default setProcessVariables().then(async () => {
|
|||||||
|
|
||||||
// should be served after setting up web route
|
// should be served after setting up web route
|
||||||
// index.html needs to be injected with some js script.
|
// index.html needs to be injected with some js script.
|
||||||
app.use(express.static(getWebBuildFolderPath()))
|
app.use(express.static(getWebBuildFolder()))
|
||||||
|
|
||||||
console.log('sasJSCoreMacros', sasJSCoreMacros)
|
|
||||||
|
|
||||||
app.use(onError)
|
app.use(onError)
|
||||||
|
|
||||||
await connectDB()
|
|
||||||
return app
|
return app
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import User from '../model/User'
|
|
||||||
import { InfoJWT } from '../types'
|
import { InfoJWT } from '../types'
|
||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
generateAuthCode,
|
|
||||||
generateRefreshToken,
|
generateRefreshToken,
|
||||||
removeTokensInDB,
|
removeTokensInDB,
|
||||||
saveTokensInDB
|
saveTokensInDB
|
||||||
@@ -24,20 +22,6 @@ export class AuthController {
|
|||||||
static deleteCode = (userId: number, clientId: string) =>
|
static deleteCode = (userId: number, clientId: string) =>
|
||||||
delete AuthController.authCodes[userId][clientId]
|
delete AuthController.authCodes[userId][clientId]
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Example<AuthorizeResponse>({
|
|
||||||
code: 'someRandomCryptoString'
|
|
||||||
})
|
|
||||||
@Post('/authorize')
|
|
||||||
public async authorize(
|
|
||||||
@Body() body: AuthorizePayload
|
|
||||||
): Promise<AuthorizeResponse> {
|
|
||||||
return authorize(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Accepts client/auth code and returns access/refresh tokens
|
* @summary Accepts client/auth code and returns access/refresh tokens
|
||||||
*
|
*
|
||||||
@@ -78,30 +62,6 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorize = async (data: any): Promise<AuthorizeResponse> => {
|
|
||||||
const { username, password, clientId } = data
|
|
||||||
|
|
||||||
// Authenticate User
|
|
||||||
const user = await User.findOne({ username })
|
|
||||||
if (!user) throw new Error('Username is not found.')
|
|
||||||
|
|
||||||
const validPass = user.comparePassword(password)
|
|
||||||
if (!validPass) throw new Error('Invalid password.')
|
|
||||||
|
|
||||||
// generate authorization code against clientId
|
|
||||||
const userInfo: InfoJWT = {
|
|
||||||
clientId,
|
|
||||||
userId: user.id
|
|
||||||
}
|
|
||||||
const code = AuthController.saveCode(
|
|
||||||
user.id,
|
|
||||||
clientId,
|
|
||||||
generateAuthCode(userInfo)
|
|
||||||
)
|
|
||||||
|
|
||||||
return { code }
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = async (data: any): Promise<TokenResponse> => {
|
const token = async (data: any): Promise<TokenResponse> => {
|
||||||
const { clientId, code } = data
|
const { clientId, code } = data
|
||||||
|
|
||||||
@@ -139,32 +99,6 @@ const logout = async (userInfo: InfoJWT) => {
|
|||||||
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthorizePayload {
|
|
||||||
/**
|
|
||||||
* Username for user
|
|
||||||
* @example "secretuser"
|
|
||||||
*/
|
|
||||||
username: string
|
|
||||||
/**
|
|
||||||
* Password for user
|
|
||||||
* @example "secretpassword"
|
|
||||||
*/
|
|
||||||
password: string
|
|
||||||
/**
|
|
||||||
* Client ID
|
|
||||||
* @example "clientID1"
|
|
||||||
*/
|
|
||||||
clientId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthorizeResponse {
|
|
||||||
/**
|
|
||||||
* Authorization code
|
|
||||||
* @example "someRandomCryptoString"
|
|
||||||
*/
|
|
||||||
code: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TokenPayload {
|
interface TokenPayload {
|
||||||
/**
|
/**
|
||||||
* Client ID
|
* Client ID
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
import { ExecuteReturnJson, ExecutionController } from './internal'
|
||||||
import { PreProgramVars } from '../types'
|
|
||||||
import { ExecuteReturnJsonResponse } from '.'
|
import { ExecuteReturnJsonResponse } from '.'
|
||||||
import { parseLogToArray } from '../utils'
|
import {
|
||||||
|
getDesktopUserAutoExecPath,
|
||||||
|
getPreProgramVariables,
|
||||||
|
ModeType,
|
||||||
|
parseLogToArray
|
||||||
|
} from '../utils'
|
||||||
|
import { readFile } from '@sasjs/utils'
|
||||||
|
|
||||||
interface ExecuteSASCodePayload {
|
interface ExecuteSASCodePayload {
|
||||||
/**
|
/**
|
||||||
@@ -30,14 +35,23 @@ export class CodeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
const executeSASCode = async (
|
||||||
|
req: express.Request,
|
||||||
|
{ code }: ExecuteSASCodePayload
|
||||||
|
) => {
|
||||||
|
const { user } = req
|
||||||
|
const userAutoExec =
|
||||||
|
process.env.MODE === ModeType.Server
|
||||||
|
? user?.autoExec
|
||||||
|
: await readFile(getDesktopUserAutoExecPath())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { webout, log, httpHeaders } =
|
||||||
(await new ExecutionController().executeProgram(
|
(await new ExecutionController().executeProgram(
|
||||||
code,
|
code,
|
||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
{ ...req.query, _debug: 131 },
|
{ ...req.query, _debug: 131 },
|
||||||
undefined,
|
{ userAutoExec },
|
||||||
true
|
true
|
||||||
)) as ExecuteReturnJson
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
@@ -56,16 +70,3 @@ const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPreProgramVariables = (req: any): PreProgramVars => {
|
|
||||||
const host = req.get('host')
|
|
||||||
const protocol = req.protocol + '://'
|
|
||||||
const { user, accessToken } = req
|
|
||||||
return {
|
|
||||||
username: user.username,
|
|
||||||
userId: user.userId,
|
|
||||||
displayName: user.displayName,
|
|
||||||
serverUrl: protocol + host,
|
|
||||||
accessToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,18 +14,25 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
FormField,
|
FormField,
|
||||||
Delete
|
Delete,
|
||||||
|
Hidden
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
import {
|
import {
|
||||||
fileExists,
|
fileExists,
|
||||||
moveFile,
|
moveFile,
|
||||||
createFolder,
|
createFolder,
|
||||||
deleteFile as deleteFileOnSystem
|
deleteFile as deleteFileOnSystem,
|
||||||
|
folderExists,
|
||||||
|
listFilesInFolder,
|
||||||
|
listSubFoldersInFolder,
|
||||||
|
isFolder,
|
||||||
|
FileTree,
|
||||||
|
isFileTree
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||||
|
|
||||||
import { FileTree, isFileTree, TreeNode } from '../types'
|
import { TreeNode } from '../types'
|
||||||
import { getTmpFilesFolderPath } from '../utils'
|
import { getFilesFolder } from '../utils'
|
||||||
|
|
||||||
interface DeployPayload {
|
interface DeployPayload {
|
||||||
appLoc: string
|
appLoc: string
|
||||||
@@ -89,9 +96,21 @@ export class DriveController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's optional to either provide `_filePath` in url as query parameter
|
* @summary Creates/updates files within SASjs Drive using uploaded JSON file.
|
||||||
* Or provide `filePath` in body as form field.
|
*
|
||||||
* But it's required to provide else API will respond with Bad Request.
|
*/
|
||||||
|
@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
|
* @summary Get file from SASjs Drive
|
||||||
* @query _filePath Location of SAS program
|
* @query _filePath Location of SAS program
|
||||||
@@ -100,28 +119,31 @@ export class DriveController {
|
|||||||
@Get('/file')
|
@Get('/file')
|
||||||
public async getFile(
|
public async getFile(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
|
@Query() _filePath: string
|
||||||
@Query() _filePath?: string,
|
|
||||||
@FormField() 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
|
* @summary Delete file from SASjs Drive
|
||||||
* @query _filePath Location of SAS program
|
* @query _filePath Location of SAS program
|
||||||
* @example _filePath "/Public/somefolder/some.file"
|
* @example _filePath "/Public/somefolder/some.file"
|
||||||
*/
|
*/
|
||||||
@Delete('/file')
|
@Delete('/file')
|
||||||
public async deleteFile(
|
public async deleteFile(@Query() _filePath: string) {
|
||||||
@Query() _filePath?: string,
|
return deleteFile(_filePath)
|
||||||
@FormField() filePath?: string
|
|
||||||
) {
|
|
||||||
return deleteFile((_filePath ?? filePath)!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,12 +214,12 @@ const getFileTree = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deploy = async (data: DeployPayload) => {
|
const deploy = async (data: DeployPayload) => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
||||||
|
|
||||||
const appLocPath = path
|
const appLocPath = path
|
||||||
.join(getTmpFilesFolderPath(), ...appLocParts)
|
.join(getFilesFolder(), ...appLocParts)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!appLocPath.includes(driveFilesPath)) {
|
if (!appLocPath.includes(driveFilesPath)) {
|
||||||
@@ -216,10 +238,10 @@ const deploy = async (data: DeployPayload) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFile = async (req: express.Request, filePath: string) => {
|
const getFile = async (req: express.Request, filePath: string) => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
@@ -227,7 +249,7 @@ const getFile = async (req: express.Request, filePath: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!(await fileExists(filePathFull))) {
|
if (!(await fileExists(filePathFull))) {
|
||||||
throw new Error('File does not exist.')
|
throw new Error("File doesn't exist.")
|
||||||
}
|
}
|
||||||
|
|
||||||
const extension = path.extname(filePathFull).toLowerCase()
|
const extension = path.extname(filePathFull).toLowerCase()
|
||||||
@@ -238,11 +260,41 @@ const getFile = async (req: express.Request, filePath: string) => {
|
|||||||
req.res?.sendFile(path.resolve(filePathFull))
|
req.res?.sendFile(path.resolve(filePathFull))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFolder = async (folderPath?: string) => {
|
||||||
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
|
if (folderPath) {
|
||||||
|
const folderPathFull = path
|
||||||
|
.join(getFilesFolder(), 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 deleteFile = async (filePath: string) => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(getTmpFilesFolderPath(), filePath)
|
.join(getFilesFolder(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
@@ -262,7 +314,7 @@ const saveFile = async (
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
): Promise<GetFileResponse> => {
|
): Promise<GetFileResponse> => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
@@ -287,7 +339,7 @@ const updateFile = async (
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
): Promise<GetFileResponse> => {
|
): Promise<GetFileResponse> => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
@@ -305,9 +357,3 @@ const updateFile = async (
|
|||||||
|
|
||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateFilePath = async (filePath: string) => {
|
|
||||||
if (!(await fileExists(filePath))) {
|
|
||||||
throw 'DriveController: File does not exists.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export * from './client'
|
|||||||
export * from './code'
|
export * from './code'
|
||||||
export * from './drive'
|
export * from './drive'
|
||||||
export * from './group'
|
export * from './group'
|
||||||
|
export * from './info'
|
||||||
export * from './session'
|
export * from './session'
|
||||||
export * from './stp'
|
export * from './stp'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
|
export * from './web'
|
||||||
|
|||||||
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,14 +8,14 @@ import {
|
|||||||
moveFile,
|
moveFile,
|
||||||
readFileBinary
|
readFileBinary
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
import { PreProgramVars, TreeNode } from '../../types'
|
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||||
import {
|
import {
|
||||||
extractHeaders,
|
extractHeaders,
|
||||||
generateFileUploadSasCode,
|
generateFileUploadSasCode,
|
||||||
getTmpFilesFolderPath,
|
getFilesFolder,
|
||||||
|
getMacrosFolder,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
isDebugOn
|
||||||
sasJSCoreMacros
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|
||||||
export interface ExecutionVars {
|
export interface ExecutionVars {
|
||||||
@@ -39,7 +39,8 @@ export class ExecutionController {
|
|||||||
preProgramVariables: PreProgramVars,
|
preProgramVariables: PreProgramVars,
|
||||||
vars: ExecutionVars,
|
vars: ExecutionVars,
|
||||||
otherArgs?: any,
|
otherArgs?: any,
|
||||||
returnJson?: boolean
|
returnJson?: boolean,
|
||||||
|
session?: Session
|
||||||
) {
|
) {
|
||||||
if (!(await fileExists(programPath)))
|
if (!(await fileExists(programPath)))
|
||||||
throw 'ExecutionController: SAS file does not exist.'
|
throw 'ExecutionController: SAS file does not exist.'
|
||||||
@@ -51,7 +52,8 @@ export class ExecutionController {
|
|||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson
|
returnJson,
|
||||||
|
session
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,23 +62,25 @@ export class ExecutionController {
|
|||||||
preProgramVariables: PreProgramVars,
|
preProgramVariables: PreProgramVars,
|
||||||
vars: ExecutionVars,
|
vars: ExecutionVars,
|
||||||
otherArgs?: any,
|
otherArgs?: any,
|
||||||
returnJson?: boolean
|
returnJson?: boolean,
|
||||||
|
sessionByFileUpload?: Session
|
||||||
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||||
const sessionController = getSessionController()
|
const sessionController = getSessionController()
|
||||||
|
|
||||||
const session = await sessionController.getSession()
|
const session =
|
||||||
|
sessionByFileUpload ?? (await sessionController.getSession())
|
||||||
session.inUse = true
|
session.inUse = true
|
||||||
session.consumed = true
|
session.consumed = true
|
||||||
|
|
||||||
const logPath = path.join(session.path, 'log.log')
|
const logPath = path.join(session.path, 'log.log')
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
const weboutPath = path.join(session.path, 'webout.txt')
|
const weboutPath = path.join(session.path, 'webout.txt')
|
||||||
const tokenFile = path.join(session.path, 'accessToken.txt')
|
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
||||||
|
|
||||||
await createFile(weboutPath, '')
|
await createFile(weboutPath, '')
|
||||||
await createFile(
|
await createFile(
|
||||||
tokenFile,
|
tokenFile,
|
||||||
preProgramVariables?.accessToken ?? 'accessToken'
|
preProgramVariables?.httpHeaders.join('\n') ?? ''
|
||||||
)
|
)
|
||||||
|
|
||||||
const varStatments = Object.keys(vars).reduce(
|
const varStatments = Object.keys(vars).reduce(
|
||||||
@@ -106,7 +110,7 @@ export class ExecutionController {
|
|||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
options insert=(SASAUTOS="${sasJSCoreMacros}");
|
options insert=(SASAUTOS="${getMacrosFolder()}");
|
||||||
|
|
||||||
/* runtime vars */
|
/* runtime vars */
|
||||||
${varStatments}
|
${varStatments}
|
||||||
@@ -115,6 +119,10 @@ filename _webout "${weboutPath}" mod;
|
|||||||
/* dynamic user-provided vars */
|
/* dynamic user-provided vars */
|
||||||
${preProgramVarStatments}
|
${preProgramVarStatments}
|
||||||
|
|
||||||
|
/* user autoexec starts */
|
||||||
|
${otherArgs?.userAutoExec ?? ''}
|
||||||
|
/* user autoexec ends */
|
||||||
|
|
||||||
/* actual job code */
|
/* actual job code */
|
||||||
${program}`
|
${program}`
|
||||||
|
|
||||||
@@ -153,7 +161,9 @@ ${program}`
|
|||||||
: ''
|
: ''
|
||||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
const fileResponse: boolean =
|
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))
|
const webout = (await fileExists(weboutPath))
|
||||||
? fileResponse
|
? fileResponse
|
||||||
@@ -174,11 +184,10 @@ ${program}`
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
httpHeaders,
|
httpHeaders,
|
||||||
result: fileResponse
|
result:
|
||||||
? webout
|
isDebugOn(vars) || session.crashed
|
||||||
: isDebugOn(vars) || session.crashed
|
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
: webout
|
||||||
: webout
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +195,7 @@ ${program}`
|
|||||||
const root: TreeNode = {
|
const root: TreeNode = {
|
||||||
name: 'files',
|
name: 'files',
|
||||||
relativePath: '',
|
relativePath: '',
|
||||||
absolutePath: getTmpFilesFolderPath(),
|
absolutePath: getFilesFolder(),
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import { Request, RequestHandler } from 'express'
|
||||||
import multer from 'multer'
|
import multer from 'multer'
|
||||||
import { uuidv4 } from '@sasjs/utils'
|
import { uuidv4 } from '@sasjs/utils'
|
||||||
import { getSessionController } from '.'
|
import { getSessionController } from '.'
|
||||||
|
|
||||||
export class FileUploadController {
|
export class FileUploadController {
|
||||||
private storage = multer.diskStorage({
|
private storage = multer.diskStorage({
|
||||||
destination: function (req: any, file: any, cb: any) {
|
destination: function (req: Request, file: any, cb: any) {
|
||||||
//Sending the intercepted files to the sessions subfolder
|
//Sending the intercepted files to the sessions subfolder
|
||||||
cb(null, req.sasSession.path)
|
cb(null, req.sasSession?.path)
|
||||||
},
|
},
|
||||||
filename: function (req: any, file: any, cb: any) {
|
filename: function (req: Request, file: any, cb: any) {
|
||||||
//req_file prefix + unique hash added to sas request files
|
//req_file prefix + unique hash added to sas request files
|
||||||
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
|
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
|
||||||
}
|
}
|
||||||
@@ -18,12 +19,14 @@ export class FileUploadController {
|
|||||||
|
|
||||||
//It will intercept request and generate unique uuid to be used as a subfolder name
|
//It will intercept request and generate unique uuid to be used as a subfolder name
|
||||||
//that will store the files uploaded
|
//that will store the files uploaded
|
||||||
public preUploadMiddleware = async (req: any, res: any, next: any) => {
|
public preUploadMiddleware: RequestHandler = async (req, res, next) => {
|
||||||
let session
|
let session
|
||||||
|
|
||||||
const sessionController = getSessionController()
|
const sessionController = getSessionController()
|
||||||
session = await sessionController.getSession()
|
session = await sessionController.getSession()
|
||||||
session.inUse = true
|
// marking consumed true, so that it's not available
|
||||||
|
// as readySession for any other request
|
||||||
|
session.consumed = true
|
||||||
|
|
||||||
req.sasSession = session
|
req.sasSession = session
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Session } from '../../types'
|
|||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import {
|
import {
|
||||||
getTmpSessionsFolderPath,
|
getSessionsFolder,
|
||||||
generateUniqueFileName,
|
generateUniqueFileName,
|
||||||
sysInitCompiledPath
|
sysInitCompiledPath
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
createFile,
|
createFile,
|
||||||
fileExists,
|
fileExists,
|
||||||
generateTimestamp,
|
generateTimestamp,
|
||||||
readFile,
|
readFile
|
||||||
moveFile
|
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
@@ -31,16 +30,17 @@ export class SessionController {
|
|||||||
? readySessions[0]
|
? readySessions[0]
|
||||||
: await this.createSession()
|
: await this.createSession()
|
||||||
|
|
||||||
if (readySessions.length < 2) this.createSession()
|
if (readySessions.length < 3) this.createSession()
|
||||||
|
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createSession(): Promise<Session> {
|
private async createSession(): Promise<Session> {
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
|
// death time of session is 15 mins from creation
|
||||||
const deathTimeStamp = (
|
const deathTimeStamp = (
|
||||||
parseInt(creationTimeStamp) +
|
parseInt(creationTimeStamp) +
|
||||||
15 * 60 * 1000 -
|
15 * 60 * 1000 -
|
||||||
@@ -87,6 +87,8 @@ ${autoExecContent}`
|
|||||||
codePath,
|
codePath,
|
||||||
'-LOG',
|
'-LOG',
|
||||||
path.join(session.path, 'log.log'),
|
path.join(session.path, 'log.log'),
|
||||||
|
'-PRINT',
|
||||||
|
path.join(session.path, 'output.lst'),
|
||||||
'-WORK',
|
'-WORK',
|
||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
@@ -138,7 +140,9 @@ ${autoExecContent}`
|
|||||||
private scheduleSessionDestroy(session: Session) {
|
private scheduleSessionDestroy(session: Session) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (session.inUse) {
|
if (session.inUse) {
|
||||||
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
|
// adding 10 more minutes
|
||||||
|
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||||
|
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||||
|
|
||||||
this.scheduleSessionDestroy(session)
|
this.scheduleSessionDestroy(session)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,41 +1,52 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types'
|
import { getFilesFolder } from '../../utils/file'
|
||||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
import {
|
||||||
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
createFolder,
|
||||||
|
createFile,
|
||||||
|
asyncForEach,
|
||||||
|
FolderMember,
|
||||||
|
ServiceMember,
|
||||||
|
FileMember,
|
||||||
|
MemberType,
|
||||||
|
FileTree
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
// REFACTOR: export FileTreeCpntroller
|
// REFACTOR: export FileTreeCpntroller
|
||||||
export const createFileTree = async (
|
export const createFileTree = async (
|
||||||
members: (FolderMember | ServiceMember)[],
|
members: (FolderMember | ServiceMember | FileMember)[],
|
||||||
parentFolders: string[] = []
|
parentFolders: string[] = []
|
||||||
) => {
|
) => {
|
||||||
const destinationPath = path.join(
|
const destinationPath = path.join(
|
||||||
getTmpFilesFolderPath(),
|
getFilesFolder(),
|
||||||
path.join(...parentFolders)
|
path.join(...parentFolders)
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
|
await asyncForEach(
|
||||||
let name = member.name
|
members,
|
||||||
|
async (member: FolderMember | ServiceMember | FileMember) => {
|
||||||
|
let name = member.name
|
||||||
|
|
||||||
if (member.type === MemberType.service) name += '.sas'
|
if (member.type === MemberType.service) name += '.sas'
|
||||||
|
|
||||||
if (member.type === MemberType.folder) {
|
if (member.type === MemberType.folder) {
|
||||||
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
||||||
Promise.reject({ error: err, failedToCreate: name })
|
Promise.reject({ error: err, failedToCreate: name })
|
||||||
)
|
)
|
||||||
|
|
||||||
await createFileTree(member.members, [...parentFolders, name]).catch(
|
await createFileTree(member.members, [...parentFolders, name]).catch(
|
||||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const encoding = member.type === MemberType.file ? 'base64' : undefined
|
const encoding = member.type === MemberType.file ? 'base64' : undefined
|
||||||
|
|
||||||
await createFile(
|
await createFile(
|
||||||
path.join(destinationPath, name),
|
path.join(destinationPath, name),
|
||||||
member.code,
|
member.code,
|
||||||
encoding
|
encoding
|
||||||
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
|
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = (req: any) => ({
|
const session = (req: express.Request) => ({
|
||||||
id: req.user.id,
|
id: req.user!.userId,
|
||||||
username: req.user.username,
|
username: req.user!.username,
|
||||||
displayName: req.user.displayName
|
displayName: req.user!.displayName
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,15 +17,16 @@ import {
|
|||||||
ExecutionController,
|
ExecutionController,
|
||||||
ExecutionVars
|
ExecutionVars
|
||||||
} from './internal'
|
} from './internal'
|
||||||
import { PreProgramVars } from '../types'
|
|
||||||
import {
|
import {
|
||||||
getTmpFilesFolderPath,
|
getPreProgramVariables,
|
||||||
|
getFilesFolder,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
isDebugOn,
|
||||||
LogLine,
|
LogLine,
|
||||||
makeFilesNamesMap,
|
makeFilesNamesMap,
|
||||||
parseLogToArray
|
parseLogToArray
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
import { MulterFile } from '../types/Upload'
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecuteReturnJsonPayload {
|
||||||
/**
|
/**
|
||||||
@@ -132,7 +133,7 @@ const executeReturnRaw = async (
|
|||||||
const query = req.query as ExecutionVars
|
const query = req.query as ExecutionVars
|
||||||
const sasCodePath =
|
const sasCodePath =
|
||||||
path
|
path
|
||||||
.join(getTmpFilesFolderPath(), _program)
|
.join(getFilesFolder(), _program)
|
||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -143,9 +144,8 @@ const executeReturnRaw = async (
|
|||||||
query
|
query
|
||||||
)) as ExecuteReturnRaw
|
)) as ExecuteReturnRaw
|
||||||
|
|
||||||
// Should over-ride response header for
|
// Should over-ride response header for debug
|
||||||
// debug on GET request to see entire log
|
// on GET request to see entire log rendering on browser.
|
||||||
// rendering on browser.
|
|
||||||
if (isDebugOn(query)) {
|
if (isDebugOn(query)) {
|
||||||
httpHeaders['content-type'] = 'text/plain'
|
httpHeaders['content-type'] = 'text/plain'
|
||||||
}
|
}
|
||||||
@@ -168,15 +168,17 @@ const executeReturnRaw = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnJson = async (
|
const executeReturnJson = async (
|
||||||
req: any,
|
req: express.Request,
|
||||||
_program: string
|
_program: string
|
||||||
): Promise<ExecuteReturnJsonResponse> => {
|
): Promise<ExecuteReturnJsonResponse> => {
|
||||||
const sasCodePath =
|
const sasCodePath =
|
||||||
path
|
path
|
||||||
.join(getTmpFilesFolderPath(), _program)
|
.join(getFilesFolder(), _program)
|
||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
const filesNamesMap = req.files?.length
|
||||||
|
? makeFilesNamesMap(req.files as MulterFile[])
|
||||||
|
: null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { webout, log, httpHeaders } =
|
||||||
@@ -185,7 +187,8 @@ const executeReturnJson = async (
|
|||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
{ ...req.query, ...req.body },
|
{ ...req.query, ...req.body },
|
||||||
{ filesNamesMap: filesNamesMap },
|
{ filesNamesMap: filesNamesMap },
|
||||||
true
|
true,
|
||||||
|
req.sasSession
|
||||||
)) as ExecuteReturnJson
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
let weboutRes: string | IRecordOfAny = webout
|
let weboutRes: string | IRecordOfAny = webout
|
||||||
@@ -210,16 +213,3 @@ const executeReturnJson = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPreProgramVariables = (req: any): PreProgramVars => {
|
|
||||||
const host = req.get('host')
|
|
||||||
const protocol = req.protocol + '://'
|
|
||||||
const { user, accessToken } = req
|
|
||||||
return {
|
|
||||||
username: user.username,
|
|
||||||
userId: user.userId,
|
|
||||||
displayName: user.displayName,
|
|
||||||
serverUrl: protocol + host,
|
|
||||||
accessToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import express from 'express'
|
||||||
import {
|
import {
|
||||||
Security,
|
Security,
|
||||||
Route,
|
Route,
|
||||||
@@ -10,7 +11,8 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Hidden
|
Hidden,
|
||||||
|
Request
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
|
|
||||||
import User, { UserPayload } from '../model/User'
|
import User, { UserPayload } from '../model/User'
|
||||||
@@ -27,6 +29,7 @@ interface UserDetailsResponse {
|
|||||||
username: string
|
username: string
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
|
autoExec?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@@ -73,13 +76,19 @@ export class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Only Admin or user itself will get user autoExec code.
|
||||||
* @summary Get user properties - such as group memberships, userName, displayName.
|
* @summary Get user properties - such as group memberships, userName, displayName.
|
||||||
* @param userId The user's identifier
|
* @param userId The user's identifier
|
||||||
* @example userId 1234
|
* @example userId 1234
|
||||||
*/
|
*/
|
||||||
@Get('{userId}')
|
@Get('{userId}')
|
||||||
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
|
public async getUser(
|
||||||
return getUser(userId)
|
@Request() req: express.Request,
|
||||||
|
@Path() userId: number
|
||||||
|
): Promise<UserDetailsResponse> {
|
||||||
|
const { user } = req
|
||||||
|
const getAutoExec = user!.isAdmin || user!.userId == userId
|
||||||
|
return getUser(userId, getAutoExec)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,7 +132,7 @@ const getAllUsers = async (): Promise<UserResponse[]> =>
|
|||||||
.exec()
|
.exec()
|
||||||
|
|
||||||
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||||
const { displayName, username, password, isAdmin, isActive } = data
|
const { displayName, username, password, isAdmin, isActive, autoExec } = data
|
||||||
|
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
const usernameExist = await User.findOne({ username })
|
const usernameExist = await User.findOne({ username })
|
||||||
@@ -138,7 +147,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
|||||||
username,
|
username,
|
||||||
password: hashPassword,
|
password: hashPassword,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isActive
|
isActive,
|
||||||
|
autoExec
|
||||||
})
|
})
|
||||||
|
|
||||||
const savedUser = await user.save()
|
const savedUser = await user.save()
|
||||||
@@ -148,38 +158,42 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
|||||||
displayName: savedUser.displayName,
|
displayName: savedUser.displayName,
|
||||||
username: savedUser.username,
|
username: savedUser.username,
|
||||||
isActive: savedUser.isActive,
|
isActive: savedUser.isActive,
|
||||||
isAdmin: savedUser.isAdmin
|
isAdmin: savedUser.isAdmin,
|
||||||
|
autoExec: savedUser.autoExec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUser = async (id: number): Promise<UserDetailsResponse> => {
|
const getUser = async (
|
||||||
|
id: number,
|
||||||
|
getAutoExec: boolean
|
||||||
|
): Promise<UserDetailsResponse> => {
|
||||||
const user = await User.findOne({ id })
|
const user = await User.findOne({ id })
|
||||||
.select({
|
|
||||||
_id: 0,
|
|
||||||
id: 1,
|
|
||||||
username: 1,
|
|
||||||
displayName: 1,
|
|
||||||
isAdmin: 1,
|
|
||||||
isActive: 1
|
|
||||||
})
|
|
||||||
.exec()
|
|
||||||
if (!user) throw new Error('User is not found.')
|
if (!user) throw new Error('User is not found.')
|
||||||
|
|
||||||
return user
|
return {
|
||||||
|
id: user.id,
|
||||||
|
displayName: user.displayName,
|
||||||
|
username: user.username,
|
||||||
|
isActive: user.isActive,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
autoExec: getAutoExec ? user.autoExec ?? '' : undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUser = async (
|
const updateUser = async (
|
||||||
id: number,
|
id: number,
|
||||||
data: UserPayload
|
data: Partial<UserPayload>
|
||||||
): Promise<UserDetailsResponse> => {
|
): Promise<UserDetailsResponse> => {
|
||||||
const { displayName, username, password, isAdmin, isActive } = data
|
const { displayName, username, password, isAdmin, isActive, autoExec } = data
|
||||||
|
|
||||||
const params: any = { displayName, isAdmin, isActive }
|
const params: any = { displayName, isAdmin, isActive, autoExec }
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
const usernameExist = await User.findOne({ username })
|
const usernameExist = await User.findOne({ username })
|
||||||
if (usernameExist?.id != id) throw new Error('Username already exists.')
|
if (usernameExist && usernameExist.id != id)
|
||||||
|
throw new Error('Username already exists.')
|
||||||
params.username = username
|
params.username = username
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,18 +203,17 @@ const updateUser = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true })
|
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true })
|
||||||
.select({
|
|
||||||
_id: 0,
|
|
||||||
id: 1,
|
|
||||||
username: 1,
|
|
||||||
displayName: 1,
|
|
||||||
isAdmin: 1,
|
|
||||||
isActive: 1
|
|
||||||
})
|
|
||||||
.exec()
|
|
||||||
if (!updatedUser) throw new Error('Unable to update user')
|
|
||||||
|
|
||||||
return updatedUser
|
if (!updatedUser) throw new Error(`Unable to find user with id: ${id}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updatedUser.id,
|
||||||
|
username: updatedUser.username,
|
||||||
|
displayName: updatedUser.displayName,
|
||||||
|
isAdmin: updatedUser.isAdmin,
|
||||||
|
isActive: updatedUser.isActive,
|
||||||
|
autoExec: updatedUser.autoExec
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteUser = async (
|
const deleteUser = async (
|
||||||
|
|||||||
158
api/src/controllers/web.ts
Normal file
158
api/src/controllers/web.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import express from 'express'
|
||||||
|
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
|
||||||
|
import { readFile } from '@sasjs/utils'
|
||||||
|
|
||||||
|
import User from '../model/User'
|
||||||
|
import Client from '../model/Client'
|
||||||
|
import { getWebBuildFolder, generateAuthCode } from '../utils'
|
||||||
|
import { InfoJWT } from '../types'
|
||||||
|
import { AuthController } from './auth'
|
||||||
|
|
||||||
|
@Route('/')
|
||||||
|
@Tags('Web')
|
||||||
|
export class WebController {
|
||||||
|
/**
|
||||||
|
* @summary Render index.html
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Get('/')
|
||||||
|
public async home() {
|
||||||
|
return home()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Accept a valid username/password
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Post('/SASLogon/login')
|
||||||
|
public async login(
|
||||||
|
@Request() req: express.Request,
|
||||||
|
@Body() body: LoginPayload
|
||||||
|
) {
|
||||||
|
return login(req, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<AuthorizeResponse>({
|
||||||
|
code: 'someRandomCryptoString'
|
||||||
|
})
|
||||||
|
@Post('/SASLogon/authorize')
|
||||||
|
public async authorize(
|
||||||
|
@Request() req: express.Request,
|
||||||
|
@Body() body: AuthorizePayload
|
||||||
|
): Promise<AuthorizeResponse> {
|
||||||
|
return authorize(req, body.clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Accept a valid username/password
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Get('/logout')
|
||||||
|
public async logout(@Request() req: express.Request) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
req.session.destroy(() => {
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const home = async () => {
|
||||||
|
const indexHtmlPath = path.join(getWebBuildFolder(), 'index.html')
|
||||||
|
|
||||||
|
// Attention! Cannot use fileExists here,
|
||||||
|
// due to limitation after building executable
|
||||||
|
const content = await readFile(indexHtmlPath)
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async (
|
||||||
|
req: express.Request,
|
||||||
|
{ username, password }: LoginPayload
|
||||||
|
) => {
|
||||||
|
// Authenticate User
|
||||||
|
const user = await User.findOne({ username })
|
||||||
|
if (!user) throw new Error('Username is not found.')
|
||||||
|
|
||||||
|
const validPass = user.comparePassword(password)
|
||||||
|
if (!validPass) throw new Error('Invalid password.')
|
||||||
|
|
||||||
|
req.session.loggedIn = true
|
||||||
|
req.session.user = {
|
||||||
|
userId: user.id,
|
||||||
|
clientId: 'web_app',
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
isActive: user.isActive,
|
||||||
|
autoExec: user.autoExec
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loggedIn: true,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorize = async (
|
||||||
|
req: express.Request,
|
||||||
|
clientId: string
|
||||||
|
): Promise<AuthorizeResponse> => {
|
||||||
|
const userId = req.session.user?.userId
|
||||||
|
if (!userId) throw new Error('Invalid userId.')
|
||||||
|
|
||||||
|
const client = await Client.findOne({ clientId })
|
||||||
|
if (!client) throw new Error('Invalid clientId.')
|
||||||
|
|
||||||
|
// generate authorization code against clientId
|
||||||
|
const userInfo: InfoJWT = {
|
||||||
|
clientId,
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
const code = AuthController.saveCode(
|
||||||
|
userId,
|
||||||
|
clientId,
|
||||||
|
generateAuthCode(userInfo)
|
||||||
|
)
|
||||||
|
|
||||||
|
return { code }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginPayload {
|
||||||
|
/**
|
||||||
|
* Username for user
|
||||||
|
* @example "secretuser"
|
||||||
|
*/
|
||||||
|
username: string
|
||||||
|
/**
|
||||||
|
* Password for user
|
||||||
|
* @example "secretpassword"
|
||||||
|
*/
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthorizePayload {
|
||||||
|
/**
|
||||||
|
* Client ID
|
||||||
|
* @example "clientID1"
|
||||||
|
*/
|
||||||
|
clientId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthorizeResponse {
|
||||||
|
/**
|
||||||
|
* Authorization code
|
||||||
|
* @example "someRandomCryptoString"
|
||||||
|
*/
|
||||||
|
code: string
|
||||||
|
}
|
||||||
@@ -1,7 +1,29 @@
|
|||||||
|
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { verifyTokenInDB } from '../utils'
|
import { csrfProtection } from '../app'
|
||||||
|
import { fetchLatestAutoExec, verifyTokenInDB } from '../utils'
|
||||||
|
|
||||||
|
export const authenticateAccessToken: RequestHandler = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
next
|
||||||
|
) => {
|
||||||
|
// if request is coming from web and has valid session
|
||||||
|
// we can validate the request and check for CSRF Token
|
||||||
|
if (req.session?.loggedIn) {
|
||||||
|
if (req.session.user) {
|
||||||
|
const user = await fetchLatestAutoExec(req.session.user)
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
if (user.isActive) {
|
||||||
|
req.user = user
|
||||||
|
return csrfProtection(req, res, next)
|
||||||
|
} else return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
|
||||||
export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
|
||||||
authenticateToken(
|
authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@@ -11,7 +33,7 @@ export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
||||||
authenticateToken(
|
authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@@ -22,16 +44,16 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authenticateToken = (
|
const authenticateToken = (
|
||||||
req: any,
|
req: Request,
|
||||||
res: any,
|
res: Response,
|
||||||
next: any,
|
next: NextFunction,
|
||||||
key: string,
|
key: string,
|
||||||
tokenType: 'accessToken' | 'refreshToken'
|
tokenType: 'accessToken' | 'refreshToken'
|
||||||
) => {
|
) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') {
|
if (MODE?.trim() !== 'server') {
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: '1234',
|
userId: 1234,
|
||||||
clientId: 'desktopModeClientId',
|
clientId: 'desktopModeClientId',
|
||||||
username: 'desktopModeUsername',
|
username: 'desktopModeUsername',
|
||||||
displayName: 'desktopModeDisplayName',
|
displayName: 'desktopModeDisplayName',
|
||||||
@@ -43,9 +65,7 @@ const authenticateToken = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authHeader = req.headers['authorization']
|
const authHeader = req.headers['authorization']
|
||||||
const token =
|
const token = authHeader?.split(' ')[1]
|
||||||
authHeader?.split(' ')[1] ??
|
|
||||||
(tokenType === 'accessToken' ? req.cookies.accessToken : '')
|
|
||||||
if (!token) return res.sendStatus(401)
|
if (!token) return res.sendStatus(401)
|
||||||
|
|
||||||
jwt.verify(token, key, async (err: any, data: any) => {
|
jwt.verify(token, key, async (err: any, data: any) => {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
export const desktopRestrict = (req: any, res: any, next: any) => {
|
import { RequestHandler } from 'express'
|
||||||
|
|
||||||
|
export const desktopRestrict: RequestHandler = (req, res, next) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server')
|
if (MODE?.trim() !== 'server')
|
||||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
export const desktopUsername = (req: any, res: any, next: any) => {
|
export const desktopUsername: RequestHandler = (req, res, next) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server')
|
if (MODE?.trim() !== 'server')
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { Request } from 'express'
|
import { Request } from 'express'
|
||||||
import multer, { FileFilterCallback, Options } from 'multer'
|
import multer, { FileFilterCallback, Options } from 'multer'
|
||||||
import { blockFileRegex, getTmpUploadsPath } from '../utils'
|
import { blockFileRegex, getUploadsFolder } from '../utils'
|
||||||
|
|
||||||
const fieldNameSize = 300
|
const fieldNameSize = 300
|
||||||
const fileSize = 10485760 // 10 MB
|
const fileSize = 104857600 // 100 MB
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: getTmpUploadsPath(),
|
destination: getUploadsFolder(),
|
||||||
filename: function (
|
filename: function (
|
||||||
_req: Request,
|
_req: Request,
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export const verifyAdmin = (req: any, res: any, next: any) => {
|
import { RequestHandler } from 'express'
|
||||||
|
|
||||||
|
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') return next()
|
if (MODE?.trim() !== 'server') return next()
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => {
|
import { RequestHandler } from 'express'
|
||||||
|
|
||||||
|
export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
const userId = parseInt(req.params.userId)
|
const userId = parseInt(req.params.userId)
|
||||||
|
|
||||||
if (!user.isAdmin && user.userId !== userId) {
|
if (!user?.isAdmin && user?.userId !== userId) {
|
||||||
return res.status(401).send('Admin account required')
|
return res.status(401).send('Admin account required')
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
|
|||||||
@@ -27,12 +27,18 @@ export interface UserPayload {
|
|||||||
* @example "true"
|
* @example "true"
|
||||||
*/
|
*/
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
/**
|
||||||
|
* User-specific auto-exec code
|
||||||
|
* @example ""
|
||||||
|
*/
|
||||||
|
autoExec?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUserDocument extends UserPayload, Document {
|
interface IUserDocument extends UserPayload, Document {
|
||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
|
autoExec: string
|
||||||
groups: Schema.Types.ObjectId[]
|
groups: Schema.Types.ObjectId[]
|
||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
}
|
}
|
||||||
@@ -66,6 +72,9 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
autoExec: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
||||||
tokens: [
|
tokens: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,46 +1,24 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
|
||||||
import { AuthController } from '../../controllers/'
|
import { AuthController } from '../../controllers/'
|
||||||
import Client from '../../model/Client'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
authenticateRefreshToken
|
authenticateRefreshToken
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
import {
|
import { authorizeValidation, tokenValidation } from '../../utils'
|
||||||
authorizeValidation,
|
|
||||||
getDesktopFields,
|
|
||||||
tokenValidation
|
|
||||||
} from '../../utils'
|
|
||||||
import { InfoJWT } from '../../types'
|
import { InfoJWT } from '../../types'
|
||||||
|
|
||||||
const authRouter = express.Router()
|
const authRouter = express.Router()
|
||||||
|
const controller = new AuthController()
|
||||||
|
|
||||||
const clientIDs = new Set()
|
authRouter.post('/token', async (req, res) => {
|
||||||
|
const { error, value: body } = tokenValidation(req.body)
|
||||||
export const populateClients = async () => {
|
|
||||||
const result = await Client.find()
|
|
||||||
clientIDs.clear()
|
|
||||||
result.forEach((r) => {
|
|
||||||
clientIDs.add(r.clientId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
authRouter.post('/authorize', async (req, res) => {
|
|
||||||
const { error, value: body } = authorizeValidation(req.body)
|
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
const { clientId } = body
|
|
||||||
|
|
||||||
// Verify client ID
|
|
||||||
if (!clientIDs.has(clientId)) {
|
|
||||||
return res.status(403).send('Invalid clientId.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AuthController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.authorize(body)
|
const response = await controller.token(body)
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -48,25 +26,12 @@ authRouter.post('/authorize', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
authRouter.post('/token', async (req, res) => {
|
authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
|
||||||
const { error, value: body } = tokenValidation(req.body)
|
const userInfo: InfoJWT = {
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
userId: req.user!.userId!,
|
||||||
|
clientId: req.user!.clientId!
|
||||||
const controller = new AuthController()
|
|
||||||
try {
|
|
||||||
const response = await controller.token(body)
|
|
||||||
const { accessToken } = response
|
|
||||||
|
|
||||||
res.cookie('accessToken', accessToken).send(response)
|
|
||||||
} catch (err: any) {
|
|
||||||
res.status(403).send(err.toString())
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
|
||||||
const userInfo: InfoJWT = req.user
|
|
||||||
|
|
||||||
const controller = new AuthController()
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.refresh(userInfo)
|
const response = await controller.refresh(userInfo)
|
||||||
|
|
||||||
@@ -76,10 +41,12 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
|
authRouter.delete('/logout', authenticateAccessToken, async (req, res) => {
|
||||||
const userInfo: InfoJWT = req.user
|
const userInfo: InfoJWT = {
|
||||||
|
userId: req.user!.userId!,
|
||||||
|
clientId: req.user!.clientId!
|
||||||
|
}
|
||||||
|
|
||||||
const controller = new AuthController()
|
|
||||||
try {
|
try {
|
||||||
await controller.logout(userInfo)
|
await controller.logout(userInfo)
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { deleteFile } from '@sasjs/utils'
|
import { deleteFile, readFile } from '@sasjs/utils'
|
||||||
|
|
||||||
import { publishAppStream } from '../appStream'
|
import { publishAppStream } from '../appStream'
|
||||||
|
|
||||||
@@ -8,7 +8,8 @@ import { DriveController } from '../../controllers/'
|
|||||||
import {
|
import {
|
||||||
deployValidation,
|
deployValidation,
|
||||||
fileBodyValidation,
|
fileBodyValidation,
|
||||||
fileParamValidation
|
fileParamValidation,
|
||||||
|
folderParamValidation
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
|
|
||||||
const controller = new DriveController()
|
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) => {
|
driveRouter.get('/file', async (req, res) => {
|
||||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
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 {
|
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) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
@@ -57,12 +118,11 @@ driveRouter.get('/file', async (req, res) => {
|
|||||||
|
|
||||||
driveRouter.delete('/file', async (req, res) => {
|
driveRouter.delete('/file', async (req, res) => {
|
||||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
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 {
|
try {
|
||||||
const response = await controller.deleteFile(query._filePath, body.filePath)
|
const response = await controller.deleteFile(query._filePath)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
groupRouter.get('/:groupId', authenticateAccessToken, async (req: any, res) => {
|
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
|
||||||
const { groupId } = req.params
|
const { groupId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.getGroup(groupId)
|
const response = await controller.getGroup(parseInt(groupId))
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -49,12 +49,15 @@ groupRouter.post(
|
|||||||
'/:groupId/:userId',
|
'/:groupId/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { groupId, userId } = req.params
|
const { groupId, userId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.addUserToGroup(groupId, userId)
|
const response = await controller.addUserToGroup(
|
||||||
|
parseInt(groupId),
|
||||||
|
parseInt(userId)
|
||||||
|
)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -66,12 +69,15 @@ groupRouter.delete(
|
|||||||
'/:groupId/:userId',
|
'/:groupId/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { groupId, userId } = req.params
|
const { groupId, userId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.removeUserFromGroup(groupId, userId)
|
const response = await controller.removeUserFromGroup(
|
||||||
|
parseInt(groupId),
|
||||||
|
parseInt(userId)
|
||||||
|
)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -83,12 +89,12 @@ groupRouter.delete(
|
|||||||
'/:groupId',
|
'/:groupId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { groupId } = req.params
|
const { groupId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
await controller.deleteGroup(groupId)
|
await controller.deleteGroup(parseInt(groupId))
|
||||||
res.status(200).send('Group Deleted!')
|
res.status(200).send('Group Deleted!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
verifyAdmin
|
verifyAdmin
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
|
import infoRouter from './info'
|
||||||
import driveRouter from './drive'
|
import driveRouter from './drive'
|
||||||
import stpRouter from './stp'
|
import stpRouter from './stp'
|
||||||
import codeRouter from './code'
|
import codeRouter from './code'
|
||||||
@@ -20,6 +21,7 @@ import sessionRouter from './session'
|
|||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.use('/info', infoRouter)
|
||||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||||
router.use('/auth', desktopRestrict, authRouter)
|
router.use('/auth', desktopRestrict, authRouter)
|
||||||
router.use(
|
router.use(
|
||||||
@@ -34,12 +36,22 @@ router.use('/group', desktopRestrict, groupRouter)
|
|||||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||||
router.use('/code', authenticateAccessToken, codeRouter)
|
router.use('/code', authenticateAccessToken, codeRouter)
|
||||||
router.use('/user', desktopRestrict, userRouter)
|
router.use('/user', desktopRestrict, userRouter)
|
||||||
|
|
||||||
router.use(
|
router.use(
|
||||||
'/',
|
'/',
|
||||||
swaggerUi.serve,
|
swaggerUi.serve,
|
||||||
swaggerUi.setup(undefined, {
|
swaggerUi.setup(undefined, {
|
||||||
swaggerOptions: {
|
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,
|
ClientController,
|
||||||
AuthController
|
AuthController
|
||||||
} from '../../../controllers/'
|
} from '../../../controllers/'
|
||||||
import { populateClients } from '../auth'
|
|
||||||
import { InfoJWT } from '../../../types'
|
import { InfoJWT } from '../../../types'
|
||||||
import {
|
import {
|
||||||
generateAccessToken,
|
generateAccessToken,
|
||||||
@@ -18,11 +17,6 @@ import {
|
|||||||
verifyTokenInDB
|
verifyTokenInDB
|
||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const clientSecret = 'someclientSecret'
|
const clientSecret = 'someclientSecret'
|
||||||
const user = {
|
const user = {
|
||||||
@@ -35,16 +29,18 @@ const user = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('auth', () => {
|
describe('auth', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const clientController = new ClientController()
|
const clientController = new ClientController()
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
await clientController.createClient({ clientId, clientSecret })
|
await clientController.createClient({ clientId, clientSecret })
|
||||||
await populateClients()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -53,114 +49,6 @@ describe('auth', () => {
|
|||||||
await mongoServer.stop()
|
await mongoServer.stop()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authorize', () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
const collections = mongoose.connection.collections
|
|
||||||
const collection = collections['users']
|
|
||||||
await collection.deleteMany({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with authorization code', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password,
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body).toHaveProperty('code')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if username is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
password: user.password,
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"username" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if password is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"password" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Bad Request if clientId is missing', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"clientId" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Forbidden if username is incorrect', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password,
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Username is not found.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Forbidden if password is incorrect', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: 'WrongPassword',
|
|
||||||
clientId
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Invalid password.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
|
||||||
await userController.createUser(user)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/authorize')
|
|
||||||
.send({
|
|
||||||
username: user.username,
|
|
||||||
password: user.password,
|
|
||||||
clientId: 'WrongClientID'
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
|
|
||||||
expect(res.text).toEqual('Invalid clientId.')
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('token', () => {
|
describe('token', () => {
|
||||||
const userInfo: InfoJWT = {
|
const userInfo: InfoJWT = {
|
||||||
clientId,
|
clientId,
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
|||||||
import { UserController, ClientController } from '../../../controllers/'
|
import { UserController, ClientController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
clientId: 'someclientID',
|
clientId: 'someclientID',
|
||||||
clientSecret: 'someclientSecret'
|
clientSecret: 'someclientSecret'
|
||||||
@@ -28,12 +23,15 @@ const newClient = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('client', () => {
|
describe('client', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const clientController = new ClientController()
|
const clientController = new ClientController()
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,30 +10,28 @@ import {
|
|||||||
readFile,
|
readFile,
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
generateTimestamp,
|
generateTimestamp,
|
||||||
copy
|
copy,
|
||||||
|
createFolder,
|
||||||
|
createFile,
|
||||||
|
ServiceMember,
|
||||||
|
FolderMember
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
import * as fileUtilModules from '../../../utils/file'
|
import * as fileUtilModules from '../../../utils/file'
|
||||||
|
|
||||||
const timestamp = generateTimestamp()
|
const timestamp = generateTimestamp()
|
||||||
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
||||||
jest
|
jest
|
||||||
.spyOn(fileUtilModules, 'getTmpFolderPath')
|
.spyOn(fileUtilModules, 'getSasjsRootFolder')
|
||||||
.mockImplementation(() => tmpFolder)
|
.mockImplementation(() => tmpFolder)
|
||||||
jest
|
jest
|
||||||
.spyOn(fileUtilModules, 'getTmpUploadsPath')
|
.spyOn(fileUtilModules, 'getUploadsFolder')
|
||||||
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||||
|
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
import { FolderMember, ServiceMember } from '../../../types'
|
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||||
const { getTmpFilesFolderPath } = fileUtilModules
|
const { getFilesFolder } = fileUtilModules
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const user = {
|
const user = {
|
||||||
@@ -44,7 +42,8 @@ const user = {
|
|||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('files', () => {
|
describe('drive', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
@@ -52,6 +51,8 @@ describe('files', () => {
|
|||||||
let accessToken: string
|
let accessToken: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ describe('files', () => {
|
|||||||
await mongoServer.stop()
|
await mongoServer.stop()
|
||||||
await deleteFolder(tmpFolder)
|
await deleteFolder(tmpFolder)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deploy', () => {
|
describe('deploy', () => {
|
||||||
const shouldFailAssertion = async (payload: any) => {
|
const shouldFailAssertion = async (payload: any) => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
@@ -155,10 +157,10 @@ describe('files', () => {
|
|||||||
expect(res.text).toEqual(
|
expect(res.text).toEqual(
|
||||||
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
||||||
)
|
)
|
||||||
await expect(folderExists(getTmpFilesFolderPath())).resolves.toEqual(true)
|
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
|
||||||
|
|
||||||
const testJobFolder = path.join(
|
const testJobFolder = path.join(
|
||||||
getTmpFilesFolderPath(),
|
getFilesFolder(),
|
||||||
'public',
|
'public',
|
||||||
'jobs',
|
'jobs',
|
||||||
'extract'
|
'extract'
|
||||||
@@ -172,17 +174,123 @@ describe('files', () => {
|
|||||||
|
|
||||||
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
||||||
|
|
||||||
await deleteFolder(getTmpFilesFolderPath())
|
await deleteFolder(path.join(getFilesFolder(), '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.getFilesFolder()
|
||||||
|
|
||||||
|
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.getFilesFolder(), 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('file', () => {
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('should create a SAS file on drive having filePath as form field', async () => {
|
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)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/drive/file')
|
.post('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.field('filePath', '/my/path/code.sas')
|
.field('filePath', pathToUpload)
|
||||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
expect(res.statusCode).toEqual(200)
|
||||||
@@ -192,10 +300,12 @@ describe('files', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should create a SAS file on drive having _filePath as query param', async () => {
|
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)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/drive/file')
|
.post('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.query({ _filePath: '/my/path/code1.sas' })
|
.query({ _filePath: pathToUpload })
|
||||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
expect(res.statusCode).toEqual(200)
|
||||||
@@ -217,10 +327,10 @@ describe('files', () => {
|
|||||||
|
|
||||||
it('should respond with Forbidden if file is already present', async () => {
|
it('should respond with Forbidden if file is already present', async () => {
|
||||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
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(
|
const pathToCopy = path.join(
|
||||||
fileUtilModules.getTmpFilesFolderPath(),
|
fileUtilModules.getFilesFolder(),
|
||||||
pathToUpload
|
pathToUpload
|
||||||
)
|
)
|
||||||
await copy(fileToAttachPath, pathToCopy)
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
@@ -310,7 +420,7 @@ describe('files', () => {
|
|||||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||||
const pathToUpload = '/my/path/code.sas'
|
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)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/drive/file')
|
.post('/SASjsApi/drive/file')
|
||||||
@@ -320,7 +430,7 @@ describe('files', () => {
|
|||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
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({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
@@ -332,7 +442,7 @@ describe('files', () => {
|
|||||||
const pathToUpload = '/my/path/code.sas'
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(
|
||||||
fileUtilModules.getTmpFilesFolderPath(),
|
fileUtilModules.getFilesFolder(),
|
||||||
pathToUpload
|
pathToUpload
|
||||||
)
|
)
|
||||||
await copy(fileToAttachPath, pathToCopy)
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
@@ -354,7 +464,7 @@ describe('files', () => {
|
|||||||
const pathToUpload = '/my/path/code.sas'
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(
|
||||||
fileUtilModules.getTmpFilesFolderPath(),
|
fileUtilModules.getFilesFolder(),
|
||||||
pathToUpload
|
pathToUpload
|
||||||
)
|
)
|
||||||
await copy(fileToAttachPath, pathToCopy)
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
@@ -386,7 +496,7 @@ describe('files', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/drive/file')
|
.patch('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.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'))
|
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||||
.expect(403)
|
.expect(403)
|
||||||
|
|
||||||
@@ -427,9 +537,9 @@ describe('files', () => {
|
|||||||
const pathToUpload = '/my/path/code.exe'
|
const pathToUpload = '/my/path/code.exe'
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.patch(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
|
.patch('/SASjsApi/drive/file')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
// .field('filePath', pathToUpload)
|
.query({ _filePath: pathToUpload })
|
||||||
.attach('file', fileToAttachPath)
|
.attach('file', fileToAttachPath)
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
@@ -468,7 +578,7 @@ describe('files', () => {
|
|||||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||||
const pathToUpload = '/my/path/code.sas'
|
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)
|
const res = await request(app)
|
||||||
.patch('/SASjsApi/drive/file')
|
.patch('/SASjsApi/drive/file')
|
||||||
@@ -478,11 +588,81 @@ describe('files', () => {
|
|||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
expect(res.text).toEqual(
|
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({})
|
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.getFilesFolder(), 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 { UserController, GroupController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
displayName: 'Test Admin',
|
displayName: 'Test Admin',
|
||||||
@@ -36,11 +31,14 @@ const userController = new UserController()
|
|||||||
const groupController = new GroupController()
|
const groupController = new GroupController()
|
||||||
|
|
||||||
describe('group', () => {
|
describe('group', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
let adminAccessToken: string
|
let adminAccessToken: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
|
|||||||
20
api/src/routes/api/spec/info.spec.ts
Normal file
20
api/src/routes/api/spec/info.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import request from 'supertest'
|
||||||
|
import appPromise from '../../../app'
|
||||||
|
|
||||||
|
describe('Info', () => {
|
||||||
|
let app: Express
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should should return configured information of the server instance', async () => {
|
||||||
|
const res = await request(app).get('/SASjsApi/info').expect(200)
|
||||||
|
|
||||||
|
expect(res.body.mode).toEqual('server')
|
||||||
|
expect(res.body.cors).toEqual('disable')
|
||||||
|
expect(res.body.whiteList).toEqual([])
|
||||||
|
expect(res.body.protocol).toEqual('http')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -6,34 +6,33 @@ import appPromise from '../../../app'
|
|||||||
import { UserController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
let app: Express
|
|
||||||
appPromise.then((_app) => {
|
|
||||||
app = _app
|
|
||||||
})
|
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
displayName: 'Test Admin',
|
displayName: 'Test Admin',
|
||||||
username: 'testAdminUsername',
|
username: 'testadminusername',
|
||||||
password: '12345678',
|
password: '12345678',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
const user = {
|
const user = {
|
||||||
displayName: 'Test User',
|
displayName: 'Test User',
|
||||||
username: 'testUsername',
|
username: 'testusername',
|
||||||
password: '87654321',
|
password: '87654321',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
autoExec: 'some sas code for auto exec;'
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
|
|
||||||
describe('user', () => {
|
describe('user', () => {
|
||||||
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
})
|
})
|
||||||
@@ -66,6 +65,21 @@ describe('user', () => {
|
|||||||
expect(res.body.displayName).toEqual(user.displayName)
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with new user having username as lowercase', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASjsApi/user')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send({ ...user, username: user.username.toUpperCase() })
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
@@ -244,7 +258,7 @@ describe('user', () => {
|
|||||||
const dbUser1 = await controller.createUser(user)
|
const dbUser1 = await controller.createUser(user)
|
||||||
const dbUser2 = await controller.createUser({
|
const dbUser2 = await controller.createUser({
|
||||||
...user,
|
...user,
|
||||||
username: 'randomUser'
|
username: 'randomuser'
|
||||||
})
|
})
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
@@ -362,7 +376,25 @@ describe('user', () => {
|
|||||||
await deleteAllUsers()
|
await deleteAllUsers()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with user', async () => {
|
it('should respond with user autoExec when same user requests', async () => {
|
||||||
|
const dbUser = await controller.createUser(user)
|
||||||
|
const userId = dbUser.id
|
||||||
|
const accessToken = await generateAndSaveToken(userId)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get(`/SASjsApi/user/${userId}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.username).toEqual(user.username)
|
||||||
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with user autoExec when admin user requests', async () => {
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
const userId = dbUser.id
|
const userId = dbUser.id
|
||||||
|
|
||||||
@@ -376,6 +408,7 @@ describe('user', () => {
|
|||||||
expect(res.body.displayName).toEqual(user.displayName)
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with user when access token is not of an admin account', async () => {
|
it('should respond with user when access token is not of an admin account', async () => {
|
||||||
@@ -397,6 +430,7 @@ describe('user', () => {
|
|||||||
expect(res.body.displayName).toEqual(user.displayName)
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
|
expect(res.body.autoExec).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
|||||||
182
api/src/routes/api/spec/web.spec.ts
Normal file
182
api/src/routes/api/spec/web.spec.ts
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
|
import request from 'supertest'
|
||||||
|
import appPromise from '../../../app'
|
||||||
|
import { UserController, ClientController } from '../../../controllers/'
|
||||||
|
|
||||||
|
const clientId = 'someclientID'
|
||||||
|
const clientSecret = 'someclientSecret'
|
||||||
|
const user = {
|
||||||
|
id: 1234,
|
||||||
|
displayName: 'Test User',
|
||||||
|
username: 'testusername',
|
||||||
|
password: '87654321',
|
||||||
|
isAdmin: false,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('web', () => {
|
||||||
|
let app: Express
|
||||||
|
let con: Mongoose
|
||||||
|
let mongoServer: MongoMemoryServer
|
||||||
|
const userController = new UserController()
|
||||||
|
const clientController = new ClientController()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await appPromise
|
||||||
|
|
||||||
|
mongoServer = await MongoMemoryServer.create()
|
||||||
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
await clientController.createClient({ clientId, clientSecret })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await con.connection.dropDatabase()
|
||||||
|
await con.connection.close()
|
||||||
|
await mongoServer.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('home', () => {
|
||||||
|
it('should respond with CSRF Token', async () => {
|
||||||
|
await request(app)
|
||||||
|
.get('/')
|
||||||
|
.expect(
|
||||||
|
'set-cookie',
|
||||||
|
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SASLogon/login', () => {
|
||||||
|
let csrfToken: string
|
||||||
|
let cookies: string
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ csrfToken, cookies } = await getCSRF(app))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const collections = mongoose.connection.collections
|
||||||
|
const collection = collections['users']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with successful login', async () => {
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('Cookie', cookies)
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.loggedIn).toBeTruthy()
|
||||||
|
expect(res.body.user).toEqual({
|
||||||
|
id: expect.any(Number),
|
||||||
|
username: user.username,
|
||||||
|
displayName: user.displayName
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SASLogon/authorize', () => {
|
||||||
|
let csrfToken: string
|
||||||
|
let cookies: string
|
||||||
|
let authCookies: string
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ csrfToken, cookies } = await getCSRF(app))
|
||||||
|
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
const credentials = {
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
}
|
||||||
|
|
||||||
|
;({ cookies: authCookies } = await performLogin(
|
||||||
|
app,
|
||||||
|
credentials,
|
||||||
|
cookies,
|
||||||
|
csrfToken
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const collections = mongoose.connection.collections
|
||||||
|
const collection = collections['users']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with authorization code', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies, cookies].join('; '))
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({ clientId })
|
||||||
|
|
||||||
|
expect(res.body).toHaveProperty('code')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Bad Request if clientId is missing', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies, cookies].join('; '))
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({})
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.text).toEqual(`"clientId" is required`)
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/authorize')
|
||||||
|
.set('Cookie', [authCookies, cookies].join('; '))
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
clientId: 'WrongClientID'
|
||||||
|
})
|
||||||
|
.expect(403)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Error: Invalid clientId.')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const getCSRF = async (app: Express) => {
|
||||||
|
// make request to get CSRF
|
||||||
|
const { header } = await request(app).get('/')
|
||||||
|
const cookies = header['set-cookie'].join()
|
||||||
|
|
||||||
|
const csrfToken = extractCSRF(cookies)
|
||||||
|
return { csrfToken, cookies }
|
||||||
|
}
|
||||||
|
|
||||||
|
const performLogin = async (
|
||||||
|
app: Express,
|
||||||
|
credentials: { username: string; password: string },
|
||||||
|
cookies: string,
|
||||||
|
csrfToken: string
|
||||||
|
) => {
|
||||||
|
const { header } = await request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('Cookie', cookies)
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send(credentials)
|
||||||
|
|
||||||
|
const newCookies: string = header['set-cookie'].join()
|
||||||
|
return { cookies: newCookies }
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractCSRF = (cookies: string) =>
|
||||||
|
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
|
||||||
|
cookies
|
||||||
|
)![2]
|
||||||
@@ -34,7 +34,7 @@ stpRouter.post(
|
|||||||
'/execute',
|
'/execute',
|
||||||
fileUploadController.preUploadMiddleware,
|
fileUploadController.preUploadMiddleware,
|
||||||
fileUploadController.getMulterUploadObject().any(),
|
fileUploadController.getMulterUploadObject().any(),
|
||||||
async (req: any, res: any) => {
|
async (req, res: any) => {
|
||||||
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
||||||
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
|
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.getUser(userId)
|
const response = await controller.getUser(req, parseInt(userId))
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -52,17 +52,17 @@ userRouter.patch(
|
|||||||
'/:userId',
|
'/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdminIfNeeded,
|
verifyAdminIfNeeded,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
|
|
||||||
// only an admin can update `isActive` and `isAdmin` fields
|
// only an admin can update `isActive` and `isAdmin` fields
|
||||||
const { error, value: body } = updateUserValidation(req.body, user.isAdmin)
|
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.updateUser(userId, body)
|
const response = await controller.updateUser(parseInt(userId), body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -74,17 +74,17 @@ userRouter.delete(
|
|||||||
'/:userId',
|
'/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdminIfNeeded,
|
verifyAdminIfNeeded,
|
||||||
async (req: any, res) => {
|
async (req, res) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
|
|
||||||
// only an admin can delete user without providing password
|
// only an admin can delete user without providing password
|
||||||
const { error, value: data } = deleteUserValidation(req.body, user.isAdmin)
|
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
try {
|
try {
|
||||||
await controller.deleteUser(userId, data, user.isAdmin)
|
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
|
||||||
res.status(200).send('Account Deleted!')
|
res.status(200).send('Account Deleted!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
|
|||||||
@@ -1,26 +1,5 @@
|
|||||||
import { AppStreamConfig } from '../../types'
|
import { AppStreamConfig } from '../../types'
|
||||||
|
import { style } from './style'
|
||||||
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: 100px;
|
|
||||||
margin: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.app-container .app img{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>`
|
|
||||||
|
|
||||||
const defaultAppLogo = '/sasjs-logo.svg'
|
const defaultAppLogo = '/sasjs-logo.svg'
|
||||||
|
|
||||||
@@ -30,7 +9,10 @@ const singleAppStreamHtml = (
|
|||||||
logo?: string
|
logo?: string
|
||||||
) =>
|
) =>
|
||||||
` <a class="app" href="${streamServiceName}" title="${appLoc}">
|
` <a class="app" href="${streamServiceName}" title="${appLoc}">
|
||||||
<img src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}" />
|
<img
|
||||||
|
src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}"
|
||||||
|
onerror="this.src = '${defaultAppLogo}';"
|
||||||
|
/>
|
||||||
${streamServiceName}
|
${streamServiceName}
|
||||||
</a>`
|
</a>`
|
||||||
|
|
||||||
@@ -48,6 +30,15 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
|
|||||||
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
|
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
|
||||||
)
|
)
|
||||||
.join('')}
|
.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>
|
</div>
|
||||||
|
<script src="/axios.min.js"></script>
|
||||||
|
<script src="/app-streams-script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import path from 'path'
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { folderExists } from '@sasjs/utils'
|
import { folderExists } from '@sasjs/utils'
|
||||||
|
|
||||||
import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
|
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
||||||
import { appStreamHtml } from './appStreamHtml'
|
import { appStreamHtml } from './appStreamHtml'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.get('/', async (_, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const content = appStreamHtml(process.appStreamConfig)
|
const content = appStreamHtml(process.appStreamConfig)
|
||||||
|
|
||||||
|
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||||
|
|
||||||
return res.send(content)
|
return res.send(content)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -20,7 +22,7 @@ export const publishAppStream = async (
|
|||||||
streamLogo?: string,
|
streamLogo?: string,
|
||||||
addEntryToFile: boolean = true
|
addEntryToFile: boolean = true
|
||||||
) => {
|
) => {
|
||||||
const driveFilesPath = getTmpFilesFolderPath()
|
const driveFilesPath = getFilesFolder()
|
||||||
|
|
||||||
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
||||||
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
||||||
@@ -40,17 +42,6 @@ export const publishAppStream = async (
|
|||||||
|
|
||||||
if (!streamServiceName) {
|
if (!streamServiceName) {
|
||||||
streamServiceName = `AppStreamName${appCount + 1}`
|
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))
|
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
|
||||||
@@ -63,7 +54,7 @@ export const publishAppStream = async (
|
|||||||
addEntryToFile
|
addEntryToFile
|
||||||
)
|
)
|
||||||
|
|
||||||
const sasJsPort = process.env.PORT ?? 5000
|
const sasJsPort = process.env.PORT || 5000
|
||||||
console.log(
|
console.log(
|
||||||
'Serving Stream App: ',
|
'Serving Stream App: ',
|
||||||
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
||||||
|
|||||||
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>`
|
||||||
@@ -4,13 +4,16 @@ import webRouter from './web'
|
|||||||
import apiRouter from './api'
|
import apiRouter from './api'
|
||||||
import appStreamRouter from './appStream'
|
import appStreamRouter from './appStream'
|
||||||
|
|
||||||
|
import { csrfProtection } from '../app'
|
||||||
|
|
||||||
export const setupRoutes = (app: Express) => {
|
export const setupRoutes = (app: Express) => {
|
||||||
app.use('/', webRouter)
|
|
||||||
app.use('/SASjsApi', apiRouter)
|
app.use('/SASjsApi', apiRouter)
|
||||||
|
|
||||||
app.use('/AppStream', function (req, res, next) {
|
app.use('/AppStream', csrfProtection, function (req, res, next) {
|
||||||
// this needs to be a function to hook on
|
// this needs to be a function to hook on
|
||||||
// whatever the current router is
|
// whatever the current router is
|
||||||
appStreamRouter(req, res, next)
|
appStreamRouter(req, res, next)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.use('/', csrfProtection, webRouter)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,59 @@
|
|||||||
import { readFile } from '@sasjs/utils'
|
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import path from 'path'
|
import { WebController } from '../../controllers/web'
|
||||||
import { getWebBuildFolderPath } from '../../utils'
|
import { authenticateAccessToken } from '../../middlewares'
|
||||||
|
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||||
|
|
||||||
const webRouter = express.Router()
|
const webRouter = express.Router()
|
||||||
|
const controller = new WebController()
|
||||||
|
|
||||||
const codeToInject = `
|
webRouter.get('/', async (req, res) => {
|
||||||
<script>
|
let response
|
||||||
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
|
|
||||||
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
|
|
||||||
</script>`
|
|
||||||
|
|
||||||
webRouter.get('/', async (_, res) => {
|
|
||||||
let content: string
|
|
||||||
try {
|
try {
|
||||||
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
response = await controller.home()
|
||||||
content = await readFile(indexHtmlPath)
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return res.send('Web Build is not present')
|
response = 'Web Build is not present'
|
||||||
|
} finally {
|
||||||
|
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||||
|
|
||||||
|
return res.send(response)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const { MODE } = process.env
|
webRouter.post('/SASLogon/login', async (req, res) => {
|
||||||
if (MODE?.trim() !== 'server') {
|
const { error, value: body } = loginWebValidation(req.body)
|
||||||
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/html')
|
try {
|
||||||
return res.send(injectedContent)
|
const response = await controller.login(req, body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return res.send(content)
|
webRouter.post(
|
||||||
|
'/SASLogon/authorize',
|
||||||
|
authenticateAccessToken,
|
||||||
|
async (req, res) => {
|
||||||
|
const { error, value: body } = authorizeValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await controller.authorize(req, body)
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
webRouter.get('/logout', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await controller.logout(req)
|
||||||
|
res.status(200).send('OK!')
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default webRouter
|
export default webRouter
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import appPromise from './app'
|
|||||||
import { getCertificates } from './utils'
|
import { getCertificates } from './utils'
|
||||||
|
|
||||||
appPromise.then(async (app) => {
|
appPromise.then(async (app) => {
|
||||||
const protocol = process.env.PROTOCOL ?? 'http'
|
const protocol = process.env.PROTOCOL || 'http'
|
||||||
const sasJsPort = process.env.PORT ?? 5000
|
const sasJsPort = process.env.PORT || 5000
|
||||||
|
|
||||||
console.log('PROTOCOL: ', protocol)
|
console.log('PROTOCOL: ', protocol)
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
export interface FileTree {
|
|
||||||
members: (FolderMember | ServiceMember)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MemberType {
|
|
||||||
folder = 'folder',
|
|
||||||
service = 'service',
|
|
||||||
file = 'file'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FolderMember {
|
|
||||||
name: string
|
|
||||||
type: MemberType.folder
|
|
||||||
members: (FolderMember | ServiceMember)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServiceMember {
|
|
||||||
name: string
|
|
||||||
type: MemberType.service | MemberType.file
|
|
||||||
code: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isFileTree = (arg: any): arg is FileTree =>
|
|
||||||
arg &&
|
|
||||||
arg.members &&
|
|
||||||
Array.isArray(arg.members) &&
|
|
||||||
arg.members.filter(
|
|
||||||
(member: FolderMember | ServiceMember) =>
|
|
||||||
!isFolderMember(member) && !isServiceMember(member)
|
|
||||||
).length === 0
|
|
||||||
|
|
||||||
const isFolderMember = (arg: any): arg is FolderMember =>
|
|
||||||
arg &&
|
|
||||||
typeof arg.name === 'string' &&
|
|
||||||
arg.type === MemberType.folder &&
|
|
||||||
arg.members &&
|
|
||||||
Array.isArray(arg.members) &&
|
|
||||||
arg.members.filter(
|
|
||||||
(member: FolderMember | ServiceMember) =>
|
|
||||||
!isFolderMember(member) &&
|
|
||||||
!isServiceMember(member) &&
|
|
||||||
!isFileMember(member)
|
|
||||||
).length === 0
|
|
||||||
|
|
||||||
const isServiceMember = (arg: any): arg is ServiceMember =>
|
|
||||||
arg &&
|
|
||||||
typeof arg.name === 'string' &&
|
|
||||||
arg.type === MemberType.service &&
|
|
||||||
arg.code &&
|
|
||||||
typeof arg.code === 'string'
|
|
||||||
|
|
||||||
const isFileMember = (arg: any): arg is ServiceMember =>
|
|
||||||
arg &&
|
|
||||||
typeof arg.name === 'string' &&
|
|
||||||
arg.type === MemberType.file &&
|
|
||||||
arg.code &&
|
|
||||||
typeof arg.code === 'string'
|
|
||||||
@@ -3,5 +3,5 @@ export interface PreProgramVars {
|
|||||||
userId: number
|
userId: number
|
||||||
displayName: string
|
displayName: string
|
||||||
serverUrl: string
|
serverUrl: string
|
||||||
accessToken: string
|
httpHeaders: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
8
api/src/types/Process.d.ts
vendored
8
api/src/types/Process.d.ts
vendored
@@ -1,8 +0,0 @@
|
|||||||
declare namespace NodeJS {
|
|
||||||
export interface Process {
|
|
||||||
sasLoc: string
|
|
||||||
driveLoc: string
|
|
||||||
sessionController?: import('../controllers/internal').SessionController
|
|
||||||
appStreamConfig: import('./').AppStreamConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { MacroVars } from '@sasjs/utils'
|
|
||||||
|
|
||||||
export interface ExecutionQuery {
|
|
||||||
_program: string
|
|
||||||
macroVars?: MacroVars
|
|
||||||
_debug?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileQuery {
|
|
||||||
filePath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isExecutionQuery = (arg: any): arg is ExecutionQuery =>
|
|
||||||
arg && !Array.isArray(arg) && typeof arg._program === 'string'
|
|
||||||
|
|
||||||
export const isFileQuery = (arg: any): arg is FileQuery =>
|
|
||||||
arg && !Array.isArray(arg) && typeof arg.filePath === 'string'
|
|
||||||
9
api/src/types/RequestUser.ts
Normal file
9
api/src/types/RequestUser.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface RequestUser {
|
||||||
|
userId: number
|
||||||
|
clientId: string
|
||||||
|
username: string
|
||||||
|
displayName: string
|
||||||
|
isAdmin: boolean
|
||||||
|
isActive: boolean
|
||||||
|
autoExec?: string
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
// TODO: uppercase types
|
// TODO: uppercase types
|
||||||
export * from './AppStreamConfig'
|
export * from './AppStreamConfig'
|
||||||
export * from './Execution'
|
export * from './Execution'
|
||||||
export * from './FileTree'
|
|
||||||
export * from './InfoJWT'
|
export * from './InfoJWT'
|
||||||
export * from './PreProgramVars'
|
export * from './PreProgramVars'
|
||||||
export * from './Request'
|
|
||||||
export * from './Session'
|
export * from './Session'
|
||||||
export * from './TreeNode'
|
export * from './TreeNode'
|
||||||
|
export * from './RequestUser'
|
||||||
|
|||||||
7
api/src/types/system/express-session.d.ts
vendored
Normal file
7
api/src/types/system/express-session.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import express from 'express'
|
||||||
|
declare module 'express-session' {
|
||||||
|
interface SessionData {
|
||||||
|
loggedIn: boolean
|
||||||
|
user: import('../').RequestUser
|
||||||
|
}
|
||||||
|
}
|
||||||
7
api/src/types/system/express.d.ts
vendored
Normal file
7
api/src/types/system/express.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
export interface Request {
|
||||||
|
accessToken?: string
|
||||||
|
user?: import('../').RequestUser
|
||||||
|
sasSession?: import('../').Session
|
||||||
|
}
|
||||||
|
}
|
||||||
1
api/src/types/system/global.d.ts
vendored
Normal file
1
api/src/types/system/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import 'jest-extended'
|
||||||
9
api/src/types/system/process.d.ts
vendored
Normal file
9
api/src/types/system/process.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
declare namespace NodeJS {
|
||||||
|
export interface Process {
|
||||||
|
sasLoc: string
|
||||||
|
driveLoc: string
|
||||||
|
sessionController?: import('../../controllers/internal').SessionController
|
||||||
|
appStreamConfig: import('../').AppStreamConfig
|
||||||
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@ import { createFile, fileExists, readFile } from '@sasjs/utils'
|
|||||||
import { publishAppStream } from '../routes/appStream'
|
import { publishAppStream } from '../routes/appStream'
|
||||||
import { AppStreamConfig } from '../types'
|
import { AppStreamConfig } from '../types'
|
||||||
|
|
||||||
import { getTmpAppStreamConfigPath } from './file'
|
import { getAppStreamConfigPath } from './file'
|
||||||
|
|
||||||
export const loadAppStreamConfig = async () => {
|
export const loadAppStreamConfig = async () => {
|
||||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
|
const appStreamConfigPath = getAppStreamConfigPath()
|
||||||
|
|
||||||
const content = (await fileExists(appStreamConfigPath))
|
const content = (await fileExists(appStreamConfigPath))
|
||||||
? await readFile(appStreamConfigPath)
|
? await readFile(appStreamConfigPath)
|
||||||
@@ -61,7 +63,7 @@ export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saveAppStreamConfig = async () => {
|
const saveAppStreamConfig = async () => {
|
||||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
const appStreamConfigPath = getAppStreamConfigPath()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createFile(
|
await createFile(
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import mongoose from 'mongoose'
|
import mongoose from 'mongoose'
|
||||||
import { populateClients } from '../routes/api/auth'
|
import { seedDB } from './seedDB'
|
||||||
|
|
||||||
export const connectDB = async () => {
|
export const connectDB = async () => {
|
||||||
// NOTE: when exporting app.js as agent for supertest
|
try {
|
||||||
// we should exclude connecting to the real database
|
await mongoose.connect(process.env.DB_CONNECT as string)
|
||||||
if (process.env.NODE_ENV === 'test') {
|
} catch (err) {
|
||||||
return
|
throw new Error('Unable to connect to DB!')
|
||||||
} 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()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Connected to DB!')
|
||||||
|
await seedDB()
|
||||||
|
|
||||||
|
return mongoose.connection
|
||||||
}
|
}
|
||||||
|
|||||||
34
api/src/utils/copySASjsCore.ts
Normal file
34
api/src/utils/copySASjsCore.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import {
|
||||||
|
asyncForEach,
|
||||||
|
createFile,
|
||||||
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
readFile
|
||||||
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
|
import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||||
|
|
||||||
|
export const copySASjsCore = async () => {
|
||||||
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
|
console.log('Copying Macros from container to drive(tmp).')
|
||||||
|
|
||||||
|
const macrosDrivePath = getMacrosFolder()
|
||||||
|
|
||||||
|
await deleteFolder(macrosDrivePath)
|
||||||
|
await createFolder(macrosDrivePath)
|
||||||
|
|
||||||
|
const macros = await readFile(sasJSCoreMacrosInfo)
|
||||||
|
|
||||||
|
await asyncForEach(macros.split('\n'), async (macroName) => {
|
||||||
|
const macroFileSourcePath = path.join(sasJSCoreMacros, macroName)
|
||||||
|
const macroContent = await readFile(macroFileSourcePath)
|
||||||
|
|
||||||
|
const macroFileDestPath = path.join(macrosDrivePath, macroName)
|
||||||
|
|
||||||
|
await createFile(macroFileDestPath, macroContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Macros Drive Path:', macrosDrivePath)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { homedir } from 'os'
|
||||||
|
|
||||||
export const apiRoot = path.join(__dirname, '..', '..')
|
export const apiRoot = path.join(__dirname, '..', '..')
|
||||||
export const codebaseRoot = path.join(apiRoot, '..')
|
export const codebaseRoot = path.join(apiRoot, '..')
|
||||||
@@ -9,27 +10,33 @@ export const sysInitCompiledPath = path.join(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||||
|
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||||
|
|
||||||
export const getWebBuildFolderPath = () =>
|
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
|
||||||
path.join(codebaseRoot, 'web', 'build')
|
|
||||||
|
|
||||||
export const getTmpFolderPath = () => process.driveLoc
|
export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
|
||||||
|
|
||||||
export const getTmpAppStreamConfigPath = () =>
|
export const getDesktopUserAutoExecPath = () =>
|
||||||
path.join(getTmpFolderPath(), 'appStreamConfig.json')
|
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
||||||
|
|
||||||
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
|
export const getSasjsRootFolder = () => process.driveLoc
|
||||||
|
|
||||||
export const getTmpFilesFolderPath = () =>
|
export const getAppStreamConfigPath = () =>
|
||||||
path.join(getTmpFolderPath(), 'files')
|
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
||||||
|
|
||||||
export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs')
|
export const getMacrosFolder = () =>
|
||||||
|
path.join(getSasjsRootFolder(), 'sasjscore')
|
||||||
|
|
||||||
export const getTmpWeboutFolderPath = () =>
|
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
||||||
path.join(getTmpFolderPath(), 'webouts')
|
|
||||||
|
|
||||||
export const getTmpSessionsFolderPath = () =>
|
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
||||||
path.join(getTmpFolderPath(), 'sessions')
|
|
||||||
|
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
|
||||||
|
|
||||||
|
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||||
|
|
||||||
|
export const getSessionsFolder = () =>
|
||||||
|
path.join(getSasjsRootFolder(), 'sessions')
|
||||||
|
|
||||||
export const generateUniqueFileName = (fileName: string, extension = '') =>
|
export const generateUniqueFileName = (fileName: string, extension = '') =>
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
|||||||
const isWindows = () => process.platform === 'win32'
|
const isWindows = () => process.platform === 'win32'
|
||||||
|
|
||||||
export const getDesktopFields = async () => {
|
export const getDesktopFields = async () => {
|
||||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
const { SAS_PATH } = process.env
|
||||||
|
|
||||||
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||||
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
||||||
|
|
||||||
return { sasLoc, driveLoc }
|
return { sasLoc }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDriveLocation = async (): Promise<string> => {
|
const getDriveLocation = async (): Promise<string> => {
|
||||||
|
|||||||
30
api/src/utils/getPreProgramVariables.ts
Normal file
30
api/src/utils/getPreProgramVariables.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Request } from 'express'
|
||||||
|
import { PreProgramVars } from '../types'
|
||||||
|
|
||||||
|
export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
||||||
|
const host = req.get('host')
|
||||||
|
const protocol = req.protocol + '://'
|
||||||
|
const { user, accessToken } = req
|
||||||
|
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']
|
||||||
|
const sessionId = req.cookies['connect.sid']
|
||||||
|
const { _csrf } = req.cookies
|
||||||
|
|
||||||
|
const httpHeaders: string[] = []
|
||||||
|
|
||||||
|
if (accessToken) httpHeaders.push(`Authorization: Bearer ${accessToken}`)
|
||||||
|
if (csrfToken) httpHeaders.push(`x-xsrf-token: ${csrfToken}`)
|
||||||
|
|
||||||
|
const cookies: string[] = []
|
||||||
|
if (sessionId) cookies.push(`connect.sid=${sessionId}`)
|
||||||
|
if (_csrf) cookies.push(`_csrf=${_csrf}`)
|
||||||
|
|
||||||
|
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: user!.username,
|
||||||
|
userId: user!.userId,
|
||||||
|
displayName: user!.displayName,
|
||||||
|
serverUrl: protocol + host,
|
||||||
|
httpHeaders
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
export * from './appStreamConfig'
|
export * from './appStreamConfig'
|
||||||
export * from './connectDB'
|
export * from './connectDB'
|
||||||
|
export * from './copySASjsCore'
|
||||||
export * from './extractHeaders'
|
export * from './extractHeaders'
|
||||||
export * from './file'
|
export * from './file'
|
||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
export * from './generateAuthCode'
|
export * from './generateAuthCode'
|
||||||
export * from './generateRefreshToken'
|
export * from './generateRefreshToken'
|
||||||
export * from './isDebugOn'
|
|
||||||
export * from './getCertificates'
|
export * from './getCertificates'
|
||||||
export * from './getDesktopFields'
|
export * from './getDesktopFields'
|
||||||
|
export * from './getPreProgramVariables'
|
||||||
|
export * from './instantiateLogger'
|
||||||
|
export * from './isDebugOn'
|
||||||
export * from './parseLogToArray'
|
export * from './parseLogToArray'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
export * from './saveTokensInDB'
|
export * from './saveTokensInDB'
|
||||||
|
export * from './seedDB'
|
||||||
export * from './setProcessVariables'
|
export * from './setProcessVariables'
|
||||||
export * from './sleep'
|
export * from './setupFolders'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
|
export * from './verifyEnvVariables'
|
||||||
export * from './verifyTokenInDB'
|
export * from './verifyTokenInDB'
|
||||||
|
|||||||
7
api/src/utils/instantiateLogger.ts
Normal file
7
api/src/utils/instantiateLogger.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { LogLevel, Logger } from '@sasjs/utils/logger'
|
||||||
|
|
||||||
|
export const instantiateLogger = () => {
|
||||||
|
const logLevel = (process.env.LOG_LEVEL || LogLevel.Info) as LogLevel
|
||||||
|
const logger = new Logger(logLevel)
|
||||||
|
process.logger = logger
|
||||||
|
}
|
||||||
35
api/src/utils/parseHelmetConfig.ts
Normal file
35
api/src/utils/parseHelmetConfig.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
export const getEnvCSPDirectives = (
|
||||||
|
HELMET_CSP_CONFIG_PATH: string | undefined
|
||||||
|
) => {
|
||||||
|
let cspConfigJson = {
|
||||||
|
'img-src': ["'self'", 'data:'],
|
||||||
|
'script-src': ["'self'", "'unsafe-inline'"],
|
||||||
|
'script-src-attr': ["'self'", "'unsafe-inline'"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof HELMET_CSP_CONFIG_PATH === 'string' &&
|
||||||
|
HELMET_CSP_CONFIG_PATH.length > 0
|
||||||
|
) {
|
||||||
|
const cspConfigPath = path.join(process.cwd(), HELMET_CSP_CONFIG_PATH)
|
||||||
|
|
||||||
|
try {
|
||||||
|
let file = fs.readFileSync(cspConfigPath).toString()
|
||||||
|
|
||||||
|
try {
|
||||||
|
cspConfigJson = JSON.parse(file)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
'Parsing Content Security Policy JSON config failed. Make sure it is valid json'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error reading HELMET CSP config file', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cspConfigJson
|
||||||
|
}
|
||||||
35
api/src/utils/seedDB.ts
Normal file
35
api/src/utils/seedDB.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Client from '../model/Client'
|
||||||
|
import User from '../model/User'
|
||||||
|
|
||||||
|
const CLIENT = {
|
||||||
|
clientId: 'clientID1',
|
||||||
|
clientSecret: 'clientSecret'
|
||||||
|
}
|
||||||
|
const ADMIN_USER = {
|
||||||
|
id: 1,
|
||||||
|
displayName: 'Super Admin',
|
||||||
|
username: 'secretuser',
|
||||||
|
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
|
||||||
|
isAdmin: true,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const seedDB = async () => {
|
||||||
|
// Checking if client is already in the database
|
||||||
|
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
|
||||||
|
if (!clientExist) {
|
||||||
|
const client = new Client(CLIENT)
|
||||||
|
await client.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking if user is already in the database
|
||||||
|
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||||
|
if (!usernameExist) {
|
||||||
|
const user = new User(ADMIN_USER)
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,29 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getRealPath } from '@sasjs/utils'
|
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||||
|
|
||||||
import { configuration } from '../../package.json'
|
import { getDesktopFields, ModeType } from '.'
|
||||||
import { getDesktopFields } from '.'
|
|
||||||
|
|
||||||
export const setProcessVariables = async () => {
|
export const setProcessVariables = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'tmp')
|
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
|
|
||||||
if (MODE?.trim() !== 'server') {
|
if (MODE === ModeType.Server) {
|
||||||
const { sasLoc, driveLoc } = await getDesktopFields()
|
process.sasLoc = process.env.SAS_PATH as string
|
||||||
|
} else {
|
||||||
|
const { sasLoc } = await getDesktopFields()
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
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')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { SASJS_ROOT } = process.env
|
||||||
|
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||||
|
await createFolder(absPath)
|
||||||
|
process.driveLoc = getRealPath(absPath)
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
}
|
}
|
||||||
|
|||||||
14
api/src/utils/setupFolders.ts
Normal file
14
api/src/utils/setupFolders.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
||||||
|
import { getDesktopUserAutoExecPath, getFilesFolder } from './file'
|
||||||
|
import { ModeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
|
export const setupFolders = async () => {
|
||||||
|
const drivePath = getFilesFolder()
|
||||||
|
await createFolder(drivePath)
|
||||||
|
|
||||||
|
if (process.env.MODE === ModeType.Desktop) {
|
||||||
|
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
||||||
|
await createFile(getDesktopUserAutoExecPath(), '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export const sleep = async (delay: number) => {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
import path from 'path'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { getTmpSessionsFolderPath } from '.'
|
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
import { listFilesInFolder } from '@sasjs/utils'
|
import { listFilesInFolder } from '@sasjs/utils'
|
||||||
|
|
||||||
|
interface FilenameMapSingle {
|
||||||
|
fieldName: string
|
||||||
|
originalName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilenamesMap {
|
||||||
|
[key: string]: FilenameMapSingle
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadedFiles extends FilenameMapSingle {
|
||||||
|
fileref: string
|
||||||
|
filepath: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It will create an object that maps hashed file names to the original names
|
* It will create an object that maps hashed file names to the original names
|
||||||
* @param files array of files to be mapped
|
* @param files array of files to be mapped
|
||||||
@@ -12,10 +24,13 @@ import { listFilesInFolder } from '@sasjs/utils'
|
|||||||
export const makeFilesNamesMap = (files: MulterFile[]) => {
|
export const makeFilesNamesMap = (files: MulterFile[]) => {
|
||||||
if (!files) return null
|
if (!files) return null
|
||||||
|
|
||||||
const filesNamesMap: { [key: string]: string } = {}
|
const filesNamesMap: FilenamesMap = {}
|
||||||
|
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
filesNamesMap[file.filename] = file.originalname
|
filesNamesMap[file.filename] = {
|
||||||
|
fieldName: file.fieldname,
|
||||||
|
originalName: file.originalname
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filesNamesMap
|
return filesNamesMap
|
||||||
@@ -28,17 +43,12 @@ export const makeFilesNamesMap = (files: MulterFile[]) => {
|
|||||||
* @returns generated sas code
|
* @returns generated sas code
|
||||||
*/
|
*/
|
||||||
export const generateFileUploadSasCode = async (
|
export const generateFileUploadSasCode = async (
|
||||||
filesNamesMap: any,
|
filesNamesMap: FilenamesMap,
|
||||||
sasSessionFolder: string
|
sasSessionFolder: string
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
let uploadSasCode = ''
|
let uploadSasCode = ''
|
||||||
let fileCount = 0
|
let fileCount = 0
|
||||||
let uploadedFilesMap: {
|
const uploadedFiles: UploadedFiles[] = []
|
||||||
fileref: string
|
|
||||||
filepath: string
|
|
||||||
filename: string
|
|
||||||
count: number
|
|
||||||
}[] = []
|
|
||||||
|
|
||||||
const sasSessionFolderList: string[] = await listFilesInFolder(
|
const sasSessionFolderList: string[] = await listFilesInFolder(
|
||||||
sasSessionFolder
|
sasSessionFolder
|
||||||
@@ -50,31 +60,32 @@ export const generateFileUploadSasCode = async (
|
|||||||
if (fileName.includes('req_file')) {
|
if (fileName.includes('req_file')) {
|
||||||
fileCount++
|
fileCount++
|
||||||
|
|
||||||
uploadedFilesMap.push({
|
uploadedFiles.push({
|
||||||
fileref: `_sjs${fileCountString}`,
|
fileref: `_sjs${fileCountString}`,
|
||||||
filepath: `${sasSessionFolder}/${fileName}`,
|
filepath: `${sasSessionFolder}/${fileName}`,
|
||||||
filename: filesNamesMap[fileName],
|
originalName: filesNamesMap[fileName].originalName,
|
||||||
|
fieldName: filesNamesMap[fileName].fieldName,
|
||||||
count: fileCount
|
count: fileCount
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
for (let uploadedMap of uploadedFilesMap) {
|
for (const uploadedFile of uploadedFiles) {
|
||||||
uploadSasCode += `\nfilename ${uploadedMap.fileref} "${uploadedMap.filepath}";`
|
uploadSasCode += `\nfilename ${uploadedFile.fileref} "${uploadedFile.filepath}";`
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
|
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
|
||||||
|
|
||||||
for (let uploadedMap of uploadedFilesMap) {
|
for (const uploadedFile of uploadedFiles) {
|
||||||
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedMap.count}=${uploadedMap.filename};`
|
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedFile.count}=${uploadedFile.originalName};`
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let uploadedMap of uploadedFilesMap) {
|
for (const uploadedFile of uploadedFiles) {
|
||||||
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedMap.count}=${uploadedMap.fileref};`
|
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedFile.count}=${uploadedFile.fileref};`
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let uploadedMap of uploadedFilesMap) {
|
for (const uploadedFile of uploadedFiles) {
|
||||||
uploadSasCode += `\n%let _WEBIN_NAME${uploadedMap.count}=${uploadedMap.filepath};`
|
uploadSasCode += `\n%let _WEBIN_NAME${uploadedFile.count}=${uploadedFile.fieldName};`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileCount > 0) {
|
if (fileCount > 0) {
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import Joi from 'joi'
|
import Joi from 'joi'
|
||||||
|
|
||||||
const usernameSchema = Joi.string().alphanum().min(6).max(20)
|
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
||||||
const passwordSchema = Joi.string().min(6).max(1024)
|
const passwordSchema = Joi.string().min(6).max(1024)
|
||||||
|
|
||||||
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
export const loginWebValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
username: usernameSchema.required(),
|
username: usernameSchema.required(),
|
||||||
password: passwordSchema.required(),
|
password: passwordSchema.required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
|
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
clientId: Joi.string().required()
|
clientId: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
@@ -31,7 +35,8 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
|
|||||||
username: usernameSchema.required(),
|
username: usernameSchema.required(),
|
||||||
password: passwordSchema.required(),
|
password: passwordSchema.required(),
|
||||||
isAdmin: Joi.boolean(),
|
isAdmin: Joi.boolean(),
|
||||||
isActive: Joi.boolean()
|
isActive: Joi.boolean(),
|
||||||
|
autoExec: Joi.string().allow('')
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const deleteUserValidation = (
|
export const deleteUserValidation = (
|
||||||
@@ -53,7 +58,8 @@ export const updateUserValidation = (
|
|||||||
const validationChecks: any = {
|
const validationChecks: any = {
|
||||||
displayName: Joi.string().min(6),
|
displayName: Joi.string().min(6),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
password: passwordSchema
|
password: passwordSchema,
|
||||||
|
autoExec: Joi.string().allow('')
|
||||||
}
|
}
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
validationChecks.isAdmin = Joi.boolean()
|
validationChecks.isAdmin = Joi.boolean()
|
||||||
@@ -98,6 +104,11 @@ export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
|||||||
_filePath: filePathSchema
|
_filePath: filePathSchema
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
|
export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
_folderPath: Joi.string()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
code: Joi.string().required()
|
code: Joi.string().required()
|
||||||
|
|||||||
211
api/src/utils/verifyEnvVariables.ts
Normal file
211
api/src/utils/verifyEnvVariables.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
export enum ModeType {
|
||||||
|
Server = 'server',
|
||||||
|
Desktop = 'desktop'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProtocolType {
|
||||||
|
HTTP = 'http',
|
||||||
|
HTTPS = 'https'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CorsType {
|
||||||
|
ENABLED = 'enable',
|
||||||
|
DISABLED = 'disable'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HelmetCoepType {
|
||||||
|
TRUE = 'true',
|
||||||
|
FALSE = 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LOG_FORMAT_MORGANType {
|
||||||
|
Combined = 'combined',
|
||||||
|
Common = 'common',
|
||||||
|
Dev = 'dev',
|
||||||
|
Short = 'short',
|
||||||
|
tiny = 'tiny'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ReturnCode {
|
||||||
|
Success,
|
||||||
|
InvalidEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
export const verifyEnvVariables = (): ReturnCode => {
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
errors.push(...verifyMODE())
|
||||||
|
|
||||||
|
errors.push(...verifyPROTOCOL())
|
||||||
|
|
||||||
|
errors.push(...verifyPORT())
|
||||||
|
|
||||||
|
errors.push(...verifyCORS())
|
||||||
|
|
||||||
|
errors.push(...verifyHELMET_COEP())
|
||||||
|
|
||||||
|
errors.push(...verifyLOG_FORMAT_MORGAN())
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
process.logger?.error(
|
||||||
|
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
||||||
|
)
|
||||||
|
return ReturnCode.InvalidEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnCode.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyMODE = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { MODE } = process.env
|
||||||
|
|
||||||
|
if (MODE) {
|
||||||
|
const modeTypes = Object.values(ModeType)
|
||||||
|
if (!modeTypes.includes(MODE as ModeType))
|
||||||
|
errors.push(`- MODE '${MODE}'\n - valid options ${modeTypes}`)
|
||||||
|
} else {
|
||||||
|
process.env.MODE = DEFAULTS.MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.MODE === ModeType.Server) {
|
||||||
|
const {
|
||||||
|
ACCESS_TOKEN_SECRET,
|
||||||
|
REFRESH_TOKEN_SECRET,
|
||||||
|
AUTH_CODE_SECRET,
|
||||||
|
SESSION_SECRET,
|
||||||
|
DB_CONNECT
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
if (!ACCESS_TOKEN_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!REFRESH_TOKEN_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!AUTH_CODE_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!SESSION_SECRET)
|
||||||
|
errors.push(
|
||||||
|
`- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test')
|
||||||
|
if (!DB_CONNECT)
|
||||||
|
errors.push(
|
||||||
|
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyPROTOCOL = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { PROTOCOL } = process.env
|
||||||
|
|
||||||
|
if (PROTOCOL) {
|
||||||
|
const protocolTypes = Object.values(ProtocolType)
|
||||||
|
if (!protocolTypes.includes(PROTOCOL as ProtocolType))
|
||||||
|
errors.push(`- PROTOCOL '${PROTOCOL}'\n - valid options ${protocolTypes}`)
|
||||||
|
} else {
|
||||||
|
process.env.PROTOCOL = DEFAULTS.PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
|
||||||
|
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
||||||
|
|
||||||
|
if (!PRIVATE_KEY)
|
||||||
|
errors.push(
|
||||||
|
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!FULL_CHAIN)
|
||||||
|
errors.push(
|
||||||
|
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyCORS = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { CORS } = process.env
|
||||||
|
|
||||||
|
if (CORS) {
|
||||||
|
const corsTypes = Object.values(CorsType)
|
||||||
|
if (!corsTypes.includes(CORS as CorsType))
|
||||||
|
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
|
||||||
|
} else {
|
||||||
|
const { MODE } = process.env
|
||||||
|
process.env.CORS =
|
||||||
|
MODE === ModeType.Server ? CorsType.DISABLED : CorsType.ENABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyPORT = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { PORT } = process.env
|
||||||
|
|
||||||
|
if (PORT) {
|
||||||
|
if (Number.isNaN(parseInt(PORT)))
|
||||||
|
errors.push(`- PORT '${PORT}'\n - should be a valid number`)
|
||||||
|
} else {
|
||||||
|
process.env.PORT = DEFAULTS.PORT
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyHELMET_COEP = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { HELMET_COEP } = process.env
|
||||||
|
|
||||||
|
if (HELMET_COEP) {
|
||||||
|
const helmetCoepTypes = Object.values(HelmetCoepType)
|
||||||
|
if (!helmetCoepTypes.includes(HELMET_COEP as HelmetCoepType))
|
||||||
|
errors.push(
|
||||||
|
`- HELMET_COEP '${HELMET_COEP}'\n - valid options ${helmetCoepTypes}`
|
||||||
|
)
|
||||||
|
HELMET_COEP
|
||||||
|
} else {
|
||||||
|
process.env.HELMET_COEP = DEFAULTS.HELMET_COEP
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyLOG_FORMAT_MORGAN = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { LOG_FORMAT_MORGAN } = process.env
|
||||||
|
|
||||||
|
if (LOG_FORMAT_MORGAN) {
|
||||||
|
const logFormatMorganTypes = Object.values(LOG_FORMAT_MORGANType)
|
||||||
|
if (
|
||||||
|
!logFormatMorganTypes.includes(LOG_FORMAT_MORGAN as LOG_FORMAT_MORGANType)
|
||||||
|
)
|
||||||
|
errors.push(
|
||||||
|
`- LOG_FORMAT_MORGAN '${LOG_FORMAT_MORGAN}'\n - valid options ${logFormatMorganTypes}`
|
||||||
|
)
|
||||||
|
LOG_FORMAT_MORGAN
|
||||||
|
} else {
|
||||||
|
process.env.LOG_FORMAT_MORGAN = DEFAULTS.LOG_FORMAT_MORGAN
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
MODE: ModeType.Desktop,
|
||||||
|
PROTOCOL: ProtocolType.HTTP,
|
||||||
|
PORT: '5000',
|
||||||
|
HELMET_COEP: HelmetCoepType.TRUE,
|
||||||
|
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common
|
||||||
|
}
|
||||||
@@ -1,11 +1,30 @@
|
|||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
|
import { RequestUser } from '../types'
|
||||||
|
|
||||||
|
export const fetchLatestAutoExec = async (
|
||||||
|
reqUser: RequestUser
|
||||||
|
): Promise<RequestUser | undefined> => {
|
||||||
|
const dbUser = await User.findOne({ id: reqUser.userId })
|
||||||
|
|
||||||
|
if (!dbUser) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: reqUser.userId,
|
||||||
|
clientId: reqUser.clientId,
|
||||||
|
username: dbUser.username,
|
||||||
|
displayName: dbUser.displayName,
|
||||||
|
isAdmin: dbUser.isAdmin,
|
||||||
|
isActive: dbUser.isActive,
|
||||||
|
autoExec: dbUser.autoExec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const verifyTokenInDB = async (
|
export const verifyTokenInDB = async (
|
||||||
userId: number,
|
userId: number,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
token: string,
|
token: string,
|
||||||
tokenType: 'accessToken' | 'refreshToken'
|
tokenType: 'accessToken' | 'refreshToken'
|
||||||
) => {
|
): Promise<RequestUser | undefined> => {
|
||||||
const dbUser = await User.findOne({ id: userId })
|
const dbUser = await User.findOne({ id: userId })
|
||||||
|
|
||||||
if (!dbUser) return undefined
|
if (!dbUser) return undefined
|
||||||
@@ -21,7 +40,8 @@ export const verifyTokenInDB = async (
|
|||||||
username: dbUser.username,
|
username: dbUser.username,
|
||||||
displayName: dbUser.displayName,
|
displayName: dbUser.displayName,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isActive: dbUser.isActive
|
isActive: dbUser.isActive,
|
||||||
|
autoExec: dbUser.autoExec
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Info",
|
||||||
|
"description": "Get Server Info"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Session",
|
"name": "Session",
|
||||||
"description": "Get Session information"
|
"description": "Get Session information"
|
||||||
@@ -42,6 +46,10 @@
|
|||||||
{
|
{
|
||||||
"name": "CODE",
|
"name": "CODE",
|
||||||
"description": "Operations on SAS code"
|
"description": "Operations on SAS code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Web",
|
||||||
|
"description": "Operations on Web"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"yaml": true,
|
"yaml": true,
|
||||||
|
|||||||
10596
package-lock.json
generated
10596
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.36",
|
"version": "0.0.76",
|
||||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||||
"repository": "https://github.com/sasjs/server",
|
"repository": "https://github.com/sasjs/server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "npm run server:prepare && npm run server:start",
|
"server": "npm run server:prepare && npm run server:start",
|
||||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..",
|
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
|
||||||
"server:start": "cd api && npm run start",
|
"server:start": "cd api && npm run start:prod",
|
||||||
"release": "standard-version",
|
|
||||||
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint-web:fix": "npx prettier --write \"web/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-web:fix": "npx prettier --write \"web/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
@@ -16,7 +15,9 @@
|
|||||||
"lint:fix": "npm run lint-api:fix && npm run lint-web:fix"
|
"lint:fix": "npm run lint-api:fix && npm run lint-web:fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.3.1",
|
"@semantic-release/changelog": "^6.0.1",
|
||||||
"standard-version": "^9.3.2"
|
"@semantic-release/exec": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
|
"@semantic-release/github": "^8.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
POST http://localhost:5000/SASjsApi/drive/deploy
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
### Get current user's info via access token
|
### Get current user's info via session ID
|
||||||
GET http://localhost:5000/SASjsApi/session
|
GET http://localhost:5000/SASjsApi/session
|
||||||
|
cookie: connect.sid=s:G2DeFdKuWhnmTOsTHmTWrxAXPx2P6TLD.JyNLxfACC1w3NlFQFfL5chyxtrqbPYmS6iButRc1goE
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
|
|
||||||
|
|
||||||
### testing upload file example
|
### testing upload file example
|
||||||
POST http://localhost:5000/SASjsApi/stp/execute/?_program=/Public/app/viya/services/editors/loadfile&table=DCCONFIG.MPE_X_TEST
|
POST http://localhost:5000/SASjsApi/stp/execute/?_program=/Public/app/viya/services/editors/loadfile&table=DCCONFIG.MPE_X_TEST
|
||||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynkYOqevUMKZrXeAy
|
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||||
|
|
||||||
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||||
Content-Disposition: form-data; name="file"; filename="DCCONFIG.MPE_X_TEST.xlsx"
|
Content-Disposition: form-data; name="fileSome11"; filename="DCCONFIG.MPE_X_TEST.xlsx"
|
||||||
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
|
||||||
|
|
||||||
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||||
Content-Disposition: form-data; name="file"; filename="DCCONFIG.MPE_X_TEST.xlsx.csv"
|
Content-Disposition: form-data; name="fileSome22"; filename="DCCONFIG.MPE_X_TEST.xlsx.csv"
|
||||||
Content-Type: application/csv
|
Content-Type: application/csv
|
||||||
|
|
||||||
_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM
|
_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
PORT_API=[place sasjs server port] default value is 5000
|
PORT_API=[place sasjs server port] default value is 5000
|
||||||
CLIENT_ID=<place clientId here>
|
|
||||||
499
web/package-lock.json
generated
499
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
|
||||||
"@mui/icons-material": "^5.0.3",
|
"@mui/icons-material": "^5.0.3",
|
||||||
"@mui/lab": "^5.0.0-alpha.50",
|
"@mui/lab": "^5.0.0-alpha.50",
|
||||||
"@mui/material": "^5.0.3",
|
"@mui/material": "^5.0.3",
|
||||||
@@ -21,9 +20,14 @@
|
|||||||
"@types/node": "^12.20.28",
|
"@types/node": "^12.20.28",
|
||||||
"@types/react": "^17.0.27",
|
"@types/react": "^17.0.27",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
|
"monaco-editor": "^0.33.0",
|
||||||
|
"monaco-editor-webpack-plugin": "^7.0.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router-dom": "^5.3.0"
|
"react-monaco-editor": "^0.48.0",
|
||||||
|
"react-router-dom": "^5.3.0",
|
||||||
|
"react-toastify": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.0",
|
"@babel/core": "^7.16.0",
|
||||||
@@ -35,6 +39,7 @@
|
|||||||
"@types/dotenv-webpack": "^7.0.3",
|
"@types/dotenv-webpack": "^7.0.3",
|
||||||
"@types/prismjs": "^1.16.6",
|
"@types/prismjs": "^1.16.6",
|
||||||
"@types/react": "^17.0.37",
|
"@types/react": "^17.0.37",
|
||||||
|
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router-dom": "^5.3.1",
|
"@types/react-router-dom": "^5.3.1",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
|
|||||||
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 { Route, HashRouter, Switch } from 'react-router-dom'
|
||||||
import { ThemeProvider } from '@mui/material/styles'
|
import { ThemeProvider } from '@mui/material/styles'
|
||||||
import { theme } from './theme'
|
import { theme } from './theme'
|
||||||
@@ -8,23 +8,23 @@ import Header from './components/header'
|
|||||||
import Home from './components/home'
|
import Home from './components/home'
|
||||||
import Drive from './containers/Drive'
|
import Drive from './containers/Drive'
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
|
import Settings from './containers/Settings'
|
||||||
|
|
||||||
import useTokens from './components/useTokens'
|
import { AppContext } from './context/appContext'
|
||||||
|
import AuthCode from './containers/AuthCode'
|
||||||
|
import { ToastContainer } from 'react-toastify'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { tokens, setTokens } = useTokens()
|
const appContext = useContext(AppContext)
|
||||||
|
|
||||||
if (!tokens) {
|
if (!appContext.loggedIn) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Header />
|
<Header />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/SASjsLogon">
|
|
||||||
<Login getCodeOnly />
|
|
||||||
</Route>
|
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Login setTokens={setTokens} />
|
<Login />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
@@ -46,10 +46,14 @@ function App() {
|
|||||||
<Route exact path="/SASjsStudio">
|
<Route exact path="/SASjsStudio">
|
||||||
<Studio />
|
<Studio />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/SASjsSettings">
|
||||||
|
<Settings />
|
||||||
|
</Route>
|
||||||
<Route exact path="/SASjsLogon">
|
<Route exact path="/SASjsLogon">
|
||||||
<Login getCodeOnly />
|
<AuthCode />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
<ToastContainer />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,26 +1,63 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import AppBar from '@mui/material/AppBar'
|
import {
|
||||||
import Toolbar from '@mui/material/Toolbar'
|
AppBar,
|
||||||
import Tabs from '@mui/material/Tabs'
|
Toolbar,
|
||||||
import Tab from '@mui/material/Tab'
|
Tabs,
|
||||||
import Button from '@mui/material/Button'
|
Tab,
|
||||||
|
Button,
|
||||||
|
Menu,
|
||||||
|
MenuItem
|
||||||
|
} from '@mui/material'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings'
|
||||||
|
|
||||||
|
import Username from './username'
|
||||||
|
import { AppContext } from '../context/appContext'
|
||||||
|
|
||||||
const NODE_ENV = process.env.NODE_ENV
|
const NODE_ENV = process.env.NODE_ENV
|
||||||
const PORT_API = process.env.PORT_API
|
const PORT_API = process.env.PORT_API
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||||
|
|
||||||
|
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
||||||
|
|
||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const [tabValue, setTabValue] = useState(pathname)
|
const appContext = useContext(AppContext)
|
||||||
|
const [tabValue, setTabValue] = useState(
|
||||||
|
validTabs.includes(pathname) ? pathname : '/'
|
||||||
|
)
|
||||||
|
const [anchorEl, setAnchorEl] = useState<
|
||||||
|
(EventTarget & HTMLButtonElement) | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTabValue(validTabs.includes(pathname) ? pathname : '/')
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
const handleMenu = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
||||||
setTabValue(value)
|
setTabValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
if (appContext.logout) {
|
||||||
|
handleClose()
|
||||||
|
appContext.logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
position="fixed"
|
position="fixed"
|
||||||
@@ -28,10 +65,10 @@ const Header = (props: any) => {
|
|||||||
>
|
>
|
||||||
<Toolbar variant="dense">
|
<Toolbar variant="dense">
|
||||||
<img
|
<img
|
||||||
src="logo-white.png"
|
src="logo.png"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
style={{
|
style={{
|
||||||
width: '50px',
|
width: '35px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
marginRight: '25px'
|
marginRight: '25px'
|
||||||
}}
|
}}
|
||||||
@@ -81,6 +118,51 @@ const Header = (props: any) => {
|
|||||||
>
|
>
|
||||||
App Stream
|
App Stream
|
||||||
</Button>
|
</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 sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/SASjsSettings"
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<SettingsIcon />}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button variant="contained" color="primary">
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user