mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
266 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a932383b4 | ||
|
|
576e18347e | ||
|
|
61815f8ae1 | ||
|
|
afff27fd21 | ||
|
|
a8ba378fd1 | ||
|
|
73c81a45dc | ||
|
|
12d424acce | ||
|
|
414fb19de3 | ||
|
|
cfddf1fb0c | ||
|
|
1f483b1afc | ||
|
|
0470239ef1 | ||
|
|
2c259fe1de | ||
|
|
b066734398 | ||
|
|
3b698fce5f | ||
|
|
5ad6ee5e0f | ||
|
|
7d11cc7916 | ||
|
|
ff1def6436 | ||
|
|
c275db184e | ||
|
|
e4239fbcc3 | ||
|
|
c6fd8fdd70 | ||
|
|
79dc2dba23 | ||
|
|
2a7223ad7d | ||
|
|
1fed5ea6ac | ||
|
|
97f689f292 | ||
|
|
53bf68a6af | ||
|
|
f37f8e95d1 | ||
|
|
80b33c7a18 | ||
|
|
b1803fe385 | ||
|
|
7dd08c3b5b | ||
|
|
b780b59b66 | ||
|
|
7b457eaec5 | ||
|
|
c017d13061 | ||
|
|
c2b5e353a5 | ||
|
|
f89389bbc6 | ||
|
|
fadcc9bd29 | ||
|
|
182def2f3e | ||
|
|
06a5f39fea | ||
|
|
143b367a0e | ||
|
|
b5fd800300 | ||
|
|
a0b52d9982 | ||
|
|
c4212665c8 | ||
|
|
97d9bc191c | ||
|
|
dd2a403985 | ||
|
|
7cfa2398e1 | ||
|
|
5888f04e08 | ||
|
|
b40de8fa6a | ||
|
|
45a2a01532 | ||
|
|
c61fec47c4 | ||
| 24d7f00c02 | |||
| b0fdaaaa79 | |||
|
|
2467616296 | ||
|
|
ceefbe48e9 | ||
|
|
426e90471e | ||
|
|
c0b57b9e76 | ||
|
|
4a8e32dd20 | ||
|
|
636301e664 | ||
|
|
25dc5dd215 | ||
|
|
503994dbd2 | ||
|
|
0dceb5c3c3 | ||
|
|
1af04fa3b3 | ||
|
|
efa81fec77 | ||
|
|
10caf1918a | ||
|
|
4ed20a3b75 | ||
|
|
98b2c5fa25 | ||
|
|
3ad327b85f | ||
|
|
dd3acce393 | ||
|
|
8065727b9b | ||
|
|
e1223ec3f8 | ||
|
|
1f89279264 | ||
|
|
a07f47a1ba | ||
|
|
2548c82dfe | ||
|
|
238aa1006f | ||
|
|
35cba97611 | ||
|
|
5f29dec16f | ||
|
|
e2a97fcb7c | ||
|
|
6adeeefcf5 | ||
|
|
c9d66b8576 | ||
|
|
5aaac24080 | ||
|
|
6d34206bbc | ||
|
|
7b39cc06d3 | ||
|
|
6e7f28a6f8 | ||
|
|
5689169ce4 | ||
|
|
6139e7bff6 | ||
|
|
2c77317bb9 | ||
|
|
57b63db9cb | ||
|
|
60a2a4fe32 | ||
|
|
09611cb416 | ||
|
|
2a9bb6e6b1 | ||
|
|
b4b60c69cf | ||
|
|
b060ad1b8e | ||
|
|
d47ed6d0e8 | ||
|
|
a6993ef5ae | ||
|
|
2571fc2ca8 | ||
|
|
992f39b63a | ||
|
|
1ea3f6d8b3 | ||
|
|
e462aebdc0 | ||
|
|
13403517a4 | ||
|
|
c3c2048e75 | ||
|
|
1d8acc36eb | ||
|
|
4c7ad56326 | ||
|
|
e57443f1ed | ||
|
|
5da93f318a | ||
|
|
a30fb1a241 | ||
|
|
4ae8f35e9a | ||
|
|
ebb46f51b6 | ||
|
|
fe24f51ca2 | ||
|
|
fd15f3fb41 | ||
|
|
7d31ee7696 | ||
|
|
667e26b080 | ||
|
|
d09876c05f | ||
|
|
fb8e18be75 | ||
|
|
7ac7a4e083 | ||
|
|
8e23786dd4 | ||
|
|
4bd01bcf29 | ||
|
|
4ad8c81e49 | ||
|
|
51f6aa34a1 | ||
|
|
486207128d | ||
|
|
1e4b0b9171 | ||
|
|
1ff820605a | ||
|
|
9c1a781b3a | ||
| 36628551ae | |||
| 23cf8fa06f | |||
| 84ee743eae | |||
|
|
19e5bd7d2d | ||
|
|
e251747302 | ||
|
|
7e7558d4cf | ||
|
|
f02996facf | ||
|
|
803c51f400 | ||
|
|
c35b2b3f59 | ||
|
|
fe0866ace7 | ||
|
|
1513c3623d | ||
|
|
7fe43ae0b7 | ||
|
|
c4cea4a12b | ||
|
|
9fc7a132ba | ||
|
|
d55a619d64 | ||
|
|
737d2a24c2 | ||
|
|
2e63831b90 | ||
|
|
c7ffde1a3b | ||
|
|
db70b1ce55 | ||
|
|
8a3fe8b217 | ||
| 9dca552e82 | |||
|
|
505f2089c7 | ||
|
|
3344c400a8 | ||
| fa6248e3ef | |||
| 9fb5f1f8e7 | |||
|
|
92e0b8a088 | ||
|
|
b484306ed8 | ||
| 5e08aacc51 | |||
| a9e4eb685d | |||
| 31b09f27cc | |||
| 9f3ec92f8e | |||
| 6c9e449614 | |||
| 68e84b0994 | |||
| f0bb51a0d5 | |||
| b93a0da3a3 | |||
|
|
e5facbf54c | ||
|
|
cb2bebbe76 | ||
|
|
9e1e0ce8cc | ||
|
|
29928753b7 | ||
|
|
edd69ecaae | ||
|
|
74ba65f9f3 | ||
|
|
f257602834 | ||
|
|
61080d4694 | ||
|
|
82633adbc4 | ||
|
|
23db7e7b7d | ||
|
|
cbaa687c9b | ||
|
|
527f70e90d | ||
|
|
122faad55f | ||
|
|
3ff6f5e865 | ||
|
|
7d5128c0d6 | ||
|
|
e1ebbfd087 | ||
|
|
e430bdb0d4 | ||
|
|
9d9769eef3 | ||
|
|
9d167abe2a | ||
|
|
18d0604bdd | ||
|
|
7b7bc6b778 | ||
|
|
fb4f3442d5 | ||
|
|
09d1b7d5d4 | ||
|
|
99839ae62f | ||
|
|
f700561e1a | ||
|
|
8b4b4b91ab | ||
|
|
acb3ae0493 | ||
|
|
f48aeb1b0b | ||
|
|
5c0e8e5344 | ||
|
|
0ac9e4af7d | ||
|
|
ee80f3f968 | ||
|
|
7f4201ba85 | ||
|
|
f830bbc058 | ||
|
|
f8e1522a5a | ||
|
|
0a5aeceab5 | ||
|
|
6dc39c0d91 | ||
|
|
117a53ceea | ||
|
|
dd56a95314 | ||
|
|
c5117abe71 | ||
|
|
84c632a861 | ||
|
|
3ddd09eba0 | ||
|
|
0c0301433c | ||
|
|
954b2e3e2e | ||
|
|
5655311b96 | ||
|
|
9ace33d783 | ||
|
|
adc5aca0f0 | ||
|
|
71c6be6b84 | ||
|
|
9c751877d1 | ||
|
|
2204d54cd6 | ||
|
|
f4eb75ff34 | ||
|
|
a3cde343b7 | ||
|
|
7a70d40dbf | ||
|
|
d27e070fc8 | ||
|
|
27e260e6a4 | ||
|
|
2796db8ead | ||
|
|
84f7c2ab89 | ||
|
|
e68090181a | ||
|
|
d2956fc641 | ||
|
|
a701bb25e7 | ||
|
|
5758bcd392 | ||
|
|
9e53470947 | ||
|
|
81f6605249 | ||
|
|
0b45402946 | ||
|
|
9ac3191891 | ||
|
|
cd00aa2af8 | ||
|
|
0147bcb701 | ||
|
|
bf53ad30f4 | ||
|
|
a003b8836b | ||
|
|
df6003df94 | ||
|
|
b1d0fdbb02 | ||
|
|
2c34395110 | ||
|
|
534e4e5bf3 | ||
|
|
6146372eba | ||
|
|
aaa469a142 | ||
|
|
4fd5bf948e | ||
|
|
99f91fbce2 | ||
|
|
98a00ec7ac | ||
|
|
b0fb858c49 | ||
|
|
83959ef99e | ||
|
|
08087495d3 | ||
|
|
3f68474839 | ||
|
|
f26886f84d | ||
|
|
ddd50eac8e | ||
|
|
bba3e8d272 | ||
|
|
30944bfa18 | ||
|
|
8822de95df | ||
|
|
02a242fe4b | ||
|
|
1beac914db | ||
|
|
a45b42107e | ||
| 3d89b753f0 | |||
| fb77d99177 | |||
| fa627aabf9 | |||
|
|
fd2629862f | ||
|
|
75291f9397 | ||
|
|
99fb5f4b2b | ||
|
|
5dc3deeb11 | ||
|
|
6b708fcad3 | ||
|
|
bc0ff84d8d | ||
|
|
1ff6965dd2 | ||
|
|
d6fa877941 | ||
|
|
940f705f5d | ||
|
|
7a6e6c8bec | ||
|
|
67d200d817 | ||
|
|
a0c27ea8d3 | ||
|
|
3d583ff21d | ||
|
|
7072e282b1 | ||
|
|
145ac45036 | ||
|
|
698180ab7e | ||
|
|
0f4e38d51d | ||
|
|
e76283daa4 | ||
|
|
6ab42ca486 |
77
.github/CONTRIBUTING.md
vendored
77
.github/CONTRIBUTING.md
vendored
@@ -2,25 +2,22 @@
|
||||
|
||||
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
|
||||
|
||||
The app can be deployed using Docker or NodeJS.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is made in the `configuration` section of `package.json`:
|
||||
Configuration is made using `.env` files (per [README.md](https://github.com/sasjs/server#env-var-configuration) settings), _except_ for one case, when running in NodeJS in production - in which case the path to the SAS executable is made in the `configuration` section of `package.json`.
|
||||
|
||||
- Provide path to SAS9 executable.
|
||||
The `.env` file should be created in the location(s) below. Each folder contains a `.env.example` file that may be adjusted and renamed.
|
||||
|
||||
* `.env` - the root .env file is used only for Docker deploys.
|
||||
* `api/.env` - this is the primary file used in NodeJS deploys
|
||||
* `web/.env` - this file is only necessary in NodeJS when running `web` and `api` seperately (on different ports).
|
||||
|
||||
|
||||
### Using dockers:
|
||||
## Using Docker
|
||||
|
||||
There is `.env.example` file present at root of the project. [for Production]
|
||||
|
||||
There is `.env.example` file present at `./api` of the project. [for Development]
|
||||
|
||||
There is `.env.example` file present at `./web` of the project. [for Development]
|
||||
|
||||
Remember to provide enviornment variables.
|
||||
|
||||
#### Development
|
||||
### Docker Development Mode
|
||||
|
||||
Command to run docker for development:
|
||||
|
||||
@@ -38,7 +35,7 @@ It will build following images if running first time:
|
||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||
|
||||
|
||||
#### Production
|
||||
### Docker Production Mode
|
||||
|
||||
Command to run docker for production:
|
||||
|
||||
@@ -54,47 +51,45 @@ It will build following images if running first time:
|
||||
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
|
||||
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
|
||||
|
||||
### Using node:
|
||||
## Using NodeJS:
|
||||
|
||||
#### Development (running api and web seperately):
|
||||
Be sure to use v16 or above, and to set your environment variables in the relevant `.env` file(s) - else defaults will be used.
|
||||
|
||||
##### API
|
||||
### NodeJS Development Mode
|
||||
|
||||
Navigate to `./api`
|
||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
SASjs Server is split between an API server (serving REST requests) and a WEB Server (everything else). These can be run together, or on seperate ports.
|
||||
|
||||
### NodeJS Dev - Single Port
|
||||
|
||||
Here the environment variables should be configured under `api.env`. Then:
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build
|
||||
cd ../api && npm i && npm start
|
||||
```
|
||||
|
||||
### NodeJS Dev - Seperate Ports
|
||||
|
||||
Set the backend variables in `api/.env` and the frontend variables in `web/.env`. Then:
|
||||
|
||||
#### API server
|
||||
```
|
||||
cd api
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
##### Web
|
||||
|
||||
Navigate to `./web`
|
||||
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
#### Web Server
|
||||
|
||||
```
|
||||
cd web
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
#### Development (running only api server and have web build served):
|
||||
#### NodeJS Production Mode
|
||||
|
||||
##### API server also serving Web build files
|
||||
|
||||
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
|
||||
Command to install and run api server.
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build && cd ../
|
||||
cd ./api && npm i && npm start
|
||||
```
|
||||
|
||||
#### Production
|
||||
|
||||
##### API & WEB
|
||||
Update the `.env` file in the *api* folder. Then:
|
||||
|
||||
```
|
||||
npm run server
|
||||
@@ -105,7 +100,7 @@ This will install/build `web` and install `api`, then start prod server.
|
||||
|
||||
## Executables
|
||||
|
||||
Command to generate executables
|
||||
In order to generate the final executables:
|
||||
|
||||
```
|
||||
cd ./web && npm i && npm build && cd ../
|
||||
@@ -113,3 +108,7 @@ cd ./api && npm i && npm run exe
|
||||
```
|
||||
|
||||
This will install/build web app and install/create executables of sasjs server at root `./executables`
|
||||
|
||||
## Releases
|
||||
|
||||
To cut a release, run `npm run release` on the main branch, then push the tags (per the console log link)
|
||||
|
||||
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -54,6 +54,7 @@ jobs:
|
||||
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
|
||||
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
|
||||
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
|
||||
SESSION_SECRET: ${{secrets.SESSION_SECRET}}
|
||||
|
||||
- name: Build Package
|
||||
working-directory: ./api
|
||||
|
||||
27
.github/workflows/release.yml
vendored
27
.github/workflows/release.yml
vendored
@@ -2,16 +2,26 @@ name: SASjs Server Executable Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [lts/*]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install Dependencies WEB
|
||||
working-directory: ./web
|
||||
run: npm ci
|
||||
@@ -39,10 +49,11 @@ jobs:
|
||||
zip macos.zip api-macos
|
||||
zip windows.zip api-win.exe
|
||||
|
||||
- name: Install Semantic Release and plugins
|
||||
run: |
|
||||
npm i
|
||||
npm i -g semantic-release
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
./executables/linux.zip
|
||||
./executables/macos.zip
|
||||
./executables/windows.zip
|
||||
run: |
|
||||
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules/
|
||||
.DS_Store
|
||||
.env*
|
||||
sas/
|
||||
sasjs_root/
|
||||
tmp/
|
||||
build/
|
||||
sasjsbuild/
|
||||
@@ -11,3 +12,4 @@ sasjscore/
|
||||
certificates/
|
||||
executables/
|
||||
.env
|
||||
api/csp.config.json
|
||||
|
||||
43
.releaserc
Normal file
43
.releaserc
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"branches": [
|
||||
"main"
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"assets": [
|
||||
{
|
||||
"path": "./executables/linux.zip",
|
||||
"label": "Linux Executable Binary"
|
||||
},
|
||||
{
|
||||
"path": "./executables/macos.zip",
|
||||
"label": "Macos Executable Binary"
|
||||
},
|
||||
{
|
||||
"path": "./executables/windows.zip",
|
||||
"label": "Windows Executable Binary"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/exec",
|
||||
{
|
||||
"publishCmd": "echo 'publish command'"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
476
CHANGELOG.md
476
CHANGELOG.md
@@ -1,6 +1,478 @@
|
||||
# Changelog
|
||||
## [0.3.5](https://github.com/sasjs/server/compare/v0.3.4...v0.3.5) (2022-05-30)
|
||||
|
||||
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
|
||||
|
||||
* bumping sasjs/core library ([61815f8](https://github.com/sasjs/server/commit/61815f8ae18be132e17c199cd8e3afbcc2fa0b60))
|
||||
|
||||
## [0.3.4](https://github.com/sasjs/server/compare/v0.3.3...v0.3.4) (2022-05-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** system username for DESKTOP mode ([a8ba378](https://github.com/sasjs/server/commit/a8ba378fd1ff374ba025a96fdfae5c6c36954465))
|
||||
|
||||
## [0.3.3](https://github.com/sasjs/server/compare/v0.3.2...v0.3.3) (2022-05-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* usage of autoexec API in DESKTOP mode ([12d424a](https://github.com/sasjs/server/commit/12d424acce8108a6f53aefbac01fddcdc5efb48f))
|
||||
|
||||
## [0.3.2](https://github.com/sasjs/server/compare/v0.3.1...v0.3.2) (2022-05-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** ability to use get/patch User API in desktop mode. ([2c259fe](https://github.com/sasjs/server/commit/2c259fe1de95d84e6929e311aaa6b895e66b42a3))
|
||||
|
||||
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** username should be lowercase ([5ad6ee5](https://github.com/sasjs/server/commit/5ad6ee5e0f5d7d6faa45b72215f1d9d55cfc37db))
|
||||
* **web:** reduced width for autoexec input ([7d11cc7](https://github.com/sasjs/server/commit/7d11cc79161e5a07f6c5392d742ef6b9d8658071))
|
||||
|
||||
# [0.3.0](https://github.com/sasjs/server/compare/v0.2.0...v0.3.0) (2022-05-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** added profile + edit + autoexec changes ([c275db1](https://github.com/sasjs/server/commit/c275db184e874f0ee3a4f08f2592cfacf1e90742))
|
||||
|
||||
# [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **autoexec:** usage in case of desktop from file ([79dc2db](https://github.com/sasjs/server/commit/79dc2dba23dc48ec218a973119392a45cb3856b5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** added autoexec + major type setting changes ([2a7223a](https://github.com/sasjs/server/commit/2a7223ad7d6b8f3d4682447fd25d9426a7c79ac3))
|
||||
|
||||
# [0.1.0](https://github.com/sasjs/server/compare/v0.0.77...v0.1.0) (2022-05-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* issue174 + issue175 + issue146 ([80b33c7](https://github.com/sasjs/server/commit/80b33c7a18c1b7727316ffeca71658346733e935))
|
||||
* **web:** click to copy + notification ([f37f8e9](https://github.com/sasjs/server/commit/f37f8e95d1a85e00ceca2413dbb5e1f3f3f72255))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **env:** added new env variable LOG_FORMAT_MORGAN ([53bf68a](https://github.com/sasjs/server/commit/53bf68a6aff44bb7b2f40d40d6554809253a01a8))
|
||||
|
||||
## [0.0.77](https://github.com/sasjs/server/compare/v0.0.76...v0.0.77) (2022-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **release:** Github workflow without npm token ([c017d13](https://github.com/sasjs/server/commit/c017d13061d21aeacd0690367992d12ca57a115b))
|
||||
|
||||
### [0.0.76](https://github.com/sasjs/server/compare/v0.0.75...v0.0.76) (2022-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* get csrf token from cookie if not present in header ([f89389b](https://github.com/sasjs/server/commit/f89389bbc6f1f8f7060db2bdeb89746cbd60f533))
|
||||
|
||||
### [0.0.75](https://github.com/sasjs/server/compare/v0.0.69...v0.0.75) (2022-05-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
|
||||
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
|
||||
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
|
||||
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
|
||||
* moved getAuthCode from api to web routes ([b40de8f](https://github.com/sasjs/server/commit/b40de8fa6a5aa763ed25a6fe6a381e483e0ab824))
|
||||
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
|
||||
* **web:** seperate container for auth code ([5888f04](https://github.com/sasjs/server/commit/5888f04e08a32c6d2c7bcfcbc3a1d32425bff3b3))
|
||||
|
||||
### [0.0.74](https://github.com/sasjs/server/compare/v0.0.73...v0.0.74) (2022-05-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
|
||||
|
||||
### [0.0.73](https://github.com/sasjs/server/compare/v0.0.72...v0.0.73) (2022-05-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
|
||||
|
||||
### [0.0.72](https://github.com/sasjs/server/compare/v0.0.71...v0.0.72) (2022-05-09)
|
||||
|
||||
### [0.0.71](https://github.com/sasjs/server/compare/v0.0.70...v0.0.71) (2022-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
|
||||
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
|
||||
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
|
||||
|
||||
### [0.0.70](https://github.com/sasjs/server/compare/v0.0.69...v0.0.70) (2022-05-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
|
||||
|
||||
### [0.0.69](https://github.com/sasjs/server/compare/v0.0.68...v0.0.69) (2022-05-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **upload:** appStream uses CSRF + Session authentication ([1f89279](https://github.com/sasjs/server/commit/1f8927926405887f3d134c0a1dd6452ffa33876e))
|
||||
|
||||
### [0.0.68](https://github.com/sasjs/server/compare/v0.0.67...v0.0.68) (2022-05-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* using monaco editor locally ([2548c82](https://github.com/sasjs/server/commit/2548c82dfe1149e62a570a00546dddd9e30049b1))
|
||||
|
||||
### [0.0.67](https://github.com/sasjs/server/compare/v0.0.66...v0.0.67) (2022-05-01)
|
||||
|
||||
### [0.0.66](https://github.com/sasjs/server/compare/v0.0.64...v0.0.66) (2022-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added swagger ui init file manually ([e2a97fc](https://github.com/sasjs/server/commit/e2a97fcb7c54a57a7ca118677cfce93fe9430d8f))
|
||||
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
|
||||
|
||||
### [0.0.65](https://github.com/sasjs/server/compare/v0.0.64...v0.0.65) (2022-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
|
||||
|
||||
### [0.0.64](https://github.com/sasjs/server/compare/v0.0.63...v0.0.64) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removed fileExists for serving web ([7b39cc0](https://github.com/sasjs/server/commit/7b39cc06d358f5ffecb87955040c4eb0fcc7469e))
|
||||
|
||||
### [0.0.63](https://github.com/sasjs/server/compare/v0.0.62...v0.0.63) (2022-04-30)
|
||||
|
||||
### [0.0.62](https://github.com/sasjs/server/compare/v0.0.61...v0.0.62) (2022-04-30)
|
||||
|
||||
### [0.0.61](https://github.com/sasjs/server/compare/v0.0.59...v0.0.61) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||
|
||||
### [0.0.60](https://github.com/sasjs/server/compare/v0.0.59...v0.0.60) (2022-04-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
|
||||
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
|
||||
|
||||
### [0.0.59](https://github.com/sasjs/server/compare/v0.0.58...v0.0.59) (2022-04-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* enabled csrf tokens for web component ([e462aeb](https://github.com/sasjs/server/commit/e462aebdc01f3c0068ed0074473a2063412dcf45))
|
||||
* enabled session based authentication for web ([5da93f3](https://github.com/sasjs/server/commit/5da93f318aad10b1c67032a467191e4dbb99f411))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fetch client from DB for each request ([4ad8c81](https://github.com/sasjs/server/commit/4ad8c81e4927c1a82220ec015a781b095c8e859e))
|
||||
* **web:** show display name instead of username ([e57443f](https://github.com/sasjs/server/commit/e57443f1ed662a022494bb93d79c3d2f10a2d082))
|
||||
|
||||
### [0.0.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bumping core library to get latest user management macros ([4862071](https://github.com/sasjs/server/commit/486207128da58fc4866bd0919c1bed2bd98097ea))
|
||||
* missing dependency ([d09876c](https://github.com/sasjs/server/commit/d09876c05f89166eec20064f7aa7ed5b867be081))
|
||||
|
||||
### [0.0.57](https://github.com/sasjs/server/compare/v0.0.56...v0.0.57) (2022-04-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* create AppContext ([84ee743](https://github.com/sasjs/server/commit/84ee743eae16e87eaa91969393bebf01e2d15a44))
|
||||
|
||||
### [0.0.56](https://github.com/sasjs/server/compare/v0.0.55...v0.0.56) (2022-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* shortening min length of username. Closes [#61](https://github.com/sasjs/server/issues/61) ([f02996f](https://github.com/sasjs/server/commit/f02996facf1019ec4022ccfbc99c1d0137074e1b))
|
||||
|
||||
### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||
* drive path in server mode ([c4cea4a](https://github.com/sasjs/server/commit/c4cea4a12b7eda4daeed995f41c0b10bcea79871))
|
||||
|
||||
### [0.0.54](https://github.com/sasjs/server/compare/v0.0.53...v0.0.54) (2022-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
|
||||
|
||||
### [0.0.53](https://github.com/sasjs/server/compare/v0.0.49...v0.0.53) (2022-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* provide clientId to web component ([db70b1c](https://github.com/sasjs/server/commit/db70b1ce555df6b29fb09c0c960d38b911c97b1b))
|
||||
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||
|
||||
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||
|
||||
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||
|
||||
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||
|
||||
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
|
||||
|
||||
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deploy:** new route added for deploy with build.json ([18d0604](https://github.com/sasjs/server/commit/18d0604bdd0b20ad468f9345474b4de034ee3a67))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove uploaded build.json from temp folder in all cases ([9d167ab](https://github.com/sasjs/server/commit/9d167abe2adb743bca161862b4561bf573182c00))
|
||||
* **stp:** return log+webout for debug on ([3ff6f5e](https://github.com/sasjs/server/commit/3ff6f5e86581cd2ac23bbe0b8e2c367fbea890ed))
|
||||
|
||||
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** updated STUDIO log and webout ([f700561](https://github.com/sasjs/server/commit/f700561e1a8d06c18ca2bdbe4605d7ab34f7a761))
|
||||
|
||||
### [0.0.46](https://github.com/sasjs/server/compare/v0.0.45...v0.0.46) (2022-03-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **drive:** GET folder contents API added ([0ac9e4a](https://github.com/sasjs/server/commit/0ac9e4af7d67c4431053e80eb2384bf5bdc3f8b3))
|
||||
|
||||
### [0.0.45](https://github.com/sasjs/server/compare/v0.0.43...v0.0.45) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||
* proving a PRINT destination during SAS invocation. ([7f4201b](https://github.com/sasjs/server/commit/7f4201ba855743144fa6d3efac2b11e816d4696e)), closes [#111](https://github.com/sasjs/server/issues/111)
|
||||
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||
|
||||
### [0.0.44](https://github.com/sasjs/server/compare/v0.0.43...v0.0.44) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||
|
||||
### [0.0.43](https://github.com/sasjs/server/compare/v0.0.42...v0.0.43) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deploy:** user can deploy to same appName with different/same appLoc ([9ace33d](https://github.com/sasjs/server/commit/9ace33d7830a9def42d741c23b46090afe0c5510))
|
||||
* fallback logo on AppStream ([5655311](https://github.com/sasjs/server/commit/5655311b9663225823c192b39a03f39d17dda730))
|
||||
|
||||
### [0.0.42](https://github.com/sasjs/server/compare/v0.0.41...v0.0.42) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* execute api, webout as raw ([9c75187](https://github.com/sasjs/server/commit/9c751877d1ed0d0677aff816169a1df7c34c6bf5))
|
||||
|
||||
### [0.0.41](https://github.com/sasjs/server/compare/v0.0.40...v0.0.41) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **scroll:** closes [#100](https://github.com/sasjs/server/issues/100) ([f4eb75f](https://github.com/sasjs/server/commit/f4eb75ff347e78ac334e55ee26fbdd247bb8eaa2))
|
||||
|
||||
### [0.0.40](https://github.com/sasjs/server/compare/v0.0.39...v0.0.40) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deploy:** validating empty file or service in filetree ([27e260e](https://github.com/sasjs/server/commit/27e260e6a453e9978830db63ab669bd48c029897))
|
||||
* macros available for SAS ([7a70d40](https://github.com/sasjs/server/commit/7a70d40dbf0cd91cb3af156755f10006b860f917))
|
||||
* moved macros from codebase to drive ([d27e070](https://github.com/sasjs/server/commit/d27e070fc83894854278df22a8223b8016a1f5f7))
|
||||
|
||||
### [0.0.39](https://github.com/sasjs/server/compare/v0.0.38...v0.0.39) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* included sasjs core macros at compile time ([e680901](https://github.com/sasjs/server/commit/e68090181acd844f86f3e81153cb5a4e3f4a307f))
|
||||
|
||||
### [0.0.38](https://github.com/sasjs/server/compare/v0.0.37...v0.0.38) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* quick fix for executables ([9e53470](https://github.com/sasjs/server/commit/9e53470947350f4b8d835a2cb6b70e3dabf247c4))
|
||||
|
||||
### [0.0.37](https://github.com/sasjs/server/compare/v0.0.36...v0.0.37) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* appStream html view ([cd00aa2](https://github.com/sasjs/server/commit/cd00aa2af8c7e0df851050a02152dfeddaec7b0f))
|
||||
* moved macros from codebase to drive ([9ac3191](https://github.com/sasjs/server/commit/9ac3191891bf53ff07135ccec6ddc83b34ea871a))
|
||||
* **webin:** closes [#99](https://github.com/sasjs/server/issues/99) ([0147bcb](https://github.com/sasjs/server/commit/0147bcb701a209266144147a3746baf1eb1ccc63))
|
||||
|
||||
### [0.0.36](https://github.com/sasjs/server/compare/v0.0.35...v0.0.36) (2022-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* App Stream, load on startup, new route added ([98a00ec](https://github.com/sasjs/server/commit/98a00ec7ace5da765f049864799be44ba6538e8a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appstream:** app logo + improvements ([df6003d](https://github.com/sasjs/server/commit/df6003df942fd52b956f3d4069d6d7615441d372))
|
||||
|
||||
### [0.0.35](https://github.com/sasjs/server/compare/v0.0.33...v0.0.35) (2022-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cors:** whitelisting is configurable through .env variables ([99f91fb](https://github.com/sasjs/server/commit/99f91fbce2a029dd963ed30c9007a9b046ea6560))
|
||||
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cors:** removed trailing slashes of urls ([4fd5bf9](https://github.com/sasjs/server/commit/4fd5bf948e4ad8a274d3176d5509163e67980061))
|
||||
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
|
||||
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
|
||||
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
|
||||
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
|
||||
|
||||
### [0.0.34](https://github.com/sasjs/server/compare/v0.0.33...v0.0.34) (2022-03-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
|
||||
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
|
||||
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
|
||||
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
|
||||
|
||||
### [0.0.33](https://github.com/sasjs/server/compare/v0.0.32...v0.0.33) (2022-03-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* serve deployed streaming apps ([d6fa877](https://github.com/sasjs/server/commit/d6fa87794155880adc23c2552c37c86ad606c292))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adde validation + code improvement ([1ff6965](https://github.com/sasjs/server/commit/1ff6965dd2f44ad74136af04b4fba8c76979ecba))
|
||||
* added api button on web component ([6b708fc](https://github.com/sasjs/server/commit/6b708fcad30d92c21713f9c97bca173c148cc875))
|
||||
|
||||
### [0.0.32](https://github.com/sasjs/server/compare/v0.0.31...v0.0.32) (2022-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** added delete option in Drive ([7a6e6c8](https://github.com/sasjs/server/commit/7a6e6c8becab31410d0a36bcc22e13d5359a6cdf))
|
||||
|
||||
### [0.0.31](https://github.com/sasjs/server/compare/v0.0.30...v0.0.31) (2022-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **drive:** new route delete file api ([3d583ff](https://github.com/sasjs/server/commit/3d583ff21d344a71aa861c7e5b1426ebc2d54c22))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added cookie for accessToken ([698180a](https://github.com/sasjs/server/commit/698180ab7e44d67d46c84352ececca5b6c83b230))
|
||||
* **drive:** update file API is same as create file ([7072e28](https://github.com/sasjs/server/commit/7072e282b1cd1a296d81512c57130237610c1c1e))
|
||||
* show content of get file api ([6ab42ca](https://github.com/sasjs/server/commit/6ab42ca4868366874f5f21bd711b7b8b72e36774))
|
||||
* **stp:** return plain/text header for GET & debug ([145ac45](https://github.com/sasjs/server/commit/145ac450365ed39279248ec9321bbe4918bee9fa))
|
||||
|
||||
### [0.0.30](https://github.com/sasjs/server/compare/v0.0.29...v0.0.30) (2022-03-05)
|
||||
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 SASjs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
182
README.md
182
README.md
@@ -1,54 +1,21 @@
|
||||
# SASjs Server
|
||||
|
||||
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or it could even run locally on your desktop. It provides the following functionality:
|
||||
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:
|
||||
|
||||
- Virtual filesystem for storing SAS programs and other content
|
||||
- Ability to execute Stored Programs from a URL
|
||||
- Ability to create web apps using simple Desktop SAS
|
||||
- REST API with Swagger Docs
|
||||
|
||||
One major benefit of using SASjs Server (alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library) is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
|
||||
One major benefit of using SASjs Server alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library, is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
|
||||
|
||||
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentiation, and a database)
|
||||
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentication, and a database)
|
||||
|
||||
## Installation
|
||||
|
||||
## Configuration
|
||||
Installation can be made programmatically using command line, or by manually downloading and running the executable.
|
||||
|
||||
When launching the app, it will make use of specific environment variables. These can be set in the following places:
|
||||
|
||||
- Configured globally in /etc/environment file
|
||||
- Export in terminal or shell script (`export VAR=VALUE`)
|
||||
- Prepend in command
|
||||
- Enter in the `.env` file alongside the executable
|
||||
|
||||
Example variables:
|
||||
|
||||
```
|
||||
MODE=[desktop|server] default considered as desktop
|
||||
CORS=[disable|enable] default considered as disable
|
||||
PROTOCOL=[http|https] default considered as http
|
||||
PORT=[5000] default value is 5000
|
||||
PORT_WEB=[port for sasjs web component(react)] default value is 3000
|
||||
SAS_PATH=/path/to/sas/executable.exe
|
||||
DRIVE_PATH=./tmp
|
||||
PROTOCOL=[http|https] default considered as http. Use pems below if htttps.
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
```
|
||||
|
||||
## Desktop Version
|
||||
|
||||
### Manual Installation
|
||||
|
||||
Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
|
||||
|
||||
Next, trigger by double clicking (windows) or executing from commandline.
|
||||
|
||||
You are presented with two prompts (if not set as ENV vars):
|
||||
|
||||
- Location of your `sas.exe` / `sas.sh` executable
|
||||
- Path to a filesystem location for Stored Programs and temporary files
|
||||
|
||||
## Programmatic Installation
|
||||
### Programmatic
|
||||
|
||||
Fetch the relevant package from github using `curl`, eg as follows (for linux):
|
||||
|
||||
@@ -59,17 +26,137 @@ unzip linux.zip
|
||||
|
||||
The app can then be launched with `./api-linux` and prompts followed (if ENV vars not set).
|
||||
|
||||
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:
|
||||
### Manual
|
||||
|
||||
1. Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
|
||||
2. Trigger by double clicking (windows) or executing from commandline.
|
||||
|
||||
You are presented with two prompts (if not set as ENV vars):
|
||||
|
||||
- Location of your `sas.exe` / `sas.sh` executable
|
||||
- Path to a filesystem location for Stored Programs and temporary files
|
||||
|
||||
## ENV Var configuration
|
||||
|
||||
When launching the app, it will make use of specific environment variables. These can be set in the following places:
|
||||
|
||||
- Configured globally in `/etc/environment` file
|
||||
- Export in terminal or shell script (`export VAR=VALUE`)
|
||||
- Prepended in the command
|
||||
- Enter in the `.env` file alongside the executable
|
||||
|
||||
Example contents of a `.env` file:
|
||||
|
||||
```
|
||||
#
|
||||
## Core Settings
|
||||
#
|
||||
|
||||
|
||||
# MODE options: [desktop|server] default: `desktop`
|
||||
# Desktop mode is single user and designed for workstation use
|
||||
# Server mode is multi-user and suitable for intranet / internet use
|
||||
MODE=
|
||||
|
||||
# Path to SAS executable (sas.exe / sas.sh)
|
||||
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
|
||||
|
||||
# options: [http|https] default: http
|
||||
PROTOCOL=
|
||||
|
||||
# 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`
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
|
||||
# ENV variables required for MODE: `server`
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
SESSION_SECRET=<secret>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
# 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
|
||||
|
||||
Normally the server process will stop when your terminal dies. To keep it going you can use the following suggested approaches:
|
||||
|
||||
1. Linux Background Job
|
||||
2. NPM package `pm2`
|
||||
|
||||
### Background Job
|
||||
|
||||
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
|
||||
|
||||
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
|
||||
|
||||
### PM2
|
||||
|
||||
Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) and execute, eg as follows:
|
||||
|
||||
```bash
|
||||
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||
export PORT=5001
|
||||
export DRIVE_PATH=./tmp
|
||||
export SASJS_ROOT=./sasjs_root
|
||||
|
||||
pm2 start api-linux
|
||||
```
|
||||
|
||||
To get the logs (and some usefull commands):
|
||||
To get the logs (and some useful commands):
|
||||
|
||||
```bash
|
||||
pm2 [list|ls|status]
|
||||
@@ -91,11 +178,10 @@ Instead of `app_name` you can pass:
|
||||
- `all` to act on all processes
|
||||
- `id` to act on a specific process id
|
||||
|
||||
|
||||
## Server Version
|
||||
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is highly recommended to change these on first use.
|
||||
|
||||
* CLIENTID: `clientID1`
|
||||
* USERNAME: `secretuser`
|
||||
* PASSWORD: `secretpassword`
|
||||
- CLIENTID: `clientID1`
|
||||
- USERNAME: `secretuser`
|
||||
- PASSWORD: `secretpassword`
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
MODE=[desktop|server] default considered as desktop
|
||||
CORS=[disable|enable] default considered as disable
|
||||
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
|
||||
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
||||
|
||||
PROTOCOL=[http|https] default considered as http
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
|
||||
PORT=[5000] default value is 5000
|
||||
PORT_WEB=[port for sasjs web component(react)] default value is 3000
|
||||
|
||||
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
|
||||
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
||||
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
SESSION_SECRET=<secret>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||
DRIVE_PATH=./tmp
|
||||
SASJS_ROOT=./sasjs_root
|
||||
|
||||
LOG_FORMAT_MORGAN=common
|
||||
1
api/.nvmrc
Normal file
1
api/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v16.14.0
|
||||
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'"]
|
||||
}
|
||||
1329
api/package-lock.json
generated
1329
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,15 +8,16 @@
|
||||
"prestart": "npm run initial",
|
||||
"prebuild": "npm run initial",
|
||||
"start": "nodemon ./src/server.ts",
|
||||
"start:prod": "node ./build/src/server.js",
|
||||
"build": "rimraf build && tsc",
|
||||
"postbuild": "npm run copy:files",
|
||||
"swagger": "tsoa spec",
|
||||
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
|
||||
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --silent --coverage",
|
||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"exe": "npm run build && npm run exe:copy && pkg .",
|
||||
"exe:copy": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
||||
"exe": "npm run build && pkg .",
|
||||
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
||||
"public:copy": "cp -r ./public/ ./build/public/",
|
||||
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
||||
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
|
||||
@@ -29,6 +30,7 @@
|
||||
"assets": [
|
||||
"./build/public/**/*",
|
||||
"./build/sasjsbuild/**/*",
|
||||
"./build/sasjscore/**/*",
|
||||
"./web/build/**/*"
|
||||
],
|
||||
"targets": [
|
||||
@@ -45,24 +47,31 @@
|
||||
},
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "4.9.0",
|
||||
"@sasjs/utils": "2.34.1",
|
||||
"@sasjs/core": "^4.27.3",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"csurf": "^1.11.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"helmet": "^5.0.2",
|
||||
"joi": "^17.4.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.3",
|
||||
"swagger-ui-express": "^4.1.6",
|
||||
"tsoa": "3.14.1"
|
||||
"swagger-ui-express": "4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/csurf": "^1.11.2",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
@@ -76,15 +85,18 @@
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "^8.0.0",
|
||||
"nodemon": "^2.0.7",
|
||||
"pkg": "^5.4.1",
|
||||
"pkg": "5.6.0",
|
||||
"prettier": "^2.3.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsoa": "3.14.1",
|
||||
"typescript": "^4.3.2"
|
||||
},
|
||||
"configuration": {
|
||||
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"sasjs_root/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
50
api/public/SASjsApi/swagger-ui-init.js
Normal file
50
api/public/SASjsApi/swagger-ui-init.js
Normal file
@@ -0,0 +1,50 @@
|
||||
window.onload = function () {
|
||||
// Build a system
|
||||
var url = window.location.search.match(/url=([^&]+)/)
|
||||
if (url && url.length > 1) {
|
||||
url = decodeURIComponent(url[1])
|
||||
} else {
|
||||
url = window.location.origin
|
||||
}
|
||||
var options = {
|
||||
customOptions: {
|
||||
url: '/swagger.yaml',
|
||||
requestInterceptor: function (request) {
|
||||
request.credentials = 'include'
|
||||
var cookie = document.cookie
|
||||
var startIndex = cookie.indexOf('XSRF-TOKEN')
|
||||
var csrf = cookie.slice(startIndex + 11).split('; ')[0]
|
||||
request.headers['X-XSRF-TOKEN'] = csrf
|
||||
return request
|
||||
}
|
||||
}
|
||||
}
|
||||
url = options.swaggerUrl || url
|
||||
var urls = options.swaggerUrls
|
||||
var customOptions = options.customOptions
|
||||
var spec1 = options.swaggerDoc
|
||||
var swaggerOptions = {
|
||||
spec: spec1,
|
||||
url: url,
|
||||
urls: urls,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||||
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
|
||||
layout: 'StandaloneLayout'
|
||||
}
|
||||
for (var attrname in customOptions) {
|
||||
swaggerOptions[attrname] = customOptions[attrname]
|
||||
}
|
||||
var ui = SwaggerUIBundle(swaggerOptions)
|
||||
|
||||
if (customOptions.oauth) {
|
||||
ui.initOAuth(customOptions.oauth)
|
||||
}
|
||||
|
||||
if (customOptions.authAction) {
|
||||
ui.authActions.authorize(customOptions.authAction)
|
||||
}
|
||||
|
||||
window.ui = ui
|
||||
}
|
||||
49
api/public/app-streams-script.js
Normal file
49
api/public/app-streams-script.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const inputElement = document.getElementById('fileId')
|
||||
|
||||
document.getElementById('uploadButton').addEventListener('click', function () {
|
||||
inputElement.click()
|
||||
})
|
||||
|
||||
inputElement.addEventListener(
|
||||
'change',
|
||||
function () {
|
||||
const fileList = this.files /* now you can work with the file list */
|
||||
|
||||
updateFileUploadMessage('Requesting ...')
|
||||
|
||||
const file = fileList[0]
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('file', file)
|
||||
|
||||
axios
|
||||
.post('/SASjsApi/drive/deploy/upload', formData)
|
||||
.then((res) => res.data)
|
||||
.then((data) => {
|
||||
return (
|
||||
data.message +
|
||||
'\nstreamServiceName: ' +
|
||||
data.streamServiceName +
|
||||
'\nrefreshing page once alert box closes.'
|
||||
)
|
||||
})
|
||||
.then((message) => {
|
||||
alert(message)
|
||||
location.reload()
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(error.response.data)
|
||||
resetFileUpload()
|
||||
updateFileUploadMessage('Upload New App')
|
||||
})
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
function updateFileUploadMessage(message) {
|
||||
document.getElementById('uploadMessage').innerHTML = message
|
||||
}
|
||||
|
||||
function resetFileUpload() {
|
||||
inputElement.value = null
|
||||
}
|
||||
3
api/public/axios.min.js
vendored
Normal file
3
api/public/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
api/public/plus.png
Normal file
BIN
api/public/plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 899 B |
21
api/public/sasjs-logo.svg
Normal file
21
api/public/sasjs-logo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F6E40C;}
|
||||
</style>
|
||||
<rect id="XMLID_1_" width="32" height="32"/>
|
||||
<g id="XMLID_654_">
|
||||
<path id="XMLID_656_" class="st0" d="M27.9,17.4c-1.1,0-2.1,0-3,0c-1.2,0-2.3,0-3.5,0c-0.5,0-0.7,0.2-0.6,0.7c0,2.1,0,4.3,0,6.4
|
||||
c0,0.5-0.2,0.8-0.6,1c-2.5,1.4-4.9,2.8-7.3,4.3c-0.4,0.2-0.6,0.2-1,0c-2.4-1.4-4.9-2.9-7.3-4.3c-0.2-0.1-0.5-0.5-0.5-0.7
|
||||
c0-3.2,0-6.4,0-9.6c0-0.1,0-0.1,0.1-0.3c0.3,0,0.5,0,0.8,0c1.9,0,3.7,0,5.6,0c0.6,0,0.7-0.2,0.7-0.7c0-2.1,0-4.2,0-6.4
|
||||
c0-0.5,0.1-0.8,0.6-1.1c2.5-1.4,4.9-2.9,7.3-4.3c0.2-0.1,0.6-0.1,0.9,0c2.5,1.4,5,2.9,7.5,4.4c0.2,0.1,0.4,0.4,0.4,0.6
|
||||
C27.9,10.6,27.9,13.9,27.9,17.4z M20.8,14.8c1.4,0,2.7,0,4,0c0.5,0,0.7-0.2,0.7-0.7c0-1.7,0-3.3,0-5c0-0.5-0.2-0.7-0.6-1
|
||||
c-1.6-0.9-3.2-1.9-4.8-2.8c-0.2-0.1-0.7-0.1-0.9,0c-1.6,0.9-3.2,1.9-4.8,2.8c-0.4,0.2-0.6,0.5-0.6,1c0,3.2,0,6.3,0,9.5
|
||||
c0,1.9,0,1.9-1.9,1.9c-0.4,0-0.6-0.1-0.6-0.6c0-0.6,0-1.3,0-1.9c0-0.5-0.2-0.6-0.6-0.6c-1.1,0-2.2,0-3.3,0c-0.5,0-0.7,0.2-0.7,0.7
|
||||
c0,1.6,0,3.3,0,4.9c0,0.5,0.2,0.8,0.6,1c1.6,0.9,3.2,1.9,4.8,2.8c0.2,0.1,0.7,0.1,0.9,0c1.6-0.9,3.2-1.9,4.8-2.8
|
||||
c0.4-0.2,0.6-0.5,0.6-1c0-3.1,0-6.1,0-9.2c0-1.9,0-1.9,1.9-1.9c0.5,0,0.7,0.2,0.7,0.7C20.8,13.3,20.8,14,20.8,14.8z"/>
|
||||
<path id="XMLID_655_" class="st0" d="M18,2.1l-6.8,3.9V2.7c0-0.3,0.3-0.6,0.6-0.6H18z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -5,36 +5,6 @@ components:
|
||||
requestBodies: {}
|
||||
responses: {}
|
||||
schemas:
|
||||
AuthorizeResponse:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 'Authorization code'
|
||||
example: someRandomCryptoString
|
||||
required:
|
||||
- code
|
||||
type: object
|
||||
additionalProperties: false
|
||||
AuthorizePayload:
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: 'Username for user'
|
||||
example: secretuser
|
||||
password:
|
||||
type: string
|
||||
description: 'Password for user'
|
||||
example: secretpassword
|
||||
clientId:
|
||||
type: string
|
||||
description: 'Client ID'
|
||||
example: clientID1
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
- clientId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
TokenResponse:
|
||||
properties:
|
||||
accessToken:
|
||||
@@ -77,6 +47,41 @@ components:
|
||||
- userId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
LoginPayload:
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: 'Username for user'
|
||||
example: secretuser
|
||||
password:
|
||||
type: string
|
||||
description: 'Password for user'
|
||||
example: secretpassword
|
||||
required:
|
||||
- username
|
||||
- password
|
||||
type: object
|
||||
additionalProperties: false
|
||||
AuthorizeResponse:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 'Authorization code'
|
||||
example: someRandomCryptoString
|
||||
required:
|
||||
- code
|
||||
type: object
|
||||
additionalProperties: false
|
||||
AuthorizePayload:
|
||||
properties:
|
||||
clientId:
|
||||
type: string
|
||||
description: 'Client ID'
|
||||
example: clientID1
|
||||
required:
|
||||
- clientId
|
||||
type: object
|
||||
additionalProperties: false
|
||||
ClientPayload:
|
||||
properties:
|
||||
clientId:
|
||||
@@ -161,6 +166,8 @@ components:
|
||||
$ref: '#/components/schemas/FolderMember'
|
||||
-
|
||||
$ref: '#/components/schemas/ServiceMember'
|
||||
-
|
||||
$ref: '#/components/schemas/FileMember'
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
@@ -186,6 +193,24 @@ components:
|
||||
- code
|
||||
type: object
|
||||
additionalProperties: false
|
||||
MemberType.file:
|
||||
enum:
|
||||
- file
|
||||
type: string
|
||||
FileMember:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/MemberType.file'
|
||||
code:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
- code
|
||||
type: object
|
||||
additionalProperties: false
|
||||
FileTree:
|
||||
properties:
|
||||
members:
|
||||
@@ -195,6 +220,8 @@ components:
|
||||
$ref: '#/components/schemas/FolderMember'
|
||||
-
|
||||
$ref: '#/components/schemas/ServiceMember'
|
||||
-
|
||||
$ref: '#/components/schemas/FileMember'
|
||||
type: array
|
||||
required:
|
||||
- members
|
||||
@@ -206,6 +233,8 @@ components:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
streamServiceName:
|
||||
type: string
|
||||
example:
|
||||
$ref: '#/components/schemas/FileTree'
|
||||
required:
|
||||
@@ -217,9 +246,12 @@ components:
|
||||
properties:
|
||||
appLoc:
|
||||
type: string
|
||||
streamWebFolder:
|
||||
type: string
|
||||
fileTree:
|
||||
$ref: '#/components/schemas/FileTree'
|
||||
required:
|
||||
- appLoc
|
||||
- fileTree
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -233,21 +265,6 @@ components:
|
||||
- status
|
||||
type: object
|
||||
additionalProperties: false
|
||||
FilePayload:
|
||||
properties:
|
||||
filePath:
|
||||
type: string
|
||||
description: 'Path of the file'
|
||||
example: /Public/somefolder/some.file
|
||||
fileContent:
|
||||
type: string
|
||||
description: 'Contents of the file'
|
||||
example: 'Contents of the File'
|
||||
required:
|
||||
- filePath
|
||||
- fileContent
|
||||
type: object
|
||||
additionalProperties: false
|
||||
TreeNode:
|
||||
properties:
|
||||
name:
|
||||
@@ -306,6 +323,8 @@ components:
|
||||
type: boolean
|
||||
isAdmin:
|
||||
type: boolean
|
||||
autoExec:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- displayName
|
||||
@@ -335,6 +354,10 @@ components:
|
||||
type: boolean
|
||||
description: 'Account should be active or not, defaults to true'
|
||||
example: 'true'
|
||||
autoExec:
|
||||
type: string
|
||||
description: 'User-specific auto-exec code'
|
||||
example: ""
|
||||
required:
|
||||
- displayName
|
||||
- username
|
||||
@@ -398,6 +421,25 @@ components:
|
||||
- description
|
||||
type: object
|
||||
additionalProperties: false
|
||||
InfoResponse:
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
cors:
|
||||
type: string
|
||||
whiteList:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
protocol:
|
||||
type: string
|
||||
required:
|
||||
- mode
|
||||
- cors
|
||||
- whiteList
|
||||
- protocol
|
||||
type: object
|
||||
additionalProperties: false
|
||||
ExecuteReturnJsonPayload:
|
||||
properties:
|
||||
_program:
|
||||
@@ -419,30 +461,6 @@ info:
|
||||
name: '4GL Ltd'
|
||||
openapi: 3.0.0
|
||||
paths:
|
||||
/SASjsApi/auth/authorize:
|
||||
post:
|
||||
operationId: Authorize
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizeResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {code: someRandomCryptoString}
|
||||
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
||||
tags:
|
||||
- Auth
|
||||
security: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizePayload'
|
||||
/SASjsApi/auth/token:
|
||||
post:
|
||||
operationId: Token
|
||||
@@ -500,6 +518,86 @@ paths:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
/:
|
||||
get:
|
||||
operationId: Home
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
summary: 'Render index.html'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
/SASLogon/login:
|
||||
post:
|
||||
operationId: Login
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
|
||||
loggedIn: {type: boolean}
|
||||
required:
|
||||
- user
|
||||
- loggedIn
|
||||
type: object
|
||||
summary: 'Accept a valid username/password'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginPayload'
|
||||
/SASLogon/authorize:
|
||||
post:
|
||||
operationId: Authorize
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizeResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {code: someRandomCryptoString}
|
||||
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthorizePayload'
|
||||
/logout:
|
||||
get:
|
||||
operationId: Logout
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
summary: 'Accept a valid username/password'
|
||||
tags:
|
||||
- Web
|
||||
security: []
|
||||
parameters: []
|
||||
/SASjsApi/client:
|
||||
post:
|
||||
operationId: CreateClient
|
||||
@@ -594,6 +692,56 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployPayload'
|
||||
/SASjsApi/drive/deploy/upload:
|
||||
post:
|
||||
operationId: DeployUpload
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'}
|
||||
'400':
|
||||
description: 'Invalid Format'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'Provided not supported data format.'}
|
||||
'500':
|
||||
description: 'Execution Error'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'Deployment failed!'}
|
||||
summary: 'Creates/updates files within SASjs Drive using uploaded JSON file.'
|
||||
tags:
|
||||
- Drive
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
required:
|
||||
- file
|
||||
/SASjsApi/drive/file:
|
||||
get:
|
||||
operationId: GetFile
|
||||
@@ -609,7 +757,34 @@ paths:
|
||||
parameters:
|
||||
-
|
||||
in: query
|
||||
name: filePath
|
||||
name: _filePath
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder/some.file
|
||||
delete:
|
||||
operationId: DeleteFile
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status: {type: string}
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
summary: 'Delete file from SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
in: query
|
||||
name: _filePath
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@@ -626,7 +801,7 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: success}
|
||||
'400':
|
||||
'403':
|
||||
description: 'File already exists'
|
||||
content:
|
||||
application/json:
|
||||
@@ -635,7 +810,7 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'File request failed.'}
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provided else API will respond with Bad Request."
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
|
||||
summary: 'Create a file in SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
@@ -677,8 +852,8 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: success}
|
||||
'400':
|
||||
description: 'Unable to get File'
|
||||
'403':
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -686,19 +861,66 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'File request failed.'}
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
|
||||
summary: 'Modify a file in SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
parameters:
|
||||
-
|
||||
description: 'Location of SAS program'
|
||||
in: query
|
||||
name: _filePath
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder/some.file.sas
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FilePayload'
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
filePath:
|
||||
type: string
|
||||
required:
|
||||
- file
|
||||
/SASjsApi/drive/folder:
|
||||
get:
|
||||
operationId: GetFolder
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
folders: {items: {type: string}, type: array}
|
||||
files: {items: {type: string}, type: array}
|
||||
required:
|
||||
- folders
|
||||
- files
|
||||
type: object
|
||||
summary: 'Get folder contents from SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
in: query
|
||||
name: _folderPath
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder
|
||||
/SASjsApi/drive/filetree:
|
||||
get:
|
||||
operationId: GetFileTree
|
||||
@@ -773,6 +995,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserDetailsResponse'
|
||||
description: 'Only Admin or user itself will get user autoExec code.'
|
||||
summary: 'Get user properties - such as group memberships, userName, displayName.'
|
||||
tags:
|
||||
- User
|
||||
@@ -1022,6 +1245,24 @@ paths:
|
||||
format: double
|
||||
type: number
|
||||
example: '6789'
|
||||
/SASjsApi/info:
|
||||
get:
|
||||
operationId: Info
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InfoResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http}
|
||||
summary: 'Get server info (mode, cors, whiteList, protocol).'
|
||||
tags:
|
||||
- Info
|
||||
security: []
|
||||
parameters: []
|
||||
/SASjsApi/session:
|
||||
get:
|
||||
operationId: Session
|
||||
@@ -1054,7 +1295,7 @@ paths:
|
||||
anyOf:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. Setting _debug=131 will\ncause the log to be streamed in the output.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response headers can be adjusted using the mfs_httpheader() macro. Any\nfile type can be returned, including binary files such as zip or xls.\n\nThis behaviour differs for POST requests, in which case the reponse is\nalways JSON."
|
||||
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. Setting _debug=131 will\ncause the log to be streamed in the output.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response headers can be adjusted using the mfs_httpheader() macro. Any\nfile type can be returned, including binary files such as zip or xls.\n\nIf _debug is >= 131, response headers will contain Content-Type: 'text/plain'\n\nThis behaviour differs for POST requests, in which case the response is\nalways JSON."
|
||||
summary: 'Execute Stored Program, return raw _webout content.'
|
||||
tags:
|
||||
- STP
|
||||
@@ -1108,6 +1349,9 @@ servers:
|
||||
-
|
||||
url: /
|
||||
tags:
|
||||
-
|
||||
name: Info
|
||||
description: 'Get Server Info'
|
||||
-
|
||||
name: Session
|
||||
description: 'Get Session information'
|
||||
@@ -1132,3 +1376,6 @@ tags:
|
||||
-
|
||||
name: CODE
|
||||
description: 'Operations on SAS code'
|
||||
-
|
||||
name: Web
|
||||
description: 'Operations on Web'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
CompileTree,
|
||||
createFile,
|
||||
loadDependenciesFile,
|
||||
readFile,
|
||||
@@ -17,7 +18,9 @@ const compiledSystemInit = async (systemInit: string) =>
|
||||
programFolders: [],
|
||||
macroFolders: [],
|
||||
buildSourceFolder: '',
|
||||
macroCorePath
|
||||
binaryFolders: [],
|
||||
macroCorePath,
|
||||
compileTree: new CompileTree('') // dummy compileTree
|
||||
}))
|
||||
|
||||
const createSysInitFile = async () => {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
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')
|
||||
|
||||
@@ -9,8 +16,6 @@ export const copySASjsCore = async () => {
|
||||
await deleteFolder(sasJSCoreMacros)
|
||||
await createFolder(sasJSCoreMacros)
|
||||
|
||||
console.log('Copying SASjs Core Macros...')
|
||||
|
||||
const foldersToCopy = ['base', 'ddl', 'fcmp', 'lua', 'server']
|
||||
|
||||
await asyncForEach(foldersToCopy, async (coreSubFolder) => {
|
||||
@@ -19,7 +24,9 @@ export const copySASjsCore = async () => {
|
||||
await copy(coreSubFolderPath, sasJSCoreMacros)
|
||||
})
|
||||
|
||||
console.log('Macros available at: ', sasJSCoreMacros)
|
||||
const fileNames = await listFilesInFolder(sasJSCoreMacros)
|
||||
|
||||
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
|
||||
}
|
||||
|
||||
copySASjsCore()
|
||||
|
||||
@@ -5,23 +5,12 @@
|
||||
_before_ any user-provided content.
|
||||
|
||||
A number of useful CORE macros are also compiled below, so that they can be
|
||||
available "out of the box".
|
||||
available by default for Stored Programs.
|
||||
|
||||
Note that the full CORE library is available to sessions in SASjs Studio.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mcf_stpsrv_header.sas
|
||||
@li mf_getuser.sas
|
||||
@li mf_getvarlist.sas
|
||||
@li mf_mkdir.sas
|
||||
@li mf_nobs.sas
|
||||
@li mf_uid.sas
|
||||
@li mfs_httpheader.sas
|
||||
@li mp_dirlist.sas
|
||||
@li mp_ds2ddl.sas
|
||||
@li mp_ds2md.sas
|
||||
@li mp_getdbml.sas
|
||||
@li mp_init.sas
|
||||
@li mp_makedata.sas
|
||||
@li mp_zip.sas
|
||||
|
||||
@li ms_webout.sas
|
||||
**/
|
||||
|
||||
|
||||
136
api/src/app.ts
136
api/src/app.ts
@@ -1,44 +1,156 @@
|
||||
import path from 'path'
|
||||
import express, { ErrorRequestHandler } from 'express'
|
||||
import csrf from 'csurf'
|
||||
import session from 'express-session'
|
||||
import MongoStore from 'connect-mongo'
|
||||
import morgan from 'morgan'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import dotenv from 'dotenv'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import { connectDB, getWebBuildFolderPath, setProcessVariables } from './utils'
|
||||
import {
|
||||
connectDB,
|
||||
copySASjsCore,
|
||||
CorsType,
|
||||
getWebBuildFolder,
|
||||
HelmetCoepType,
|
||||
instantiateLogger,
|
||||
loadAppStreamConfig,
|
||||
ModeType,
|
||||
ProtocolType,
|
||||
ReturnCode,
|
||||
setProcessVariables,
|
||||
setupFolders,
|
||||
verifyEnvVariables
|
||||
} from './utils'
|
||||
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
instantiateLogger()
|
||||
|
||||
if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
||||
|
||||
const app = express()
|
||||
|
||||
const { MODE, CORS, PORT_WEB } = process.env
|
||||
const whiteList = [
|
||||
`http://localhost:${PORT_WEB ?? 3000}`,
|
||||
'https://sas.analytium.co.uk:8343'
|
||||
]
|
||||
app.use(cookieParser())
|
||||
|
||||
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
||||
console.log('All CORS Requests are enabled')
|
||||
const {
|
||||
MODE,
|
||||
CORS,
|
||||
WHITELIST,
|
||||
PROTOCOL,
|
||||
HELMET_CSP_CONFIG_PATH,
|
||||
HELMET_COEP,
|
||||
LOG_FORMAT_MORGAN
|
||||
} = process.env
|
||||
|
||||
app.use(morgan(LOG_FORMAT_MORGAN as string))
|
||||
|
||||
export const cookieOptions = {
|
||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
|
||||
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
||||
HELMET_CSP_CONFIG_PATH
|
||||
)
|
||||
if (PROTOCOL === ProtocolType.HTTP)
|
||||
cspConfigJson['upgrade-insecure-requests'] = null
|
||||
|
||||
/***********************************
|
||||
* CSRF Protection *
|
||||
***********************************/
|
||||
export const csrfProtection = csrf({ cookie: cookieOptions })
|
||||
|
||||
/***********************************
|
||||
* Handle security and origin *
|
||||
***********************************/
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
...helmet.contentSecurityPolicy.getDefaultDirectives(),
|
||||
...cspConfigJson
|
||||
}
|
||||
},
|
||||
crossOriginEmbedderPolicy: HELMET_COEP === HelmetCoepType.TRUE
|
||||
})
|
||||
)
|
||||
|
||||
/***********************************
|
||||
* Enabling CORS *
|
||||
***********************************/
|
||||
if (CORS === CorsType.ENABLED) {
|
||||
const whiteList: string[] = []
|
||||
WHITELIST?.split(' ')
|
||||
?.filter((url) => !!url)
|
||||
.forEach((url) => {
|
||||
if (url.startsWith('http'))
|
||||
// removing trailing slash of URLs listing for CORS
|
||||
whiteList.push(url.replace(/\/$/, ''))
|
||||
})
|
||||
|
||||
console.log('All CORS Requests are enabled for:', whiteList)
|
||||
app.use(cors({ credentials: true, origin: whiteList }))
|
||||
}
|
||||
|
||||
app.use(express.json({ limit: '50mb' }))
|
||||
app.use(morgan('tiny'))
|
||||
/***********************************
|
||||
* DB Connection & *
|
||||
* Express Sessions *
|
||||
* With Mongo Store *
|
||||
***********************************/
|
||||
if (MODE === ModeType.Server) {
|
||||
let store: MongoStore | undefined
|
||||
|
||||
// NOTE: when exporting app.js as agent for supertest
|
||||
// we should exclude connecting to the real database
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
|
||||
|
||||
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
|
||||
}
|
||||
|
||||
app.use(
|
||||
session({
|
||||
secret: process.env.SESSION_SECRET as string,
|
||||
saveUninitialized: false, // don't create session until something stored
|
||||
resave: false, //don't save session if unmodified
|
||||
store,
|
||||
cookie: cookieOptions
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
app.use(express.json({ limit: '100mb' }))
|
||||
app.use(express.static(path.join(__dirname, '../public')))
|
||||
app.use(express.static(getWebBuildFolderPath()))
|
||||
|
||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
if (err.code === 'EBADCSRFTOKEN')
|
||||
return res.status(400).send('Invalid CSRF token!')
|
||||
|
||||
console.error(err.stack)
|
||||
res.status(500).send('Something broke!')
|
||||
}
|
||||
|
||||
export default setProcessVariables().then(async () => {
|
||||
await setupFolders()
|
||||
await copySASjsCore()
|
||||
|
||||
// loading these modules after setting up variables due to
|
||||
// multer's usage of process var process.driveLoc
|
||||
const { setupRoutes } = await import('./routes/setupRoutes')
|
||||
setupRoutes(app)
|
||||
|
||||
await loadAppStreamConfig()
|
||||
|
||||
// should be served after setting up web route
|
||||
// index.html needs to be injected with some js script.
|
||||
app.use(express.static(getWebBuildFolder()))
|
||||
|
||||
app.use(onError)
|
||||
|
||||
await connectDB()
|
||||
return app
|
||||
})
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import User from '../model/User'
|
||||
import { InfoJWT } from '../types'
|
||||
import {
|
||||
generateAccessToken,
|
||||
generateAuthCode,
|
||||
generateRefreshToken,
|
||||
removeTokensInDB,
|
||||
saveTokensInDB
|
||||
@@ -24,20 +22,6 @@ export class AuthController {
|
||||
static deleteCode = (userId: number, clientId: string) =>
|
||||
delete AuthController.authCodes[userId][clientId]
|
||||
|
||||
/**
|
||||
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
|
||||
*
|
||||
*/
|
||||
@Example<AuthorizeResponse>({
|
||||
code: 'someRandomCryptoString'
|
||||
})
|
||||
@Post('/authorize')
|
||||
public async authorize(
|
||||
@Body() body: AuthorizePayload
|
||||
): Promise<AuthorizeResponse> {
|
||||
return authorize(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Accepts client/auth code and returns access/refresh tokens
|
||||
*
|
||||
@@ -78,30 +62,6 @@ export class AuthController {
|
||||
}
|
||||
}
|
||||
|
||||
const authorize = async (data: any): Promise<AuthorizeResponse> => {
|
||||
const { username, password, clientId } = data
|
||||
|
||||
// Authenticate User
|
||||
const user = await User.findOne({ username })
|
||||
if (!user) throw new Error('Username is not found.')
|
||||
|
||||
const validPass = user.comparePassword(password)
|
||||
if (!validPass) throw new Error('Invalid password.')
|
||||
|
||||
// generate authorization code against clientId
|
||||
const userInfo: InfoJWT = {
|
||||
clientId,
|
||||
userId: user.id
|
||||
}
|
||||
const code = AuthController.saveCode(
|
||||
user.id,
|
||||
clientId,
|
||||
generateAuthCode(userInfo)
|
||||
)
|
||||
|
||||
return { code }
|
||||
}
|
||||
|
||||
const token = async (data: any): Promise<TokenResponse> => {
|
||||
const { clientId, code } = data
|
||||
|
||||
@@ -139,32 +99,6 @@ const logout = async (userInfo: InfoJWT) => {
|
||||
await removeTokensInDB(userInfo.userId, userInfo.clientId)
|
||||
}
|
||||
|
||||
interface AuthorizePayload {
|
||||
/**
|
||||
* Username for user
|
||||
* @example "secretuser"
|
||||
*/
|
||||
username: string
|
||||
/**
|
||||
* Password for user
|
||||
* @example "secretpassword"
|
||||
*/
|
||||
password: string
|
||||
/**
|
||||
* Client ID
|
||||
* @example "clientID1"
|
||||
*/
|
||||
clientId: string
|
||||
}
|
||||
|
||||
interface AuthorizeResponse {
|
||||
/**
|
||||
* Authorization code
|
||||
* @example "someRandomCryptoString"
|
||||
*/
|
||||
code: string
|
||||
}
|
||||
|
||||
interface TokenPayload {
|
||||
/**
|
||||
* Client ID
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import express from 'express'
|
||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
||||
import { PreProgramVars } from '../types'
|
||||
import { ExecuteReturnJsonResponse } from '.'
|
||||
import { parseLogToArray } from '../utils'
|
||||
import {
|
||||
getPreProgramVariables,
|
||||
getUserAutoExec,
|
||||
ModeType,
|
||||
parseLogToArray
|
||||
} from '../utils'
|
||||
|
||||
interface ExecuteSASCodePayload {
|
||||
/**
|
||||
@@ -30,14 +34,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 getUserAutoExec()
|
||||
|
||||
try {
|
||||
const { webout, log, httpHeaders } =
|
||||
(await new ExecutionController().executeProgram(
|
||||
code,
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, _debug: 131 },
|
||||
undefined,
|
||||
{ userAutoExec },
|
||||
true
|
||||
)) as ExecuteReturnJson
|
||||
|
||||
@@ -56,16 +69,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,34 +13,37 @@ import {
|
||||
Get,
|
||||
Patch,
|
||||
UploadedFile,
|
||||
FormField
|
||||
FormField,
|
||||
Delete,
|
||||
Hidden
|
||||
} from 'tsoa'
|
||||
import { fileExists, createFile, moveFile, createFolder } from '@sasjs/utils'
|
||||
import {
|
||||
fileExists,
|
||||
moveFile,
|
||||
createFolder,
|
||||
deleteFile as deleteFileOnSystem,
|
||||
folderExists,
|
||||
listFilesInFolder,
|
||||
listSubFoldersInFolder,
|
||||
isFolder,
|
||||
FileTree,
|
||||
isFileTree
|
||||
} from '@sasjs/utils'
|
||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||
|
||||
import { FileTree, isFileTree, TreeNode } from '../types'
|
||||
import { getTmpFilesFolderPath } from '../utils'
|
||||
import { TreeNode } from '../types'
|
||||
import { getFilesFolder } from '../utils'
|
||||
|
||||
interface DeployPayload {
|
||||
appLoc?: string
|
||||
appLoc: string
|
||||
streamWebFolder?: string
|
||||
fileTree: FileTree
|
||||
}
|
||||
interface FilePayload {
|
||||
/**
|
||||
* Path of the file
|
||||
* @example "/Public/somefolder/some.file"
|
||||
*/
|
||||
filePath: string
|
||||
/**
|
||||
* Contents of the file
|
||||
* @example "Contents of the File"
|
||||
*/
|
||||
fileContent: string
|
||||
}
|
||||
|
||||
interface DeployResponse {
|
||||
status: string
|
||||
message: string
|
||||
streamServiceName?: string
|
||||
example?: FileTree
|
||||
}
|
||||
|
||||
@@ -93,22 +96,60 @@ export class DriveController {
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Creates/updates files within SASjs Drive using uploaded JSON file.
|
||||
*
|
||||
*/
|
||||
@Example<DeployResponse>(successDeployResponse)
|
||||
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
|
||||
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
|
||||
@Post('/deploy/upload')
|
||||
public async deployUpload(
|
||||
@UploadedFile() file: Express.Multer.File, // passing here for API docs
|
||||
@Query() @Hidden() body?: DeployPayload // Hidden decorator has be optional
|
||||
): Promise<DeployResponse> {
|
||||
return deploy(body!)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Get file from SASjs Drive
|
||||
* @query filePath Location of SAS program
|
||||
* @example filePath "/Public/somefolder/some.file"
|
||||
* @query _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file"
|
||||
*/
|
||||
@Get('/file')
|
||||
public async getFile(
|
||||
@Request() request: express.Request,
|
||||
@Query() filePath: string
|
||||
@Query() _filePath: string
|
||||
) {
|
||||
return getFile(request, 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)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Delete file from SASjs Drive
|
||||
* @query _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file"
|
||||
*/
|
||||
@Delete('/file')
|
||||
public async deleteFile(@Query() _filePath: string) {
|
||||
return deleteFile(_filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provided else API will respond with Bad Request.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Create a file in SASjs Drive
|
||||
* @param _filePath Location of SAS program
|
||||
@@ -118,7 +159,7 @@ export class DriveController {
|
||||
@Example<UpdateFileResponse>({
|
||||
status: 'success'
|
||||
})
|
||||
@Response<UpdateFileResponse>(400, 'File already exists', {
|
||||
@Response<UpdateFileResponse>(403, 'File already exists', {
|
||||
status: 'failure',
|
||||
message: 'File request failed.'
|
||||
})
|
||||
@@ -132,21 +173,29 @@ export class DriveController {
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Modify a file in SASjs Drive
|
||||
* @param _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file.sas"
|
||||
*
|
||||
*/
|
||||
@Example<UpdateFileResponse>({
|
||||
status: 'success'
|
||||
})
|
||||
@Response<UpdateFileResponse>(400, 'Unable to get File', {
|
||||
@Response<UpdateFileResponse>(403, `File doesn't exist`, {
|
||||
status: 'failure',
|
||||
message: 'File request failed.'
|
||||
})
|
||||
@Patch('/file')
|
||||
public async updateFile(
|
||||
@Body() body: FilePayload
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
): Promise<UpdateFileResponse> {
|
||||
return updateFile(body)
|
||||
return updateFile((_filePath ?? filePath)!, file)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,14 +214,23 @@ const getFileTree = () => {
|
||||
}
|
||||
|
||||
const deploy = async (data: DeployPayload) => {
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
||||
|
||||
const appLocPath = path
|
||||
.join(getFilesFolder(), ...appLocParts)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!appLocPath.includes(driveFilesPath)) {
|
||||
throw new Error('appLoc cannot be outside drive.')
|
||||
}
|
||||
|
||||
if (!isFileTree(data.fileTree)) {
|
||||
throw { code: 400, ...invalidDeployFormatResponse }
|
||||
}
|
||||
|
||||
await createFileTree(
|
||||
data.fileTree.members,
|
||||
data.appLoc ? data.appLoc.replace(/^\//, '').split('/') : []
|
||||
).catch((err) => {
|
||||
await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
|
||||
throw { code: 500, ...execDeployErrorResponse, ...err }
|
||||
})
|
||||
|
||||
@@ -180,28 +238,83 @@ const deploy = async (data: DeployPayload) => {
|
||||
}
|
||||
|
||||
const getFile = async (req: express.Request, filePath: string) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const filePathFull = path
|
||||
.join(getTmpFilesFolderPath(), filePath)
|
||||
.join(getFilesFolder(), filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot get file outside drive.')
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error("File doesn't exist.")
|
||||
}
|
||||
|
||||
const extension = path.extname(filePathFull).toLowerCase()
|
||||
if (extension === '.sas') {
|
||||
req.res?.setHeader('Content-type', 'text/plain')
|
||||
}
|
||||
|
||||
req.res?.sendFile(path.resolve(filePathFull))
|
||||
}
|
||||
|
||||
const getFolder = async (folderPath?: string) => {
|
||||
const driveFilesPath = 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 driveFilesPath = getFilesFolder()
|
||||
|
||||
const filePathFull = path
|
||||
.join(getFilesFolder(), filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot delete file outside drive.')
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error('File does not exist.')
|
||||
}
|
||||
|
||||
req.res?.download(filePathFull)
|
||||
await deleteFileOnSystem(filePathFull)
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const saveFile = async (
|
||||
filePath: string,
|
||||
multerFile: Express.Multer.File
|
||||
): Promise<GetFileResponse> => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const filePathFull = path
|
||||
.join(driveFilesPath, filePath)
|
||||
@@ -222,29 +335,25 @@ const saveFile = async (
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const updateFile = async (body: FilePayload): Promise<GetFileResponse> => {
|
||||
const { filePath, fileContent } = body
|
||||
try {
|
||||
const filePathFull = path
|
||||
.join(getTmpFilesFolderPath(), filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
const updateFile = async (
|
||||
filePath: string,
|
||||
multerFile: Express.Multer.File
|
||||
): Promise<GetFileResponse> => {
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
await validateFilePath(filePathFull)
|
||||
await createFile(filePathFull, fileContent)
|
||||
const filePathFull = path
|
||||
.join(driveFilesPath, filePath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
return { status: 'success' }
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'failure',
|
||||
message: 'File request failed.',
|
||||
error: typeof err === 'object' ? err.toString() : err
|
||||
}
|
||||
if (!filePathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot modify file outside drive.')
|
||||
}
|
||||
}
|
||||
|
||||
const validateFilePath = async (filePath: string) => {
|
||||
if (!(await fileExists(filePath))) {
|
||||
throw 'DriveController: File does not exists.'
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error(`File doesn't exist.`)
|
||||
}
|
||||
|
||||
await moveFile(multerFile.path, filePathFull)
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ export * from './client'
|
||||
export * from './code'
|
||||
export * from './drive'
|
||||
export * from './group'
|
||||
export * from './info'
|
||||
export * from './session'
|
||||
export * from './stp'
|
||||
export * from './user'
|
||||
export * from './web'
|
||||
|
||||
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,13 +8,14 @@ import {
|
||||
moveFile,
|
||||
readFileBinary
|
||||
} from '@sasjs/utils'
|
||||
import { PreProgramVars, TreeNode } from '../../types'
|
||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||
import {
|
||||
extractHeaders,
|
||||
generateFileUploadSasCode,
|
||||
getTmpFilesFolderPath,
|
||||
getFilesFolder,
|
||||
getMacrosFolder,
|
||||
HTTPHeaders,
|
||||
sasJSCoreMacros
|
||||
isDebugOn
|
||||
} from '../../utils'
|
||||
|
||||
export interface ExecutionVars {
|
||||
@@ -38,7 +39,8 @@ export class ExecutionController {
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
returnJson?: boolean,
|
||||
session?: Session
|
||||
) {
|
||||
if (!(await fileExists(programPath)))
|
||||
throw 'ExecutionController: SAS file does not exist.'
|
||||
@@ -50,7 +52,8 @@ export class ExecutionController {
|
||||
preProgramVariables,
|
||||
vars,
|
||||
otherArgs,
|
||||
returnJson
|
||||
returnJson,
|
||||
session
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,23 +62,25 @@ export class ExecutionController {
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
returnJson?: boolean,
|
||||
sessionByFileUpload?: Session
|
||||
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||
const sessionController = getSessionController()
|
||||
|
||||
const session = await sessionController.getSession()
|
||||
const session =
|
||||
sessionByFileUpload ?? (await sessionController.getSession())
|
||||
session.inUse = true
|
||||
session.consumed = true
|
||||
|
||||
const logPath = path.join(session.path, 'log.log')
|
||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||
const weboutPath = path.join(session.path, 'webout.txt')
|
||||
const tokenFile = path.join(session.path, 'accessToken.txt')
|
||||
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
||||
|
||||
await createFile(weboutPath, '')
|
||||
await createFile(
|
||||
tokenFile,
|
||||
preProgramVariables?.accessToken ?? 'accessToken'
|
||||
preProgramVariables?.httpHeaders.join('\n') ?? ''
|
||||
)
|
||||
|
||||
const varStatments = Object.keys(vars).reduce(
|
||||
@@ -105,7 +110,7 @@ export class ExecutionController {
|
||||
`
|
||||
|
||||
program = `
|
||||
options insert=(SASAUTOS="${sasJSCoreMacros}");
|
||||
options insert=(SASAUTOS="${getMacrosFolder()}");
|
||||
|
||||
/* runtime vars */
|
||||
${varStatments}
|
||||
@@ -114,11 +119,15 @@ filename _webout "${weboutPath}" mod;
|
||||
/* dynamic user-provided vars */
|
||||
${preProgramVarStatments}
|
||||
|
||||
/* user autoexec starts */
|
||||
${otherArgs?.userAutoExec ?? ''}
|
||||
/* user autoexec ends */
|
||||
|
||||
/* actual job code */
|
||||
${program}`
|
||||
|
||||
// if no files are uploaded filesNamesMap will be undefined
|
||||
if (otherArgs && otherArgs.filesNamesMap) {
|
||||
if (otherArgs?.filesNamesMap) {
|
||||
const uploadSasCode = await generateFileUploadSasCode(
|
||||
otherArgs.filesNamesMap,
|
||||
session.path
|
||||
@@ -152,7 +161,9 @@ ${program}`
|
||||
: ''
|
||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||
const fileResponse: boolean =
|
||||
httpHeaders.hasOwnProperty('content-type') && !returnJson
|
||||
httpHeaders.hasOwnProperty('content-type') &&
|
||||
!returnJson && // not a POST Request
|
||||
!isDebugOn(vars) // Debug is not enabled
|
||||
|
||||
const webout = (await fileExists(weboutPath))
|
||||
? fileResponse
|
||||
@@ -160,9 +171,6 @@ ${program}`
|
||||
: await readFile(weboutPath)
|
||||
: ''
|
||||
|
||||
const debugValue =
|
||||
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
||||
|
||||
// it should be deleted by scheduleSessionDestroy
|
||||
session.inUse = false
|
||||
|
||||
@@ -170,18 +178,16 @@ ${program}`
|
||||
return {
|
||||
httpHeaders,
|
||||
webout,
|
||||
log:
|
||||
(debugValue && debugValue >= 131) || session.crashed ? log : undefined
|
||||
log: isDebugOn(vars) || session.crashed ? log : undefined
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
httpHeaders,
|
||||
result: fileResponse
|
||||
? webout
|
||||
: (debugValue && debugValue >= 131) || session.crashed
|
||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||
: webout
|
||||
result:
|
||||
isDebugOn(vars) || session.crashed
|
||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||
: webout
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +195,7 @@ ${program}`
|
||||
const root: TreeNode = {
|
||||
name: 'files',
|
||||
relativePath: '',
|
||||
absolutePath: getTmpFilesFolderPath(),
|
||||
absolutePath: getFilesFolder(),
|
||||
children: []
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Request, RequestHandler } from 'express'
|
||||
import multer from 'multer'
|
||||
import { uuidv4 } from '@sasjs/utils'
|
||||
import { getSessionController } from '.'
|
||||
const multer = require('multer')
|
||||
|
||||
export class FileUploadController {
|
||||
private storage = multer.diskStorage({
|
||||
destination: function (req: any, file: any, cb: any) {
|
||||
destination: function (req: Request, file: any, cb: any) {
|
||||
//Sending the intercepted files to the sessions subfolder
|
||||
cb(null, req.sasSession.path)
|
||||
cb(null, req.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
|
||||
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
|
||||
//that will store the files uploaded
|
||||
public preUploadMiddleware = async (req: any, res: any, next: any) => {
|
||||
public preUploadMiddleware: RequestHandler = async (req, res, next) => {
|
||||
let session
|
||||
|
||||
const sessionController = getSessionController()
|
||||
session = await sessionController.getSession()
|
||||
session.inUse = true
|
||||
// marking consumed true, so that it's not available
|
||||
// as readySession for any other request
|
||||
session.consumed = true
|
||||
|
||||
req.sasSession = session
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Session } from '../../types'
|
||||
import { promisify } from 'util'
|
||||
import { execFile } from 'child_process'
|
||||
import {
|
||||
getTmpSessionsFolderPath,
|
||||
getSessionsFolder,
|
||||
generateUniqueFileName,
|
||||
sysInitCompiledPath
|
||||
} from '../../utils'
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
createFile,
|
||||
fileExists,
|
||||
generateTimestamp,
|
||||
readFile,
|
||||
moveFile
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
@@ -31,16 +30,17 @@ export class SessionController {
|
||||
? readySessions[0]
|
||||
: await this.createSession()
|
||||
|
||||
if (readySessions.length < 2) this.createSession()
|
||||
if (readySessions.length < 3) this.createSession()
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
private async createSession(): Promise<Session> {
|
||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||
|
||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||
// death time of session is 15 mins from creation
|
||||
const deathTimeStamp = (
|
||||
parseInt(creationTimeStamp) +
|
||||
15 * 60 * 1000 -
|
||||
@@ -87,6 +87,8 @@ ${autoExecContent}`
|
||||
codePath,
|
||||
'-LOG',
|
||||
path.join(session.path, 'log.log'),
|
||||
'-PRINT',
|
||||
path.join(session.path, 'output.lst'),
|
||||
'-WORK',
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
@@ -138,7 +140,9 @@ ${autoExecContent}`
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
setTimeout(async () => {
|
||||
if (session.inUse) {
|
||||
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
|
||||
@@ -1,37 +1,52 @@
|
||||
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types'
|
||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
||||
import path from 'path'
|
||||
import { getFilesFolder } from '../../utils/file'
|
||||
import {
|
||||
createFolder,
|
||||
createFile,
|
||||
asyncForEach,
|
||||
FolderMember,
|
||||
ServiceMember,
|
||||
FileMember,
|
||||
MemberType,
|
||||
FileTree
|
||||
} from '@sasjs/utils'
|
||||
|
||||
// REFACTOR: export FileTreeCpntroller
|
||||
export const createFileTree = async (
|
||||
members: (FolderMember | ServiceMember)[],
|
||||
members: (FolderMember | ServiceMember | FileMember)[],
|
||||
parentFolders: string[] = []
|
||||
) => {
|
||||
const destinationPath = path.join(
|
||||
getTmpFilesFolderPath(),
|
||||
getFilesFolder(),
|
||||
path.join(...parentFolders)
|
||||
)
|
||||
|
||||
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
|
||||
let name = member.name
|
||||
await asyncForEach(
|
||||
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) {
|
||||
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
||||
Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
if (member.type === MemberType.folder) {
|
||||
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
||||
Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
|
||||
await createFileTree(member.members, [...parentFolders, name]).catch(
|
||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
} else {
|
||||
await createFile(path.join(destinationPath, name), member.code).catch(
|
||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
await createFileTree(member.members, [...parentFolders, name]).catch(
|
||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
} else {
|
||||
const encoding = member.type === MemberType.file ? 'base64' : undefined
|
||||
|
||||
await createFile(
|
||||
path.join(destinationPath, name),
|
||||
member.code,
|
||||
encoding
|
||||
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ export class SessionController {
|
||||
}
|
||||
}
|
||||
|
||||
const session = (req: any) => ({
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
displayName: req.user.displayName
|
||||
const session = (req: express.Request) => ({
|
||||
id: req.user!.userId,
|
||||
username: req.user!.username,
|
||||
displayName: req.user!.displayName
|
||||
})
|
||||
|
||||
@@ -17,14 +17,16 @@ import {
|
||||
ExecutionController,
|
||||
ExecutionVars
|
||||
} from './internal'
|
||||
import { PreProgramVars } from '../types'
|
||||
import {
|
||||
getTmpFilesFolderPath,
|
||||
getPreProgramVariables,
|
||||
getFilesFolder,
|
||||
HTTPHeaders,
|
||||
isDebugOn,
|
||||
LogLine,
|
||||
makeFilesNamesMap,
|
||||
parseLogToArray
|
||||
} from '../utils'
|
||||
import { MulterFile } from '../types/Upload'
|
||||
|
||||
interface ExecuteReturnJsonPayload {
|
||||
/**
|
||||
@@ -62,7 +64,9 @@ export class STPController {
|
||||
* The response headers can be adjusted using the mfs_httpheader() macro. Any
|
||||
* file type can be returned, including binary files such as zip or xls.
|
||||
*
|
||||
* This behaviour differs for POST requests, in which case the reponse is
|
||||
* If _debug is >= 131, response headers will contain Content-Type: 'text/plain'
|
||||
*
|
||||
* This behaviour differs for POST requests, in which case the response is
|
||||
* always JSON.
|
||||
*
|
||||
* @summary Execute Stored Program, return raw _webout content.
|
||||
@@ -129,7 +133,7 @@ const executeReturnRaw = async (
|
||||
const query = req.query as ExecutionVars
|
||||
const sasCodePath =
|
||||
path
|
||||
.join(getTmpFilesFolderPath(), _program)
|
||||
.join(getFilesFolder(), _program)
|
||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||
|
||||
try {
|
||||
@@ -140,6 +144,12 @@ const executeReturnRaw = async (
|
||||
query
|
||||
)) as ExecuteReturnRaw
|
||||
|
||||
// Should over-ride response header for debug
|
||||
// on GET request to see entire log rendering on browser.
|
||||
if (isDebugOn(query)) {
|
||||
httpHeaders['content-type'] = 'text/plain'
|
||||
}
|
||||
|
||||
req.res?.set(httpHeaders)
|
||||
|
||||
if (result instanceof Buffer) {
|
||||
@@ -158,15 +168,17 @@ const executeReturnRaw = async (
|
||||
}
|
||||
|
||||
const executeReturnJson = async (
|
||||
req: any,
|
||||
req: express.Request,
|
||||
_program: string
|
||||
): Promise<ExecuteReturnJsonResponse> => {
|
||||
const sasCodePath =
|
||||
path
|
||||
.join(getTmpFilesFolderPath(), _program)
|
||||
.join(getFilesFolder(), _program)
|
||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||
|
||||
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
||||
const filesNamesMap = req.files?.length
|
||||
? makeFilesNamesMap(req.files as MulterFile[])
|
||||
: null
|
||||
|
||||
try {
|
||||
const { webout, log, httpHeaders } =
|
||||
@@ -175,7 +187,8 @@ const executeReturnJson = async (
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, ...req.body },
|
||||
{ filesNamesMap: filesNamesMap },
|
||||
true
|
||||
true,
|
||||
req.sasSession
|
||||
)) as ExecuteReturnJson
|
||||
|
||||
let weboutRes: string | IRecordOfAny = webout
|
||||
@@ -200,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 {
|
||||
Security,
|
||||
Route,
|
||||
@@ -10,10 +11,13 @@ import {
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Hidden
|
||||
Hidden,
|
||||
Request
|
||||
} from 'tsoa'
|
||||
import { desktopUser } from '../middlewares'
|
||||
|
||||
import User, { UserPayload } from '../model/User'
|
||||
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
|
||||
|
||||
export interface UserResponse {
|
||||
id: number
|
||||
@@ -27,6 +31,7 @@ interface UserDetailsResponse {
|
||||
username: string
|
||||
isActive: boolean
|
||||
isAdmin: boolean
|
||||
autoExec?: string
|
||||
}
|
||||
|
||||
@Security('bearerAuth')
|
||||
@@ -73,13 +78,23 @@ export class UserController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Only Admin or user itself will get user autoExec code.
|
||||
* @summary Get user properties - such as group memberships, userName, displayName.
|
||||
* @param userId The user's identifier
|
||||
* @example userId 1234
|
||||
*/
|
||||
@Get('{userId}')
|
||||
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
|
||||
return getUser(userId)
|
||||
public async getUser(
|
||||
@Request() req: express.Request,
|
||||
@Path() userId: number
|
||||
): Promise<UserDetailsResponse> {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
|
||||
|
||||
const { user } = req
|
||||
const getAutoExec = user!.isAdmin || user!.userId == userId
|
||||
return getUser(userId, getAutoExec)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,6 +114,11 @@ export class UserController {
|
||||
@Path() userId: number,
|
||||
@Body() body: UserPayload
|
||||
): Promise<UserDetailsResponse> {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE === ModeType.Desktop)
|
||||
return updateDesktopAutoExec(body.autoExec ?? '')
|
||||
|
||||
return updateUser(userId, body)
|
||||
}
|
||||
|
||||
@@ -123,7 +143,7 @@ const getAllUsers = async (): Promise<UserResponse[]> =>
|
||||
.exec()
|
||||
|
||||
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
const { displayName, username, password, isAdmin, isActive } = data
|
||||
const { displayName, username, password, isAdmin, isActive, autoExec } = data
|
||||
|
||||
// Checking if user is already in the database
|
||||
const usernameExist = await User.findOne({ username })
|
||||
@@ -138,7 +158,8 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
username,
|
||||
password: hashPassword,
|
||||
isAdmin,
|
||||
isActive
|
||||
isActive,
|
||||
autoExec
|
||||
})
|
||||
|
||||
const savedUser = await user.save()
|
||||
@@ -148,38 +169,50 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||
displayName: savedUser.displayName,
|
||||
username: savedUser.username,
|
||||
isActive: savedUser.isActive,
|
||||
isAdmin: savedUser.isAdmin
|
||||
isAdmin: savedUser.isAdmin,
|
||||
autoExec: savedUser.autoExec
|
||||
}
|
||||
}
|
||||
|
||||
const getUser = async (id: number): Promise<UserDetailsResponse> => {
|
||||
const getUser = async (
|
||||
id: number,
|
||||
getAutoExec: boolean
|
||||
): Promise<UserDetailsResponse> => {
|
||||
const user = await User.findOne({ id })
|
||||
.select({
|
||||
_id: 0,
|
||||
id: 1,
|
||||
username: 1,
|
||||
displayName: 1,
|
||||
isAdmin: 1,
|
||||
isActive: 1
|
||||
})
|
||||
.exec()
|
||||
|
||||
if (!user) throw new Error('User is not found.')
|
||||
|
||||
return user
|
||||
return {
|
||||
id: user.id,
|
||||
displayName: user.displayName,
|
||||
username: user.username,
|
||||
isActive: user.isActive,
|
||||
isAdmin: user.isAdmin,
|
||||
autoExec: getAutoExec ? user.autoExec ?? '' : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const getDesktopAutoExec = async () => {
|
||||
return {
|
||||
...desktopUser,
|
||||
id: desktopUser.userId,
|
||||
autoExec: await getUserAutoExec()
|
||||
}
|
||||
}
|
||||
|
||||
const updateUser = async (
|
||||
id: number,
|
||||
data: UserPayload
|
||||
data: Partial<UserPayload>
|
||||
): Promise<UserDetailsResponse> => {
|
||||
const { displayName, username, password, isAdmin, isActive } = data
|
||||
const { displayName, username, password, isAdmin, isActive, autoExec } = data
|
||||
|
||||
const params: any = { displayName, isAdmin, isActive }
|
||||
const params: any = { displayName, isAdmin, isActive, autoExec }
|
||||
|
||||
if (username) {
|
||||
// Checking if user is already in the database
|
||||
const usernameExist = await User.findOne({ username })
|
||||
if (usernameExist?.id != id) throw new Error('Username already exists.')
|
||||
if (usernameExist && usernameExist.id != id)
|
||||
throw new Error('Username already exists.')
|
||||
params.username = username
|
||||
}
|
||||
|
||||
@@ -189,18 +222,26 @@ const updateUser = async (
|
||||
}
|
||||
|
||||
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 updateDesktopAutoExec = async (autoExec: string) => {
|
||||
await updateUserAutoExec(autoExec)
|
||||
return {
|
||||
...desktopUser,
|
||||
id: desktopUser.userId,
|
||||
autoExec
|
||||
}
|
||||
}
|
||||
|
||||
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,36 @@
|
||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { verifyTokenInDB } from '../utils'
|
||||
import { csrfProtection } from '../app'
|
||||
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
|
||||
import { desktopUser } from './desktop'
|
||||
|
||||
export const authenticateAccessToken: RequestHandler = async (
|
||||
req,
|
||||
res,
|
||||
next
|
||||
) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE === ModeType.Desktop) {
|
||||
req.user = desktopUser
|
||||
return next()
|
||||
}
|
||||
|
||||
// if request is coming from web and has valid session
|
||||
// it can be validated.
|
||||
if (req.session?.loggedIn) {
|
||||
if (req.session.user) {
|
||||
const user = await fetchLatestAutoExec(req.session.user)
|
||||
|
||||
if (user) {
|
||||
if (user.isActive) {
|
||||
req.user = user
|
||||
return csrfProtection(req, res, next)
|
||||
} else return res.sendStatus(401)
|
||||
}
|
||||
}
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
||||
authenticateToken(
|
||||
req,
|
||||
res,
|
||||
@@ -11,7 +40,7 @@ export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
||||
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
||||
authenticateToken(
|
||||
req,
|
||||
res,
|
||||
@@ -22,16 +51,16 @@ export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
||||
}
|
||||
|
||||
const authenticateToken = (
|
||||
req: any,
|
||||
res: any,
|
||||
next: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
key: string,
|
||||
tokenType: 'accessToken' | 'refreshToken'
|
||||
) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') {
|
||||
req.user = {
|
||||
userId: '1234',
|
||||
userId: 1234,
|
||||
clientId: 'desktopModeClientId',
|
||||
username: 'desktopModeUsername',
|
||||
displayName: 'desktopModeDisplayName',
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
export const desktopRestrict = (req: any, res: any, next: any) => {
|
||||
import { RequestHandler, Request } from 'express'
|
||||
import { userInfo } from 'os'
|
||||
import { RequestUser } from '../types'
|
||||
import { ModeType } from '../utils'
|
||||
|
||||
const regexUser = /^\/SASjsApi\/user\/[0-9]*$/ // /SASjsApi/user/1
|
||||
|
||||
const allowedInDesktopMode: { [key: string]: RegExp[] } = {
|
||||
GET: [regexUser],
|
||||
PATCH: [regexUser]
|
||||
}
|
||||
|
||||
const reqAllowedInDesktopMode = (request: Request): boolean => {
|
||||
const { method, originalUrl: url } = request
|
||||
|
||||
return !!allowedInDesktopMode[method]?.find((urlRegex) => urlRegex.test(url))
|
||||
}
|
||||
|
||||
export const desktopRestrict: RequestHandler = (req, res, next) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server')
|
||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||
|
||||
if (MODE === ModeType.Desktop) {
|
||||
if (!reqAllowedInDesktopMode(req))
|
||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
export const desktopUsername = (req: any, res: any, next: any) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server')
|
||||
return res.status(200).send({
|
||||
userId: 12345,
|
||||
username: 'DESKTOPusername',
|
||||
displayName: 'DESKTOP User'
|
||||
})
|
||||
|
||||
next()
|
||||
export const desktopUser: RequestUser = {
|
||||
userId: 12345,
|
||||
clientId: 'desktop_app',
|
||||
username: userInfo().username,
|
||||
displayName: userInfo().username,
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import path from 'path'
|
||||
import { Request } from 'express'
|
||||
import multer, { FileFilterCallback, Options } from 'multer'
|
||||
import { getTmpUploadsPath } from '../utils'
|
||||
import { blockFileRegex, getUploadsFolder } from '../utils'
|
||||
|
||||
const acceptableExtensions = ['.sas']
|
||||
const fieldNameSize = 300
|
||||
const fileSize = 10485760 // 10 MB
|
||||
const fileSize = 104857600 // 100 MB
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: getTmpUploadsPath(),
|
||||
destination: getUploadsFolder(),
|
||||
filename: function (
|
||||
_req: Request,
|
||||
file: Express.Multer.File,
|
||||
@@ -31,15 +30,11 @@ const fileFilter: Options['fileFilter'] = (
|
||||
file: Express.Multer.File,
|
||||
callback: FileFilterCallback
|
||||
) => {
|
||||
const fileExtension = path.extname(file.originalname).toLocaleLowerCase()
|
||||
|
||||
if (!acceptableExtensions.includes(fileExtension)) {
|
||||
const fileExtension = path.extname(file.originalname)
|
||||
const shouldBlockUpload = blockFileRegex.test(file.originalname)
|
||||
if (shouldBlockUpload) {
|
||||
return callback(
|
||||
new Error(
|
||||
`File extension '${fileExtension}' not acceptable. Valid extension(s): ${acceptableExtensions.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
new Error(`File extension '${fileExtension}' not acceptable.`)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const verifyAdmin = (req: any, res: any, next: any) => {
|
||||
import { RequestHandler } from 'express'
|
||||
|
||||
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') return next()
|
||||
|
||||
|
||||
@@ -1,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 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')
|
||||
}
|
||||
next()
|
||||
|
||||
@@ -27,12 +27,18 @@ export interface UserPayload {
|
||||
* @example "true"
|
||||
*/
|
||||
isActive?: boolean
|
||||
/**
|
||||
* User-specific auto-exec code
|
||||
* @example ""
|
||||
*/
|
||||
autoExec?: string
|
||||
}
|
||||
|
||||
interface IUserDocument extends UserPayload, Document {
|
||||
id: number
|
||||
isAdmin: boolean
|
||||
isActive: boolean
|
||||
autoExec: string
|
||||
groups: Schema.Types.ObjectId[]
|
||||
tokens: [{ [key: string]: string }]
|
||||
}
|
||||
@@ -66,6 +72,9 @@ const userSchema = new Schema<IUserDocument>({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoExec: {
|
||||
type: String
|
||||
},
|
||||
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
||||
tokens: [
|
||||
{
|
||||
|
||||
@@ -1,58 +1,22 @@
|
||||
import express from 'express'
|
||||
|
||||
import { AuthController } from '../../controllers/'
|
||||
import Client from '../../model/Client'
|
||||
|
||||
import {
|
||||
authenticateAccessToken,
|
||||
authenticateRefreshToken
|
||||
} from '../../middlewares'
|
||||
|
||||
import {
|
||||
authorizeValidation,
|
||||
getDesktopFields,
|
||||
tokenValidation
|
||||
} from '../../utils'
|
||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
||||
import { InfoJWT } from '../../types'
|
||||
|
||||
const authRouter = express.Router()
|
||||
|
||||
const clientIDs = new Set()
|
||||
|
||||
export const populateClients = async () => {
|
||||
const result = await Client.find()
|
||||
clientIDs.clear()
|
||||
result.forEach((r) => {
|
||||
clientIDs.add(r.clientId)
|
||||
})
|
||||
}
|
||||
|
||||
authRouter.post('/authorize', async (req, res) => {
|
||||
const { error, value: body } = authorizeValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const { clientId } = body
|
||||
|
||||
// Verify client ID
|
||||
if (!clientIDs.has(clientId)) {
|
||||
return res.status(403).send('Invalid clientId.')
|
||||
}
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.authorize(body)
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
const controller = new AuthController()
|
||||
|
||||
authRouter.post('/token', async (req, res) => {
|
||||
const { error, value: body } = tokenValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.token(body)
|
||||
|
||||
@@ -62,10 +26,12 @@ authRouter.post('/token', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
||||
const userInfo: InfoJWT = req.user
|
||||
authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
|
||||
const userInfo: InfoJWT = {
|
||||
userId: req.user!.userId!,
|
||||
clientId: req.user!.clientId!
|
||||
}
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
const response = await controller.refresh(userInfo)
|
||||
|
||||
@@ -75,10 +41,12 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
|
||||
const userInfo: InfoJWT = req.user
|
||||
authRouter.delete('/logout', authenticateAccessToken, async (req, res) => {
|
||||
const userInfo: InfoJWT = {
|
||||
userId: req.user!.userId!,
|
||||
clientId: req.user!.clientId!
|
||||
}
|
||||
|
||||
const controller = new AuthController()
|
||||
try {
|
||||
await controller.logout(userInfo)
|
||||
} catch (e) {}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import express from 'express'
|
||||
import { deleteFile } from '@sasjs/utils'
|
||||
import { deleteFile, readFile } from '@sasjs/utils'
|
||||
|
||||
import { publishAppStream } from '../appStream'
|
||||
|
||||
import { multerSingle } from '../../middlewares/multer'
|
||||
import { DriveController } from '../../controllers/'
|
||||
import {
|
||||
getFileDriveValidation,
|
||||
updateFileDriveValidation,
|
||||
uploadFileBodyValidation,
|
||||
uploadFileParamValidation
|
||||
deployValidation,
|
||||
fileBodyValidation,
|
||||
fileParamValidation,
|
||||
folderParamValidation
|
||||
} from '../../utils'
|
||||
|
||||
const controller = new DriveController()
|
||||
@@ -15,8 +17,22 @@ const controller = new DriveController()
|
||||
const driveRouter = express.Router()
|
||||
|
||||
driveRouter.post('/deploy', async (req, res) => {
|
||||
const { error, value: body } = deployValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.deploy(req.body)
|
||||
const response = await controller.deploy(body)
|
||||
|
||||
if (body.streamWebFolder) {
|
||||
const { streamServiceName } = await publishAppStream(
|
||||
body.appLoc,
|
||||
body.streamWebFolder,
|
||||
body.streamServiceName,
|
||||
body.streamLogo
|
||||
)
|
||||
response.streamServiceName = streamServiceName
|
||||
}
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
@@ -27,12 +43,87 @@ driveRouter.post('/deploy', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.post(
|
||||
'/deploy/upload',
|
||||
(...arg) => multerSingle('file', arg),
|
||||
async (req, res) => {
|
||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||
|
||||
const fileContent = await readFile(req.file.path)
|
||||
|
||||
let jsonContent
|
||||
try {
|
||||
jsonContent = JSON.parse(fileContent)
|
||||
} catch (err) {
|
||||
deleteFile(req.file.path)
|
||||
return res.status(400).send('File containing invalid JSON content.')
|
||||
}
|
||||
|
||||
const { error, value: body } = deployValidation(jsonContent)
|
||||
if (error) {
|
||||
deleteFile(req.file.path)
|
||||
return res.status(400).send(error.details[0].message)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await controller.deployUpload(req.file, body)
|
||||
|
||||
if (body.streamWebFolder) {
|
||||
const { streamServiceName } = await publishAppStream(
|
||||
body.appLoc,
|
||||
body.streamWebFolder,
|
||||
body.streamServiceName,
|
||||
body.streamLogo
|
||||
)
|
||||
response.streamServiceName = streamServiceName
|
||||
}
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err)
|
||||
} finally {
|
||||
deleteFile(req.file.path)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
driveRouter.get('/file', async (req, res) => {
|
||||
const { error, value: query } = getFileDriveValidation(req.query)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
await controller.getFile(req, query.filePath)
|
||||
await controller.getFile(req, query._filePath)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.get('/folder', async (req, res) => {
|
||||
const { error: errQ, value: query } = folderParamValidation(req.query)
|
||||
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.getFolder(query._folderPath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.delete('/file', async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.deleteFile(query._filePath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
@@ -42,12 +133,12 @@ driveRouter.post(
|
||||
'/file',
|
||||
(...arg) => multerSingle('file', arg),
|
||||
async (req, res) => {
|
||||
const { error: errQ, value: query } = uploadFileParamValidation(req.query)
|
||||
const { error: errB, value: body } = uploadFileBodyValidation(req.body)
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) {
|
||||
if (req.file) await deleteFile(req.file.path)
|
||||
return res.status(400).send(errB.details[0].message)
|
||||
return res.status(400).send(errQ.details[0].message)
|
||||
}
|
||||
|
||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||
@@ -66,21 +157,33 @@ driveRouter.post(
|
||||
}
|
||||
)
|
||||
|
||||
driveRouter.patch('/file', async (req, res) => {
|
||||
const { error, value: body } = updateFileDriveValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
driveRouter.patch(
|
||||
'/file',
|
||||
(...arg) => multerSingle('file', arg),
|
||||
async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
try {
|
||||
const response = await controller.updateFile(body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
if (errQ && errB) {
|
||||
if (req.file) await deleteFile(req.file.path)
|
||||
return res.status(400).send(errQ.details[0].message)
|
||||
}
|
||||
|
||||
delete err.code
|
||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||
|
||||
res.status(statusCode).send(err)
|
||||
try {
|
||||
const response = await controller.updateFile(
|
||||
req.file,
|
||||
query._filePath,
|
||||
body.filePath
|
||||
)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
await deleteFile(req.file.path)
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
driveRouter.get('/fileTree', async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -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 controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.getGroup(groupId)
|
||||
const response = await controller.getGroup(parseInt(groupId))
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
@@ -49,12 +49,15 @@ groupRouter.post(
|
||||
'/:groupId/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
const { groupId, userId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.addUserToGroup(groupId, userId)
|
||||
const response = await controller.addUserToGroup(
|
||||
parseInt(groupId),
|
||||
parseInt(userId)
|
||||
)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
@@ -66,12 +69,15 @@ groupRouter.delete(
|
||||
'/:groupId/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
const { groupId, userId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
const response = await controller.removeUserFromGroup(groupId, userId)
|
||||
const response = await controller.removeUserFromGroup(
|
||||
parseInt(groupId),
|
||||
parseInt(userId)
|
||||
)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
@@ -83,12 +89,12 @@ groupRouter.delete(
|
||||
'/:groupId',
|
||||
authenticateAccessToken,
|
||||
verifyAdmin,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
const { groupId } = req.params
|
||||
|
||||
const controller = new GroupController()
|
||||
try {
|
||||
await controller.deleteGroup(groupId)
|
||||
await controller.deleteGroup(parseInt(groupId))
|
||||
res.status(200).send('Group Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
|
||||
@@ -5,10 +5,10 @@ import swaggerUi from 'swagger-ui-express'
|
||||
import {
|
||||
authenticateAccessToken,
|
||||
desktopRestrict,
|
||||
desktopUsername,
|
||||
verifyAdmin
|
||||
} from '../../middlewares'
|
||||
|
||||
import infoRouter from './info'
|
||||
import driveRouter from './drive'
|
||||
import stpRouter from './stp'
|
||||
import codeRouter from './code'
|
||||
@@ -20,7 +20,8 @@ import sessionRouter from './session'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||
router.use('/info', infoRouter)
|
||||
router.use('/session', authenticateAccessToken, sessionRouter)
|
||||
router.use('/auth', desktopRestrict, authRouter)
|
||||
router.use(
|
||||
'/client',
|
||||
@@ -34,12 +35,22 @@ router.use('/group', desktopRestrict, groupRouter)
|
||||
router.use('/stp', authenticateAccessToken, stpRouter)
|
||||
router.use('/code', authenticateAccessToken, codeRouter)
|
||||
router.use('/user', desktopRestrict, userRouter)
|
||||
|
||||
router.use(
|
||||
'/',
|
||||
swaggerUi.serve,
|
||||
swaggerUi.setup(undefined, {
|
||||
swaggerOptions: {
|
||||
url: '/swagger.yaml'
|
||||
url: '/swagger.yaml',
|
||||
requestInterceptor: (request: any) => {
|
||||
request.credentials = 'include'
|
||||
|
||||
const cookie = document.cookie
|
||||
const startIndex = cookie.indexOf('XSRF-TOKEN')
|
||||
const csrf = cookie.slice(startIndex + 11).split('; ')[0]
|
||||
request.headers['X-XSRF-TOKEN'] = csrf
|
||||
return request
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
16
api/src/routes/api/info.ts
Normal file
16
api/src/routes/api/info.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import express from 'express'
|
||||
import { InfoController } from '../../controllers'
|
||||
|
||||
const infoRouter = express.Router()
|
||||
|
||||
infoRouter.get('/', async (req, res) => {
|
||||
const controller = new InfoController()
|
||||
try {
|
||||
const response = controller.info()
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default infoRouter
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
ClientController,
|
||||
AuthController
|
||||
} from '../../../controllers/'
|
||||
import { populateClients } from '../auth'
|
||||
import { InfoJWT } from '../../../types'
|
||||
import {
|
||||
generateAccessToken,
|
||||
@@ -18,11 +17,6 @@ import {
|
||||
verifyTokenInDB
|
||||
} from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const clientSecret = 'someclientSecret'
|
||||
const user = {
|
||||
@@ -35,16 +29,18 @@ const user = {
|
||||
}
|
||||
|
||||
describe('auth', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
await clientController.createClient({ clientId, clientSecret })
|
||||
await populateClients()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -53,114 +49,6 @@ describe('auth', () => {
|
||||
await mongoServer.stop()
|
||||
})
|
||||
|
||||
describe('authorize', () => {
|
||||
afterEach(async () => {
|
||||
const collections = mongoose.connection.collections
|
||||
const collection = collections['users']
|
||||
await collection.deleteMany({})
|
||||
})
|
||||
|
||||
it('should respond with authorization code', async () => {
|
||||
await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
clientId
|
||||
})
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toHaveProperty('code')
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if username is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
password: user.password,
|
||||
clientId
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"username" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if password is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
clientId
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"password" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if clientId is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password
|
||||
})
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"clientId" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if username is incorrect', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
clientId
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Username is not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if password is incorrect', async () => {
|
||||
await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: 'WrongPassword',
|
||||
clientId
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Invalid password.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if clientId is incorrect', async () => {
|
||||
await userController.createUser(user)
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/auth/authorize')
|
||||
.send({
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
clientId: 'WrongClientID'
|
||||
})
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Invalid clientId.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('token', () => {
|
||||
const userInfo: InfoJWT = {
|
||||
clientId,
|
||||
|
||||
@@ -6,11 +6,6 @@ import appPromise from '../../../app'
|
||||
import { UserController, ClientController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const client = {
|
||||
clientId: 'someclientID',
|
||||
clientSecret: 'someclientSecret'
|
||||
@@ -28,12 +23,15 @@ const newClient = {
|
||||
}
|
||||
|
||||
describe('client', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const userController = new UserController()
|
||||
const clientController = new ClientController()
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
|
||||
@@ -10,30 +10,28 @@ import {
|
||||
readFile,
|
||||
deleteFolder,
|
||||
generateTimestamp,
|
||||
copy
|
||||
copy,
|
||||
createFolder,
|
||||
createFile,
|
||||
ServiceMember,
|
||||
FolderMember
|
||||
} from '@sasjs/utils'
|
||||
import * as fileUtilModules from '../../../utils/file'
|
||||
|
||||
const timestamp = generateTimestamp()
|
||||
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
||||
jest
|
||||
.spyOn(fileUtilModules, 'getTmpFolderPath')
|
||||
.spyOn(fileUtilModules, 'getSasjsRootFolder')
|
||||
.mockImplementation(() => tmpFolder)
|
||||
jest
|
||||
.spyOn(fileUtilModules, 'getTmpUploadsPath')
|
||||
.spyOn(fileUtilModules, 'getUploadsFolder')
|
||||
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||
|
||||
import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import { getTreeExample } from '../../../controllers/internal'
|
||||
import { FolderMember, ServiceMember } from '../../../types'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||
const { getTmpFilesFolderPath } = fileUtilModules
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
const { getFilesFolder } = fileUtilModules
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const user = {
|
||||
@@ -44,7 +42,8 @@ const user = {
|
||||
isActive: true
|
||||
}
|
||||
|
||||
describe('files', () => {
|
||||
describe('drive', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const controller = new UserController()
|
||||
@@ -52,6 +51,8 @@ describe('files', () => {
|
||||
let accessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
@@ -69,19 +70,25 @@ describe('files', () => {
|
||||
await mongoServer.stop()
|
||||
await deleteFolder(tmpFolder)
|
||||
})
|
||||
|
||||
describe('deploy', () => {
|
||||
const shouldFailAssertion = async (payload: any) => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/deploy')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(payload)
|
||||
.send({ appLoc: '/Public', fileTree: payload })
|
||||
|
||||
expect(res.statusCode).toEqual(400)
|
||||
expect(res.body).toEqual({
|
||||
status: 'failure',
|
||||
message: 'Provided not supported data format.',
|
||||
example: getTreeExample()
|
||||
})
|
||||
|
||||
if (payload === undefined) {
|
||||
expect(res.text).toEqual('"fileTree" is required')
|
||||
} else {
|
||||
expect(res.body).toEqual({
|
||||
status: 'failure',
|
||||
message: 'Provided not supported data format.',
|
||||
example: getTreeExample()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
it('should respond with payload example if valid payload was not provided', async () => {
|
||||
@@ -140,20 +147,21 @@ describe('files', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with payload example if valid payload was not provided', async () => {
|
||||
it('should successfully deploy if valid payload was provided', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/deploy')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ fileTree: getTreeExample() })
|
||||
.send({ appLoc: '/public', fileTree: getTreeExample() })
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.text).toEqual(
|
||||
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
||||
)
|
||||
await expect(folderExists(getTmpFilesFolderPath())).resolves.toEqual(true)
|
||||
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
|
||||
|
||||
const testJobFolder = path.join(
|
||||
getTmpFilesFolderPath(),
|
||||
getFilesFolder(),
|
||||
'public',
|
||||
'jobs',
|
||||
'extract'
|
||||
)
|
||||
@@ -166,17 +174,123 @@ describe('files', () => {
|
||||
|
||||
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('create', () => {
|
||||
it('should create a SAS file on drive having filePath as form field', async () => {
|
||||
const pathToUpload = `/my/path/code-1.sas`
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', '/my/path/code.sas')
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
@@ -186,10 +300,12 @@ describe('files', () => {
|
||||
})
|
||||
|
||||
it('should create a SAS file on drive having _filePath as query param', async () => {
|
||||
const pathToUpload = `/my/path/code-2.sas`
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: '/my/path/code1.sas' })
|
||||
.query({ _filePath: pathToUpload })
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
@@ -211,10 +327,10 @@ describe('files', () => {
|
||||
|
||||
it('should respond with Forbidden if file is already present', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
fileUtilModules.getFilesFolder(),
|
||||
pathToUpload
|
||||
)
|
||||
await copy(fileToAttachPath, pathToCopy)
|
||||
@@ -254,22 +370,22 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"filePath" is required`)
|
||||
expect(res.text).toEqual(`"_filePath" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.oth'
|
||||
const pathToUpload = '/my/path/code.exe'
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
// .field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Valid extensions for filePath: .sas')
|
||||
expect(res.text).toEqual('Invalid file extension')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -287,7 +403,7 @@ describe('files', () => {
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth')
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
@@ -297,16 +413,14 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`File extension '.oth' not acceptable. Valid extension(s): .sas`
|
||||
)
|
||||
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
@@ -316,11 +430,239 @@ describe('files', () => {
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 10 MB'
|
||||
'File size is over limit. File limit is: 100 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('update', () => {
|
||||
it('should update a SAS file on drive having filePath as form field', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getFilesFolder(),
|
||||
pathToUpload
|
||||
)
|
||||
await copy(fileToAttachPath, pathToCopy)
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should update a SAS file on drive having _filePath as query param', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getFilesFolder(),
|
||||
pathToUpload
|
||||
)
|
||||
await copy(fileToAttachPath, pathToCopy)
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({
|
||||
status: 'success'
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.field('filePath', '/my/path/code.sas')
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
.expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if file is not present', async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', `/my/path/code-3.sas`)
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/../path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if filePath is missing', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"_filePath" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.exe'
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: pathToUpload })
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Invalid file extension')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if file is missing', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('"file" is not present.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', attachedFile, 'another.sas')
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 100 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('get', () => {
|
||||
it('should get a SAS file on drive having _filePath as query param', async () => {
|
||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const fileToCopyContent = await readFile(fileToCopyPath)
|
||||
const filePath = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(fileUtilModules.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 { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
@@ -36,11 +31,14 @@ const userController = new UserController()
|
||||
const groupController = new GroupController()
|
||||
|
||||
describe('group', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
let adminAccessToken: string
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
|
||||
|
||||
20
api/src/routes/api/spec/info.spec.ts
Normal file
20
api/src/routes/api/spec/info.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Express } from 'express'
|
||||
import request from 'supertest'
|
||||
import appPromise from '../../../app'
|
||||
|
||||
describe('Info', () => {
|
||||
let app: Express
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
})
|
||||
|
||||
it('should should return configured information of the server instance', async () => {
|
||||
const res = await request(app).get('/SASjsApi/info').expect(200)
|
||||
|
||||
expect(res.body.mode).toEqual('server')
|
||||
expect(res.body.cors).toEqual('disable')
|
||||
expect(res.body.whiteList).toEqual([])
|
||||
expect(res.body.protocol).toEqual('http')
|
||||
})
|
||||
})
|
||||
@@ -6,34 +6,33 @@ import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
|
||||
let app: Express
|
||||
appPromise.then((_app) => {
|
||||
app = _app
|
||||
})
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
displayName: 'Test Admin',
|
||||
username: 'testAdminUsername',
|
||||
username: 'testadminusername',
|
||||
password: '12345678',
|
||||
isAdmin: true,
|
||||
isActive: true
|
||||
}
|
||||
const user = {
|
||||
displayName: 'Test User',
|
||||
username: 'testUsername',
|
||||
username: 'testusername',
|
||||
password: '87654321',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
autoExec: 'some sas code for auto exec;'
|
||||
}
|
||||
|
||||
const controller = new UserController()
|
||||
|
||||
describe('user', () => {
|
||||
let app: Express
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await appPromise
|
||||
|
||||
mongoServer = await MongoMemoryServer.create()
|
||||
con = await mongoose.connect(mongoServer.getUri())
|
||||
})
|
||||
@@ -66,6 +65,21 @@ describe('user', () => {
|
||||
expect(res.body.displayName).toEqual(user.displayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||
})
|
||||
|
||||
it('should respond with new user having username as lowercase', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/user')
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send({ ...user, username: user.username.toUpperCase() })
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.username).toEqual(user.username)
|
||||
expect(res.body.displayName).toEqual(user.displayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
@@ -244,7 +258,7 @@ describe('user', () => {
|
||||
const dbUser1 = await controller.createUser(user)
|
||||
const dbUser2 = await controller.createUser({
|
||||
...user,
|
||||
username: 'randomUser'
|
||||
username: 'randomuser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
@@ -362,7 +376,25 @@ describe('user', () => {
|
||||
await deleteAllUsers()
|
||||
})
|
||||
|
||||
it('should respond with user', async () => {
|
||||
it('should respond with user autoExec when same user requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const userId = dbUser.id
|
||||
const accessToken = await generateAndSaveToken(userId)
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/SASjsApi/user/${userId}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.username).toEqual(user.username)
|
||||
expect(res.body.displayName).toEqual(user.displayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||
})
|
||||
|
||||
it('should respond with user autoExec when admin user requests', async () => {
|
||||
const dbUser = await controller.createUser(user)
|
||||
const userId = dbUser.id
|
||||
|
||||
@@ -376,6 +408,7 @@ describe('user', () => {
|
||||
expect(res.body.displayName).toEqual(user.displayName)
|
||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
||||
})
|
||||
|
||||
it('should respond with 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.isAdmin).toEqual(user.isAdmin)
|
||||
expect(res.body.isActive).toEqual(user.isActive)
|
||||
expect(res.body.autoExec).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
|
||||
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',
|
||||
fileUploadController.preUploadMiddleware,
|
||||
fileUploadController.getMulterUploadObject().any(),
|
||||
async (req: any, res: any) => {
|
||||
async (req, res: any) => {
|
||||
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
||||
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
||||
|
||||
@@ -47,10 +47,11 @@ stpRouter.post(
|
||||
query?._program
|
||||
)
|
||||
|
||||
if (response instanceof Buffer) {
|
||||
res.writeHead(200, (req as any).sasHeaders)
|
||||
return res.end(response)
|
||||
}
|
||||
// TODO: investigate if this code is required
|
||||
// if (response instanceof Buffer) {
|
||||
// res.writeHead(200, (req as any).sasHeaders)
|
||||
// return res.end(response)
|
||||
// }
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -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 controller = new UserController()
|
||||
try {
|
||||
const response = await controller.getUser(userId)
|
||||
const response = await controller.getUser(req, parseInt(userId))
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
@@ -52,17 +52,17 @@ userRouter.patch(
|
||||
'/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdminIfNeeded,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
const { user } = req
|
||||
const { userId } = req.params
|
||||
|
||||
// only an admin can update `isActive` and `isAdmin` fields
|
||||
const { error, value: body } = updateUserValidation(req.body, user.isAdmin)
|
||||
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
const response = await controller.updateUser(userId, body)
|
||||
const response = await controller.updateUser(parseInt(userId), body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
@@ -74,17 +74,17 @@ userRouter.delete(
|
||||
'/:userId',
|
||||
authenticateAccessToken,
|
||||
verifyAdminIfNeeded,
|
||||
async (req: any, res) => {
|
||||
async (req, res) => {
|
||||
const { user } = req
|
||||
const { userId } = req.params
|
||||
|
||||
// only an admin can delete user without providing password
|
||||
const { error, value: data } = deleteUserValidation(req.body, user.isAdmin)
|
||||
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
const controller = new UserController()
|
||||
try {
|
||||
await controller.deleteUser(userId, data, user.isAdmin)
|
||||
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
|
||||
res.status(200).send('Account Deleted!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
|
||||
44
api/src/routes/appStream/appStreamHtml.ts
Normal file
44
api/src/routes/appStream/appStreamHtml.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { AppStreamConfig } from '../../types'
|
||||
import { style } from './style'
|
||||
|
||||
const defaultAppLogo = '/sasjs-logo.svg'
|
||||
|
||||
const singleAppStreamHtml = (
|
||||
streamServiceName: string,
|
||||
appLoc: string,
|
||||
logo?: string
|
||||
) =>
|
||||
` <a class="app" href="${streamServiceName}" title="${appLoc}">
|
||||
<img
|
||||
src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}"
|
||||
onerror="this.src = '${defaultAppLogo}';"
|
||||
/>
|
||||
${streamServiceName}
|
||||
</a>`
|
||||
|
||||
export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
|
||||
<html>
|
||||
<head>
|
||||
<base href="/AppStream/">
|
||||
${style}
|
||||
</head>
|
||||
<body>
|
||||
<h1>App Stream</h1>
|
||||
<div class="app-container">
|
||||
${Object.entries(appStreamConfig)
|
||||
.map(([streamServiceName, entry]) =>
|
||||
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
|
||||
)
|
||||
.join('')}
|
||||
<a class="app" title="Upload build.json">
|
||||
<input id="fileId" type="file" hidden />
|
||||
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
|
||||
<img src="/plus.png" />
|
||||
</button>
|
||||
<span id="uploadMessage">Upload New App</span>
|
||||
</a>
|
||||
</div>
|
||||
<script src="/axios.min.js"></script>
|
||||
<script src="/app-streams-script.js"></script>
|
||||
</body>
|
||||
</html>`
|
||||
67
api/src/routes/appStream/index.ts
Normal file
67
api/src/routes/appStream/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import { folderExists } from '@sasjs/utils'
|
||||
|
||||
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
||||
import { appStreamHtml } from './appStreamHtml'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const content = appStreamHtml(process.appStreamConfig)
|
||||
|
||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||
|
||||
return res.send(content)
|
||||
})
|
||||
|
||||
export const publishAppStream = async (
|
||||
appLoc: string,
|
||||
streamWebFolder: string,
|
||||
streamServiceName?: string,
|
||||
streamLogo?: string,
|
||||
addEntryToFile: boolean = true
|
||||
) => {
|
||||
const driveFilesPath = getFilesFolder()
|
||||
|
||||
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
||||
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
||||
if (!appLocPath.includes(driveFilesPath)) {
|
||||
throw new Error('appLoc cannot be outside drive.')
|
||||
}
|
||||
|
||||
const pathToDeployment = path.join(appLocPath, 'services', streamWebFolder)
|
||||
if (!pathToDeployment.includes(appLocPath)) {
|
||||
throw new Error('streamWebFolder cannot be outside appLoc.')
|
||||
}
|
||||
|
||||
if (await folderExists(pathToDeployment)) {
|
||||
const appCount = process.appStreamConfig
|
||||
? Object.keys(process.appStreamConfig).length
|
||||
: 0
|
||||
|
||||
if (!streamServiceName) {
|
||||
streamServiceName = `AppStreamName${appCount + 1}`
|
||||
}
|
||||
|
||||
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
|
||||
|
||||
addEntryToAppStreamConfig(
|
||||
streamServiceName,
|
||||
appLoc,
|
||||
streamWebFolder,
|
||||
streamLogo,
|
||||
addEntryToFile
|
||||
)
|
||||
|
||||
const sasJsPort = process.env.PORT || 5000
|
||||
console.log(
|
||||
'Serving Stream App: ',
|
||||
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
||||
)
|
||||
return { streamServiceName }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export default router
|
||||
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>`
|
||||
@@ -2,8 +2,18 @@ import { Express } from 'express'
|
||||
|
||||
import webRouter from './web'
|
||||
import apiRouter from './api'
|
||||
import appStreamRouter from './appStream'
|
||||
|
||||
import { csrfProtection } from '../app'
|
||||
|
||||
export const setupRoutes = (app: Express) => {
|
||||
app.use('/', webRouter)
|
||||
app.use('/SASjsApi', apiRouter)
|
||||
|
||||
app.use('/AppStream', csrfProtection, function (req, res, next) {
|
||||
// this needs to be a function to hook on
|
||||
// whatever the current router is
|
||||
appStreamRouter(req, res, next)
|
||||
})
|
||||
|
||||
app.use('/', csrfProtection, webRouter)
|
||||
}
|
||||
|
||||
@@ -1,34 +1,60 @@
|
||||
import { readFile } from '@sasjs/utils'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import { getWebBuildFolderPath } from '../../utils'
|
||||
import { WebController } from '../../controllers/web'
|
||||
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
|
||||
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||
|
||||
const webRouter = express.Router()
|
||||
const controller = new WebController()
|
||||
|
||||
const codeToInject = `
|
||||
<script>
|
||||
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
|
||||
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
|
||||
</script>`
|
||||
|
||||
webRouter.get('/', async (_, res) => {
|
||||
let content: string
|
||||
webRouter.get('/', async (req, res) => {
|
||||
let response
|
||||
try {
|
||||
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
|
||||
content = await readFile(indexHtmlPath)
|
||||
response = await controller.home()
|
||||
} catch (_) {
|
||||
return res.send('Web Build is not present')
|
||||
response = 'Web Build is not present'
|
||||
} finally {
|
||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||
|
||||
return res.send(response)
|
||||
}
|
||||
})
|
||||
|
||||
const { MODE } = process.env
|
||||
if (MODE?.trim() !== 'server') {
|
||||
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
|
||||
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
|
||||
const { error, value: body } = loginWebValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
return res.send(injectedContent)
|
||||
try {
|
||||
const response = await controller.login(req, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
return res.send(content)
|
||||
webRouter.post(
|
||||
'/SASLogon/authorize',
|
||||
desktopRestrict,
|
||||
authenticateAccessToken,
|
||||
async (req, res) => {
|
||||
const { error, value: body } = authorizeValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.authorize(req, body)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
webRouter.get('/logout', desktopRestrict, async (req, res) => {
|
||||
try {
|
||||
await controller.logout(req)
|
||||
res.status(200).send('OK!')
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
export default webRouter
|
||||
|
||||
@@ -4,8 +4,8 @@ import appPromise from './app'
|
||||
import { getCertificates } from './utils'
|
||||
|
||||
appPromise.then(async (app) => {
|
||||
const protocol = process.env.PROTOCOL ?? 'http'
|
||||
const sasJsPort = process.env.PORT ?? 5000
|
||||
const protocol = process.env.PROTOCOL || 'http'
|
||||
const sasJsPort = process.env.PORT || 5000
|
||||
|
||||
console.log('PROTOCOL: ', protocol)
|
||||
|
||||
|
||||
7
api/src/types/AppStreamConfig.ts
Normal file
7
api/src/types/AppStreamConfig.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface AppStreamConfig {
|
||||
[key: string]: {
|
||||
appLoc: string
|
||||
streamWebFolder: string
|
||||
streamLogo?: string
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
export interface FileTree {
|
||||
members: (FolderMember | ServiceMember)[]
|
||||
}
|
||||
|
||||
export enum MemberType {
|
||||
folder = 'folder',
|
||||
service = 'service'
|
||||
}
|
||||
|
||||
export interface FolderMember {
|
||||
name: string
|
||||
type: MemberType.folder
|
||||
members: (FolderMember | ServiceMember)[]
|
||||
}
|
||||
|
||||
export interface ServiceMember {
|
||||
name: string
|
||||
type: MemberType.service
|
||||
code: string
|
||||
}
|
||||
|
||||
export const isFileTree = (arg: any): arg is FileTree =>
|
||||
arg &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isFolderMember(member) && !isServiceMember(member)
|
||||
).length === 0
|
||||
|
||||
const isFolderMember = (arg: any): arg is FolderMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.folder &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isFolderMember(member) && !isServiceMember(member)
|
||||
).length === 0
|
||||
|
||||
const isServiceMember = (arg: any): arg is ServiceMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.service &&
|
||||
arg.code &&
|
||||
typeof arg.code === 'string'
|
||||
@@ -3,5 +3,5 @@ export interface PreProgramVars {
|
||||
userId: number
|
||||
displayName: string
|
||||
serverUrl: string
|
||||
accessToken: string
|
||||
httpHeaders: string[]
|
||||
}
|
||||
|
||||
7
api/src/types/Process.d.ts
vendored
7
api/src/types/Process.d.ts
vendored
@@ -1,7 +0,0 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
sasLoc: string
|
||||
driveLoc: string
|
||||
sessionController?: import('../controllers/internal').SessionController
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MacroVars } from '@sasjs/utils'
|
||||
|
||||
export interface ExecutionQuery {
|
||||
_program: string
|
||||
macroVars?: MacroVars
|
||||
_debug?: number
|
||||
}
|
||||
|
||||
export interface FileQuery {
|
||||
filePath: string
|
||||
}
|
||||
|
||||
export const isExecutionQuery = (arg: any): arg is ExecutionQuery =>
|
||||
arg && !Array.isArray(arg) && typeof arg._program === 'string'
|
||||
|
||||
export const isFileQuery = (arg: any): arg is FileQuery =>
|
||||
arg && !Array.isArray(arg) && typeof arg.filePath === 'string'
|
||||
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,8 +1,8 @@
|
||||
// TODO: uppercase types
|
||||
export * from './AppStreamConfig'
|
||||
export * from './Execution'
|
||||
export * from './FileTree'
|
||||
export * from './InfoJWT'
|
||||
export * from './PreProgramVars'
|
||||
export * from './Request'
|
||||
export * from './Session'
|
||||
export * from './TreeNode'
|
||||
export * from './RequestUser'
|
||||
|
||||
7
api/src/types/system/express-session.d.ts
vendored
Normal file
7
api/src/types/system/express-session.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import express from 'express'
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
loggedIn: boolean
|
||||
user: import('../').RequestUser
|
||||
}
|
||||
}
|
||||
7
api/src/types/system/express.d.ts
vendored
Normal file
7
api/src/types/system/express.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare namespace Express {
|
||||
export interface Request {
|
||||
accessToken?: string
|
||||
user?: import('../').RequestUser
|
||||
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
|
||||
}
|
||||
}
|
||||
89
api/src/utils/appStreamConfig.ts
Normal file
89
api/src/utils/appStreamConfig.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { createFile, fileExists, readFile } from '@sasjs/utils'
|
||||
import { publishAppStream } from '../routes/appStream'
|
||||
import { AppStreamConfig } from '../types'
|
||||
|
||||
import { getAppStreamConfigPath } from './file'
|
||||
|
||||
export const loadAppStreamConfig = async () => {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
|
||||
const appStreamConfigPath = getAppStreamConfigPath()
|
||||
|
||||
const content = (await fileExists(appStreamConfigPath))
|
||||
? await readFile(appStreamConfigPath)
|
||||
: '{}'
|
||||
|
||||
let appStreamConfig: AppStreamConfig
|
||||
try {
|
||||
appStreamConfig = JSON.parse(content)
|
||||
|
||||
if (!isValidAppStreamConfig(appStreamConfig)) throw 'invalid type'
|
||||
} catch (_) {
|
||||
appStreamConfig = {}
|
||||
}
|
||||
process.appStreamConfig = {}
|
||||
|
||||
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
|
||||
const { appLoc, streamWebFolder, streamLogo } = entry
|
||||
|
||||
publishAppStream(
|
||||
appLoc,
|
||||
streamWebFolder,
|
||||
streamServiceName,
|
||||
streamLogo,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
console.log('App Stream Config loaded!')
|
||||
}
|
||||
|
||||
export const addEntryToAppStreamConfig = (
|
||||
streamServiceName: string,
|
||||
appLoc: string,
|
||||
streamWebFolder: string,
|
||||
streamLogo?: string,
|
||||
addEntryToFile: boolean = true
|
||||
) => {
|
||||
if (streamServiceName && appLoc && streamWebFolder) {
|
||||
process.appStreamConfig[streamServiceName] = {
|
||||
appLoc,
|
||||
streamWebFolder,
|
||||
streamLogo
|
||||
}
|
||||
if (addEntryToFile) saveAppStreamConfig()
|
||||
}
|
||||
}
|
||||
|
||||
export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
|
||||
if (streamServiceName) {
|
||||
delete process.appStreamConfig[streamServiceName]
|
||||
saveAppStreamConfig()
|
||||
}
|
||||
}
|
||||
|
||||
const saveAppStreamConfig = async () => {
|
||||
const appStreamConfigPath = getAppStreamConfigPath()
|
||||
|
||||
try {
|
||||
await createFile(
|
||||
appStreamConfigPath,
|
||||
JSON.stringify(process.appStreamConfig, null, 2)
|
||||
)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const isValidAppStreamConfig = (config: any) => {
|
||||
if (config) {
|
||||
return !Object.entries(config).some(([streamServiceName, entry]) => {
|
||||
const { appLoc, streamWebFolder, streamLogo } = entry as any
|
||||
|
||||
return (
|
||||
typeof streamServiceName !== 'string' ||
|
||||
typeof appLoc !== 'string' ||
|
||||
typeof streamWebFolder !== 'string'
|
||||
)
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,25 +1,15 @@
|
||||
import mongoose from 'mongoose'
|
||||
import { populateClients } from '../routes/api/auth'
|
||||
import { seedDB } from './seedDB'
|
||||
|
||||
export const connectDB = async () => {
|
||||
// NOTE: when exporting app.js as agent for supertest
|
||||
// we should exclude connecting to the real database
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return
|
||||
} else {
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server') {
|
||||
console.log('Running in Destop Mode, no DB to connect.')
|
||||
return
|
||||
}
|
||||
|
||||
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
|
||||
if (err) throw err
|
||||
|
||||
console.log('Connected to db!')
|
||||
|
||||
await populateClients()
|
||||
})
|
||||
try {
|
||||
await mongoose.connect(process.env.DB_CONNECT as string)
|
||||
} catch (err) {
|
||||
throw new Error('Unable to connect to DB!')
|
||||
}
|
||||
|
||||
console.log('Connected to DB!')
|
||||
await seedDB()
|
||||
|
||||
return mongoose.connection
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
8
api/src/utils/desktopAutoExec.ts
Normal file
8
api/src/utils/desktopAutoExec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createFile, readFile } from '@sasjs/utils'
|
||||
import { getDesktopUserAutoExecPath } from './file'
|
||||
|
||||
export const getUserAutoExec = async (): Promise<string> =>
|
||||
readFile(getDesktopUserAutoExecPath())
|
||||
|
||||
export const updateUserAutoExec = async (autoExecContent: string) =>
|
||||
createFile(getDesktopUserAutoExecPath(), autoExecContent)
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
||||
export const apiRoot = path.join(__dirname, '..', '..')
|
||||
export const codebaseRoot = path.join(apiRoot, '..')
|
||||
@@ -9,24 +10,33 @@ export const sysInitCompiledPath = path.join(
|
||||
)
|
||||
|
||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||
|
||||
export const getWebBuildFolderPath = () =>
|
||||
path.join(codebaseRoot, 'web', 'build')
|
||||
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
|
||||
|
||||
export const getTmpFolderPath = () => process.driveLoc
|
||||
export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
|
||||
|
||||
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
|
||||
export const getDesktopUserAutoExecPath = () =>
|
||||
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
||||
|
||||
export const getTmpFilesFolderPath = () =>
|
||||
path.join(getTmpFolderPath(), 'files')
|
||||
export const getSasjsRootFolder = () => process.driveLoc
|
||||
|
||||
export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs')
|
||||
export const getAppStreamConfigPath = () =>
|
||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
||||
|
||||
export const getTmpWeboutFolderPath = () =>
|
||||
path.join(getTmpFolderPath(), 'webouts')
|
||||
export const getMacrosFolder = () =>
|
||||
path.join(getSasjsRootFolder(), 'sasjscore')
|
||||
|
||||
export const getTmpSessionsFolderPath = () =>
|
||||
path.join(getTmpFolderPath(), 'sessions')
|
||||
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
||||
|
||||
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
||||
|
||||
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 = '') =>
|
||||
[
|
||||
|
||||
@@ -5,12 +5,12 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
||||
const isWindows = () => process.platform === 'win32'
|
||||
|
||||
export const getDesktopFields = async () => {
|
||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||
const { SAS_PATH } = process.env
|
||||
|
||||
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> => {
|
||||
|
||||
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,4 +1,7 @@
|
||||
export * from './appStreamConfig'
|
||||
export * from './connectDB'
|
||||
export * from './copySASjsCore'
|
||||
export * from './desktopAutoExec'
|
||||
export * from './extractHeaders'
|
||||
export * from './file'
|
||||
export * from './generateAccessToken'
|
||||
@@ -6,11 +9,16 @@ export * from './generateAuthCode'
|
||||
export * from './generateRefreshToken'
|
||||
export * from './getCertificates'
|
||||
export * from './getDesktopFields'
|
||||
export * from './getPreProgramVariables'
|
||||
export * from './instantiateLogger'
|
||||
export * from './isDebugOn'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
export * from './saveTokensInDB'
|
||||
export * from './seedDB'
|
||||
export * from './setProcessVariables'
|
||||
export * from './sleep'
|
||||
export * from './setupFolders'
|
||||
export * from './upload'
|
||||
export * from './validation'
|
||||
export * from './verifyEnvVariables'
|
||||
export * from './verifyTokenInDB'
|
||||
|
||||
7
api/src/utils/instantiateLogger.ts
Normal file
7
api/src/utils/instantiateLogger.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LogLevel, Logger } from '@sasjs/utils/logger'
|
||||
|
||||
export const instantiateLogger = () => {
|
||||
const logLevel = (process.env.LOG_LEVEL || LogLevel.Info) as LogLevel
|
||||
const logger = new Logger(logLevel)
|
||||
process.logger = logger
|
||||
}
|
||||
8
api/src/utils/isDebugOn.ts
Normal file
8
api/src/utils/isDebugOn.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ExecutionVars } from '../controllers/internal'
|
||||
|
||||
export const isDebugOn = (vars: ExecutionVars) => {
|
||||
const debugValue =
|
||||
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
|
||||
|
||||
return !!(debugValue && debugValue >= 131)
|
||||
}
|
||||
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 { getRealPath } from '@sasjs/utils'
|
||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||
|
||||
import { configuration } from '../../package.json'
|
||||
import { getDesktopFields } from '.'
|
||||
import { getDesktopFields, ModeType } from '.'
|
||||
|
||||
export const setProcessVariables = async () => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
process.driveLoc = path.join(process.cwd(), 'tmp')
|
||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
||||
return
|
||||
}
|
||||
|
||||
const { MODE } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server') {
|
||||
const { sasLoc, driveLoc } = await getDesktopFields()
|
||||
if (MODE === ModeType.Server) {
|
||||
process.sasLoc = process.env.SAS_PATH as string
|
||||
} else {
|
||||
const { sasLoc } = await getDesktopFields()
|
||||
|
||||
process.sasLoc = sasLoc
|
||||
process.driveLoc = driveLoc
|
||||
} else {
|
||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||
|
||||
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
||||
process.driveLoc = getRealPath(
|
||||
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
|
||||
)
|
||||
}
|
||||
|
||||
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('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 { 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
|
||||
* @param files array of files to be mapped
|
||||
@@ -12,10 +24,13 @@ import { listFilesInFolder } from '@sasjs/utils'
|
||||
export const makeFilesNamesMap = (files: MulterFile[]) => {
|
||||
if (!files) return null
|
||||
|
||||
const filesNamesMap: { [key: string]: string } = {}
|
||||
const filesNamesMap: FilenamesMap = {}
|
||||
|
||||
for (let file of files) {
|
||||
filesNamesMap[file.filename] = file.fieldname
|
||||
filesNamesMap[file.filename] = {
|
||||
fieldName: file.fieldname,
|
||||
originalName: file.originalname
|
||||
}
|
||||
}
|
||||
|
||||
return filesNamesMap
|
||||
@@ -28,17 +43,12 @@ export const makeFilesNamesMap = (files: MulterFile[]) => {
|
||||
* @returns generated sas code
|
||||
*/
|
||||
export const generateFileUploadSasCode = async (
|
||||
filesNamesMap: any,
|
||||
filesNamesMap: FilenamesMap,
|
||||
sasSessionFolder: string
|
||||
): Promise<string> => {
|
||||
let uploadSasCode = ''
|
||||
let fileCount = 0
|
||||
let uploadedFilesMap: {
|
||||
fileref: string
|
||||
filepath: string
|
||||
filename: string
|
||||
count: number
|
||||
}[] = []
|
||||
const uploadedFiles: UploadedFiles[] = []
|
||||
|
||||
const sasSessionFolderList: string[] = await listFilesInFolder(
|
||||
sasSessionFolder
|
||||
@@ -50,31 +60,32 @@ export const generateFileUploadSasCode = async (
|
||||
if (fileName.includes('req_file')) {
|
||||
fileCount++
|
||||
|
||||
uploadedFilesMap.push({
|
||||
uploadedFiles.push({
|
||||
fileref: `_sjs${fileCountString}`,
|
||||
filepath: `${sasSessionFolder}/${fileName}`,
|
||||
filename: filesNamesMap[fileName],
|
||||
originalName: filesNamesMap[fileName].originalName,
|
||||
fieldName: filesNamesMap[fileName].fieldName,
|
||||
count: fileCount
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\nfilename ${uploadedMap.fileref} "${uploadedMap.filepath}";`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\nfilename ${uploadedFile.fileref} "${uploadedFile.filepath}";`
|
||||
}
|
||||
|
||||
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedMap.count}=${uploadedMap.filepath};`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedFile.count}=${uploadedFile.originalName};`
|
||||
}
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedMap.count}=${uploadedMap.fileref};`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedFile.count}=${uploadedFile.fileref};`
|
||||
}
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\n%let _WEBIN_NAME${uploadedMap.count}=${uploadedMap.filename};`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\n%let _WEBIN_NAME${uploadedFile.count}=${uploadedFile.fieldName};`
|
||||
}
|
||||
|
||||
if (fileCount > 0) {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
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)
|
||||
|
||||
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||
|
||||
export const loginWebValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
username: usernameSchema.required(),
|
||||
password: passwordSchema.required()
|
||||
}).validate(data)
|
||||
|
||||
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
username: usernameSchema.required(),
|
||||
password: passwordSchema.required(),
|
||||
clientId: Joi.string().required()
|
||||
}).validate(data)
|
||||
|
||||
@@ -29,7 +35,8 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
|
||||
username: usernameSchema.required(),
|
||||
password: passwordSchema.required(),
|
||||
isAdmin: Joi.boolean(),
|
||||
isActive: Joi.boolean()
|
||||
isActive: Joi.boolean(),
|
||||
autoExec: Joi.string().allow('')
|
||||
}).validate(data)
|
||||
|
||||
export const deleteUserValidation = (
|
||||
@@ -51,7 +58,8 @@ export const updateUserValidation = (
|
||||
const validationChecks: any = {
|
||||
displayName: Joi.string().min(6),
|
||||
username: usernameSchema,
|
||||
password: passwordSchema
|
||||
password: passwordSchema,
|
||||
autoExec: Joi.string().allow('')
|
||||
}
|
||||
if (isAdmin) {
|
||||
validationChecks.isAdmin = Joi.boolean()
|
||||
@@ -66,29 +74,39 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
||||
clientSecret: Joi.string().required()
|
||||
}).validate(data)
|
||||
|
||||
export const getFileDriveValidation = (data: any): Joi.ValidationResult =>
|
||||
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
filePath: Joi.string().required()
|
||||
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
||||
streamServiceName: Joi.string(),
|
||||
streamWebFolder: Joi.string(),
|
||||
streamLogo: Joi.string(),
|
||||
fileTree: Joi.any().required()
|
||||
}).validate(data)
|
||||
|
||||
export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
|
||||
const filePathSchema = Joi.string()
|
||||
.custom((value, helpers) => {
|
||||
if (blockFileRegex.test(value)) return helpers.error('string.pattern.base')
|
||||
|
||||
return value
|
||||
})
|
||||
.required()
|
||||
.messages({
|
||||
'string.pattern.base': `Invalid file extension`
|
||||
})
|
||||
|
||||
export const fileBodyValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
filePath: Joi.string().required(),
|
||||
fileContent: Joi.string().required()
|
||||
filePath: filePathSchema
|
||||
}).validate(data)
|
||||
|
||||
export const uploadFileBodyValidation = (data: any): Joi.ValidationResult =>
|
||||
export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
filePath: Joi.string().pattern(/.sas$/).required().messages({
|
||||
'string.pattern.base': `Valid extensions for filePath: .sas`
|
||||
})
|
||||
_filePath: filePathSchema
|
||||
}).validate(data)
|
||||
|
||||
export const uploadFileParamValidation = (data: any): Joi.ValidationResult =>
|
||||
export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_filePath: Joi.string().pattern(/.sas$/).required().messages({
|
||||
'string.pattern.base': `Valid extensions for filePath: .sas`
|
||||
})
|
||||
_folderPath: Joi.string()
|
||||
}).validate(data)
|
||||
|
||||
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
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 { RequestUser } from '../types'
|
||||
|
||||
export const fetchLatestAutoExec = async (
|
||||
reqUser: RequestUser
|
||||
): Promise<RequestUser | undefined> => {
|
||||
const dbUser = await User.findOne({ id: reqUser.userId })
|
||||
|
||||
if (!dbUser) return undefined
|
||||
|
||||
return {
|
||||
userId: reqUser.userId,
|
||||
clientId: reqUser.clientId,
|
||||
username: dbUser.username,
|
||||
displayName: dbUser.displayName,
|
||||
isAdmin: dbUser.isAdmin,
|
||||
isActive: dbUser.isActive,
|
||||
autoExec: dbUser.autoExec
|
||||
}
|
||||
}
|
||||
|
||||
export const verifyTokenInDB = async (
|
||||
userId: number,
|
||||
clientId: string,
|
||||
token: string,
|
||||
tokenType: 'accessToken' | 'refreshToken'
|
||||
) => {
|
||||
): Promise<RequestUser | undefined> => {
|
||||
const dbUser = await User.findOne({ id: userId })
|
||||
|
||||
if (!dbUser) return undefined
|
||||
@@ -21,7 +40,8 @@ export const verifyTokenInDB = async (
|
||||
username: dbUser.username,
|
||||
displayName: dbUser.displayName,
|
||||
isAdmin: dbUser.isAdmin,
|
||||
isActive: dbUser.isActive
|
||||
isActive: dbUser.isActive,
|
||||
autoExec: dbUser.autoExec
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Info",
|
||||
"description": "Get Server Info"
|
||||
},
|
||||
{
|
||||
"name": "Session",
|
||||
"description": "Get Session information"
|
||||
@@ -42,6 +46,10 @@
|
||||
{
|
||||
"name": "CODE",
|
||||
"description": "Operations on SAS code"
|
||||
},
|
||||
{
|
||||
"name": "Web",
|
||||
"description": "Operations on Web"
|
||||
}
|
||||
],
|
||||
"yaml": true,
|
||||
|
||||
10612
package-lock.json
generated
10612
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",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.76",
|
||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||
"repository": "https://github.com/sasjs/server",
|
||||
"scripts": {
|
||||
"server": "npm run server:prepare && npm run server:start",
|
||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..",
|
||||
"server:start": "cd api && npm run start",
|
||||
"release": "standard-version",
|
||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
|
||||
"server:start": "cd api && npm run start:prod",
|
||||
"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-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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.3.1",
|
||||
"standard-version": "^9.3.2"
|
||||
"@semantic-release/changelog": "^6.0.1",
|
||||
"@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
|
||||
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
|
||||
cookie: connect.sid=s:G2DeFdKuWhnmTOsTHmTWrxAXPx2P6TLD.JyNLxfACC1w3NlFQFfL5chyxtrqbPYmS6iButRc1goE
|
||||
25
restClient/stp.rest
Normal file
25
restClient/stp.rest
Normal file
@@ -0,0 +1,25 @@
|
||||
### testing upload file example
|
||||
POST http://localhost:5000/SASjsApi/stp/execute/?_program=/Public/app/viya/services/editors/loadfile&table=DCCONFIG.MPE_X_TEST
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||
|
||||
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||
Content-Disposition: form-data; name="fileSome11"; filename="DCCONFIG.MPE_X_TEST.xlsx"
|
||||
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
|
||||
|
||||
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||
Content-Disposition: form-data; name="fileSome22"; filename="DCCONFIG.MPE_X_TEST.xlsx.csv"
|
||||
Content-Type: application/csv
|
||||
|
||||
_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM
|
||||
,0,this is dummy data 321,Option 1,42,1960-02-12,1960-01-01 00:00:42,00:00:42,3,44
|
||||
,1,more dummy data 123,Option 2,42,1960-02-12,1960-01-01 00:00:42,00:07:02,3,44
|
||||
,1039,39 bottles of beer on the wall,Option 1,0.8716847965827607,1962-05-30,1960-01-01 00:05:21,00:01:30,89,6
|
||||
,1045,45 bottles of beer on the wall,Option 1,0.7279699667021492,1960-03-24,1960-01-01 07:18:54,00:01:08,89,83
|
||||
,1047,47 bottles of beer on the wall,Option 1,0.6224654082313484,1961-06-07,1960-01-01 09:45:23,00:01:33,76,98
|
||||
,1048,48 bottles of beer on the wall,Option 1,0.0874847523344144,1962-03-01,1960-01-01 13:06:13,00:00:02,76,63
|
||||
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||
Content-Disposition: form-data; name="_debug"
|
||||
|
||||
131
|
||||
------WebKitFormBoundarynkYOqevUMKZrXeAy--
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user