1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +00:00

Compare commits

...

148 Commits

Author SHA1 Message Date
Saad Jutt
940f705f5d chore(release): 0.0.32 2022-03-14 06:26:38 +05:00
Saad Jutt
7a6e6c8bec feat(web): added delete option in Drive 2022-03-14 06:26:12 +05:00
Saad Jutt
67d200d817 chore(release): 0.0.31 2022-03-14 05:36:24 +05:00
Muhammad Saad
a0c27ea8d3 Merge pull request #83 from sasjs/issue-52
fix: added cookie for accessToken
2022-03-14 05:35:59 +05:00
Saad Jutt
3d583ff21d feat(drive): new route delete file api 2022-03-14 05:30:10 +05:00
Saad Jutt
7072e282b1 fix(drive): update file API is same as create file 2022-03-14 05:05:59 +05:00
Saad Jutt
145ac45036 fix(stp): return plain/text header for GET & debug 2022-03-14 04:42:30 +05:00
Saad Jutt
698180ab7e fix: added cookie for accessToken 2022-03-09 05:06:06 +05:00
Muhammad Saad
0f4e38d51d Merge pull request #81 from sasjs/issue-79
fix: show content of get file api
2022-03-07 01:29:40 +05:00
Saad Jutt
e76283daa4 chore: few improvements 2022-03-06 18:46:09 +05:00
Saad Jutt
6ab42ca486 fix: show content of get file api 2022-03-06 18:45:07 +05:00
Saad Jutt
fa4da7624b chore(release): 0.0.30 2022-03-06 03:02:37 +05:00
Muhammad Saad
9f5509d2d4 Merge pull request #77 from sasjs/issue-65
fix: macros are available Sessions with SASAUTOS
2022-03-06 02:44:38 +05:00
Saad Jutt
efaf38d303 fix: get file instead of it's content 2022-03-06 02:33:56 +05:00
Saad Jutt
95843fa4c7 fix: macros are available Sessions with SASAUTOS 2022-03-06 01:57:14 +05:00
Muhammad Saad
5ba7661a83 Merge pull request #75 from sasjs/improve-file-upload
Improve file upload
2022-03-02 18:56:28 +05:00
Saad Jutt
ed5c58e10e chore: added restclient sample requests 2022-02-28 23:39:03 +05:00
Saad Jutt
5fce7d8f71 chore: added some docs to file upload 2022-02-28 22:54:49 +05:00
Saad Jutt
feeec4eb14 fix(upload): added query param as well for filepath 2022-02-28 22:34:18 +05:00
Saad Jutt
8c1941a87b fix: improvement in flow of uploading 2022-02-28 22:12:39 +05:00
Muhammad Saad
765969db11 Merge pull request #74 from sasjs/issue-67
File uploading multi-part
2022-02-28 04:15:18 +05:00
Saad Jutt
e60f17268d fix: multi-part file upload + validations + specs 2022-02-28 04:06:13 +05:00
Saad Jutt
ce0a5e1229 fix: organized code for usage of multer 2022-02-28 04:04:30 +05:00
Saad Jutt
c5738792b0 chore: debugger for api 2022-02-28 04:03:00 +05:00
Muhammad Saad
94e036dd10 Merge pull request #73 from sasjs/issue-72
fix(stp): return json for webout
2022-02-26 02:46:07 +05:00
Saad Jutt
da375b8086 chore: lint fixes 2022-02-26 02:41:55 +05:00
Allan Bowe
7312763339 fix: updating docs 2022-02-22 19:35:58 +00:00
Saad Jutt
5005f203b8 fix(stp): return json for webout 2022-02-21 04:13:04 +05:00
Muhammad Saad
232a73fd17 Merge pull request #71 from sasjs/return-file-in-response
Return file in response
2022-02-20 05:24:48 +04:00
Saad Jutt
ef41691e40 fix(file): fixes response headers 2022-02-20 06:18:44 +05:00
Saad Jutt
3e6234e601 fix: return buffer in case of file response 2022-02-20 05:40:03 +05:00
Saad Jutt
0a4b202428 fix: hot fix for web component 2022-02-20 05:03:48 +05:00
Muhammad Saad
a11893ece1 Merge pull request #70 from sasjs/parse-log-to-array
feat: parse log to array
2022-02-20 03:54:19 +04:00
Saad Jutt
c5ad72c931 feat: parse log to array 2022-02-20 04:50:09 +05:00
Muhammad Saad
034f3173bd Merge pull request #69 from sasjs/route-code-fixes
fix: code api is updated return type
2022-02-20 03:13:19 +04:00
Saad Jutt
e2a6810e95 fix: code api is updated return type 2022-02-20 02:57:45 +05:00
Muhammad Saad
373d66f8af Merge pull request #66 from sasjs/set-response-headers
Set response headers
2022-02-18 07:08:45 +04:00
Saad Jutt
0b5f958f45 fix: minor changes 2022-02-18 08:03:01 +05:00
Saad Jutt
da899b90e2 fix: added http headers to /code api as well 2022-02-18 07:25:48 +05:00
Saad Jutt
2c4aa420b3 feat: set response headers provded by SAS Code execution 2022-02-18 07:22:11 +05:00
munja
cd32912379 chore: updating readme with env config options 2022-02-16 22:00:36 +00:00
munja
93dcb1753b chore(release): 0.0.29 2022-02-16 21:43:01 +00:00
Allan Bowe
35cf301905 Merge pull request #63 from sasjs/issue58
fix: adding .. in folder path
2022-02-16 23:42:45 +02:00
Allan Bowe
5931fc1e71 fix: adding .. in folder path 2022-02-16 21:41:25 +00:00
Allan Bowe
18d845799c Merge pull request #59 from sasjs/issue58
fix: adding sasjs stpsrv_header() path to autoexec.  Relates to #58
2022-02-16 23:22:53 +02:00
munja
8c872bde92 chore(release): 0.0.28 2022-02-16 21:14:53 +00:00
Allan Bowe
f953472efd Merge pull request #62 from sasjs/headerfix
feat: default macros and bumping core
2022-02-16 23:07:31 +02:00
munja
f10138b0f2 fix: moving core 2022-02-16 21:05:22 +00:00
munja
6f19d3d0ea feat: default macros and bumping core 2022-02-16 21:03:16 +00:00
munja
a7facb005a chore: updating README with correct clientid 2022-02-16 14:52:56 +00:00
munja
88acf9df5d chore(release): 0.0.27 2022-02-16 14:00:34 +00:00
Allan Bowe
b0880b142a Merge pull request #60 from sasjs/errfix
feat: removing stpsrv_header and updating README with auth details
2022-02-16 15:59:56 +02:00
munja
d3674c7f94 feat: removing stpsrv_header and updating README with auth details 2022-02-16 13:19:50 +00:00
Saad Jutt
adccca6c7f chore: updated README.md 2022-02-15 21:53:54 +05:00
Yury Shkoda
8b83ccc4c2 Merge pull request #51 from sasjs/cli-issue-1108
feat: return json response with the log ob job execution
2022-02-15 11:08:17 +03:00
Yury Shkoda
556944b1d5 chore(git): Merge remote-tracking branch 'origin/main' into cli-issue-1108 2022-02-15 10:25:38 +03:00
Saad Jutt
b14e07ee6e chore(release): 0.0.26 2022-02-15 04:09:55 +05:00
Muhammad Saad
048bd9f78c Merge pull request #57 from sasjs/final-release-should-also-has-https-server
fix: release should also has https protocol
2022-02-15 03:07:17 +04:00
Saad Jutt
d7e1aca7e3 fix: refactored + removed unused package 2022-02-15 04:04:38 +05:00
Saad Jutt
de47d78a00 chore: Merge branch 'main' into final-release-should-also-has-https-server 2022-02-15 03:12:28 +05:00
JahanzaibRao15
58b6f439b3 Fixed vulnerabilities and remove unused or redundant dependencies (#55)
* chore: fix vulnerabilities and remove unused or redundant dependencies

* chore: Added start script and vulnerabilities screenshot

* fix: quick fix

* fix: converted to typescript + bugs removed

Co-authored-by: jahanzaibrao-dev <jahanzaib@tetrahex.com>
Co-authored-by: Saad Jutt <ihsan.distranger@gmail.com>
2022-02-15 03:11:03 +05:00
munja
ce9bde5717 fix: adding sasjs stpsrv_header() path to autoexec. Relates to #58 2022-02-14 18:49:35 +00:00
Saad Jutt
0cfe724ffa fix: release should also has https protocol 2022-02-14 19:12:37 +05:00
Yury Shkoda
fde4bc051d chore(execution): roll back changes related to returnLog var 2022-02-14 15:43:56 +03:00
Muhammad Saad
367b0f1f89 Merge pull request #54 from sasjs/token-expiry-updated
fix: updated token expiry times
2022-02-11 21:05:31 +04:00
Saad Jutt
d17a3dd590 fix: updated token expiry times 2022-02-11 21:54:27 +05:00
munja
bee5deed2a chore(release): 0.0.25 2022-02-11 17:30:39 +01:00
Muhammad Saad
e6e46838b3 Merge pull request #53 from sasjs/corebump
fix: adding global macvar and bumping sasjs/core with additional server support
2022-02-11 20:28:16 +04:00
munja
404f1ec059 fix: adding global macvar and bumping sasjs/core with additional server support 2022-02-11 17:24:55 +01:00
munja
09d36bc754 chore(release): 0.0.24 2022-02-11 12:53:06 +01:00
Muhammad Saad
3722bbaec3 Merge pull request #52 from sasjs/forever
chore(readme): forever package
2022-02-11 15:43:23 +04:00
munja
480ee4da83 fix: removing sysmacdelete 2022-02-11 12:11:34 +01:00
munja
dd853fe13b chore(docs): adding contributing 2022-02-11 12:05:05 +01:00
munja
e1142a33a0 chore: automated commit 2022-02-11 11:27:47 +01:00
munja
d4e8d91cae chore(readme): forever package 2022-02-11 10:47:40 +01:00
Saad Jutt
9a74ec545d chore: docker fix for SAS executable 2022-02-11 14:30:25 +05:00
Yury Shkoda
f2000a1227 chore: fix typos and remove unused code 2022-02-10 09:07:14 +03:00
Yury Shkoda
bf5767eadf feat(stp-execution): add returnLog option to execution query 2022-02-10 09:06:29 +03:00
Saad Jutt
e3f5206758 chore(release): 0.0.23 2022-02-08 21:46:00 +05:00
Saad Jutt
fffd21b348 chore: quick fixes 2022-02-08 21:45:56 +05:00
Saad Jutt
2d74ef5e12 chore(release): 0.0.22 2022-02-08 21:45:41 +05:00
munja
224743a439 Merge branch 'main' of github.com:sasjs/server 2022-02-01 15:19:56 +01:00
munja
f39a76da17 chore(release): 0.0.21 2022-02-01 15:19:36 +01:00
Allan Bowe
6107d02c8e Merge pull request #49 from sasjs/autoexecfix
fix: adding missing global vars to autoexec
2022-02-01 16:18:38 +02:00
munja
1966b17f27 fix: adding missing global vars to autoexec 2022-02-01 15:14:31 +01:00
Allan Bowe
87c8aa5146 Merge pull request #47 from sasjs/fixnot
fix: avoid uninitialised note
2022-01-26 18:30:21 +02:00
munja
e4c027ad51 fix: avoid uninitialised note 2022-01-26 17:27:08 +01:00
munja
083355fdba chore(release): 0.0.20 2022-01-20 14:03:05 +01:00
munja
a3b57f6e28 fix: fixing versioning blooper 2022-01-20 14:03:00 +01:00
munja
b0ffa145bc chore(release): 0.0.2 2022-01-20 13:57:35 +01:00
munja
a8df5f4afd fix: bumping core version 2022-01-20 13:57:31 +01:00
munja
62de960e86 chore(release): 0.0.19 2022-01-20 10:39:34 +01:00
munja
31532c0efa fix: bumping sasjs/core and updating descriptions 2022-01-20 10:37:22 +01:00
Allan Bowe
732230524d Merge pull request #46 from sasjs/allanbowe-patch-1
Update README.md
2022-01-20 11:06:25 +02:00
Allan Bowe
6dc281313e Update README.md 2022-01-19 22:34:41 +00:00
munja
92db3c7c82 chore(release): 0.0.18 2022-01-08 20:24:57 +01:00
munja
d8b75a47d3 fix: compressing release files for faster download times 2022-01-08 20:22:22 +01:00
Saad Jutt
d70fc1032f chore(release): 0.0.17 2022-01-08 02:15:38 +05:00
Muhammad Saad
794ee8f6e0 Merge pull request #45 from sasjs/hot-fix
fix: bug removed, log is clean now
2022-01-08 01:11:04 +04:00
Saad Jutt
43769e711d fix: bug removed, log is clean now 2022-01-08 00:42:32 +05:00
Saad Jutt
30528a1528 chore(release): 0.0.16 2022-01-07 16:47:01 +05:00
Muhammad Saad
b7e1753d25 Merge pull request #44 from sasjs/cors-update
Cors update
2022-01-07 15:46:24 +04:00
Saad Jutt
9c5772a303 chore: clean up 2022-01-07 16:42:38 +05:00
Saad Jutt
7a3d710153 fix: session should be marked as consumed 2022-01-07 16:34:46 +05:00
Saad Jutt
0a6ebe6e62 chore: debugging 2022-01-07 15:38:11 +05:00
Saad Jutt
6cbc657da3 fix: recreate crashed session 2022-01-06 23:12:44 +05:00
Saad Jutt
cd838915fd fix: added sas9 server address 2022-01-06 18:40:23 +05:00
Saad Jutt
4e486fda69 chore(release): 0.0.15 2022-01-06 17:23:29 +05:00
Muhammad Saad
79cac53fdb Merge pull request #42 from sasjs/run-sas-code-route
Run sas code route
2022-01-06 16:23:07 +04:00
Saad Jutt
450d99f06e fix(web): sticky tabs on Studio + extra run code button removed 2022-01-05 17:41:17 +05:00
Saad Jutt
51ee8c0825 fix(web): autosave and autofocus 2021-12-30 12:56:23 +05:00
Saad Jutt
a1151606f2 fix(web): parsing of webout 2021-12-30 12:18:48 +05:00
Saad Jutt
38193c83dd chore: Merge branch 'main' into run-sas-code-route 2021-12-30 12:18:16 +05:00
Muhammad Saad
59ecc36f2b Merge pull request #43 from sasjs/allanbowe-patch-1
Update home.tsx
2021-12-30 12:07:14 +05:00
Allan Bowe
8bc459c9a7 Update home.tsx 2021-12-29 19:15:21 +00:00
Saad Jutt
f1f1e47f76 chore(web): display webout as well 2021-12-29 01:00:53 +05:00
Saad Jutt
679e9de245 chore: return webout and log seperately 2021-12-29 00:27:54 +05:00
Saad Jutt
f0ac996b3c chore(web): build fix 2021-12-28 22:18:18 +05:00
Saad Jutt
2d77222ae8 fix(studio): web component updated 2021-12-28 22:02:11 +05:00
Saad Jutt
e6e5a5fd64 chore(code): updated route to code/execute 2021-12-21 14:24:27 +05:00
Saad Jutt
e1eb04494a fix: updated route for sas code 2021-12-21 12:36:58 +05:00
Allan Bowe
b7fa8e5f80 Merge pull request #40 from sasjs/fix-specs
chore: added driveLoc for specs
2021-12-20 11:39:55 +02:00
Saad Jutt
ef4fae4496 chore: added driveLoc for specs 2021-12-20 10:47:47 +05:00
munja
3e5a4e0555 chore(release): 0.0.14 2021-12-19 11:35:08 +00:00
Allan Bowe
cf9a8091ea Merge pull request #39 from sasjs/master
chore: updating package.json
2021-12-19 13:33:41 +02:00
munja
0edc45dd0a chore: updating package.json 2021-12-19 11:32:48 +00:00
munja
ceca370e27 fix: switch to main branch 2021-12-19 11:30:27 +00:00
Allan Bowe
f235b9c2f9 Merge pull request #38 from sasjs/weboutfix
fix: actually a README change, the fix was in the previous commit (up…
2021-12-18 23:24:57 +02:00
Allan Bowe
d86c841f1f fix: actually a README change, the fix was in the previous commit (updating ms_webout) that should have been a PR, to trigger a release 2021-12-18 20:31:40 +00:00
Allan Bowe
076b866c02 fix: bumping sasjs/core with adjustment to ms_webout() 2021-12-18 20:26:07 +00:00
Saad Jutt
19d4430b31 chore(release): 0.0.13 2021-12-16 16:13:05 +05:00
Saad Jutt
e5be0e6789 fix: output for Studio 2021-12-16 12:44:57 +05:00
Saad Jutt
27129a8921 feat(studio): run selected code + open in studio 2021-12-16 12:14:32 +05:00
Saad Jutt
da11c03d55 chore: sasjs/core version bumped 2021-12-15 19:57:09 +05:00
Saad Jutt
4fbdda0365 chore: .env.example updated 2021-12-15 18:58:04 +05:00
Saad Jutt
efacb1e916 chore(release): 0.0.12 2021-12-15 18:24:17 +05:00
Saad Jutt
d19ce253b4 fix: use env if provided for desktop mode 2021-12-15 18:24:04 +05:00
Saad Jutt
e11a4b66e7 chore(release): 0.0.11 2021-12-15 17:51:42 +05:00
Muhammad Saad
d0a1457f44 feat: added authorization route for web (#37) 2021-12-15 17:51:19 +05:00
Saad Jutt
34e54934fd chore(release): 0.0.10 2021-12-07 16:04:07 +05:00
Allan Bowe
4873e6054f Merge pull request #36 from sasjs/check-session-token-validity
chore: added check session + code refactor
2021-12-07 08:49:00 +00:00
Saad Jutt
b00aa4e17b chore: additional info on SAS session crash 2021-12-07 10:37:36 +05:00
Saad Jutt
9fccfe6f35 chore: Merge branch 'master' into check-session-token-validity 2021-12-07 10:37:02 +05:00
Saad Jutt
0a9d734e09 chore(release): 0.0.9 2021-12-07 10:35:56 +05:00
Muhammad Saad
a0822e6b61 fix: release with files (#35) 2021-12-07 10:33:11 +05:00
Saad Jutt
43545fa04b chore: added check session + code refactor 2021-12-05 08:17:54 +05:00
Saad Jutt
a80e5c8ead chore(release): 0.0.3 2021-11-30 16:32:52 +05:00
Saad Jutt
c8634953ca chore: trigger release 2021-11-30 16:29:30 +05:00
85 changed files with 13246 additions and 38311 deletions

View File

@@ -1,4 +1,5 @@
SAS_EXEC=<path to folder containing SAS executable 'sas'>
SAS_EXEC_PATH=<path to folder containing SAS executable>
SAS_EXEC_NAME=<name of SAS executable file>
PORT_API=<port for sasjs server (api)>
PORT_WEB=<port for sasjs web component(react)>
ACCESS_TOKEN_SECRET=<secret>

115
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,115 @@
# CONTRIBUTING
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
## Configuration
Configuration is made in the `configuration` section of `package.json`:
- Provide path to SAS9 executable.
### Using dockers:
There is `.env.example` file present at root of the project. [for Production]
There is `.env.example` file present at `./api` of the project. [for Development]
There is `.env.example` file present at `./web` of the project. [for Development]
Remember to provide enviornment variables.
#### Development
Command to run docker for development:
```
docker-compose up -d
```
It uses default docker compose file i.e. `docker-compose.yml` present at root.
It will build following images if running first time:
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
- `sasjs_server_web` - image for sasjs web component app based on _ReactJS_
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
#### Production
Command to run docker for production:
```
docker-compose -f docker-compose.prod.yml up -d
```
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
It will build following images if running first time:
- `sasjs_server_prod` - image for sasjs server app containing api and web component's build served at route `/`
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
### Using node:
#### Development (running api and web seperately):
##### API
Navigate to `./api`
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
```
npm install
npm start
```
##### Web
Navigate to `./web`
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
```
npm install
npm start
```
#### Development (running only api server and have web build served):
##### API server also serving Web build files
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm start
```
#### Production
##### API & WEB
```
npm run server
```
This will install/build `web` and install `api`, then start prod server.
## Executables
Command to generate executables
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm run exe
```
This will install/build web app and install/create executables of sasjs server at root `./executables`

View File

@@ -2,8 +2,8 @@ name: SASjs Server Executable Release
on:
push:
branches:
- master
tags:
- 'v*.*.*'
jobs:
release:
@@ -32,19 +32,17 @@ jobs:
env:
CI: true
- name: Create Tag with Release
uses: Klemensas/action-autotag@stable
with:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Checkout again with new Tag
uses: actions/checkout@v2
- name: Compress Executables
working-directory: ./executables
run: |
zip linux.zip api-linux
zip macos.zip api-macos
zip windows.zip api-win.exe
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
./executables/api-linux
./executables/api-macos
./executables/api-win.exe
token: ${{ secrets.GH_TOKEN }}
./executables/linux.zip
./executables/macos.zip
./executables/windows.zip

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@ sas/
tmp/
build/
sasjsbuild/
sasjscore/
certificates/
executables/
.env

10
.gitpod.yml Normal file
View File

@@ -0,0 +1,10 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
# and commit this file to your remote git repository to share the goodness with others.
tasks:
- init: npm install
vscode:
extensions:
- dbaeumer.vscode-eslint
- sasjs.sasjs-for-vscode

375
CHANGELOG.md Normal file
View File

@@ -0,0 +1,375 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.0.32](https://github.com/sasjs/server/compare/v0.0.31...v0.0.32) (2022-03-14)
### Features
* **web:** added delete option in Drive ([7a6e6c8](https://github.com/sasjs/server/commit/7a6e6c8becab31410d0a36bcc22e13d5359a6cdf))
### [0.0.31](https://github.com/sasjs/server/compare/v0.0.30...v0.0.31) (2022-03-14)
### Features
* **drive:** new route delete file api ([3d583ff](https://github.com/sasjs/server/commit/3d583ff21d344a71aa861c7e5b1426ebc2d54c22))
### Bug Fixes
* added cookie for accessToken ([698180a](https://github.com/sasjs/server/commit/698180ab7e44d67d46c84352ececca5b6c83b230))
* **drive:** update file API is same as create file ([7072e28](https://github.com/sasjs/server/commit/7072e282b1cd1a296d81512c57130237610c1c1e))
* show content of get file api ([6ab42ca](https://github.com/sasjs/server/commit/6ab42ca4868366874f5f21bd711b7b8b72e36774))
* **stp:** return plain/text header for GET & debug ([145ac45](https://github.com/sasjs/server/commit/145ac450365ed39279248ec9321bbe4918bee9fa))
### [0.0.30](https://github.com/sasjs/server/compare/v0.0.29...v0.0.30) (2022-03-05)
### Features
* parse log to array ([c5ad72c](https://github.com/sasjs/server/commit/c5ad72c931ec8fbd7d5a6475838adcbd380c8aee))
* set response headers provded by SAS Code execution ([2c4aa42](https://github.com/sasjs/server/commit/2c4aa420b3119890cafde4265ed5dddbc9d6a636))
### Bug Fixes
* added http headers to /code api as well ([da899b9](https://github.com/sasjs/server/commit/da899b90e26d5ee393eefc302be985eb7c9055a5))
* code api is updated return type ([e2a6810](https://github.com/sasjs/server/commit/e2a6810e9531a8102d3c51fd8df2e1f78f0d965f))
* **file:** fixes response headers ([ef41691](https://github.com/sasjs/server/commit/ef41691e408ef1c1c7a921cc1050bdd533651331))
* get file instead of it's content ([efaf38d](https://github.com/sasjs/server/commit/efaf38d3039391392ce0e14a3accddd8f34ea7d6))
* hot fix for web component ([0a4b202](https://github.com/sasjs/server/commit/0a4b202428e14effc8014a6813cecf7761ce3715))
* improvement in flow of uploading ([8c1941a](https://github.com/sasjs/server/commit/8c1941a87bc184be4e0e09eeff73fc6cb69e3041))
* macros are available Sessions with SASAUTOS ([95843fa](https://github.com/sasjs/server/commit/95843fa4c711aa695ee63ad265b8def4ba56360d))
* minor changes ([0b5f958](https://github.com/sasjs/server/commit/0b5f958f456d291ec7a8697236657c7819d5c654))
* multi-part file upload + validations + specs ([e60f172](https://github.com/sasjs/server/commit/e60f17268d1fa9ab623313026d46bd3f63756f69))
* organized code for usage of multer ([ce0a5e1](https://github.com/sasjs/server/commit/ce0a5e1229bed69c450061fac2bc19711448da56))
* return buffer in case of file response ([3e6234e](https://github.com/sasjs/server/commit/3e6234e6019c5f3ae4280fac079ecc9cb0effc07))
* **stp:** return json for webout ([5005f20](https://github.com/sasjs/server/commit/5005f203b8d6b1d577cdf094b83886bd1fc817a2))
* updating docs ([7312763](https://github.com/sasjs/server/commit/7312763339d6769826328561e2c8d11bbfc0c9f4))
* **upload:** added query param as well for filepath ([feeec4e](https://github.com/sasjs/server/commit/feeec4eb149e9a47e5a52320d1fc95243bf5eb15))
### [0.0.29](https://github.com/sasjs/server/compare/v0.0.28...v0.0.29) (2022-02-16)
### Bug Fixes
* adding .. in folder path ([5931fc1](https://github.com/sasjs/server/commit/5931fc1e712c545ef80454dea5b36e684017c367))
* adding sasjs stpsrv_header() path to autoexec. Relates to [#58](https://github.com/sasjs/server/issues/58) ([ce9bde5](https://github.com/sasjs/server/commit/ce9bde5717369de2d76dc183319be8830b2362b2))
### [0.0.28](https://github.com/sasjs/server/compare/v0.0.27...v0.0.28) (2022-02-16)
### Features
* default macros and bumping core ([6f19d3d](https://github.com/sasjs/server/commit/6f19d3d0ea3815815f246a3e455495c72c8604c7))
### Bug Fixes
* moving core ([f10138b](https://github.com/sasjs/server/commit/f10138b0f2005a958f63cb3a8351e1afa52f086a))
### [0.0.27](https://github.com/sasjs/server/compare/v0.0.26...v0.0.27) (2022-02-16)
### Features
* removing stpsrv_header and updating README with auth details ([d3674c7](https://github.com/sasjs/server/commit/d3674c7f9449d77977e482cd63ccdf7e974fa838))
* **stp-execution:** add returnLog option to execution query ([bf5767e](https://github.com/sasjs/server/commit/bf5767eadfb87f7ed902659347a18361a6a6c74b))
### [0.0.26](https://github.com/sasjs/server/compare/v0.0.25...v0.0.26) (2022-02-14)
### Bug Fixes
* refactored + removed unused package ([d7e1aca](https://github.com/sasjs/server/commit/d7e1aca7e33c3264c784d406fa766e29a6b15ae9))
* release should also has https protocol ([0cfe724](https://github.com/sasjs/server/commit/0cfe724ffa089b84a9f8bca49c9033b56f51c9cb))
* updated token expiry times ([d17a3dd](https://github.com/sasjs/server/commit/d17a3dd5900d5eb88120af8575e3fc7c2cb71ed6))
### [0.0.25](https://github.com/sasjs/server/compare/v0.0.24...v0.0.25) (2022-02-11)
### Bug Fixes
* adding global macvar and bumping sasjs/core with additional server support ([404f1ec](https://github.com/sasjs/server/commit/404f1ec0593a027ed5e84b1d6a84cb9f2d09d99e))
### [0.0.24](https://github.com/sasjs/server/compare/v0.0.23...v0.0.24) (2022-02-11)
### Bug Fixes
* removing sysmacdelete ([480ee4d](https://github.com/sasjs/server/commit/480ee4da831d2a89888c58ebec26bd89802ee2f5))
### [0.0.23](https://github.com/sasjs/server/compare/v0.0.22...v0.0.23) (2022-02-08)
### [0.0.22](https://github.com/sasjs/server/compare/v0.0.17...v0.0.22) (2022-02-08)
### Bug Fixes
* adding missing global vars to autoexec ([1966b17](https://github.com/sasjs/server/commit/1966b17f27e66bf1c9673ef6e1c11f4868b4f816))
* avoid uninitialised note ([e4c027a](https://github.com/sasjs/server/commit/e4c027ad5121302b9ae093b2b76dc27f51a94365))
* bumping core version ([a8df5f4](https://github.com/sasjs/server/commit/a8df5f4afd6c4522270d0a60ab8153dfbdf79e16))
* bumping sasjs/core and updating descriptions ([31532c0](https://github.com/sasjs/server/commit/31532c0efa41e53f87377a2c7c41d21c7909e3a0))
* compressing release files for faster download times ([d8b75a4](https://github.com/sasjs/server/commit/d8b75a47d305e0772ccbf8837ba4d7347b94cc93))
* fixing versioning blooper ([a3b57f6](https://github.com/sasjs/server/commit/a3b57f6e28448fe98e634383041a5633541c8c02))
### [0.0.21](https://github.com/sasjs/server/compare/v0.0.20...v0.0.21) (2022-02-01)
### Bug Fixes
* avoid uninitialised note ([e4c027a](https://github.com/sasjs/server/commit/e4c027ad5121302b9ae093b2b76dc27f51a94365))
### [0.0.20](https://github.com/sasjs/server/compare/v0.0.2...v0.0.20) (2022-01-20)
### Bug Fixes
* fixing versioning blooper ([a3b57f6](https://github.com/sasjs/server/commit/a3b57f6e28448fe98e634383041a5633541c8c02))
### [0.0.19](https://github.com/sasjs/server/compare/v0.0.18...v0.0.19) (2022-01-20)
### Bug Fixes
* bumping sasjs/core and updating descriptions ([31532c0](https://github.com/sasjs/server/commit/31532c0efa41e53f87377a2c7c41d21c7909e3a0))
### [0.0.18](https://github.com/sasjs/server/compare/v0.0.17...v0.0.18) (2022-01-08)
### Bug Fixes
* compressing release files for faster download times ([d8b75a4](https://github.com/sasjs/server/commit/d8b75a47d305e0772ccbf8837ba4d7347b94cc93))
### [0.0.17](https://github.com/sasjs/server/compare/v0.0.16...v0.0.17) (2022-01-07)
### Bug Fixes
* bug removed, log is clean now ([43769e7](https://github.com/sasjs/server/commit/43769e711d37a4f670786545630139a2d926dc76))
### [0.0.16](https://github.com/sasjs/server/compare/v0.0.15...v0.0.16) (2022-01-07)
### Bug Fixes
* added sas9 server address ([cd83891](https://github.com/sasjs/server/commit/cd838915fdb216ee364ea677747409311b1214fb))
* recreate crashed session ([6cbc657](https://github.com/sasjs/server/commit/6cbc657da3eb7fa821a678443a3ae4079c2a1f09))
* session should be marked as consumed ([7a3d710](https://github.com/sasjs/server/commit/7a3d710153f37d12160ff45f8f97fb4fcc75d684))
### [0.0.15](https://github.com/sasjs/server/compare/v0.0.14...v0.0.15) (2022-01-06)
### Bug Fixes
* **studio:** web component updated ([2d77222](https://github.com/sasjs/server/commit/2d77222ae8a139acd9d96466d0e68291c4ebd70e))
* updated route for sas code ([e1eb044](https://github.com/sasjs/server/commit/e1eb04494a5650726c95990f74fc719eced4ccb5))
* **web:** autosave and autofocus ([51ee8c0](https://github.com/sasjs/server/commit/51ee8c0825f021d1d67b2d765d5b434cbf248a1f))
* **web:** parsing of webout ([a115160](https://github.com/sasjs/server/commit/a1151606f21e0007e2b1ca1245d592d96866f62a))
* **web:** sticky tabs on Studio + extra run code button removed ([450d99f](https://github.com/sasjs/server/commit/450d99f06e5929eb1679e6203284e4faa44e19b0))
### [0.0.14](https://github.com/sasjs/server/compare/v0.0.13...v0.0.14) (2021-12-19)
### Bug Fixes
* actually a README change, the fix was in the previous commit (updating ms_webout) that should have been a PR, to trigger a release ([d86c841](https://github.com/sasjs/server/commit/d86c841f1fb94455ac3500f215a42b4acb8b0017))
* bumping sasjs/core with adjustment to ms_webout() ([076b866](https://github.com/sasjs/server/commit/076b866c020fb017512c2764801022a57fe4cca8))
* switch to main branch ([ceca370](https://github.com/sasjs/server/commit/ceca370e2757baf2e8ebb90dab6dfd27f7b990fc))
### [0.0.13](https://github.com/sasjs/server/compare/v0.0.12...v0.0.13) (2021-12-16)
### Features
* **studio:** run selected code + open in studio ([27129a8](https://github.com/sasjs/server/commit/27129a8921084c72968383fdbc2ecbd2f417456c))
### Bug Fixes
* output for Studio ([e5be0e6](https://github.com/sasjs/server/commit/e5be0e678965b05c64bcc8f55c48a366e0ff55a3))
### [0.0.12](https://github.com/sasjs/server/compare/v0.0.11...v0.0.12) (2021-12-15)
### Bug Fixes
* use env if provided for desktop mode ([d19ce25](https://github.com/sasjs/server/commit/d19ce253b4e2d2a7dd912d43a553d4c1bd60ba58))
### [0.0.11](https://github.com/sasjs/server/compare/v0.0.10...v0.0.11) (2021-12-15)
### Features
* added authorization route for web ([#37](https://github.com/sasjs/server/issues/37)) ([d0a1457](https://github.com/sasjs/server/commit/d0a1457f44a3d8993b57106e5e681c4e51fe8e7d))
### [0.0.10](https://github.com/sasjs/server/compare/v0.0.9...v0.0.10) (2021-12-07)
### [0.0.9](https://github.com/sasjs/server/compare/v0.0.3...v0.0.9) (2021-12-07)
### Bug Fixes
* release with files ([#35](https://github.com/sasjs/server/issues/35)) ([a0822e6](https://github.com/sasjs/server/commit/a0822e6b61905257475121ffd907fd1f79ed146b))
### [0.0.8](https://github.com/saadjutt01/server/compare/v0.0.7...v0.0.8) (2021-12-07)
### [0.0.7](https://github.com/saadjutt01/server/compare/v0.0.6...v0.0.7) (2021-12-07)
### [0.0.6](https://github.com/saadjutt01/server/compare/v0.0.5...v0.0.6) (2021-12-07)
### [0.0.5](https://github.com/saadjutt01/server/compare/v0.0.4...v0.0.5) (2021-12-07)
### 0.0.4 (2021-12-07)
### Features
* add api endpoint for sasjs drive ([96b5fef](https://github.com/saadjutt01/server/commit/96b5fef3021f67f66e5e3b854319230618421852))
* add new type TreeNode ([bc3cb7b](https://github.com/saadjutt01/server/commit/bc3cb7bb20a1202d17aaf8bbcddd1feef4fff724))
* add pug and directory tree dependencies ([3ffa168](https://github.com/saadjutt01/server/commit/3ffa168c8bafc989caf1a744cebc20d36c6aa11b))
* add sasjsExecutor controller ([279fbf2](https://github.com/saadjutt01/server/commit/279fbf2a9a0bd6bc0938f9a66e9685fb93d86089))
* add top app bar with tab navigation ([a506bc9](https://github.com/saadjutt01/server/commit/a506bc9dd9d201b89fc9ffd1a552c16bd170f058))
* add views and styles for rendering html ([a446f5c](https://github.com/saadjutt01/server/commit/a446f5c4f73a4e829a2c5eec041e3adffeddff52))
* adding _metaperson and _metauser to Stored Programs ([b3147ec](https://github.com/saadjutt01/server/commit/b3147ec680646b3d9c7e89152e472dddc8a36075))
* **api-utility:** create getWebBuildFolderPath utility ([9648c51](https://github.com/saadjutt01/server/commit/9648c51b5491d8b6bbe5497273efa2d11e2486d2))
* **api:** set up endpoint for sas code execution ([f6046b1](https://github.com/saadjutt01/server/commit/f6046b15ae30cd8ace685cf283339871de658b7d))
* authentication with jwt ([22dfcfd](https://github.com/saadjutt01/server/commit/22dfcfddb9abd355a63d1ee5acd925c759e86d69))
* compile systemInit and inject to autoExec ([b75139d](https://github.com/saadjutt01/server/commit/b75139dda5cacc7e10a4d635eb2a222f7dfa3fec))
* **deploy:** add appLoc ([f0f1e1d](https://github.com/saadjutt01/server/commit/f0f1e1d57ea1e961fc3b1cfcbd4cb259a77a90d0))
* **deploy:** add route to deploy a file tree to @sasjs/server ([b4bf72f](https://github.com/saadjutt01/server/commit/b4bf72f70401a81b6d5d0104332a1fbc5f71562b))
* **execute:** add macroVars to job execution ([39e486b](https://github.com/saadjutt01/server/commit/39e486b8cb5efbadc86eb7029b60c7073744eb2b))
* **execute:** add sas controller ([bf1db4d](https://github.com/saadjutt01/server/commit/bf1db4dd47d2488bac073cd468db920ff9fd533d))
* **execution:** add ExecutionController working with session ([8b25641](https://github.com/saadjutt01/server/commit/8b2564120def137f80647064e28062b880d58efe))
* **executor:** improved api response ([707b503](https://github.com/saadjutt01/server/commit/707b50394267217e717aa72f74dbeba3852a93e6))
* **executor:** response with webout ([52275ba](https://github.com/saadjutt01/server/commit/52275ba67d97d5cbdf6c5511c9bd789bd6ca6b4e))
* **express:** increase payload max size ([7b403c1](https://github.com/saadjutt01/server/commit/7b403c151e889cae975944546bb4bb53eff1dd26))
* frontend app for sasjs server ([db8eb8d](https://github.com/saadjutt01/server/commit/db8eb8dd7197bbe36f2d10cabbb58b3eb7ce7c33))
* generate executables for sasjs/server with web component ([514a262](https://github.com/saadjutt01/server/commit/514a262340dc34007de75caf08ad03969e7110c1))
* Groups are added + docs ([2fe9d5c](https://github.com/saadjutt01/server/commit/2fe9d5ca9ce1fb376f03534f8685d65efb2f68a6))
* improved deploy and execute endpoints ([5b4e562](https://github.com/saadjutt01/server/commit/5b4e5626fc7ae3e020819e3ebd334cc3712ae8e7))
* JWT saved in DB + logout api added ([46c5a75](https://github.com/saadjutt01/server/commit/46c5a75ac4fb26ebec219118eb204f1b5049ae90))
* **routes:** separate routes into web and api ([dabef59](https://github.com/saadjutt01/server/commit/dabef597287a59f3bfaff54a18de465f820aa514))
* **session:** add SessionController ([6a34fa1](https://github.com/saadjutt01/server/commit/6a34fa1b1dae07fe032352bea0644ab7a6f9c3f9))
* **session:** add SessionController and ExecutionController ([6e0b04a](https://github.com/saadjutt01/server/commit/6e0b04a6e548ac31baee726c9249b7e25f50f0bf))
* user operation apis added ([728f277](https://github.com/saadjutt01/server/commit/728f277f5ce136d62951071833cd6db478b07e4a))
### Bug Fixes
* **api-cdrive-oller:** throw erow error when file not found ([03d1d60](https://github.com/saadjutt01/server/commit/03d1d60660fc46421ef6ad9cee8493dd884e309a))
* change api endpoint SASjsExecutor/do -> SASjsApi/stp/execute ([d93673f](https://github.com/saadjutt01/server/commit/d93673f2a51098c6af8abc4a793081d4591e27de))
* cors enabled for desktop mode ([2bb10c7](https://github.com/saadjutt01/server/commit/2bb10c71661b5de7ed515c82e5b1967b88449972))
* DB names updates + refresh api is added ([9f17b17](https://github.com/saadjutt01/server/commit/9f17b17e3138ce49f24447cd5ae457e3e90ad4da))
* debug not passed ([d9555e1](https://github.com/saadjutt01/server/commit/d9555e151b0e1d1a4068efdf8ee9ed53b25b9b89))
* **deploy:** fix payload processing ([361b539](https://github.com/saadjutt01/server/commit/361b539271cf95bbe570cca9e44635ab563d3f9e))
* **deps:** removed malicious dependency ([c4b9402](https://github.com/saadjutt01/server/commit/c4b9402f017b76dc412a17a10313f1fd5a3891ef))
* **docker:** docker-compose for prod+development ([4a363c5](https://github.com/saadjutt01/server/commit/4a363c5b9796283199debcc8afa810c6f561f8e6))
* **executor:** create tmp files before execution ([cdbc3fd](https://github.com/saadjutt01/server/commit/cdbc3fd298e2a581773448bdddcad93de3b3544d))
* **executor:** fix nosplash argument and api response ([715b1de](https://github.com/saadjutt01/server/commit/715b1dec68377eefe03aa8203a73debe77842436))
* fix web route ([6c7a6b6](https://github.com/saadjutt01/server/commit/6c7a6b6c6af28c29b391162e4e332da6524b1c61))
* **github:** fixed github flow ([8dab288](https://github.com/saadjutt01/server/commit/8dab28861dfa7c4c7fefc7fe038df50f58d04547))
* **github:** removed npm token ([bbb94d6](https://github.com/saadjutt01/server/commit/bbb94d61ce39c84a6c0c44186e89787ab0e76a8c))
* immplementation of files api fixed ([299319e](https://github.com/saadjutt01/server/commit/299319e2dbe06c7ca99e403fcbdec2ad1db8b7e4))
* load file when url contains filePath ([99d5577](https://github.com/saadjutt01/server/commit/99d55775aaac3b2caaa4b10d4ed698f6cd7fcb2a))
* modify the directory tree algorithm to include relative path with each node ([91e2e2b](https://github.com/saadjutt01/server/commit/91e2e2bc4a46da0d149578593559efdb87681bd4))
* norefferer issue in home page external links fix ([e2b12b7](https://github.com/saadjutt01/server/commit/e2b12b74f52c3ce4541fde9af6af0093b56b157b))
* on clicking execute button open new tab for response ([02f5371](https://github.com/saadjutt01/server/commit/02f5371f57b311ff700ba8108f9d5168da8c22a4))
* prettier ([716ae81](https://github.com/saadjutt01/server/commit/716ae81d9293b42dd2a7047ac52d75401b3b8798))
* **prod-server:** use port from configuration ([4d8efbb](https://github.com/saadjutt01/server/commit/4d8efbb88d32154d84e80b79780e2e3de2f519e4))
* readme overview| ([b3342f0](https://github.com/saadjutt01/server/commit/b3342f00031d19080fb72e3460f023c5f44bac95))
* remove .sas extension from _program parameter at the end of string ([56cb2d1](https://github.com/saadjutt01/server/commit/56cb2d1d512beadb5cfdc4ab4034ac917311ff23))
* removing renegade dash ([4ff4d39](https://github.com/saadjutt01/server/commit/4ff4d39e954e895b46ddc3e2919f7f2c4e1ce01d))
* **root-package.json:** lint:fix command fixed in root package json ([ec6333f](https://github.com/saadjutt01/server/commit/ec6333f6aa67c1b94f54b017ed27eb3b21b4207f))
* **routes:** fix routes imports ([49c152a](https://github.com/saadjutt01/server/commit/49c152a398b60f6b0a0c25a68eb4c1c291984872))
* **semantic-release:** fixed package.json ([ef45787](https://github.com/saadjutt01/server/commit/ef45787019f1e61d0e4e2acee334236e8aca23cc))
* sending _webout as result object in response JSON ([b97523e](https://github.com/saadjutt01/server/commit/b97523e55584cc7d9d682cfeaab8f5b70a10b899))
* session refactoring with Saad & Allan ([cbe07b4](https://github.com/saadjutt01/server/commit/cbe07b4abb2e936037874af1a088cd038e0fc731))
* **ts:** enable files ([37b6936](https://github.com/saadjutt01/server/commit/37b6936cca3cff9c1ca26ec7b4b938a357c448df))
* update api calls from client side ([031e492](https://github.com/saadjutt01/server/commit/031e492d44674dec4f2b3bc1f5bf7affac5716bd))
* update api endpoints ([936a205](https://github.com/saadjutt01/server/commit/936a205e66073b9178089c6ab10d6ac3bf323c54))
* update sasjs drive controller from function base to class base ([3fe475d](https://github.com/saadjutt01/server/commit/3fe475d477c466556659b48c70eeac5153ff5b0e))
* update SASjsApi/stp/execute post api endpoints to capture url params ([d981444](https://github.com/saadjutt01/server/commit/d9814441bb1d269ec2404e50f51124f998c65c40))
* use hash router instead of browser router in react app ([c72867d](https://github.com/saadjutt01/server/commit/c72867d5a70550660c8c37220aa33693716a93f1))
* **web:** infinite call to api end point fixed ([ac745c8](https://github.com/saadjutt01/server/commit/ac745c8f5c3e4aa2ac8d6ca23bb1276452d4018b))
* **web:** remove unnecessary packages and files ([0fb4301](https://github.com/saadjutt01/server/commit/0fb43019668f5a13f6e77fdb4b3e543006b509c0))
* **weeb:** add catch block with each axios request ([552a358](https://github.com/saadjutt01/server/commit/552a3584ec9345bc1dec0ff5377bf773a7928d62))
* **workflow:** fix 'SASjs Server Build' ([174d94a](https://github.com/saadjutt01/server/commit/174d94a23c5036d61a4f2e11296283f128d4dafa))
### 0.0.3 (2021-11-30)
### Features
* add api endpoint for sasjs drive ([96b5fef](https://github.com/sasjs/server/commit/96b5fef3021f67f66e5e3b854319230618421852))
* add new type TreeNode ([bc3cb7b](https://github.com/sasjs/server/commit/bc3cb7bb20a1202d17aaf8bbcddd1feef4fff724))
* add pug and directory tree dependencies ([3ffa168](https://github.com/sasjs/server/commit/3ffa168c8bafc989caf1a744cebc20d36c6aa11b))
* add sasjsExecutor controller ([279fbf2](https://github.com/sasjs/server/commit/279fbf2a9a0bd6bc0938f9a66e9685fb93d86089))
* add top app bar with tab navigation ([a506bc9](https://github.com/sasjs/server/commit/a506bc9dd9d201b89fc9ffd1a552c16bd170f058))
* add views and styles for rendering html ([a446f5c](https://github.com/sasjs/server/commit/a446f5c4f73a4e829a2c5eec041e3adffeddff52))
* adding _metaperson and _metauser to Stored Programs ([b3147ec](https://github.com/sasjs/server/commit/b3147ec680646b3d9c7e89152e472dddc8a36075))
* **api-utility:** create getWebBuildFolderPath utility ([9648c51](https://github.com/sasjs/server/commit/9648c51b5491d8b6bbe5497273efa2d11e2486d2))
* **api:** set up endpoint for sas code execution ([f6046b1](https://github.com/sasjs/server/commit/f6046b15ae30cd8ace685cf283339871de658b7d))
* authentication with jwt ([22dfcfd](https://github.com/sasjs/server/commit/22dfcfddb9abd355a63d1ee5acd925c759e86d69))
* compile systemInit and inject to autoExec ([b75139d](https://github.com/sasjs/server/commit/b75139dda5cacc7e10a4d635eb2a222f7dfa3fec))
* **deploy:** add appLoc ([f0f1e1d](https://github.com/sasjs/server/commit/f0f1e1d57ea1e961fc3b1cfcbd4cb259a77a90d0))
* **deploy:** add route to deploy a file tree to @sasjs/server ([b4bf72f](https://github.com/sasjs/server/commit/b4bf72f70401a81b6d5d0104332a1fbc5f71562b))
* **execute:** add macroVars to job execution ([39e486b](https://github.com/sasjs/server/commit/39e486b8cb5efbadc86eb7029b60c7073744eb2b))
* **execute:** add sas controller ([bf1db4d](https://github.com/sasjs/server/commit/bf1db4dd47d2488bac073cd468db920ff9fd533d))
* **execution:** add ExecutionController working with session ([8b25641](https://github.com/sasjs/server/commit/8b2564120def137f80647064e28062b880d58efe))
* **executor:** improved api response ([707b503](https://github.com/sasjs/server/commit/707b50394267217e717aa72f74dbeba3852a93e6))
* **executor:** response with webout ([52275ba](https://github.com/sasjs/server/commit/52275ba67d97d5cbdf6c5511c9bd789bd6ca6b4e))
* **express:** increase payload max size ([7b403c1](https://github.com/sasjs/server/commit/7b403c151e889cae975944546bb4bb53eff1dd26))
* frontend app for sasjs server ([db8eb8d](https://github.com/sasjs/server/commit/db8eb8dd7197bbe36f2d10cabbb58b3eb7ce7c33))
* generate executables for sasjs/server with web component ([514a262](https://github.com/sasjs/server/commit/514a262340dc34007de75caf08ad03969e7110c1))
* Groups are added + docs ([2fe9d5c](https://github.com/sasjs/server/commit/2fe9d5ca9ce1fb376f03534f8685d65efb2f68a6))
* improved deploy and execute endpoints ([5b4e562](https://github.com/sasjs/server/commit/5b4e5626fc7ae3e020819e3ebd334cc3712ae8e7))
* JWT saved in DB + logout api added ([46c5a75](https://github.com/sasjs/server/commit/46c5a75ac4fb26ebec219118eb204f1b5049ae90))
* **routes:** separate routes into web and api ([dabef59](https://github.com/sasjs/server/commit/dabef597287a59f3bfaff54a18de465f820aa514))
* **session:** add SessionController ([6a34fa1](https://github.com/sasjs/server/commit/6a34fa1b1dae07fe032352bea0644ab7a6f9c3f9))
* **session:** add SessionController and ExecutionController ([6e0b04a](https://github.com/sasjs/server/commit/6e0b04a6e548ac31baee726c9249b7e25f50f0bf))
* user operation apis added ([728f277](https://github.com/sasjs/server/commit/728f277f5ce136d62951071833cd6db478b07e4a))
### Bug Fixes
* **api-cdrive-oller:** throw erow error when file not found ([03d1d60](https://github.com/sasjs/server/commit/03d1d60660fc46421ef6ad9cee8493dd884e309a))
* change api endpoint SASjsExecutor/do -> SASjsApi/stp/execute ([d93673f](https://github.com/sasjs/server/commit/d93673f2a51098c6af8abc4a793081d4591e27de))
* cors enabled for desktop mode ([2bb10c7](https://github.com/sasjs/server/commit/2bb10c71661b5de7ed515c82e5b1967b88449972))
* DB names updates + refresh api is added ([9f17b17](https://github.com/sasjs/server/commit/9f17b17e3138ce49f24447cd5ae457e3e90ad4da))
* debug not passed ([d9555e1](https://github.com/sasjs/server/commit/d9555e151b0e1d1a4068efdf8ee9ed53b25b9b89))
* **deploy:** fix payload processing ([361b539](https://github.com/sasjs/server/commit/361b539271cf95bbe570cca9e44635ab563d3f9e))
* **deps:** removed malicious dependency ([c4b9402](https://github.com/sasjs/server/commit/c4b9402f017b76dc412a17a10313f1fd5a3891ef))
* **docker:** docker-compose for prod+development ([4a363c5](https://github.com/sasjs/server/commit/4a363c5b9796283199debcc8afa810c6f561f8e6))
* **executor:** create tmp files before execution ([cdbc3fd](https://github.com/sasjs/server/commit/cdbc3fd298e2a581773448bdddcad93de3b3544d))
* **executor:** fix nosplash argument and api response ([715b1de](https://github.com/sasjs/server/commit/715b1dec68377eefe03aa8203a73debe77842436))
* fix web route ([6c7a6b6](https://github.com/sasjs/server/commit/6c7a6b6c6af28c29b391162e4e332da6524b1c61))
* **github:** fixed github flow ([8dab288](https://github.com/sasjs/server/commit/8dab28861dfa7c4c7fefc7fe038df50f58d04547))
* **github:** removed npm token ([bbb94d6](https://github.com/sasjs/server/commit/bbb94d61ce39c84a6c0c44186e89787ab0e76a8c))
* immplementation of files api fixed ([299319e](https://github.com/sasjs/server/commit/299319e2dbe06c7ca99e403fcbdec2ad1db8b7e4))
* load file when url contains filePath ([99d5577](https://github.com/sasjs/server/commit/99d55775aaac3b2caaa4b10d4ed698f6cd7fcb2a))
* modify the directory tree algorithm to include relative path with each node ([91e2e2b](https://github.com/sasjs/server/commit/91e2e2bc4a46da0d149578593559efdb87681bd4))
* norefferer issue in home page external links fix ([e2b12b7](https://github.com/sasjs/server/commit/e2b12b74f52c3ce4541fde9af6af0093b56b157b))
* on clicking execute button open new tab for response ([02f5371](https://github.com/sasjs/server/commit/02f5371f57b311ff700ba8108f9d5168da8c22a4))
* prettier ([716ae81](https://github.com/sasjs/server/commit/716ae81d9293b42dd2a7047ac52d75401b3b8798))
* **prod-server:** use port from configuration ([4d8efbb](https://github.com/sasjs/server/commit/4d8efbb88d32154d84e80b79780e2e3de2f519e4))
* readme overview| ([b3342f0](https://github.com/sasjs/server/commit/b3342f00031d19080fb72e3460f023c5f44bac95))
* remove .sas extension from _program parameter at the end of string ([56cb2d1](https://github.com/sasjs/server/commit/56cb2d1d512beadb5cfdc4ab4034ac917311ff23))
* removing renegade dash ([4ff4d39](https://github.com/sasjs/server/commit/4ff4d39e954e895b46ddc3e2919f7f2c4e1ce01d))
* **root-package.json:** lint:fix command fixed in root package json ([ec6333f](https://github.com/sasjs/server/commit/ec6333f6aa67c1b94f54b017ed27eb3b21b4207f))
* **routes:** fix routes imports ([49c152a](https://github.com/sasjs/server/commit/49c152a398b60f6b0a0c25a68eb4c1c291984872))
* **semantic-release:** fixed package.json ([ef45787](https://github.com/sasjs/server/commit/ef45787019f1e61d0e4e2acee334236e8aca23cc))
* sending _webout as result object in response JSON ([b97523e](https://github.com/sasjs/server/commit/b97523e55584cc7d9d682cfeaab8f5b70a10b899))
* session refactoring with Saad & Allan ([cbe07b4](https://github.com/sasjs/server/commit/cbe07b4abb2e936037874af1a088cd038e0fc731))
* **ts:** enable files ([37b6936](https://github.com/sasjs/server/commit/37b6936cca3cff9c1ca26ec7b4b938a357c448df))
* update api calls from client side ([031e492](https://github.com/sasjs/server/commit/031e492d44674dec4f2b3bc1f5bf7affac5716bd))
* update api endpoints ([936a205](https://github.com/sasjs/server/commit/936a205e66073b9178089c6ab10d6ac3bf323c54))
* update sasjs drive controller from function base to class base ([3fe475d](https://github.com/sasjs/server/commit/3fe475d477c466556659b48c70eeac5153ff5b0e))
* update SASjsApi/stp/execute post api endpoints to capture url params ([d981444](https://github.com/sasjs/server/commit/d9814441bb1d269ec2404e50f51124f998c65c40))
* use hash router instead of browser router in react app ([c72867d](https://github.com/sasjs/server/commit/c72867d5a70550660c8c37220aa33693716a93f1))
* **web:** infinite call to api end point fixed ([ac745c8](https://github.com/sasjs/server/commit/ac745c8f5c3e4aa2ac8d6ca23bb1276452d4018b))
* **web:** remove unnecessary packages and files ([0fb4301](https://github.com/sasjs/server/commit/0fb43019668f5a13f6e77fdb4b3e543006b509c0))
* **weeb:** add catch block with each axios request ([552a358](https://github.com/sasjs/server/commit/552a3584ec9345bc1dec0ff5377bf773a7928d62))
* **workflow:** fix 'SASjs Server Build' ([174d94a](https://github.com/sasjs/server/commit/174d94a23c5036d61a4f2e11296283f128d4dafa))

View File

@@ -1,5 +1,4 @@
FROM node:lts-alpine
RUN npm install -g @sasjs/cli
WORKDIR /usr/server/api
COPY ["package.json","package-lock.json", "./"]
RUN npm ci

136
README.md
View File

@@ -8,110 +8,94 @@ SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It
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)
## Configuration
Configuration is made in the `configuration` section of `package.json`:
When launching the app, it will make use of specific environment variables. These can be set in the following places:
- Provide path to SAS9 executable.
- 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
### Using dockers:
There is `.env.example` file present at root of the project. [for Production]
There is `.env.example` file present at `./api` of the project. [for Development]
There is `.env.example` file present at `./web` of the project. [for Development]
Remember to provide enviornment variables.
#### Development
Command to run docker for development:
Example variables:
```
docker-compose up -d
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
```
It uses default docker compose file i.e. `docker-compose.yml` present at root.
It will build following images if running first time:
## Desktop Version
- `sasjs_server_api` - image for sasjs api server app based on _ExpressJS_
- `sasjs_server_web` - image for sasjs web component app based on _ReactJS_
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
### Manual Installation
#### Production
Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
Command to run docker for production:
Next, trigger by double clicking (windows) or executing from commandline.
```
docker-compose -f docker-compose.prod.yml up -d
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
Fetch the relevant package from github using `curl`, eg as follows (for linux):
```bash
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
unzip linux.zip
```
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
It will build following images if running first time:
The app can then be launched with `./api-linux` and prompts followed (if ENV vars not set).
- `sasjs_server_prod` - image for sasjs server app containing api and web component's build served at route `/`
- `mongodb` - image for mongo database
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
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:
### Using node:
```bash
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
export PORT=5001
export DRIVE_PATH=./tmp
#### Development (running api and web seperately):
##### API
Navigate to `./api`
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
```
npm install
npm start
pm2 start api-linux
```
##### Web
To get the logs (and some usefull commands):
Navigate to `./web`
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
```
npm install
npm start
```bash
pm2 [list|ls|status]
pm2 logs
pm2 logs --lines 200
```
#### Development (running only api server and have web build served):
##### API server also serving Web build files
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
Managing processes:
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm start
pm2 restart app_name
pm2 reload app_name
pm2 stop app_name
pm2 delete app_name
```
#### Production
Instead of `app_name` you can pass:
##### API & WEB
- `all` to act on all processes
- `id` to act on a specific process id
```
npm run server
```
This will install/build `web` and install `api`, then start prod server.
## Server Version
## Executables
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
Command to generate executables
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm run exe
```
This will install/build web app and install/create executables of sasjs server at root `./executables`
* CLIENTID: `clientID1`
* USERNAME: `secretuser`
* PASSWORD: `secretpassword`

89
SASjsServer.drawio Normal file
View File

@@ -0,0 +1,89 @@
<mxfile host="65bd71144e">
<diagram id="HJy_QFGaI9JSrArARLup" name="Page-1">
<mxGraphModel dx="1908" dy="2140" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="4" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
<mxGeometry x="-360" y="-120" width="40" height="80" as="geometry"/>
</mxCell>
<mxCell id="7" value="SASjs Server" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;fontSize=30;" vertex="1" parent="1">
<mxGeometry x="30" y="-150" width="360" height="850" as="geometry"/>
</mxCell>
<mxCell id="8" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="28">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-340" y="23" as="sourcePoint"/>
<mxPoint x="115" y="22.586363636363558" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="11" value="&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px ; line-height: 18px&quot;&gt;&lt;span style=&quot;color: #a31515&quot;&gt;/SASjsApi/auth/authorize&lt;br&gt;(username,password,clientId)&lt;/span&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="8">
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="14" value="" style="edgeStyle=none;html=1;exitX=-0.002;exitY=0.874;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="28">
<mxGeometry relative="1" as="geometry">
<mxPoint x="110" y="80" as="sourcePoint"/>
<mxPoint x="-340" y="80" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="16" value="&lt;font color=&quot;#a31515&quot; face=&quot;menlo, monaco, courier new, monospace&quot;&gt;&lt;span style=&quot;font-size: 12px&quot;&gt;`code`&lt;/span&gt;&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="14">
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="21" value="End user" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontStyle=0" vertex="1" parent="1">
<mxGeometry x="-360" y="545" width="40" height="80" as="geometry"/>
</mxCell>
<mxCell id="22" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" target="30">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-340" y="165" as="sourcePoint"/>
<mxPoint x="115" y="165" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="23" value="&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px ; line-height: 18px&quot;&gt;&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; line-height: 18px&quot;&gt;&lt;span style=&quot;color: #a31515&quot;&gt;/SASjsApi/auth/token&lt;/span&gt;&lt;/div&gt;&lt;span style=&quot;color: #a31515&quot;&gt;(clientId,code)&lt;/span&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="22">
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="24" value="" style="edgeStyle=none;html=1;exitX=0.009;exitY=0.905;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="30">
<mxGeometry relative="1" as="geometry">
<mxPoint x="210" y="222.5" as="sourcePoint"/>
<mxPoint x="-340" y="223" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="25" value="&lt;font color=&quot;#a31515&quot; face=&quot;menlo, monaco, courier new, monospace&quot;&gt;&lt;span style=&quot;font-size: 12px&quot;&gt;`&lt;/span&gt;&lt;/font&gt;&lt;span style=&quot;color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px&quot;&gt;accessToken&lt;/span&gt;&lt;span style=&quot;font-size: 12px ; color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace&quot;&gt;` &amp;amp; `&lt;/span&gt;&lt;span style=&quot;color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px&quot;&gt;refreshToken&lt;/span&gt;&lt;span style=&quot;color: rgb(163 , 21 , 21) ; font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px&quot;&gt;`&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="24">
<mxGeometry x="0.1931" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="26" value="" style="endArrow=none;dashed=1;html=1;dashPattern=1 3;strokeWidth=2;" edge="1" parent="1" source="21" target="4">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="40" y="240" as="sourcePoint"/>
<mxPoint x="90" y="190" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="28" value="&lt;span&gt;Validates&lt;/span&gt;&lt;br&gt;&lt;span&gt;username/password/clientId&lt;/span&gt;&lt;br&gt;&lt;span&gt;and issue short&lt;/span&gt;&lt;br&gt;&lt;span&gt;Authorization code&lt;/span&gt;" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="115" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="30" value="Validates&lt;br&gt;clientId &amp;amp; authorization code&lt;br&gt;and issue&lt;br&gt;Access Token &amp;amp; Refresh Token" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="115" y="140" width="190" height="90" as="geometry"/>
</mxCell>
<mxCell id="32" value="Protected APIs&lt;br&gt;Authenticate requests &lt;br&gt;with provided Bearer Token" style="whiteSpace=wrap;html=1;verticalAlign=top;fontStyle=0;" vertex="1" parent="1">
<mxGeometry x="50" y="280" width="320" height="400" as="geometry"/>
</mxCell>
<mxCell id="33" value="" style="edgeStyle=none;html=1;entryX=0;entryY=0.373;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" target="32">
<mxGeometry relative="1" as="geometry">
<mxPoint x="-340" y="432.5" as="sourcePoint"/>
<mxPoint x="-10" y="430" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="34" value="&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; font-size: 12px ; line-height: 18px&quot;&gt;&lt;div style=&quot;font-family: &amp;#34;menlo&amp;#34; , &amp;#34;monaco&amp;#34; , &amp;#34;courier new&amp;#34; , monospace ; line-height: 18px&quot;&gt;&lt;font color=&quot;#a31515&quot;&gt;Request with Access Token&lt;/font&gt;&lt;/div&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="33">
<mxGeometry x="-0.1257" y="2" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@@ -1,8 +1,14 @@
MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable
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
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
DRIVE_PATH=./tmp

13
api/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": ["run-script", "start"],
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
}
]
}

9130
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,28 @@
{
"name": "api",
"version": "0.0.1",
"version": "0.0.2",
"description": "Api of SASjs server",
"main": "./src/server.ts",
"scripts": {
"initial": "npm run swagger && npm run compileSysInit",
"initial": "npm run swagger && npm run compileSysInit && npm run copySASjsCore",
"prestart": "npm run initial",
"prestart:prod": "npm run initial",
"prebuild": "npm run initial",
"start": "nodemon ./src/server.ts",
"start:prod": "nodemon ./src/prod-server.ts",
"build": "rimraf build && tsc",
"swagger": "tsoa spec",
"semantic-release": "semantic-release -d",
"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 web:copy",
"exe:copy": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
"public:copy": "cp -r ./public/ ./build/public/",
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
"web:copy": "rimraf web && mkdir web && cp -r ../web/build/ ./web/build/",
"compileSysInit": "ts-node ./scripts/compileSysInit.ts"
"compileSysInit": "ts-node ./scripts/compileSysInit.ts",
"copySASjsCore": "ts-node ./scripts/copySASjsCore.ts"
},
"bin": "./build/src/server.js",
"pkg": {
@@ -41,14 +40,15 @@
},
"release": {
"branches": [
"master"
"main"
]
},
"author": "Analytium Ltd",
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^2.48.6",
"@sasjs/core": "4.9.0",
"@sasjs/utils": "2.34.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.17.1",
"joi": "^17.4.2",
@@ -58,10 +58,11 @@
"morgan": "^1.10.0",
"multer": "^1.4.3",
"swagger-ui-express": "^4.1.6",
"tsoa": "^3.14.0"
"tsoa": "3.14.1"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.12",
"@types/jest": "^26.0.24",
@@ -73,19 +74,19 @@
"@types/supertest": "^2.0.11",
"@types/swagger-ui-express": "^4.1.3",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
"mongodb-memory-server": "^8.0.0",
"nodemon": "^2.0.7",
"pkg": "^5.4.1",
"pkg": "5.5.2",
"prettier": "^2.3.1",
"rimraf": "^3.0.2",
"semantic-release": "^17.4.3",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-node": "^10.0.0",
"typescript": "^4.3.2"
},
"configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4"
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
}
}

View File

@@ -92,6 +92,58 @@ components:
- clientSecret
type: object
additionalProperties: false
IRecordOfAny:
properties: {}
type: object
additionalProperties: {}
LogLine:
properties:
line:
type: string
required:
- line
type: object
additionalProperties: false
HTTPHeaders:
properties: {}
type: object
additionalProperties:
type: string
ExecuteReturnJsonResponse:
properties:
status:
type: string
_webout:
anyOf:
-
type: string
-
$ref: '#/components/schemas/IRecordOfAny'
log:
items:
$ref: '#/components/schemas/LogLine'
type: array
message:
type: string
httpHeaders:
$ref: '#/components/schemas/HTTPHeaders'
required:
- status
- _webout
- log
- httpHeaders
type: object
additionalProperties: false
ExecuteSASCodePayload:
properties:
code:
type: string
description: 'Code of SAS program'
example: '* SAS Code HERE;'
required:
- code
type: object
additionalProperties: false
MemberType.folder:
enum:
- folder
@@ -171,18 +223,6 @@ components:
- fileTree
type: object
additionalProperties: false
GetFileResponse:
properties:
status:
type: string
fileContent:
type: string
message:
type: string
required:
- status
type: object
additionalProperties: false
UpdateFileResponse:
properties:
status:
@@ -193,21 +233,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:
@@ -358,20 +383,6 @@ components:
- description
type: object
additionalProperties: false
ExecuteReturnJsonResponse:
properties:
status:
type: string
log:
type: string
_webout:
type: string
message:
type: string
required:
- status
type: object
additionalProperties: false
ExecuteReturnJsonPayload:
properties:
_program:
@@ -387,10 +398,10 @@ components:
bearerFormat: JWT
info:
title: api
version: 0.0.1
version: 0.0.2
description: 'Api of SASjs server'
contact:
name: 'Analytium Ltd'
name: '4GL Ltd'
openapi: 3.0.0
paths:
/SASjsApi/auth/authorize:
@@ -500,6 +511,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ClientPayload'
/SASjsApi/code/execute:
post:
operationId: ExecuteSASCode
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
description: 'Execute SAS code.'
summary: 'Run SAS Code and returns log'
tags:
- CODE
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExecuteSASCodePayload'
/SASjsApi/drive/deploy:
post:
operationId: Deploy
@@ -548,24 +583,9 @@ paths:
get:
operationId: GetFile
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/GetFileResponse'
examples:
'Example 1':
value: {status: success, fileContent: 'Contents of the File'}
'400':
description: 'Unable to get File'
content:
application/json:
schema:
$ref: '#/components/schemas/GetFileResponse'
examples:
'Example 1':
value: {status: failure, message: 'File request failed.'}
'204':
description: 'No content'
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Get file from SASjs Drive'
tags:
- Drive
@@ -575,11 +595,57 @@ paths:
parameters:
-
in: query
name: filePath
required: true
name: _filePath
required: false
schema:
type: string
example: /Public/somefolder/some.file
requestBody:
required: false
content:
multipart/form-data:
schema:
type: object
properties:
filePath:
type: string
delete:
operationId: DeleteFile
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
status: {type: string}
required:
- status
type: object
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Delete file from SASjs Drive'
tags:
- Drive
security:
-
bearerAuth: []
parameters:
-
in: query
name: _filePath
required: false
schema:
type: string
example: /Public/somefolder/some.file
requestBody:
required: false
content:
multipart/form-data:
schema:
type: object
properties:
filePath:
type: string
post:
operationId: SaveFile
responses:
@@ -592,7 +658,7 @@ paths:
examples:
'Example 1':
value: {status: success}
'400':
'403':
description: 'File already exists'
content:
application/json:
@@ -601,19 +667,36 @@ 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: 'Create 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
patch:
operationId: UpdateFile
responses:
@@ -626,8 +709,8 @@ paths:
examples:
'Example 1':
value: {status: success}
'400':
description: 'Unable to get File'
'403':
description: ""
content:
application/json:
schema:
@@ -635,19 +718,36 @@ 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/filetree:
get:
operationId: GetFileTree
@@ -971,6 +1071,26 @@ paths:
format: double
type: number
example: '6789'
/SASjsApi/session:
get:
operationId: Session
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
examples:
'Example 1':
value: {id: 123, username: johnusername, displayName: John}
summary: 'Get session info (username).'
tags:
- Session
security:
-
bearerAuth: []
parameters: []
/SASjsApi/stp/execute:
get:
operationId: ExecuteReturnRaw
@@ -980,9 +1100,11 @@ paths:
content:
application/json:
schema:
type: string
description: "Trigger a SAS program using it's location in the _program parameter.\r\nEnable debugging using the _debug parameter.\r\nAdditional URL parameters are turned into SAS macro variables.\r\nAny files provided are placed into the session and\r\ncorresponding _WEBIN_XXX variables are created."
summary: 'Execute Stored Program, return raw content'
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\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
security:
@@ -990,6 +1112,7 @@ paths:
bearerAuth: []
parameters:
-
description: 'Location of SAS program'
in: query
name: _program
required: true
@@ -1005,7 +1128,10 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ExecuteReturnJsonResponse'
description: "Trigger a SAS program using it's location in the _program parameter.\r\nEnable debugging using the _debug parameter.\r\nAdditional URL parameters are turned into SAS macro variables.\r\nAny files provided are placed into the session and\r\ncorresponding _WEBIN_XXX variables are created."
examples:
'Example 1':
value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}}
description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. In any case, the log is\nalways returned in the log object.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response will be a JSON object with the following root attributes: log,\nwebout, headers.\n\nThe webout will be a nested JSON object ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content.\n\nResponse headers from the mfs_httpheader macro are simply listed in the\nheaders object, for POST requests they have no effect on the actual\nresponse header."
summary: 'Execute Stored Program, return JSON'
tags:
- STP
@@ -1014,6 +1140,7 @@ paths:
bearerAuth: []
parameters:
-
description: 'Location of SAS program'
in: query
name: _program
required: false
@@ -1030,6 +1157,9 @@ servers:
-
url: /
tags:
-
name: Session
description: 'Get Session information'
-
name: User
description: 'Operations about users'
@@ -1048,3 +1178,6 @@ tags:
-
name: STP
description: 'Operations about STP'
-
name: CODE
description: 'Operations on SAS code'

View File

@@ -21,7 +21,6 @@ const compiledSystemInit = async (systemInit: string) =>
}))
const createSysInitFile = async () => {
console.log('macroCorePath', macroCorePath)
const systemInitContent = await readFile(
path.join(__dirname, 'systemInit.sas')
)

View File

@@ -0,0 +1,21 @@
import path from 'path'
import { asyncForEach, copy, createFolder, deleteFolder } from '@sasjs/utils'
import { apiRoot, sasJSCoreMacros } from '../src/utils'
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
export const copySASjsCore = async () => {
await deleteFolder(sasJSCoreMacros)
await createFolder(sasJSCoreMacros)
const foldersToCopy = ['base', 'ddl', 'fcmp', 'lua', 'server']
await asyncForEach(foldersToCopy, async (coreSubFolder) => {
const coreSubFolderPath = path.join(macroCorePath, coreSubFolder)
await copy(coreSubFolderPath, sasJSCoreMacros)
})
}
copySASjsCore()

View File

@@ -4,10 +4,24 @@
@details This program is inserted into every sasjs/server program invocation,
_before_ any user-provided content.
A number of useful CORE macros are also compiled below, so that they can be
available "out of the box".
<h4> SAS Macros </h4>
@li mcf_stpsrv_header.sas
@li mf_getuser.sas
@li mf_getvarlist.sas
@li mf_mkdir.sas
@li mf_nobs.sas
@li mf_uid.sas
@li mfs_httpheader.sas
@li mp_dirlist.sas
@li mp_ds2ddl.sas
@li mp_ds2md.sas
@li mp_getdbml.sas
@li mp_init.sas
@li mp_makedata.sas
@li mp_zip.sas
**/
%mcf_stpsrv_header(wrap=YES, insert_cmplib=YES)

View File

@@ -1,33 +1,53 @@
import path from 'path'
import express from 'express'
import express, { ErrorRequestHandler } from 'express'
import morgan from 'morgan'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
import cors from 'cors'
import webRouter from './routes/web'
import apiRouter from './routes/api'
import { connectDB, getWebBuildFolderPath } from './utils'
import {
connectDB,
getWebBuildFolderPath,
sasJSCoreMacros,
setProcessVariables
} from './utils'
dotenv.config()
const app = express()
const { MODE, CORS, PORT_WEB } = process.env
const whiteList = [
`http://localhost:${PORT_WEB ?? 3000}`,
'https://sas.analytium.co.uk:8343'
]
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
console.log('All CORS Requests are enabled')
app.use(
cors({ credentials: true, origin: `http://localhost:${PORT_WEB ?? 3000}` })
)
app.use(cors({ credentials: true, origin: whiteList }))
}
app.use(express.json({ limit: '50mb' }))
app.use(cookieParser())
app.use(morgan('tiny'))
app.use(express.static(path.join(__dirname, '../public')))
app.use('/', webRouter)
app.use('/SASjsApi', apiRouter)
app.use(express.json({ limit: '50mb' }))
app.use(express.static(path.join(__dirname, '../public')))
app.use(express.static(getWebBuildFolderPath()))
export default connectDB().then(() => app)
const onError: ErrorRequestHandler = (err, req, res, next) => {
console.error(err.stack)
res.status(500).send('Something broke!')
}
export default setProcessVariables().then(async () => {
// 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)
console.log('sasJSCoreMacros', sasJSCoreMacros)
app.use(onError)
await connectDB()
return app
})

View File

@@ -0,0 +1,71 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal'
import { PreProgramVars } from '../types'
import { ExecuteReturnJsonResponse } from '.'
import { parseLogToArray } from '../utils'
interface ExecuteSASCodePayload {
/**
* Code of SAS program
* @example "* SAS Code HERE;"
*/
code: string
}
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('CODE')
export class CodeController {
/**
* Execute SAS code.
* @summary Run SAS Code and returns log
*/
@Post('/execute')
public async executeSASCode(
@Request() request: express.Request,
@Body() body: ExecuteSASCodePayload
): Promise<ExecuteReturnJsonResponse> {
return executeSASCode(request, body)
}
}
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
try {
const { webout, log, httpHeaders } =
(await new ExecutionController().executeProgram(
code,
getPreProgramVariables(req),
{ ...req.query, _debug: 131 },
undefined,
true
)) as ExecuteReturnJson
return {
status: 'success',
_webout: webout as string,
log: parseLogToArray(log),
httpHeaders
}
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}
const getPreProgramVariables = (req: any): PreProgramVars => {
const host = req.get('host')
const protocol = req.protocol + '://'
const { user, accessToken } = req
return {
username: user.username,
userId: user.userId,
displayName: user.displayName,
serverUrl: protocol + host,
accessToken
}
}

View File

@@ -1,5 +1,8 @@
import path from 'path'
import express, { Express } from 'express'
import {
Security,
Request,
Route,
Tags,
Example,
@@ -8,31 +11,26 @@ import {
Response,
Query,
Get,
Patch
Patch,
UploadedFile,
FormField,
Delete
} from 'tsoa'
import { fileExists, readFile, createFile } from '@sasjs/utils'
import {
fileExists,
moveFile,
createFolder,
deleteFile as deleteFileOnSystem
} from '@sasjs/utils'
import { createFileTree, ExecutionController, getTreeExample } from './internal'
import { FileTree, isFileTree, TreeNode } from '../types'
import path from 'path'
import { getTmpFilesFolderPath } from '../utils'
interface DeployPayload {
appLoc?: 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
@@ -89,57 +87,91 @@ 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 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"
*/
@Example<GetFileResponse>({
status: 'success',
fileContent: 'Contents of the File'
})
@Response<GetFileResponse>(400, 'Unable to get File', {
status: 'failure',
message: 'File request failed.'
})
@Get('/file')
public async getFile(@Query() filePath: string): Promise<GetFileResponse> {
return getFile(filePath)
public async getFile(
@Request() request: express.Request,
@Query() _filePath?: string,
@FormField() filePath?: string
) {
return getFile(request, (_filePath ?? filePath)!)
}
/**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
*
* @summary Delete file from SASjs Drive
* @query _filePath Location of SAS program
* @example _filePath "/Public/somefolder/some.file"
*/
@Delete('/file')
public async deleteFile(
@Query() _filePath?: string,
@FormField() filePath?: string
) {
return deleteFile((_filePath ?? filePath)!)
}
/**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
*
* @summary Create a file in SASjs Drive
* @param _filePath Location of SAS program
* @example _filePath "/Public/somefolder/some.file.sas"
*
*/
@Example<UpdateFileResponse>({
status: 'success'
})
@Response<UpdateFileResponse>(400, 'File already exists', {
@Response<UpdateFileResponse>(403, 'File already exists', {
status: 'failure',
message: 'File request failed.'
})
@Post('/file')
public async saveFile(
@Body() body: FilePayload
@UploadedFile() file: Express.Multer.File,
@Query() _filePath?: string,
@FormField() filePath?: string
): Promise<UpdateFileResponse> {
return saveFile(body)
return saveFile((_filePath ?? filePath)!, file)
}
/**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
*
* @summary Modify a file in SASjs Drive
* @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)
}
/**
@@ -153,7 +185,7 @@ export class DriveController {
}
const getFileTree = () => {
const tree = new ExecutionController().buildDirectorytree()
const tree = new ExecutionController().buildDirectoryTree()
return { status: 'success', tree }
}
@@ -172,68 +204,95 @@ const deploy = async (data: DeployPayload) => {
return successDeployResponse
}
const getFile = async (filePath: string): Promise<GetFileResponse> => {
try {
const getFile = async (req: express.Request, filePath: string) => {
const driveFilesPath = getTmpFilesFolderPath()
const filePathFull = path
.join(getTmpFilesFolderPath(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
await validateFilePath(filePathFull)
const fileContent = await readFile(filePathFull)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot get file outside drive.')
}
return { status: 'success', fileContent: fileContent }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'File request failed.',
error: typeof err === 'object' ? err.toString() : err
if (!(await fileExists(filePathFull))) {
throw new Error('File does not exist.')
}
const extension = path.extname(filePathFull).toLowerCase()
if (extension === '.sas') {
req.res?.setHeader('Content-type', 'text/plain')
}
req.res?.sendFile(path.resolve(filePathFull))
}
const saveFile = async (body: FilePayload): Promise<GetFileResponse> => {
const { filePath, fileContent } = body
try {
const deleteFile = async (filePath: string) => {
const driveFilesPath = getTmpFilesFolderPath()
const filePathFull = path
.join(getTmpFilesFolderPath(), filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot delete file outside drive.')
}
if (!(await fileExists(filePathFull))) {
throw new Error('File does not exist.')
}
await deleteFileOnSystem(filePathFull)
return { status: 'success' }
}
const saveFile = async (
filePath: string,
multerFile: Express.Multer.File
): Promise<GetFileResponse> => {
const driveFilesPath = getTmpFilesFolderPath()
const filePathFull = path
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot put file outside drive.')
}
if (await fileExists(filePathFull)) {
throw 'DriveController: File already exists.'
throw new Error('File already exists.')
}
await createFile(filePathFull, fileContent)
const folderPath = path.dirname(filePathFull)
await createFolder(folderPath)
await moveFile(multerFile.path, filePathFull)
return { status: 'success' }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'File request failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}
const updateFile = async (body: FilePayload): Promise<GetFileResponse> => {
const { filePath, fileContent } = body
try {
const updateFile = async (
filePath: string,
multerFile: Express.Multer.File
): Promise<GetFileResponse> => {
const driveFilesPath = getTmpFilesFolderPath()
const filePathFull = path
.join(getTmpFilesFolderPath(), filePath)
.join(driveFilesPath, filePath)
.replace(new RegExp('/', 'g'), path.sep)
await validateFilePath(filePathFull)
await createFile(filePathFull, fileContent)
if (!filePathFull.includes(driveFilesPath)) {
throw new Error('Cannot modify file outside drive.')
}
if (!(await fileExists(filePathFull))) {
throw new Error(`File doesn't exist.`)
}
await moveFile(multerFile.path, filePathFull)
return { status: 'success' }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'File request failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}
const validateFilePath = async (filePath: string) => {

View File

@@ -1,6 +1,8 @@
export * from './auth'
export * from './client'
export * from './code'
export * from './drive'
export * from './group'
export * from './session'
export * from './stp'
export * from './user'

View File

@@ -1,34 +1,79 @@
import path from 'path'
import fs from 'fs'
import { getSessionController } from './'
import { readFile, fileExists, createFile, moveFile } from '@sasjs/utils'
import {
readFile,
fileExists,
createFile,
moveFile,
readFileBinary
} from '@sasjs/utils'
import { PreProgramVars, TreeNode } from '../../types'
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
import {
extractHeaders,
generateFileUploadSasCode,
getTmpFilesFolderPath,
HTTPHeaders,
isDebugOn,
sasJSCoreMacros
} from '../../utils'
export interface ExecutionVars {
[key: string]: string | number | undefined
}
export interface ExecuteReturnRaw {
httpHeaders: HTTPHeaders
result: string | Buffer
}
export interface ExecuteReturnJson {
httpHeaders: HTTPHeaders
webout: string | Buffer
log?: string
}
export class ExecutionController {
async execute(
async executeFile(
programPath: string,
preProgramVariables: PreProgramVars,
vars: { [key: string]: string | number | undefined },
vars: ExecutionVars,
otherArgs?: any,
returnJson?: boolean
) {
if (!(await fileExists(programPath)))
throw 'ExecutionController: SAS file does not exist.'
let program = await readFile(programPath)
const program = await readFile(programPath)
return this.executeProgram(
program,
preProgramVariables,
vars,
otherArgs,
returnJson
)
}
async executeProgram(
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
otherArgs?: any,
returnJson?: boolean
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController = getSessionController()
const session = await sessionController.getSession()
session.inUse = true
session.consumed = true
const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt')
const weboutPath = path.join(session.path, 'webout.txt')
await createFile(weboutPath, '')
const tokenFile = path.join(session.path, 'accessToken.txt')
await createFile(weboutPath, '')
await createFile(
tokenFile,
preProgramVariables?.accessToken ?? 'accessToken'
@@ -39,6 +84,7 @@ export class ExecutionController {
`${computed}%let ${key}=${vars[key]};\n`,
''
)
const preProgramVarStatments = `
%let _sasjs_tokenfile=${tokenFile};
%let _sasjs_username=${preProgramVariables?.username};
@@ -48,9 +94,20 @@ export class ExecutionController {
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _metaperson=&_sasjs_displayname;
%let _metauser=&_sasjs_username;
%let sasjsprocessmode=Stored Program;`
%let sasjsprocessmode=Stored Program;
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
%macro _sasjs_server_init();
%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode;
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
%mend;
%_sasjs_server_init()
`
program = `
options insert=(SASAUTOS="${sasJSCoreMacros}");
/* runtime vars */
${varStatments}
filename _webout "${weboutPath}" mod;
@@ -85,44 +142,47 @@ ${program}`
await createFile(codePath + '.bkp', program)
await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session array
// we now need to poll the session status
while (!session.completed) {
await delay(50)
}
const log =
((await fileExists(logPath)) ? await readFile(logPath) : '') +
session.crashed
const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
const headersContent = (await fileExists(headersPath))
? await readFile(headersPath)
: ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
const fileResponse: boolean =
httpHeaders.hasOwnProperty('content-type') && !returnJson
const webout = (await fileExists(weboutPath))
? await readFile(weboutPath)
? fileResponse
? await readFileBinary(weboutPath)
: await readFile(weboutPath)
: ''
const debugValue =
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
let debugResponse: string | undefined
if ((debugValue && debugValue >= 131) || session.crashed) {
debugResponse = `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
}
// it should be deleted by scheduleSessionDestroy
session.inUse = false
sessionController.deleteSession(session)
if (returnJson) {
const response: any = {
webout: webout
return {
httpHeaders,
webout,
log: isDebugOn(vars) || session.crashed ? log : undefined
}
if ((debugValue && debugValue >= 131) || session.crashed) {
response.log = log
}
return response
return {
httpHeaders,
result: fileResponse
? webout
: isDebugOn(vars) || session.crashed
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
: webout
}
return debugResponse ?? webout
}
buildDirectorytree() {
buildDirectoryTree() {
const root: TreeNode = {
name: 'files',
relativePath: '',

View File

@@ -18,7 +18,7 @@ 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 = async (req: any, res: any, next: any) => {
let session
const sessionController = getSessionController()

View File

@@ -12,7 +12,8 @@ import {
createFile,
fileExists,
generateTimestamp,
readFile
readFile,
moveFile
} from '@sasjs/utils'
const execFilePromise = promisify(execFile)
@@ -20,8 +21,11 @@ const execFilePromise = promisify(execFile)
export class SessionController {
private sessions: Session[] = []
private getReadySessions = (): Session[] =>
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
public async getSession() {
const readySessions = this.sessions.filter((sess: Session) => sess.ready)
const readySessions = this.getReadySessions()
const session = readySessions.length
? readySessions[0]
@@ -32,7 +36,7 @@ export class SessionController {
return session
}
private async createSession() {
private async createSession(): Promise<Session> {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
@@ -47,6 +51,7 @@ export class SessionController {
id: sessionId,
ready: false,
inUse: false,
consumed: false,
completed: false,
creationTimeStamp,
deathTimeStamp,
@@ -62,7 +67,10 @@ export class SessionController {
// the autoexec file is executed on SAS startup
const autoExecPath = path.join(sessionFolder, 'autoexec.sas')
const contentForAutoExec = `/* compiled systemInit */\n${compiledSystemInitContent}\n/* autoexec */\n${autoExecContent}`
const contentForAutoExec = `/* compiled systemInit */
${compiledSystemInitContent}
/* autoexec */
${autoExecContent}`
await createFile(autoExecPath, contentForAutoExec)
// create empty code.sas as SAS will not start without a SYSIN
@@ -92,7 +100,7 @@ export class SessionController {
.catch((err) => {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id)
console.log('session crashed', session.id, session.crashed)
})
// we have a triggered session - add to array
@@ -105,15 +113,16 @@ export class SessionController {
return session
}
public async waitForSession(session: Session) {
private async waitForSession(session: Session) {
const codeFilePath = path.join(session.path, 'code.sas')
// TODO: don't wait forever
while ((await fileExists(codeFilePath)) && !session.crashed) {}
console.log('session crashed?', !!session.crashed, session.crashed)
if (session.crashed)
console.log('session crashed! while waiting to be ready', session.crashed)
session.ready = true
return Promise.resolve(session)
}
public async deleteSession(session: Session) {
@@ -121,12 +130,10 @@ export class SessionController {
await deleteFolder(session.path)
// remove the session from the session array
if (session.ready) {
this.sessions = this.sessions.filter(
(sess: Session) => sess.id !== session.id
)
}
}
private scheduleSessionDestroy(session: Session) {
setTimeout(async () => {
@@ -153,6 +160,7 @@ const autoExecContent = `
data _null_;
/* remove the dummy SYSIN */
length fname $8;
call missing(fname);
rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname);

View File

@@ -0,0 +1,30 @@
import express from 'express'
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
import { UserResponse } from './user'
@Security('bearerAuth')
@Route('SASjsApi/session')
@Tags('Session')
export class SessionController {
/**
* @summary Get session info (username).
*
*/
@Example<UserResponse>({
id: 123,
username: 'johnusername',
displayName: 'John'
})
@Get('/')
public async session(
@Request() request: express.Request
): Promise<UserResponse> {
return session(request)
}
}
const session = (req: any) => ({
id: req.user.id,
username: req.user.username,
displayName: req.user.displayName
})

View File

@@ -1,19 +1,31 @@
import express, { response } from 'express'
import express from 'express'
import path from 'path'
import {
Request,
Security,
Route,
Tags,
Example,
Post,
Body,
Get,
Query
Query,
Example
} from 'tsoa'
import { ExecutionController } from './internal'
import {
ExecuteReturnJson,
ExecuteReturnRaw,
ExecutionController,
ExecutionVars
} from './internal'
import { PreProgramVars } from '../types'
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
import {
getTmpFilesFolderPath,
HTTPHeaders,
isDebugOn,
LogLine,
makeFilesNamesMap,
parseLogToArray
} from '../utils'
interface ExecuteReturnJsonPayload {
/**
@@ -22,11 +34,16 @@ interface ExecuteReturnJsonPayload {
*/
_program?: string
}
interface ExecuteReturnJsonResponse {
interface IRecordOfAny {
[key: string]: any
}
export interface ExecuteReturnJsonResponse {
status: string
log?: string
_webout?: string
_webout: string | IRecordOfAny
log: LogLine[]
message?: string
httpHeaders: HTTPHeaders
}
@Security('bearerAuth')
@@ -34,32 +51,69 @@ interface ExecuteReturnJsonResponse {
@Tags('STP')
export class STPController {
/**
* Trigger a SAS program using it's location in the _program parameter.
* Enable debugging using the _debug parameter.
* Trigger a SAS program using it's location in the _program URL parameter.
* Enable debugging using the _debug URL parameter. Setting _debug=131 will
* cause the log to be streamed in the output.
*
* Additional URL parameters are turned into SAS macro variables.
* Any files provided are placed into the session and
* corresponding _WEBIN_XXX variables are created.
* @summary Execute Stored Program, return raw content
* @query _program Location of SAS program
*
* Any files provided in the request body are placed into the SAS session with
* corresponding _WEBIN_XXX variables created.
*
* The response headers can be adjusted using the mfs_httpheader() macro. Any
* file type can be returned, including binary files such as zip or xls.
*
* If _debug is >= 131, response headers will contain Content-Type: 'text/plain'
*
* This behaviour differs for POST requests, in which case the response is
* always JSON.
*
* @summary Execute Stored Program, return raw _webout content.
* @param _program Location of SAS program
* @example _program "/Public/somefolder/some.file"
*/
@Get('/execute')
public async executeReturnRaw(
@Request() request: express.Request,
@Query() _program: string
): Promise<string> {
): Promise<string | Buffer> {
return executeReturnRaw(request, _program)
}
/**
* Trigger a SAS program using it's location in the _program parameter.
* Enable debugging using the _debug parameter.
* Trigger a SAS program using it's location in the _program URL parameter.
* Enable debugging using the _debug URL parameter. In any case, the log is
* always returned in the log object.
*
* Additional URL parameters are turned into SAS macro variables.
* Any files provided are placed into the session and
* corresponding _WEBIN_XXX variables are created.
*
* Any files provided in the request body are placed into the SAS session with
* corresponding _WEBIN_XXX variables created.
*
* The response will be a JSON object with the following root attributes: log,
* webout, headers.
*
* The webout will be a nested JSON object ONLY if the response-header
* contains a content-type of application/json AND it is valid JSON.
* Otherwise it will be a stringified version of the webout content.
*
* Response headers from the mfs_httpheader macro are simply listed in the
* headers object, for POST requests they have no effect on the actual
* response header.
*
* @summary Execute Stored Program, return JSON
* @query _program Location of SAS program
* @param _program Location of SAS program
* @example _program "/Public/somefolder/some.file"
*/
@Example<ExecuteReturnJsonResponse>({
status: 'success',
_webout: 'webout content',
log: [],
httpHeaders: {
'Content-type': 'application/zip',
'Cache-Control': 'public, max-age=1000'
}
})
@Post('/execute')
public async executeReturnJson(
@Request() request: express.Request,
@@ -74,21 +128,35 @@ export class STPController {
const executeReturnRaw = async (
req: express.Request,
_program: string
): Promise<string> => {
const query = req.query as { [key: string]: string | number | undefined }
): Promise<string | Buffer> => {
const query = req.query as ExecutionVars
const sasCodePath =
path
.join(getTmpFilesFolderPath(), _program)
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
try {
const result = await new ExecutionController().execute(
const { result, httpHeaders } =
(await new ExecutionController().executeFile(
sasCodePath,
getPreProgramVariables(req),
query
)
)) as ExecuteReturnRaw
return result as string
// Should over-ride response header for
// debug on GET request to see entire log
// rendering on browser.
if (isDebugOn(query)) {
httpHeaders['content-type'] = 'text/plain'
}
req.res?.set(httpHeaders)
if (result instanceof Buffer) {
;(req as any).sasHeaders = httpHeaders
}
return result
} catch (err: any) {
throw {
code: 400,
@@ -111,17 +179,27 @@ const executeReturnJson = async (
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
try {
const jsonResult: any = await new ExecutionController().execute(
const { webout, log, httpHeaders } =
(await new ExecutionController().executeFile(
sasCodePath,
getPreProgramVariables(req),
{ ...req.query, ...req.body },
{ filesNamesMap: filesNamesMap },
true
)
)) as ExecuteReturnJson
let weboutRes: string | IRecordOfAny = webout
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
try {
weboutRes = JSON.parse(webout as string)
} catch (_) {}
}
return {
status: 'success',
_webout: jsonResult.webout,
log: jsonResult.log
_webout: weboutRes,
log: parseLogToArray(log),
httpHeaders
}
} catch (err: any) {
throw {

View File

@@ -26,7 +26,7 @@ const authenticateToken = (
res: any,
next: any,
key: string,
tokenType: 'accessToken' | 'refreshToken' = 'accessToken'
tokenType: 'accessToken' | 'refreshToken'
) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
@@ -43,7 +43,9 @@ const authenticateToken = (
}
const authHeader = req.headers['authorization']
const token = authHeader?.split(' ')[1]
const token =
authHeader?.split(' ')[1] ??
(tokenType === 'accessToken' ? req.cookies.accessToken : '')
if (!token) return res.sendStatus(401)
jwt.verify(token, key, async (err: any, data: any) => {

View File

@@ -0,0 +1,18 @@
export const desktopRestrict = (req: any, res: any, next: any) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server')
return res.status(403).send('Not Allowed while in Desktop Mode.')
next()
}
export const desktopUsername = (req: any, res: any, next: any) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server')
return res.status(200).send({
userId: 12345,
username: 'DESKTOPusername',
displayName: 'DESKTOP User'
})
next()
}

View File

@@ -1,7 +0,0 @@
export const desktopRestrict = (req: any, res: any, next: any) => {
const { MODE } = process.env
if (MODE?.trim() !== 'server')
return res.status(403).send('Not Allowed while in Desktop Mode.')
next()
}

View File

@@ -1,4 +1,4 @@
export * from './authenticateToken'
export * from './desktopRestrict'
export * from './desktop'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'

View File

@@ -0,0 +1,77 @@
import path from 'path'
import { Request } from 'express'
import multer, { FileFilterCallback, Options } from 'multer'
import { getTmpUploadsPath } from '../utils'
const acceptableExtensions = ['.sas']
const fieldNameSize = 300
const fileSize = 10485760 // 10 MB
const storage = multer.diskStorage({
destination: getTmpUploadsPath(),
filename: function (
_req: Request,
file: Express.Multer.File,
callback: (error: Error | null, filename: string) => void
) {
callback(
null,
file.fieldname + path.extname(file.originalname) + '-' + Date.now()
)
}
})
const limits: Options['limits'] = {
fieldNameSize,
fileSize
}
const fileFilter: Options['fileFilter'] = (
req: Request,
file: Express.Multer.File,
callback: FileFilterCallback
) => {
const fileExtension = path.extname(file.originalname).toLocaleLowerCase()
if (!acceptableExtensions.includes(fileExtension)) {
return callback(
new Error(
`File extension '${fileExtension}' not acceptable. Valid extension(s): ${acceptableExtensions.join(
', '
)}`
)
)
}
const uploadFileSize = parseInt(req.headers['content-length'] ?? '')
if (uploadFileSize > fileSize) {
return callback(
new Error(
`File size is over limit. File limit is: ${fileSize / 1024 / 1024} MB`
)
)
}
callback(null, true)
}
const options: Options = { storage, limits, fileFilter }
const multerInstance = multer(options)
export const multerSingle = (fileName: string, arg: any) => {
const [req, res, next] = arg
const upload = multerInstance.single(fileName)
upload(req, res, function (err) {
if (err instanceof multer.MulterError) {
return res.status(500).send(err.message)
} else if (err) {
return res.status(400).send(err.message)
}
// Everything went fine.
next()
})
}
export default multerInstance

View File

@@ -1,21 +0,0 @@
import path from 'path'
import { readFileSync } from 'fs'
import * as https from 'https'
import appPromise from './app'
const keyPath = path.join('..', 'certificates', 'privkey.pem')
const certPath = path.join('..', 'certificates', 'fullchain.pem')
const key = readFileSync(keyPath)
const cert = readFileSync(certPath)
appPromise.then((app) => {
const httpsServer = https.createServer({ key, cert }, app)
const sasJsPort = process.env.PORT ?? 5000
httpsServer.listen(sasJsPort, () => {
console.log(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
)
})
})

View File

@@ -55,8 +55,9 @@ authRouter.post('/token', async (req, res) => {
const controller = new AuthController()
try {
const response = await controller.token(body)
const { accessToken } = response
res.send(response)
res.cookie('accessToken', accessToken).send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}

View File

@@ -0,0 +1,31 @@
import express from 'express'
import { runSASValidation } from '../../utils'
import { CodeController } from '../../controllers/'
const runRouter = express.Router()
const controller = new CodeController()
runRouter.post('/execute', async (req, res) => {
const { error, value: body } = runSASValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeSASCode(req, body)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
return res.end(response)
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter

View File

@@ -1,11 +1,15 @@
import express from 'express'
import { deleteFile } from '@sasjs/utils'
import { multerSingle } from '../../middlewares/multer'
import { DriveController } from '../../controllers/'
import { getFileDriveValidation, updateFileDriveValidation } from '../../utils'
import { fileBodyValidation, fileParamValidation } from '../../utils'
const controller = new DriveController()
const driveRouter = express.Router()
driveRouter.post('/deploy', async (req, res) => {
const controller = new DriveController()
try {
const response = await controller.deploy(req.body)
res.send(response)
@@ -19,58 +23,89 @@ driveRouter.post('/deploy', async (req, res) => {
})
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)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) return res.status(400).send(errB.details[0].message)
const controller = new DriveController()
try {
const response = await controller.getFile(query.filePath)
res.send(response)
await controller.getFile(req, query._filePath, body.filePath)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
res.status(403).send(err.toString())
}
})
driveRouter.post('/file', async (req, res) => {
const { error, value: body } = updateFileDriveValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
driveRouter.delete('/file', async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) return res.status(400).send(errB.details[0].message)
const controller = new DriveController()
try {
const response = await controller.saveFile(body)
const response = await controller.deleteFile(query._filePath, body.filePath)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
res.status(403).send(err.toString())
}
})
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.post(
'/file',
(...arg) => multerSingle('file', arg),
async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) {
if (req.file) await deleteFile(req.file.path)
return res.status(400).send(errB.details[0].message)
}
if (!req.file) return res.status(400).send('"file" is not present.')
const controller = new DriveController()
try {
const response = await controller.updateFile(body)
const response = await controller.saveFile(
req.file,
query._filePath,
body.filePath
)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
await deleteFile(req.file.path)
res.status(403).send(err.toString())
}
})
}
)
driveRouter.patch(
'/file',
(...arg) => multerSingle('file', arg),
async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) {
if (req.file) await deleteFile(req.file.path)
return res.status(400).send(errB.details[0].message)
}
if (!req.file) return res.status(400).send('"file" is not present.')
try {
const response = await controller.updateFile(
req.file,
query._filePath,
body.filePath
)
res.send(response)
} catch (err: any) {
await deleteFile(req.file.path)
res.status(403).send(err.toString())
}
}
)
driveRouter.get('/fileTree', async (req, res) => {
const controller = new DriveController()
try {
const response = await controller.getFileTree()
res.send(response)

View File

@@ -5,18 +5,22 @@ import swaggerUi from 'swagger-ui-express'
import {
authenticateAccessToken,
desktopRestrict,
desktopUsername,
verifyAdmin
} from '../../middlewares'
import driveRouter from './drive'
import stpRouter from './stp'
import codeRouter from './code'
import userRouter from './user'
import groupRouter from './group'
import clientRouter from './client'
import authRouter from './auth'
import sessionRouter from './session'
const router = express.Router()
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
router.use('/auth', desktopRestrict, authRouter)
router.use(
'/client',
@@ -28,6 +32,7 @@ router.use(
router.use('/drive', authenticateAccessToken, driveRouter)
router.use('/group', desktopRestrict, groupRouter)
router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/code', authenticateAccessToken, codeRouter)
router.use('/user', desktopRestrict, userRouter)
router.use(
'/',

View File

@@ -0,0 +1,16 @@
import express from 'express'
import { SessionController } from '../../controllers'
const sessionRouter = express.Router()
sessionRouter.get('/', async (req, res) => {
const controller = new SessionController()
try {
const response = await controller.session(req)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default sessionRouter

View File

@@ -1,15 +1,34 @@
import path from 'path'
import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import {
folderExists,
fileExists,
readFile,
deleteFolder,
generateTimestamp,
copy
} from '@sasjs/utils'
import * as fileUtilModules from '../../../utils/file'
const timestamp = generateTimestamp()
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
jest
.spyOn(fileUtilModules, 'getTmpFolderPath')
.mockImplementation(() => tmpFolder)
jest
.spyOn(fileUtilModules, 'getTmpUploadsPath')
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import { getTreeExample } from '../../../controllers/internal'
import { getTmpFilesFolderPath } from '../../../utils/file'
import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils'
import path from 'path'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { FolderMember, ServiceMember } from '../../../types'
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
const { getTmpFilesFolderPath } = fileUtilModules
let app: Express
appPromise.then((_app) => {
@@ -30,28 +49,27 @@ describe('files', () => {
let mongoServer: MongoMemoryServer
const controller = new UserController()
let accessToken: string
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
describe('deploy', () => {
let accessToken: string
let dbUser: any
beforeAll(async () => {
dbUser = await controller.createUser(user)
const dbUser = await controller.createUser(user)
accessToken = generateAccessToken({
clientId,
userId: dbUser.id
})
await saveTokensInDB(dbUser.id, clientId, accessToken, 'refreshToken')
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
await deleteFolder(tmpFolder)
})
describe('deploy', () => {
const shouldFailAssertion = async (payload: any) => {
const res = await request(app)
.post('/SASjsApi/drive/deploy')
@@ -144,8 +162,6 @@ describe('files', () => {
const exampleService = getExampleService()
const testJobFile = path.join(testJobFolder, exampleService.name) + '.sas'
console.log(`[testJobFile]`, testJobFile)
await expect(fileExists(testJobFile)).resolves.toEqual(true)
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
@@ -153,6 +169,319 @@ describe('files', () => {
await deleteFolder(getTmpFilesFolderPath())
})
})
describe('file', () => {
describe('create', () => {
it('should create a SAS file on drive having filePath as form field', async () => {
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', '/my/path/code.sas')
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should create a SAS file on drive having _filePath as query param', async () => {
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/my/path/code1.sas' })
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.post('/SASjsApi/drive/file')
.field('filePath', '/my/path/code.sas')
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is already present', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
pathToUpload
)
await copy(fileToAttachPath, pathToCopy)
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
expect(res.text).toEqual('Error: File already exists.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/../path/code.sas'
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
expect(res.text).toEqual('Error: Cannot put file outside drive.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if filePath is missing', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual(`"filePath" is required`)
expect(res.body).toEqual({})
})
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.oth'
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual('Valid extensions for filePath: .sas')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if file is missing', async () => {
const pathToUpload = '/my/path/code.sas'
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.expect(400)
expect(res.text).toEqual('"file" is not present.')
expect(res.body).toEqual({})
})
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth')
const pathToUpload = '/my/path/code.sas'
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual(
`File extension '.oth' not acceptable. Valid extension(s): .sas`
)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if attached file exceeds file limit', async () => {
const pathToUpload = '/my/path/code.sas'
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', attachedFile, 'another.sas')
.expect(400)
expect(res.text).toEqual(
'File size is over limit. File limit is: 10 MB'
)
expect(res.body).toEqual({})
})
})
describe('update', () => {
it('should update a SAS file on drive having filePath as form field', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
pathToUpload
)
await copy(fileToAttachPath, pathToCopy)
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should update a SAS file on drive having _filePath as query param', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
pathToUpload
)
await copy(fileToAttachPath, pathToCopy)
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({
status: 'success'
})
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/drive/file')
.field('filePath', '/my/path/code.sas')
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
.expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is not present', async () => {
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', `/my/path/code-${generateTimestamp()}.sas`)
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
.expect(403)
expect(res.text).toEqual(`Error: File doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/../path/code.sas'
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', fileToAttachPath)
.expect(403)
expect(res.text).toEqual('Error: Cannot modify file outside drive.')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if filePath is missing', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.attach('file', fileToAttachPath)
.expect(400)
expect(res.text).toEqual(`"filePath" is required`)
expect(res.body).toEqual({})
})
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.oth'
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('Valid extensions for filePath: .sas')
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.oth')
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 '.oth' not acceptable. Valid extension(s): .sas`
)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if attached file exceeds file limit', async () => {
const pathToUpload = '/my/path/code.sas'
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', pathToUpload)
.attach('file', attachedFile, 'another.sas')
.expect(400)
expect(res.text).toEqual(
'File size is over limit. File limit is: 10 MB'
)
expect(res.body).toEqual({})
})
})
})
})
const getExampleService = (): ServiceMember =>

View File

@@ -0,0 +1 @@
some code of sas

View File

@@ -0,0 +1 @@
some code of sas

View File

@@ -14,6 +14,12 @@ stpRouter.get('/execute', async (req, res) => {
try {
const response = await controller.executeReturnRaw(req, query._program)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
return res.end(response)
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
@@ -26,7 +32,7 @@ stpRouter.get('/execute', async (req, res) => {
stpRouter.post(
'/execute',
fileUploadController.preuploadMiddleware,
fileUploadController.preUploadMiddleware,
fileUploadController.getMulterUploadObject().any(),
async (req: any, res: any) => {
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
@@ -40,6 +46,12 @@ stpRouter.post(
body,
query?._program
)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
return res.end(response)
}
res.send(response)
} catch (err: any) {
const statusCode = err.code

View File

@@ -0,0 +1,9 @@
import { Express } from 'express'
import webRouter from './web'
import apiRouter from './api'
export const setupRoutes = (app: Express) => {
app.use('/', webRouter)
app.use('/SASjsApi', apiRouter)
}

View File

@@ -1,10 +1,28 @@
import appPromise from './app'
import { createServer } from 'https'
appPromise.then((app) => {
import appPromise from './app'
import { getCertificates } from './utils'
appPromise.then(async (app) => {
const protocol = process.env.PROTOCOL ?? 'http'
const sasJsPort = process.env.PORT ?? 5000
console.log('PROTOCOL: ', protocol)
if (protocol !== 'https') {
app.listen(sasJsPort, () => {
console.log(
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
)
})
} else {
const { key, cert } = await getCertificates()
const httpsServer = createServer({ key, cert }, app)
httpsServer.listen(sasJsPort, () => {
console.log(
`⚡️[server]: Server is running at https://localhost:${sasJsPort}`
)
})
}
})

View File

@@ -1,7 +1,7 @@
declare namespace NodeJS {
export interface Process {
sasLoc: string
driveLoc?: string
driveLoc: string
sessionController?: import('../controllers/internal').SessionController
}
}

View File

@@ -5,6 +5,7 @@ export interface Session {
deathTimeStamp: string
path: string
inUse: boolean
consumed: boolean
completed: boolean
crashed?: string
}

View File

@@ -1,32 +1,19 @@
import path from 'path'
import mongoose from 'mongoose'
import { configuration } from '../../package.json'
import { getDesktopFields } from '.'
import { populateClients } from '../routes/api/auth'
export const connectDB = async () => {
// NOTE: when exporting app.js as agent for supertest
// we should exlcude connecting to the real database
if (process.env.NODE_ENV !== 'test') {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
const { sasLoc, driveLoc } = await getDesktopFields()
process.sasLoc = sasLoc
process.driveLoc = driveLoc
// we should exclude connecting to the real database
if (process.env.NODE_ENV === 'test') {
return
} else {
const { SAS_PATH } = process.env
const sasDir = SAS_PATH ?? configuration.sasPath
const { MODE } = process.env
process.sasLoc = path.join(sasDir, 'sas')
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
return
}
console.log('sasLoc: ', process.sasLoc)
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err

View File

@@ -0,0 +1,25 @@
const headerUtils = require('http-headers-validation')
export interface HTTPHeaders {
[key: string]: string
}
export const extractHeaders = (content?: string): HTTPHeaders => {
const headersObj: HTTPHeaders = {}
const headersArr = content
?.split('\n')
.map((line) => line.trim())
.filter((line) => !!line)
headersArr?.forEach((headerStr) => {
const [key, value] = headerStr.split(':').map((data) => data.trim())
if (value && headerUtils.validateHeader(key, value)) {
headersObj[key.toLowerCase()] = value
} else {
delete headersObj[key.toLowerCase()]
}
})
return headersObj
}

View File

@@ -1,5 +1,4 @@
import path from 'path'
import { getRealPath } from '@sasjs/utils'
export const apiRoot = path.join(__dirname, '..', '..')
export const codebaseRoot = path.join(apiRoot, '..')
@@ -9,11 +8,14 @@ export const sysInitCompiledPath = path.join(
'systemInitCompiled.sas'
)
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
export const getWebBuildFolderPath = () =>
path.join(codebaseRoot, 'web', 'build')
export const getTmpFolderPath = () =>
process.driveLoc ?? getRealPath(path.join(process.cwd(), 'tmp'))
export const getTmpFolderPath = () => process.driveLoc
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
export const getTmpFilesFolderPath = () =>
path.join(getTmpFolderPath(), 'files')

View File

@@ -3,5 +3,5 @@ import { InfoJWT } from '../types'
export const generateAccessToken = (data: InfoJWT) =>
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
expiresIn: '1h'
expiresIn: '1day'
})

View File

@@ -3,5 +3,5 @@ import { InfoJWT } from '../types'
export const generateRefreshToken = (data: InfoJWT) =>
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
expiresIn: '1day'
expiresIn: '30 days'
})

View File

@@ -0,0 +1,36 @@
import path from 'path'
import { fileExists, getString, readFile } from '@sasjs/utils'
export const getCertificates = async () => {
const { PRIVATE_KEY, FULL_CHAIN } = process.env
const keyPath = PRIVATE_KEY ?? (await getFileInput('Private Key (PEM)'))
const certPath = FULL_CHAIN ?? (await getFileInput('Full Chain (PEM)'))
console.log('keyPath: ', keyPath)
console.log('certPath: ', certPath)
const key = await readFile(keyPath)
const cert = await readFile(certPath)
return { key, cert }
}
const getFileInput = async (filename: string): Promise<string> => {
const validator = async (filePath: string) => {
if (!filePath) return `Path to ${filename} is required.`
if (!(await fileExists(path.join(process.cwd(), filePath)))) {
return 'No file found at provided path.'
}
return true
}
const targetName = await getString(
`Please enter path to ${filename} (relative path): `,
validator
)
return targetName
}

View File

@@ -5,8 +5,10 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
const isWindows = () => process.platform === 'win32'
export const getDesktopFields = async () => {
const sasLoc = await getSASLocation()
const driveLoc = await getDriveLocation()
const { SAS_PATH, DRIVE_PATH } = process.env
const sasLoc = SAS_PATH ?? (await getSASLocation())
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
return { sasLoc, driveLoc }
}

View File

@@ -1,11 +1,16 @@
export * from './connectDB'
export * from './extractHeaders'
export * from './file'
export * from './generateAccessToken'
export * from './generateAuthCode'
export * from './generateRefreshToken'
export * from './isDebugOn'
export * from './getCertificates'
export * from './getDesktopFields'
export * from './parseLogToArray'
export * from './removeTokensInDB'
export * from './saveTokensInDB'
export * from './setProcessVariables'
export * from './sleep'
export * from './upload'
export * from './validation'

View 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)
}

View File

@@ -0,0 +1,9 @@
export interface LogLine {
line: string
}
export const parseLogToArray = (content?: string): LogLine[] => {
if (!content) return []
return content.split('\n').map((line) => ({ line: line }))
}

View File

@@ -0,0 +1,31 @@
import path from 'path'
import { getRealPath } from '@sasjs/utils'
import { configuration } from '../../package.json'
import { getDesktopFields } from '.'
export const setProcessVariables = async () => {
if (process.env.NODE_ENV === 'test') {
process.driveLoc = path.join(process.cwd(), 'tmp')
return
}
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
const { sasLoc, driveLoc } = await getDesktopFields()
process.sasLoc = sasLoc
process.driveLoc = driveLoc
} else {
const { SAS_PATH, DRIVE_PATH } = process.env
process.sasLoc = SAS_PATH ?? configuration.sasPath
process.driveLoc = getRealPath(
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
)
}
console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc)
}

View File

@@ -0,0 +1,52 @@
import { extractHeaders } from '..'
describe('extractHeaders', () => {
it('should return valid http headers', () => {
const headers = extractHeaders(`
Content-type: application/csv
Cache-Control: public, max-age=2000
Content-type: application/text
Cache-Control: public, max-age=1500
Content-type: application/zip
Cache-Control: public, max-age=1000
`)
expect(headers).toEqual({
'content-type': 'application/zip',
'cache-control': 'public, max-age=1000'
})
})
it('should not return http headers if last occurrence is blank', () => {
const headers = extractHeaders(`
Content-type: application/csv
Cache-Control: public, max-age=1000
Content-type: application/text
Content-type:
`)
expect(headers).toEqual({ 'cache-control': 'public, max-age=1000' })
})
it('should return only valid http headers', () => {
const headers = extractHeaders(`
Content-type[]: application/csv
Content//-type: application/text
Content()-type: application/zip
`)
expect(headers).toEqual({})
})
it('should return http headers if empty', () => {
const headers = extractHeaders('')
expect(headers).toEqual({})
})
it('should return http headers if not provided', () => {
const headers = extractHeaders()
expect(headers).toEqual({})
})
})

View File

@@ -0,0 +1,33 @@
import { parseLogToArray } from '..'
describe('parseLogToArray', () => {
it('should parse log to array type', () => {
const log = parseLogToArray(`
line 1 of log content
line 2 of log content
line 3 of log content
line 4 of log content
`)
expect(log).toEqual([
{ line: '' },
{ line: 'line 1 of log content' },
{ line: 'line 2 of log content' },
{ line: 'line 3 of log content' },
{ line: 'line 4 of log content' },
{ line: ' ' }
])
})
it('should parse log to array type if empty', () => {
const log = parseLogToArray('')
expect(log).toEqual([])
})
it('should parse log to array type if not provided', () => {
const log = parseLogToArray()
expect(log).toEqual([])
})
})

View File

@@ -66,15 +66,23 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
clientSecret: Joi.string().required()
}).validate(data)
export const getFileDriveValidation = (data: any): Joi.ValidationResult =>
export const fileBodyValidation = (data: any): Joi.ValidationResult =>
Joi.object({
filePath: Joi.string().required()
filePath: Joi.string().pattern(/.sas$/).required().messages({
'string.pattern.base': `Valid extensions for filePath: .sas`
})
}).validate(data)
export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
export const fileParamValidation = (data: any): Joi.ValidationResult =>
Joi.object({
filePath: Joi.string().required(),
fileContent: Joi.string().required()
_filePath: Joi.string().pattern(/.sas$/).required().messages({
'string.pattern.base': `Valid extensions for filePath: .sas`
})
}).validate(data)
export const runSASValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required()
}).validate(data)
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>

View File

@@ -11,6 +11,10 @@
}
},
"tags": [
{
"name": "Session",
"description": "Get Session information"
},
{
"name": "User",
"description": "Operations about users"
@@ -34,6 +38,10 @@
{
"name": "STP",
"description": "Operations about STP"
},
{
"name": "CODE",
"description": "Operations on SAS code"
}
],
"yaml": true,

View File

@@ -14,14 +14,14 @@ services:
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
AUTH_CODE_SECRET: ${AUTH_CODE_SECRET}
DB_CONNECT: mongodb://mongodb:27017/sasjs
SAS_PATH: /usr/server/sasexe
SAS_PATH: /usr/server/sasexe/${SAS_EXEC_NAME}
expose:
- ${PORT_API}
ports:
- ${PORT_API}:${PORT_API}
volumes:
- type: bind
source: ${SAS_EXEC}
source: ${SAS_EXEC_PATH}
target: /usr/server/sasexe
read_only: true
links:

View File

@@ -7,7 +7,7 @@ services:
context: .
dockerfile: DockerfileApi
environment:
MODE: ${MODE}
MODE: 'server'
CORS: ${CORS}
PORT: ${PORT_API}
PORT_WEB: ${PORT_WEB}
@@ -43,7 +43,7 @@ services:
- ./web:/usr/server/web
mongodb:
image: mongo:latest
image: mongo:5.0.4
ports:
- 27017:27017
volumes:

3710
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,13 @@
{
"name": "server",
"version": "0.0.2",
"version": "0.0.32",
"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:prod",
"server:start": "cd api && npm run start",
"release": "standard-version",
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"lint-web:fix": "npx prettier --write \"web/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
@@ -14,6 +16,7 @@
"lint:fix": "npm run lint-api:fix && npm run lint-web:fix"
},
"devDependencies": {
"prettier": "^2.3.1"
"prettier": "^2.3.1",
"standard-version": "^9.3.2"
}
}

22
restClient/auth.rest Normal file
View File

@@ -0,0 +1,22 @@
### Get Auth Code
POST http://localhost:5000/SASjsApi/auth/authorize
Content-Type: application/json
{
"username": "secretuser",
"password": "secretpassword",
"client_id": "clientID1"
}
### Exchange AuthCode with Access/Refresh Tokens
POST http://localhost:5000/SASjsApi/auth/token
Content-Type: application/json
{
"client_id": "clientID1",
"client_secret": "clientID1secret",
"code": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDYxLCJleHAiOjE2MzU4MDQwOTF9.jV7DpBWG7XAGODs22zAW_kWOqVLZvOxmmYJGpSNQ-KM"
}
### Perform logout to deactivate access token instantly
DELETE http://localhost:5000/SASjsApi/auth/logout

45
restClient/drive.rest Normal file
View File

@@ -0,0 +1,45 @@
###
POST http://localhost:5000/SASjsApi/drive/deploy
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I
### multipart upload to sas server file
POST http://localhost:5000/SASjsApi/drive/file
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="filePath"
/saad/files/new.sas
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
Content-Type: application/octet-stream
< ./sample.sas
------WebKitFormBoundary7MA4YWxkTrZu0gW--
### multipart upload to sas server file text
POST http://localhost:5000/SASjsApi/drive/file
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW \n
Content-Disposition: form-data; name="filePath"
/saad/files/new2.sas
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
Content-Type: text/plain
SOME CONTENTS OF SAS FILE IN REQUEST
------WebKitFormBoundary7MA4YWxkTrZu0gW--
Users
"username": "username1",
"password": "some password",
"username": "username2",
"password": "some password",
Admins
"username": "secretuser",
"password": "secretpassword",

View File

@@ -23,7 +23,7 @@ Content-Type: application/json
"client_secret": "newClientSecret"
}
###
POST https://sas.analytium.co.uk:5002/SASjsApi/auth/authorize
POST http://localhost:5000/SASjsApi/auth/authorize
Content-Type: application/json
{
@@ -45,6 +45,41 @@ Content-Type: application/json
###
DELETE http://localhost:5000/SASjsApi/auth/logout
###
GET http://localhost:5000/SASjsApi/session
### multipart upload to sas server file
POST http://localhost:5000/SASjsApi/drive/file
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="filePath"
/saad/files/new.sas
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
Content-Type: application/octet-stream
< ./sample.sas
------WebKitFormBoundary7MA4YWxkTrZu0gW--
### multipart upload to sas server file text
POST http://localhost:5000/SASjsApi/drive/file
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW \n
Content-Disposition: form-data; name="filePath"
/saad/files/new2.sas
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="sample_new.sas"
Content-Type: text/plain
SOME CONTENTS OF SAS FILE IN REQUEST
------WebKitFormBoundary7MA4YWxkTrZu0gW--
Users
"username": "username1",

1
restClient/sample.sas Normal file
View File

@@ -0,0 +1 @@
some code of sas

2
restClient/session.rest Normal file
View File

@@ -0,0 +1,2 @@
### Get current user's info via access token
GET http://localhost:5000/SASjsApi/session

10
restClient/users.rest Normal file
View File

@@ -0,0 +1,10 @@
### Create User
POST http://localhost:5000/SASjsApi/user
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InNlY3JldHVzZXIiLCJpc2FkbWluIjp0cnVlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODAzOTc3LCJleHAiOjE2MzU4OTAzNzd9.f-FLgLwryKvB5XrihdzaGZajO3d5E5OHEEuJI_03GRI
Content-Type: application/json
{
"displayname": "User 2",
"username": "username2",
"password": "some password"
}

4
web/.babelrc Normal file
View File

@@ -0,0 +1,4 @@
{
"presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View File

@@ -1,2 +1,2 @@
REACT_APP_PORT_API=[place sasjs server port] default value is 5000
REACT_APP_CLIENT_ID=<place clientId here>
PORT_API=[place sasjs server port] default value is 5000
CLIENT_ID=<place clientId here>

35236
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,8 @@
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"start": "npx webpack-dev-server --config webpack.dev.ts --hot",
"build": "npx webpack --config webpack.prod.ts"
},
"dependencies": {
"@emotion/react": "^11.4.1",
@@ -22,19 +20,43 @@
"@types/jest": "^26.0.24",
"@types/node": "^12.20.28",
"@types/react": "^17.0.27",
"@types/react-dom": "^17.0.9",
"axios": "^0.22.0",
"axios": "^0.24.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
"typescript": "^4.4.3"
"react-router-dom": "^5.3.0"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/node": "^7.16.0",
"@babel/plugin-proposal-class-properties": "^7.16.0",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/preset-typescript": "^7.16.0",
"@types/dotenv-webpack": "^7.0.3",
"@types/prismjs": "^1.16.6",
"@types/react": "^17.0.37",
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.1",
"babel-loader": "^8.2.3",
"babel-plugin-prismjs": "^2.1.0",
"prettier": "^2.4.1"
"copy-webpack-plugin": "^10.0.0",
"css-loader": "^6.5.1",
"dotenv-webpack": "^7.1.0",
"eslint": "^8.5.0",
"eslint-config-react-app": "^7.0.0",
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "5.5.0",
"path": "0.12.7",
"prettier": "^2.4.1",
"sass": "^1.44.0",
"sass-loader": "^12.3.0",
"style-loader": "^3.3.1",
"ts-loader": "^9.2.6",
"typescript": "^4.5.2",
"webpack": "5.64.3",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "4.7.4"
},
"eslintConfig": {
"extends": [

View File

@@ -19,7 +19,14 @@ function App() {
<ThemeProvider theme={theme}>
<HashRouter>
<Header />
<Switch>
<Route exact path="/SASjsLogon">
<Login getCodeOnly />
</Route>
<Route path="/">
<Login setTokens={setTokens} />
</Route>
</Switch>
</HashRouter>
</ThemeProvider>
)
@@ -39,6 +46,9 @@ function App() {
<Route exact path="/SASjsStudio">
<Studio />
</Route>
<Route exact path="/SASjsLogon">
<Login getCodeOnly />
</Route>
</Switch>
</HashRouter>
</ThemeProvider>

View File

@@ -23,7 +23,7 @@ const Home = () => {
and contributions are welcomed.
</p>
<p>
SASjs Server is maintained by the SASjs Apps team -{' '}
SASjs Server is maintained by the SAS Apps team -{' '}
<a
href="https://sasapps.io/contact-us"
target="_blank"

View File

@@ -1,24 +1,29 @@
import React, { useState } from 'react'
import { useLocation } from 'react-router-dom'
import PropTypes from 'prop-types'
import { CssBaseline, Box, TextField, Button } from '@mui/material'
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json'
}
const { NODE_ENV, REACT_APP_PORT_API } = process.env
const NODE_ENV = process.env.NODE_ENV
const PORT_API = process.env.PORT_API
const baseUrl =
NODE_ENV === 'development'
? `http://localhost:${REACT_APP_PORT_API ?? 5000}`
: ''
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
const getAuthCode = async (credentials: any) => {
return fetch(`${baseUrl}/SASjsApi/auth/authorize`, {
method: 'POST',
headers,
body: JSON.stringify(credentials)
}).then((data) => data.json())
}).then(async (response) => {
const resText = await response.text()
if (response.status !== 200) throw resText
return JSON.parse(resText)
})
}
const getTokens = async (payload: any) => {
return fetch(`${baseUrl}/SASjsApi/auth/token`, {
@@ -28,20 +33,40 @@ const getTokens = async (payload: any) => {
}).then((data) => data.json())
}
const Login = ({ setTokens }: any) => {
const [username, setUserName] = useState()
const [password, setPassword] = useState()
const Login = ({ setTokens, getCodeOnly }: any) => {
const location = useLocation()
const [username, setUserName] = useState('')
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState('')
let error: boolean
const [displayCode, setDisplayCode] = useState(null)
const handleSubmit = async (e: any) => {
error = false
setErrorMessage('')
e.preventDefault()
const { REACT_APP_CLIENT_ID: clientId } = process.env
let clientId = process.env.CLIENT_ID
if (getCodeOnly) {
const params = new URLSearchParams(location.search)
const responseType = params.get('response_type')
if (responseType === 'code')
clientId = params.get('client_id') ?? undefined
}
const { code } = await getAuthCode({
clientId,
username,
password
}).catch((err: string) => {
error = true
setErrorMessage(err)
return {}
})
if (!error) {
if (getCodeOnly) return setDisplayCode(code)
const { accessToken, refreshToken } = await getTokens({
clientId,
code
@@ -49,6 +74,22 @@ const Login = ({ setTokens }: any) => {
setTokens(accessToken, refreshToken)
}
}
if (displayCode) {
return (
<Box className="main">
<CssBaseline />
<br />
<h2>Authorization Code</h2>
<Typography m={2} p={3} style={{ overflowWrap: 'anywhere' }}>
{displayCode}
</Typography>
<br />
</Box>
)
}
return (
<Box
@@ -61,7 +102,12 @@ const Login = ({ setTokens }: any) => {
>
<CssBaseline />
<br />
<h2>Welcome to SASjs Server!</h2>
<h2 style={{ width: 'auto' }}>Welcome to SASjs Server!</h2>
{getCodeOnly && (
<p style={{ width: 'auto' }}>
Provide credentials to get authorization code.
</p>
)}
<br />
<TextField
@@ -80,6 +126,7 @@ const Login = ({ setTokens }: any) => {
onChange={(e: any) => setPassword(e.target.value)}
required
/>
{errorMessage && <span>{errorMessage}</span>}
<Button type="submit" variant="outlined">
Submit
</Button>
@@ -88,7 +135,8 @@ const Login = ({ setTokens }: any) => {
}
Login.propTypes = {
setTokens: PropTypes.func.isRequired
setTokens: PropTypes.func,
getCodeOnly: PropTypes.bool
}
export default Login

View File

@@ -3,15 +3,8 @@ import { useEffect, useState } from 'react'
export default function useTokens() {
const getTokens = () => {
const accessTokenString = localStorage.getItem('accessToken')
const accessToken: string = accessTokenString
? JSON.parse(accessTokenString)
: undefined
const refreshTokenString = localStorage.getItem('refreshToken')
const refreshToken: string = refreshTokenString
? JSON.parse(refreshTokenString)
: undefined
const accessToken = localStorage.getItem('accessToken')
const refreshToken = localStorage.getItem('refreshToken')
if (accessToken && refreshToken) {
setAxiosRequestHeader(accessToken)
@@ -31,8 +24,8 @@ export default function useTokens() {
setAxiosResponse(setTokens)
const saveTokens = (accessToken: string, refreshToken: string) => {
localStorage.setItem('accessToken', JSON.stringify(accessToken))
localStorage.setItem('refreshToken', JSON.stringify(refreshToken))
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
setAxiosRequestHeader(accessToken)
setTokens({ accessToken, refreshToken })
}
@@ -43,11 +36,10 @@ export default function useTokens() {
}
}
const { NODE_ENV, REACT_APP_PORT_API } = process.env
const NODE_ENV = process.env.NODE_ENV
const PORT_API = process.env.PORT_API
const baseUrl =
NODE_ENV === 'development'
? `http://localhost:${REACT_APP_PORT_API ?? 5000}`
: ''
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
const isAbsoluteURLRegex = /^(?:\w+:)\/\//

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import Editor from '@monaco-editor/react'
@@ -22,9 +23,9 @@ const Main = (props: any) => {
if (props.selectedFilePath) {
setIsLoading(true)
axios
.get(`/SASjsApi/drive/file?filePath=${props.selectedFilePath}`)
.get(`/SASjsApi/drive/file?_filePath=${props.selectedFilePath}`)
.then((res: any) => {
setFileContent(res.data.fileContent)
setFileContent(res.data)
})
.catch((err) => {
console.log(err)
@@ -35,17 +36,40 @@ const Main = (props: any) => {
}
}, [props.selectedFilePath])
const handleDeleteBtnClick = () => {
setIsLoading(true)
const filePath = props.selectedFilePath
axios
.delete(`/SASjsApi/drive/file?_filePath=${filePath}`)
.then((res) => {
setFileContent('')
window.history.pushState('', '', `${baseUrl}/#/SASjsDrive`)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
setIsLoading(false)
})
}
const handleEditSaveBtnClick = () => {
if (!editMode) {
setFileContentBeforeEdit(fileContent)
setEditMode(true)
} else {
setIsLoading(true)
const formData = new FormData()
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
formData.append('file', stringBlob, 'filename.sas')
formData.append('filePath', props.selectedFilePath)
axios
.patch(`/SASjsApi/drive/file`, {
filePath: props.selectedFilePath,
fileContent: fileContent
})
.patch(`/SASjsApi/drive/file`, formData)
.then((res) => {
setEditMode(false)
})
@@ -89,10 +113,10 @@ const Main = (props: any) => {
style={{ position: 'absolute', left: '50%', top: '50%' }}
/>
)}
{!isLoading && props?.selectedFilePath !== '' && !editMode && (
{!isLoading && props?.selectedFilePath && !editMode && (
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
)}
{!isLoading && props?.selectedFilePath !== '' && editMode && (
{!isLoading && props?.selectedFilePath && editMode && (
<Editor
height="95%"
value={fileContent}
@@ -107,20 +131,36 @@ const Main = (props: any) => {
direction="row"
sx={{ justifyContent: 'center', marginTop: '20px' }}
>
<Button
variant="contained"
onClick={handleDeleteBtnClick}
disabled={isLoading || !props?.selectedFilePath}
>
Delete
</Button>
<Button
variant="contained"
onClick={handleEditSaveBtnClick}
disabled={isLoading || props?.selectedFilePath === ''}
disabled={isLoading || !props?.selectedFilePath}
>
{!editMode ? 'Edit' : 'Save'}
</Button>
<Button
variant="contained"
onClick={handleCancelExecuteBtnClick}
disabled={isLoading || props?.selectedFilePath === ''}
disabled={isLoading || !props?.selectedFilePath}
>
{editMode ? 'Cancel' : 'Execute'}
</Button>
{props?.selectedFilePath && (
<Button
variant="contained"
component={Link}
to={`/SASjsStudio?_program=${props.selectedFilePath}`}
>
Open in Studio
</Button>
)}
</Stack>
</Box>
)

View File

@@ -64,7 +64,10 @@ const SideBar = (props: any) => {
}, [setFilePathOnMount])
const handleSelect = (node: TreeNode) => {
if (!node.children.length) {
if (node.children.length) return
if (!node.name.includes('.')) return
window.history.pushState(
'',
'',
@@ -72,7 +75,6 @@ const SideBar = (props: any) => {
)
setSelectedFilePath(node.relativePath)
}
}
const renderTree = (nodes: TreeNode) => (
<TreeItem

View File

@@ -1,14 +1,169 @@
import React from 'react'
import React, { useEffect, useRef, useState } from 'react'
import axios from 'axios'
import CssBaseline from '@mui/material/CssBaseline'
import Box from '@mui/material/Box'
import { Button, Paper, Stack, Tab } from '@mui/material'
import { makeStyles } from '@mui/styles'
import Editor, { OnMount } from '@monaco-editor/react'
import { useLocation } from 'react-router-dom'
import { TabContext, TabList, TabPanel } from '@mui/lab'
const useStyles = makeStyles(() => ({
root: {
fontSize: '1rem',
color: 'gray',
'&.Mui-selected': {
color: 'black'
}
}
}))
const Studio = () => {
const location = useLocation()
const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('')
const [webout, setWebout] = useState('')
const [tab, setTab] = React.useState('1')
const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue)
}
const editorRef = useRef(null as any)
const handleEditorDidMount: OnMount = (editor) => {
editor.focus()
editorRef.current = editor
}
const getSelection = () => {
const editor = editorRef.current as any
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
return selection ?? ''
}
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
const runCode = (code: string) => {
axios
.post(`/SASjsApi/code/execute`, { code })
.then((res: any) => {
const parsedLog = res?.data?.log
.map((logLine: any) => logLine.line)
.join('\n')
setLog(`<div><h2>SAS Log</h2><pre>${parsedLog}</pre></div>`)
let weboutString: string
try {
weboutString = res.data.webout
.split('>>weboutBEGIN<<')[1]
.split('>>weboutEND<<')[0]
} catch (_) {
weboutString = res?.data?.webout ?? ''
}
let webout: string
try {
webout = JSON.stringify(JSON.parse(weboutString), null, 4)
} catch (_) {
webout = weboutString
}
setWebout(`<pre><code>${webout}</code></pre>`)
setTab('2')
})
.catch((err) => console.log(err))
}
useEffect(() => {
const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content)
}, [])
useEffect(() => {
if (fileContent.length) {
localStorage.setItem('fileContent', fileContent)
}
}, [fileContent])
useEffect(() => {
const params = new URLSearchParams(location.search)
const programPath = params.get('_program')
if (programPath?.length)
axios
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
.then((res: any) => setFileContent(res.data.fileContent))
.catch((err) => console.log(err))
}, [location.search])
const classes = useStyles()
return (
<Box className="main">
<CssBaseline />
<h2>This is container for SASjs studio</h2>
<>
<br />
<br />
<br />
<Box sx={{ width: '100%', typography: 'body1' }}>
<TabContext value={tab}>
<Box
sx={{
borderBottom: 1,
borderColor: 'divider'
}}
style={{ position: 'fixed', background: 'white', width: '100%' }}
>
<TabList onChange={handleTabChange} centered>
<Tab className={classes.root} label="Code" value="1" />
<Tab className={classes.root} label="Log" value="2" />
<Tab className={classes.root} label="Webout" value="3" />
</TabList>
</Box>
<TabPanel value="1">
{/* <Toolbar /> */}
<Paper
sx={{
height: '70vh',
marginTop: '50px',
padding: '10px',
overflow: 'auto',
position: 'relative'
}}
elevation={3}
>
<Editor
height="95%"
value={fileContent}
onMount={handleEditorDidMount}
onChange={(val) => {
if (val) setFileContent(val)
}}
/>
</Paper>
<Stack
spacing={3}
direction="row"
sx={{ justifyContent: 'center', marginTop: '20px' }}
>
<Button variant="contained" onClick={handleRunBtnClick}>
Run SAS Code
</Button>
</Stack>
</TabPanel>
<TabPanel value="2">
<div
id="sas_log"
style={{ marginTop: '50px' }}
dangerouslySetInnerHTML={{ __html: log }}
/>
</TabPanel>
<TabPanel value="3">
<div
style={{ marginTop: '50px' }}
dangerouslySetInnerHTML={{ __html: webout }}
/>
</TabPanel>
</TabContext>
</Box>
</>
)
}

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="SASjs Server Web Interface" />
@@ -10,7 +10,7 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.

60
web/webpack.common.ts Normal file
View File

@@ -0,0 +1,60 @@
import path from 'path'
import { Configuration } from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import CopyPlugin from 'copy-webpack-plugin'
import dotenv from 'dotenv-webpack'
const config: Configuration = {
entry: path.join(__dirname, 'src', 'index.tsx'),
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx']
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
options: {
compilerOptions: {
noEmit: false
}
}
}
]
},
{
test: /\.css$/,
exclude: ['/node_modules/'],
use: ['style-loader', 'css-loader']
},
{
test: /\.scss$/,
exclude: ['/node_modules/'],
use: ['style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(jpg|jpeg|png|gif|mp3|svg)$/,
use: ['file-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src', 'index.html')
}),
new CopyPlugin({
patterns: [{ from: 'public' }]
}),
new dotenv()
]
}
export default config

28
web/webpack.dev.ts Normal file
View File

@@ -0,0 +1,28 @@
import path from 'path'
import { Configuration as WebpackConfiguration } from 'webpack'
import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'
import { merge } from 'webpack-merge'
import common from './webpack.common'
interface Configuration extends WebpackConfiguration {
devServer?: WebpackDevServerConfiguration
}
const devConfig: Configuration = merge(common, {
mode: 'development',
output: {
path: path.join(__dirname, 'build'),
filename: 'index.bundle.js',
publicPath: '/'
},
devServer: {
static: {
directory: path.join(__dirname, 'build')
},
historyApiFallback: true,
port: 3000
}
})
export default devConfig

19
web/webpack.prod.ts Normal file
View File

@@ -0,0 +1,19 @@
import path from 'path'
import { Configuration } from 'webpack'
import { merge } from 'webpack-merge'
import common from './webpack.common'
const prodConfig: Configuration = merge(common, {
mode: 'production',
output: {
path: path.join(__dirname, 'build'),
filename: 'index.bundle.js',
publicPath: './'
},
performance: {
hints: false
}
})
export default prodConfig