mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c872bde92 | ||
|
|
f953472efd | ||
|
|
f10138b0f2 | ||
|
|
6f19d3d0ea | ||
|
|
a7facb005a | ||
|
|
88acf9df5d | ||
|
|
b0880b142a | ||
|
|
d3674c7f94 | ||
|
|
adccca6c7f | ||
|
|
8b83ccc4c2 | ||
|
|
556944b1d5 | ||
|
|
b14e07ee6e | ||
|
|
048bd9f78c | ||
|
|
d7e1aca7e3 | ||
|
|
de47d78a00 | ||
|
|
58b6f439b3 | ||
|
|
0cfe724ffa | ||
|
|
fde4bc051d | ||
|
|
367b0f1f89 | ||
|
|
d17a3dd590 | ||
|
|
bee5deed2a | ||
|
|
e6e46838b3 | ||
|
|
404f1ec059 | ||
|
|
09d36bc754 | ||
|
|
3722bbaec3 | ||
|
|
480ee4da83 | ||
|
|
dd853fe13b | ||
|
|
e1142a33a0 | ||
|
|
d4e8d91cae | ||
|
|
9a74ec545d | ||
|
|
f2000a1227 | ||
|
|
bf5767eadf | ||
|
|
e3f5206758 | ||
|
|
fffd21b348 | ||
|
|
2d74ef5e12 | ||
|
|
224743a439 | ||
|
|
f39a76da17 | ||
|
|
6107d02c8e | ||
|
|
1966b17f27 | ||
|
|
87c8aa5146 | ||
|
|
e4c027ad51 | ||
|
|
083355fdba | ||
|
|
a3b57f6e28 | ||
|
|
b0ffa145bc | ||
|
|
a8df5f4afd | ||
|
|
62de960e86 | ||
|
|
31532c0efa | ||
|
|
732230524d | ||
|
|
6dc281313e | ||
|
|
92db3c7c82 | ||
|
|
d8b75a47d3 | ||
|
|
d70fc1032f | ||
|
|
794ee8f6e0 | ||
|
|
43769e711d | ||
|
|
30528a1528 | ||
|
|
b7e1753d25 | ||
|
|
9c5772a303 | ||
|
|
7a3d710153 | ||
|
|
0a6ebe6e62 | ||
|
|
6cbc657da3 | ||
|
|
cd838915fd | ||
|
|
4e486fda69 | ||
|
|
79cac53fdb | ||
|
|
450d99f06e | ||
|
|
51ee8c0825 | ||
|
|
a1151606f2 | ||
|
|
38193c83dd | ||
|
|
59ecc36f2b | ||
|
|
8bc459c9a7 | ||
|
|
f1f1e47f76 | ||
|
|
679e9de245 | ||
|
|
f0ac996b3c | ||
|
|
2d77222ae8 | ||
|
|
e6e5a5fd64 | ||
|
|
e1eb04494a | ||
|
|
b7fa8e5f80 | ||
|
|
ef4fae4496 | ||
|
|
3e5a4e0555 | ||
|
|
cf9a8091ea | ||
|
|
0edc45dd0a | ||
|
|
ceca370e27 | ||
|
|
f235b9c2f9 | ||
|
|
d86c841f1f | ||
|
|
076b866c02 | ||
|
|
19d4430b31 | ||
|
|
e5be0e6789 | ||
|
|
27129a8921 | ||
|
|
da11c03d55 | ||
|
|
4fbdda0365 | ||
|
|
efacb1e916 | ||
|
|
d19ce253b4 | ||
|
|
e11a4b66e7 | ||
|
|
d0a1457f44 | ||
|
|
34e54934fd | ||
|
|
4873e6054f | ||
|
|
b00aa4e17b | ||
|
|
9fccfe6f35 | ||
|
|
0a9d734e09 | ||
|
|
a0822e6b61 | ||
|
|
43545fa04b |
@@ -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
115
.github/CONTRIBUTING.md
vendored
Normal 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`
|
||||
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@@ -2,8 +2,8 @@ name: SASjs Server Executable Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -32,17 +32,17 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Create Tag with Release
|
||||
uses: Klemensas/action-autotag@stable
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
- 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
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
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
|
||||
|
||||
240
CHANGELOG.md
240
CHANGELOG.md
@@ -2,6 +2,246 @@
|
||||
|
||||
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.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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
119
README.md
119
README.md
@@ -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
89
SASjsServer.drawio
Normal 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="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/authorize<br>(username,password,clientId)</span></div>" 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="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`code`</span></font>" 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="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><span style="color: #a31515">/SASjsApi/auth/token</span></div><span style="color: #a31515">(clientId,code)</span></div>" 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="<font color="#a31515" face="menlo, monaco, courier new, monospace"><span style="font-size: 12px">`</span></font><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">accessToken</span><span style="font-size: 12px ; color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace">` &amp; `</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">refreshToken</span><span style="color: rgb(163 , 21 , 21) ; font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px">`</span>" 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="<span>Validates</span><br><span>username/password/clientId</span><br><span>and issue short</span><br><span>Authorization code</span>" 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<br>clientId &amp; authorization code<br>and issue<br>Access Token &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<br>Authenticate requests <br>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="<div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; font-size: 12px ; line-height: 18px"><div style="font-family: &#34;menlo&#34; , &#34;monaco&#34; , &#34;courier new&#34; , monospace ; line-height: 18px"><font color="#a31515">Request with Access Token</font></div></div>" 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>
|
||||
@@ -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
|
||||
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
|
||||
|
||||
8693
api/package-lock.json
generated
8693
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -362,14 +372,15 @@ components:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
log:
|
||||
type: string
|
||||
_webout:
|
||||
type: string
|
||||
log:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- _webout
|
||||
type: object
|
||||
additionalProperties: false
|
||||
ExecuteReturnJsonPayload:
|
||||
@@ -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:
|
||||
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
|
||||
@@ -971,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
|
||||
@@ -981,7 +1036,7 @@ paths:
|
||||
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."
|
||||
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
||||
summary: 'Execute Stored Program, return raw content'
|
||||
tags:
|
||||
- STP
|
||||
@@ -1005,7 +1060,7 @@ 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."
|
||||
description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created."
|
||||
summary: 'Execute Stored Program, return JSON'
|
||||
tags:
|
||||
- STP
|
||||
@@ -1030,6 +1085,9 @@ servers:
|
||||
-
|
||||
url: /
|
||||
tags:
|
||||
-
|
||||
name: Session
|
||||
description: 'Get Session information'
|
||||
-
|
||||
name: User
|
||||
description: 'Operations about users'
|
||||
@@ -1048,3 +1106,6 @@ tags:
|
||||
-
|
||||
name: STP
|
||||
description: 'Operations about STP'
|
||||
-
|
||||
name: CODE
|
||||
description: 'Operations on SAS code'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' }))
|
||||
|
||||
63
api/src/controllers/code.ts
Normal file
63
api/src/controllers/code.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export class DriveController {
|
||||
}
|
||||
|
||||
const getFileTree = () => {
|
||||
const tree = new ExecutionController().buildDirectorytree()
|
||||
const tree = new ExecutionController().buildDirectoryTree()
|
||||
return { status: 'success', tree }
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,15 @@ 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;
|
||||
|
||||
%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 +114,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,29 +127,23 @@ ${program}`
|
||||
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 {
|
||||
webout,
|
||||
log:
|
||||
(debugValue && debugValue >= 131) || session.crashed ? log : undefined
|
||||
}
|
||||
if ((debugValue && debugValue >= 131) || session.crashed) {
|
||||
response.log = log
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
return debugResponse ?? webout
|
||||
|
||||
return (debugValue && debugValue >= 131) || session.crashed
|
||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||
: webout
|
||||
}
|
||||
|
||||
buildDirectorytree() {
|
||||
buildDirectoryTree() {
|
||||
const root: TreeNode = {
|
||||
name: 'files',
|
||||
relativePath: '',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
@@ -92,7 +97,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 +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,11 +127,9 @@ 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
|
||||
)
|
||||
}
|
||||
this.sessions = this.sessions.filter(
|
||||
(sess: Session) => sess.id !== session.id
|
||||
)
|
||||
}
|
||||
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
@@ -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);
|
||||
|
||||
30
api/src/controllers/session.ts
Normal file
30
api/src/controllers/session.ts
Normal 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
|
||||
})
|
||||
@@ -1,17 +1,7 @@
|
||||
import express, { response } from 'express'
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import {
|
||||
Request,
|
||||
Security,
|
||||
Route,
|
||||
Tags,
|
||||
Example,
|
||||
Post,
|
||||
Body,
|
||||
Get,
|
||||
Query
|
||||
} from 'tsoa'
|
||||
import { ExecutionController } from './internal'
|
||||
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
||||
import { ExecutionController, ExecutionVars } from './internal'
|
||||
import { PreProgramVars } from '../types'
|
||||
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
|
||||
|
||||
@@ -24,8 +14,8 @@ interface ExecuteReturnJsonPayload {
|
||||
}
|
||||
interface ExecuteReturnJsonResponse {
|
||||
status: string
|
||||
_webout: string
|
||||
log?: string
|
||||
_webout?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
@@ -50,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.
|
||||
@@ -75,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
|
||||
@@ -111,17 +102,17 @@ const executeReturnJson = async (
|
||||
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
||||
|
||||
try {
|
||||
const jsonResult: any = await new ExecutionController().execute(
|
||||
const { webout, log } = (await new ExecutionController().executeFile(
|
||||
sasCodePath,
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, ...req.body },
|
||||
{ filesNamesMap: filesNamesMap },
|
||||
true
|
||||
)
|
||||
)) as { webout: string; log: string }
|
||||
return {
|
||||
status: 'success',
|
||||
_webout: jsonResult.webout,
|
||||
log: jsonResult.log
|
||||
_webout: webout,
|
||||
log
|
||||
}
|
||||
} catch (err: any) {
|
||||
throw {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
18
api/src/middlewares/desktop.ts
Normal file
18
api/src/middlewares/desktop.ts
Normal 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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './authenticateToken'
|
||||
export * from './desktopRestrict'
|
||||
export * from './desktop'
|
||||
export * from './verifyAdmin'
|
||||
export * from './verifyAdminIfNeeded'
|
||||
|
||||
@@ -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}`
|
||||
)
|
||||
})
|
||||
})
|
||||
25
api/src/routes/api/code.ts
Normal file
25
api/src/routes/api/code.ts
Normal 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
|
||||
@@ -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(
|
||||
'/',
|
||||
|
||||
16
api/src/routes/api/session.ts
Normal file
16
api/src/routes/api/session.ts
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
app.listen(sasJsPort, () => {
|
||||
console.log(
|
||||
`⚡️[server]: Server is running at http://localhost:${sasJsPort}`
|
||||
)
|
||||
})
|
||||
|
||||
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}`
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
2
api/src/types/Process.d.ts
vendored
2
api/src/types/Process.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
sasLoc: string
|
||||
driveLoc?: string
|
||||
driveLoc: string
|
||||
sessionController?: import('../controllers/internal').SessionController
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Session {
|
||||
deathTimeStamp: string
|
||||
path: string
|
||||
inUse: boolean
|
||||
consumed: boolean
|
||||
completed: boolean
|
||||
crashed?: string
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
return
|
||||
} else {
|
||||
const { SAS_PATH } = process.env
|
||||
const sasDir = SAS_PATH ?? configuration.sasPath
|
||||
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||
|
||||
process.sasLoc = path.join(sasDir, 'sas')
|
||||
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)
|
||||
|
||||
if (MODE?.trim() !== 'server') return
|
||||
|
||||
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
|
||||
if (err) throw err
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
33
api/src/utils/getCertificates.ts
Normal file
33
api/src/utils/getCertificates.ts
Normal 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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
16
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.28",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "server",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.28",
|
||||
"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": {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.28",
|
||||
"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
4
web/.babelrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||
}
|
||||
@@ -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>
|
||||
35256
web/package-lock.json
generated
35256
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": [
|
||||
|
||||
@@ -19,7 +19,14 @@ function App() {
|
||||
<ThemeProvider theme={theme}>
|
||||
<HashRouter>
|
||||
<Header />
|
||||
<Login setTokens={setTokens} />
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,26 +33,62 @@ 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 {}
|
||||
})
|
||||
|
||||
const { accessToken, refreshToken } = await getTokens({
|
||||
clientId,
|
||||
code
|
||||
})
|
||||
if (!error) {
|
||||
if (getCodeOnly) return setDisplayCode(code)
|
||||
|
||||
setTokens(accessToken, refreshToken)
|
||||
const { accessToken, refreshToken } = await getTokens({
|
||||
clientId,
|
||||
code
|
||||
})
|
||||
|
||||
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 (
|
||||
@@ -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
|
||||
|
||||
@@ -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+:)\/\//
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
</Box>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
60
web/webpack.common.ts
Normal 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
28
web/webpack.dev.ts
Normal 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
19
web/webpack.prod.ts
Normal 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
|
||||
Reference in New Issue
Block a user