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

Compare commits

...

98 Commits

Author SHA1 Message Date
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
55 changed files with 7659 additions and 37734 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

@@ -32,10 +32,17 @@ jobs:
env:
CI: true
- 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
./executables/linux.zip
./executables/macos.zip
./executables/windows.zip

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

View File

@@ -2,6 +2,161 @@
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.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)

119
README.md
View File

@@ -8,110 +8,87 @@ 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).
## Configuration
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentiation, and a database)
Configuration is made in the `configuration` section of `package.json`:
## Desktop Version
- Provide path to SAS9 executable.
### Manual Installation
### Using dockers:
Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
There is `.env.example` file present at root of the project. [for Production]
Next, trigger by double clicking (windows) or executing from commandline.
There is `.env.example` file present at `./api` of the project. [for Development]
You are presented with two prompts:
There is `.env.example` file present at `./web` of the project. [for Development]
- Location of your `sas.exe` / `sas.sh` executable
- Path to a filesystem location for Stored Programs and temporary files
Remember to provide enviornment variables.
## Programmatic Installation
#### Development
Fetch the relevant package from github using `curl`, eg as follows (for linux):
Command to run docker for development:
```
docker-compose up -d
```bash
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
unzip linux.zip
```
It uses default docker compose file i.e. `docker-compose.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.
- `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_
When launching the app, it will make use of specific environment variables. These can be set in the following places:
#### Production
- 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
Command to run docker for production:
Example variables:
```
docker-compose -f docker-compose.prod.yml up -d
PORT=5004
SAS_PATH=/path/to/sas/executable.exe
DRIVE_PATH=./tmp
```
It uses specified docker compose file i.e. `docker-compose.prod.yml` present at root.
It will build following images if running first time:
Setting these prompts variables will avoid the need for prompts.
- `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.
## Executables
## Server Version
Command to generate executables
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
```
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

8689
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,15 @@
{
"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",
"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}\"",
@@ -41,12 +38,12 @@
},
"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",
"cors": "^2.8.5",
@@ -58,7 +55,7 @@
"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",
@@ -79,13 +76,12 @@
"pkg": "^5.4.1",
"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,16 @@ components:
- clientSecret
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
@@ -388,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:
@@ -501,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:
type: string
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
@@ -972,6 +1006,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
@@ -1027,26 +1081,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
/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: []
servers:
-
url: /
@@ -1072,3 +1106,6 @@ tags:
-
name: STP
description: 'Operations about STP'
-
name: CODE
description: 'Operations on SAS code'

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

@@ -13,11 +13,14 @@ 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' }))

View File

@@ -0,0 +1,63 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecutionController } from './internal'
import { PreProgramVars } from '../types'
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<string> {
return executeSASCode(request, body)
}
}
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
try {
const result = await new ExecutionController().executeProgram(
code,
getPreProgramVariables(req),
{ ...req.query, _debug: 131 },
undefined,
true
)
return result as string
} 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

@@ -153,7 +153,7 @@ export class DriveController {
}
const getFileTree = () => {
const tree = new ExecutionController().buildDirectorytree()
const tree = new ExecutionController().buildDirectoryTree()
return { status: 'success', tree }
}

View File

@@ -1,7 +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'
export * from './session'

View File

@@ -5,23 +5,43 @@ import { readFile, fileExists, createFile, moveFile } from '@sasjs/utils'
import { PreProgramVars, TreeNode } from '../../types'
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
export interface ExecutionVars {
[key: string]: string | number | undefined
}
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
) {
const sessionController = getSessionController()
const session = await sessionController.getSession()
session.inUse = true
session.consumed = true
const logPath = path.join(session.path, 'log.log')
@@ -39,6 +59,7 @@ export class ExecutionController {
`${computed}%let ${key}=${vars[key]};\n`,
''
)
const preProgramVarStatments = `
%let _sasjs_tokenfile=${tokenFile};
%let _sasjs_username=${preProgramVariables?.username};
@@ -48,7 +69,16 @@ 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 = `
/* runtime vars */
@@ -85,14 +115,12 @@ ${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 webout = (await fileExists(weboutPath))
? await readFile(weboutPath)
: ''
@@ -100,8 +128,8 @@ ${program}`
const debugValue =
typeof vars._debug === 'string' ? parseInt(vars._debug) : vars._debug
// it should be deleted by scheduleSessionDestroy
session.inUse = false
sessionController.deleteSession(session)
if (returnJson) {
return {
@@ -116,7 +144,7 @@ ${program}`
: 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,
@@ -105,15 +110,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 +127,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 +157,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

@@ -1,7 +1,7 @@
import express from 'express'
import path from 'path'
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
import { ExecutionController } from './internal'
import { ExecutionController, ExecutionVars } from './internal'
import { PreProgramVars } from '../types'
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
@@ -40,6 +40,7 @@ export class STPController {
): Promise<string> {
return executeReturnRaw(request, _program)
}
/**
* Trigger a SAS program using it's location in the _program parameter.
* Enable debugging using the _debug parameter.
@@ -65,14 +66,14 @@ const executeReturnRaw = async (
req: express.Request,
_program: string
): Promise<string> => {
const query = req.query as { [key: string]: string | number | undefined }
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 = await new ExecutionController().executeFile(
sasCodePath,
getPreProgramVariables(req),
query
@@ -101,7 +102,7 @@ const executeReturnJson = async (
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
try {
const { webout, log } = (await new ExecutionController().execute(
const { webout, log } = (await new ExecutionController().executeFile(
sasCodePath,
getPreProgramVariables(req),
{ ...req.query, ...req.body },

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

@@ -0,0 +1,25 @@
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)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter

View File

@@ -11,6 +11,7 @@ import {
import driveRouter from './drive'
import stpRouter from './stp'
import codeRouter from './code'
import userRouter from './user'
import groupRouter from './group'
import clientRouter from './client'
@@ -31,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

@@ -1,6 +1,5 @@
import express from 'express'
import { SessionController } from '../../controllers'
import { authenticateAccessToken } from '../../middlewares'
const sessionRouter = express.Router()

View File

@@ -26,7 +26,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)

View File

@@ -1,10 +1,26 @@
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
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

@@ -3,12 +3,17 @@ import mongoose from 'mongoose'
import { configuration } from '../../package.json'
import { getDesktopFields } from '.'
import { populateClients } from '../routes/api/auth'
import { getRealPath } from '@sasjs/utils'
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') {
// we should exclude connecting to the real database
if (process.env.NODE_ENV === 'test') {
process.driveLoc = path.join(process.cwd(), 'tmp')
return
} else {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
@@ -16,16 +21,19 @@ export const connectDB = async () => {
process.sasLoc = sasLoc
process.driveLoc = driveLoc
} else {
const { SAS_PATH, DRIVE_PATH } = process.env
return
process.sasLoc = SAS_PATH ?? configuration.sasPath
process.driveLoc = getRealPath(
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
)
}
const { SAS_PATH } = process.env
const sasDir = SAS_PATH ?? configuration.sasPath
process.sasLoc = path.join(sasDir, 'sas')
console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc)
if (MODE?.trim() !== 'server') return
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err

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, '..')
@@ -12,8 +11,7 @@ export const sysInitCompiledPath = path.join(
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 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,33 @@
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)'))
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

@@ -3,6 +3,7 @@ export * from './file'
export * from './generateAccessToken'
export * from './generateAuthCode'
export * from './generateRefreshToken'
export * from './getCertificates'
export * from './getDesktopFields'
export * from './removeTokensInDB'
export * from './saveTokensInDB'

View File

@@ -77,6 +77,11 @@ export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
fileContent: Joi.string().required()
}).validate(data)
export const runSASValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required()
}).validate(data)
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
Joi.object({
_program: Joi.string().required()

View File

@@ -38,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:

16
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "server",
"version": "0.0.10",
"version": "0.0.29",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "server",
"version": "0.0.10",
"version": "0.0.29",
"devDependencies": {
"prettier": "^2.3.1",
"standard-version": "^9.3.2"
@@ -865,9 +865,9 @@
}
},
"node_modules/graceful-fs": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
"version": "4.2.9",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
"dev": true
},
"node_modules/handlebars": {
@@ -2787,9 +2787,9 @@
}
},
"graceful-fs": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
"integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
"version": "4.2.9",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==",
"dev": true
},
"handlebars": {

View File

@@ -1,11 +1,12 @@
{
"name": "server",
"version": "0.0.10",
"version": "0.0.29",
"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}\"",

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>

35214
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'
@@ -89,10 +90,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}
@@ -110,17 +111,26 @@ const Main = (props: any) => {
<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

@@ -1,14 +1,165 @@
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) => {
setLog(`<div><h2>SAS Log</h2><pre>${res?.data?.log}</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