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

Compare commits

...

98 Commits

Author SHA1 Message Date
cc6f8a64b5 fix(web-header): show users display name instead of username 2022-04-27 22:43:23 +05:00
Allan Bowe
4ae8f35e9a chore(release): 0.0.58 2022-04-24 20:25:08 +00:00
Allan Bowe
667e26b080 Merge pull request #142 from sasjs/contribute
npm scripts updated
2022-04-24 02:48:07 +03:00
munja
d09876c05f fix: missing dependency 2022-04-24 00:46:14 +01:00
Saad Jutt
fb8e18be75 chore: fix vulnerabilites 2022-04-24 04:38:28 +05:00
Saad Jutt
7ac7a4e083 chore: added start:prod npm script 2022-04-24 04:36:42 +05:00
Allan Bowe
8e23786dd4 Update CONTRIBUTING.md 2022-04-24 00:31:41 +01:00
Allan Bowe
4bd01bcf29 Update CONTRIBUTING.md 2022-04-24 00:30:29 +01:00
Allan Bowe
51f6aa34a1 Merge pull request #140 from sasjs/corebump
fix: bumping core library to get latest user management macros
2022-04-24 02:02:16 +03:00
Allan Bowe
486207128d fix: bumping core library to get latest user management macros 2022-04-23 22:54:34 +00:00
Allan Bowe
1e4b0b9171 Update README.md 2022-04-21 12:38:31 +01:00
Allan Bowe
1ff820605a chore(release): 0.0.57 2022-04-21 09:27:23 +00:00
Muhammad Saad
9c1a781b3a Merge pull request #136 from sasjs/issue-78
feat: add user name and logout functionality
2022-04-20 16:57:32 -07:00
36628551ae chore(web): use AppContext instead of useTokens Hook 2022-04-21 04:37:40 +05:00
23cf8fa06f chore(web): add user name at top right 2022-04-21 04:36:20 +05:00
84ee743eae feat: create AppContext 2022-04-21 04:34:27 +05:00
Allan Bowe
19e5bd7d2d chore: updating README with docs on linux jobs as approach for running server as background job 2022-04-20 13:16:06 +00:00
Allan Bowe
e251747302 chore(release): 0.0.56 2022-04-20 08:44:48 +00:00
Allan Bowe
7e7558d4cf Merge pull request #133 from sasjs/allanbowe/lengths-of-username-password-61
fix: shortening min length of username.  Closes #61
2022-04-20 11:44:15 +03:00
Allan Bowe
f02996facf fix: shortening min length of username. Closes #61 2022-04-20 08:43:38 +00:00
Saad Jutt
803c51f400 chore(release): 0.0.55 2022-04-20 07:15:33 +05:00
Muhammad Saad
c35b2b3f59 Merge pull request #132 from sasjs/fix-drive-path
Fix drive path
2022-04-19 18:56:13 -07:00
Saad Jutt
fe0866ace7 chore: Merge branch 'main' into fix-drive-path 2022-04-20 06:53:31 +05:00
Muhammad Saad
1513c3623d Merge pull request #131 from sasjs/fix-specs
test: fixed unhandled timeout
2022-04-19 18:52:52 -07:00
Saad Jutt
7fe43ae0b7 test: fixed unhandled timeout 2022-04-20 06:50:11 +05:00
Saad Jutt
c4cea4a12b fix: drive path in server mode 2022-04-20 05:54:56 +05:00
Saad Jutt
9fc7a132ba test: fixed unhandled timout 2022-04-20 00:06:21 +05:00
Allan Bowe
d55a619d64 chore(release): 0.0.54 2022-04-19 18:40:30 +00:00
Allan Bowe
737d2a24c2 Merge pull request #130 from sasjs/db-seed
fix: added db seed at server startup
2022-04-19 21:39:38 +03:00
Saad Jutt
2e63831b90 fix: added db seed at server startup 2022-04-19 23:25:05 +05:00
Saad Jutt
c7ffde1a3b chore(release): 0.0.53 2022-04-19 21:27:07 +05:00
Saad Jutt
db70b1ce55 fix: provide clientId to web component 2022-04-19 21:26:55 +05:00
Muhammad Saad
8a3fe8b217 Merge pull request #129 from sasjs/improve-UX-in-drive
fix: improve user experience in sasjs drive
2022-04-18 13:10:57 -07:00
9dca552e82 fix(drive):when page is refreshed or reloaded show expand file tree according to filePath in url 2022-04-19 00:40:37 +05:00
Allan Bowe
505f2089c7 chore(release): 0.0.52 2022-04-17 21:26:59 +00:00
Muhammad Saad
3344c400a8 Merge pull request #127 from sasjs/add-server-info-api-endpoint
feat: add api endpoint for getting server info
2022-04-17 12:48:12 -07:00
fa6248e3ef chore: swagger.yml updated 2022-04-17 23:53:20 +05:00
9fb5f1f8e7 feat: add api for getting server info 2022-04-17 23:48:08 +05:00
munja
92e0b8a088 chore(release): 0.0.51 2022-04-15 14:30:43 +01:00
Allan Bowe
b484306ed8 Merge pull request #126 from sasjs/issue-119
running code with CTRL+ENTER
2022-04-15 16:29:23 +03:00
5e08aacc51 chore: css fix 2022-04-15 14:53:36 +02:00
a9e4eb685d chore: style fix 2022-04-15 14:26:45 +02:00
31b09f27cc style: lint 2022-04-15 14:23:36 +02:00
9f3ec92f8e chore: run button style fix 2022-04-15 14:23:15 +02:00
6c9e449614 style: lint 2022-04-14 19:56:22 +02:00
68e84b0994 feat: run button running man, sub menu added 2022-04-14 19:38:44 +02:00
f0bb51a0d5 chore: placement of ctrl enter label 2022-04-13 22:12:40 +02:00
b93a0da3a3 feat: running code with CTRL+ENTER 2022-04-13 15:27:41 +02:00
Allan Bowe
e5facbf54c Update README.md 2022-04-13 12:24:42 +01:00
Allan Bowe
cb2bebbe76 Update README.md 2022-04-12 12:47:55 +01:00
Allan Bowe
9e1e0ce8cc chore(release): 0.0.50 2022-04-07 15:25:04 +00:00
Allan Bowe
29928753b7 Update CONTRIBUTING.md 2022-04-07 16:24:36 +01:00
Allan Bowe
edd69ecaae Merge pull request #122 from sasjs/issue121
Fixed couple of bugs + feature implemented
2022-04-07 18:23:34 +03:00
Saad Jutt
74ba65f9f3 feat(appstream): Upload an app from appStream page 2022-04-07 20:18:36 +05:00
Saad Jutt
f257602834 fix: web component UI fix for studio scrolling 2022-04-07 19:10:45 +05:00
Saad Jutt
61080d4694 fix: web component added tooltip for webout in studio 2022-04-07 18:59:31 +05:00
Saad Jutt
82633adbc4 chore: removed unused util 2022-04-07 18:48:31 +05:00
Saad Jutt
23db7e7b7d fix: session death time has to be a valid string number 2022-04-07 18:48:22 +05:00
Saad Jutt
cbaa687c9b chore(release): 0.0.49 2022-04-02 07:09:39 +05:00
Saad Jutt
527f70e90d fix(stp): read file in non-binary mode if debug one 2022-04-02 07:09:27 +05:00
Saad Jutt
122faad55f chore(release): 0.0.48 2022-04-02 07:06:18 +05:00
Saad Jutt
3ff6f5e865 fix(stp): return log+webout for debug on 2022-04-02 07:06:09 +05:00
Muhammad Saad
7d5128c0d6 Merge pull request #115 from sasjs/issue109
feat(deploy): new route added for deploy with build.json
2022-04-02 06:45:28 +05:00
Saad Jutt
e1ebbfd087 chore: increased file upload size to 100mb 2022-04-02 06:04:34 +05:00
Saad Jutt
e430bdb0d4 test(upload): spec updated for file upload exceeding limit 2022-04-02 05:51:24 +05:00
Saad Jutt
9d9769eef3 chore: increased file upload size to 100mb 2022-04-02 05:36:53 +05:00
Saad Jutt
9d167abe2a fix: remove uploaded build.json from temp folder in all cases 2022-04-02 05:29:34 +05:00
Saad Jutt
18d0604bdd feat(deploy): new route added for deploy with build.json 2022-04-02 05:23:25 +05:00
Saad Jutt
7b7bc6b778 chore: fix vulnerabilities 2022-03-31 01:54:40 +05:00
Saad Jutt
fb4f3442d5 chore(release): 0.0.47 2022-03-30 01:04:45 +05:00
Muhammad Saad
09d1b7d5d4 Merge pull request #114 from sasjs/show-contents-as-api-responded
fix: show contents as api responded
2022-03-30 01:04:29 +05:00
Saad Jutt
99839ae62f chore: fix vulnerabilities 2022-03-30 00:55:59 +05:00
Saad Jutt
f700561e1a fix(web): updated STUDIO log and webout 2022-03-30 00:55:38 +05:00
Saad Jutt
8b4b4b91ab chore(release): 0.0.46 2022-03-30 00:01:18 +05:00
Muhammad Saad
acb3ae0493 Merge pull request #113 from sasjs/issue-108
feat(drive): GET folder contents API added
2022-03-30 00:00:52 +05:00
Saad Jutt
f48aeb1b0b chore(release): 0.0.45 2022-03-29 23:38:09 +05:00
Saad Jutt
5c0e8e5344 chore: Merge branch 'main' into issue-108 2022-03-29 23:28:03 +05:00
Saad Jutt
0ac9e4af7d feat(drive): GET folder contents API added 2022-03-29 23:27:44 +05:00
Muhammad Saad
ee80f3f968 Merge pull request #112 from sasjs/allanbowe/lst-destination-needs-111
fix: providing a PRINT destination during SAS invocation.
2022-03-29 23:11:32 +05:00
Allan Bowe
7f4201ba85 fix: proving a PRINT destination during SAS invocation.
Closes #111
2022-03-29 16:11:15 +00:00
munja
f830bbc058 chore(release): 0.0.44 2022-03-29 11:25:00 +01:00
Allan Bowe
f8e1522a5a Merge pull request #110 from sasjs/issue-108
fix: increased req body size
2022-03-28 14:22:44 +03:00
Saad Jutt
0a5aeceab5 fix: DELETE req cannot have body 2022-03-28 05:05:03 +05:00
Saad Jutt
6dc39c0d91 fix: increased req body size 2022-03-28 04:53:24 +05:00
Saad Jutt
117a53ceea fix(session): increased session + bug fixed 2022-03-24 20:22:06 +05:00
Saad Jutt
dd56a95314 fix(stp): use same session from file upload 2022-03-24 18:06:28 +05:00
Saad Jutt
c5117abe71 chore: README.md updated 2022-03-24 05:56:08 +05:00
Saad Jutt
84c632a861 chore(release): 0.0.43 2022-03-24 04:25:40 +05:00
Muhammad Saad
3ddd09eba0 Merge pull request #105 from sasjs/deploy-app-with-current-names-also
Deploy app with current names also
2022-03-24 04:25:12 +05:00
Saad Jutt
0c0301433c test: fixed 2022-03-24 04:22:30 +05:00
Saad Jutt
954b2e3e2e chore: removed test file 2022-03-24 01:12:04 +05:00
Saad Jutt
5655311b96 fix: fallback logo on AppStream 2022-03-24 01:07:06 +05:00
Saad Jutt
9ace33d783 fix(deploy): user can deploy to same appName with different/same appLoc 2022-03-24 00:54:59 +05:00
Saad Jutt
adc5aca0f0 chore(release): 0.0.42 2022-03-23 22:49:29 +05:00
Muhammad Saad
71c6be6b84 Merge pull request #104 from sasjs/webout-raw
fix: execute api, webout as raw
2022-03-23 22:47:07 +05:00
Saad Jutt
9c751877d1 fix: execute api, webout as raw 2022-03-23 22:41:02 +05:00
Saad Jutt
2204d54cd6 chore(release): 0.0.41 2022-03-23 21:32:06 +05:00
Saad Jutt
f4eb75ff34 fix(scroll): closes #100 2022-03-23 21:31:54 +05:00
64 changed files with 1526 additions and 495 deletions

View File

@@ -2,25 +2,22 @@
Contributions are very welcome! Feel free to raise an issue or start a discussion, for help in getting started.
The app can be deployed using Docker or NodeJS.
## Configuration
Configuration is made in the `configuration` section of `package.json`:
Configuration is made using `.env` files (per [README.md](https://github.com/sasjs/server#env-var-configuration) settings), _except_ for one case, when running in NodeJS in production - in which case the path to the SAS executable is made in the `configuration` section of `package.json`.
- Provide path to SAS9 executable.
The `.env` file should be created in the location(s) below. Each folder contains a `.env.example` file that may be adjusted and renamed.
* `.env` - the root .env file is used only for Docker deploys.
* `api/.env` - this is the primary file used in NodeJS deploys
* `web/.env` - this file is only necessary in NodeJS when running `web` and `api` seperately (on different ports).
### Using dockers:
## Using Docker
There is `.env.example` file present at root of the project. [for Production]
There is `.env.example` file present at `./api` of the project. [for Development]
There is `.env.example` file present at `./web` of the project. [for Development]
Remember to provide enviornment variables.
#### Development
### Docker Development Mode
Command to run docker for development:
@@ -38,7 +35,7 @@ It will build following images if running first time:
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
#### Production
### Docker Production Mode
Command to run docker for production:
@@ -54,47 +51,45 @@ It will build following images if running first time:
- `mongo-seed-users` - will be populating user data specified in _./mongo-seed/users/user.json_
- `mongo-seed-clients` - will be populating client data specified in _./mongo-seed/clients/client.json_
### Using node:
## Using NodeJS:
#### Development (running api and web seperately):
Be sure to use v16 or above, and to set your environment variables in the relevant `.env` file(s) - else defaults will be used.
##### API
### NodeJS Development Mode
Navigate to `./api`
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
SASjs Server is split between an API server (serving REST requests) and a WEB Server (everything else). These can be run together, or on seperate ports.
### NodeJS Dev - Single Port
Here the environment variables should be configured under `api.env`. Then:
```
cd ./web && npm i && npm build
cd ../api && npm i && npm start
```
### NodeJS Dev - Seperate Ports
Set the backend variables in `api/.env` and the frontend variables in `web/.env`. Then:
#### API server
```
cd api
npm install
npm start
```
##### Web
Navigate to `./web`
There is `.env.example` file present at `./web` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
#### Web Server
```
cd web
npm install
npm start
```
#### Development (running only api server and have web build served):
#### NodeJS Production Mode
##### API server also serving Web build files
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm start
```
#### Production
##### API & WEB
Update the `.env` file in the *api* folder. Then:
```
npm run server
@@ -105,7 +100,7 @@ This will install/build `web` and install `api`, then start prod server.
## Executables
Command to generate executables
In order to generate the final executables:
```
cd ./web && npm i && npm build && cd ../
@@ -113,3 +108,7 @@ cd ./api && npm i && npm run exe
```
This will install/build web app and install/create executables of sasjs server at root `./executables`
## Releases
To cut a release, run `npm run release` on the main branch, then push the tags (per the console log link)

View File

@@ -2,6 +2,167 @@
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.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)
### Bug Fixes
* bumping core library to get latest user management macros ([4862071](https://github.com/sasjs/server/commit/486207128da58fc4866bd0919c1bed2bd98097ea))
* missing dependency ([d09876c](https://github.com/sasjs/server/commit/d09876c05f89166eec20064f7aa7ed5b867be081))
### [0.0.57](https://github.com/sasjs/server/compare/v0.0.56...v0.0.57) (2022-04-21)
### Features
* create AppContext ([84ee743](https://github.com/sasjs/server/commit/84ee743eae16e87eaa91969393bebf01e2d15a44))
### [0.0.56](https://github.com/sasjs/server/compare/v0.0.55...v0.0.56) (2022-04-20)
### Bug Fixes
* shortening min length of username. Closes [#61](https://github.com/sasjs/server/issues/61) ([f02996f](https://github.com/sasjs/server/commit/f02996facf1019ec4022ccfbc99c1d0137074e1b))
### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)
### Bug Fixes
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
* drive path in server mode ([c4cea4a](https://github.com/sasjs/server/commit/c4cea4a12b7eda4daeed995f41c0b10bcea79871))
### [0.0.54](https://github.com/sasjs/server/compare/v0.0.53...v0.0.54) (2022-04-19)
### Bug Fixes
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
### [0.0.53](https://github.com/sasjs/server/compare/v0.0.49...v0.0.53) (2022-04-19)
### Features
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
### Bug Fixes
* provide clientId to web component ([db70b1c](https://github.com/sasjs/server/commit/db70b1ce555df6b29fb09c0c960d38b911c97b1b))
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
### Features
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
### Features
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
### Features
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
### Bug Fixes
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
### Bug Fixes
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
### Features
* **deploy:** new route added for deploy with build.json ([18d0604](https://github.com/sasjs/server/commit/18d0604bdd0b20ad468f9345474b4de034ee3a67))
### Bug Fixes
* remove uploaded build.json from temp folder in all cases ([9d167ab](https://github.com/sasjs/server/commit/9d167abe2adb743bca161862b4561bf573182c00))
* **stp:** return log+webout for debug on ([3ff6f5e](https://github.com/sasjs/server/commit/3ff6f5e86581cd2ac23bbe0b8e2c367fbea890ed))
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)
### Bug Fixes
* **web:** updated STUDIO log and webout ([f700561](https://github.com/sasjs/server/commit/f700561e1a8d06c18ca2bdbe4605d7ab34f7a761))
### [0.0.46](https://github.com/sasjs/server/compare/v0.0.45...v0.0.46) (2022-03-29)
### Features
* **drive:** GET folder contents API added ([0ac9e4a](https://github.com/sasjs/server/commit/0ac9e4af7d67c4431053e80eb2384bf5bdc3f8b3))
### [0.0.45](https://github.com/sasjs/server/compare/v0.0.43...v0.0.45) (2022-03-29)
### Bug Fixes
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
* proving a PRINT destination during SAS invocation. ([7f4201b](https://github.com/sasjs/server/commit/7f4201ba855743144fa6d3efac2b11e816d4696e)), closes [#111](https://github.com/sasjs/server/issues/111)
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
### [0.0.44](https://github.com/sasjs/server/compare/v0.0.43...v0.0.44) (2022-03-29)
### Bug Fixes
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
### [0.0.43](https://github.com/sasjs/server/compare/v0.0.42...v0.0.43) (2022-03-23)
### Bug Fixes
* **deploy:** user can deploy to same appName with different/same appLoc ([9ace33d](https://github.com/sasjs/server/commit/9ace33d7830a9def42d741c23b46090afe0c5510))
* fallback logo on AppStream ([5655311](https://github.com/sasjs/server/commit/5655311b9663225823c192b39a03f39d17dda730))
### [0.0.42](https://github.com/sasjs/server/compare/v0.0.41...v0.0.42) (2022-03-23)
### Bug Fixes
* execute api, webout as raw ([9c75187](https://github.com/sasjs/server/commit/9c751877d1ed0d0677aff816169a1df7c34c6bf5))
### [0.0.41](https://github.com/sasjs/server/compare/v0.0.40...v0.0.41) (2022-03-23)
### Bug Fixes
* **scroll:** closes [#100](https://github.com/sasjs/server/issues/100) ([f4eb75f](https://github.com/sasjs/server/commit/f4eb75ff347e78ac334e55ee26fbdd247bb8eaa2))
### [0.0.40](https://github.com/sasjs/server/compare/v0.0.39...v0.0.40) (2022-03-23)

View File

@@ -48,11 +48,22 @@ When launching the app, it will make use of specific environment variables. Thes
Example contents of a `.env` file:
```
MODE=desktop # options: [desktop|server] default: `desktop`
CORS=disable # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
WHITELIST= # options: <http://localhost:3000 https://abc.com ...> space separated urls
PROTOCOL=http # options: [http|https] default: http
PORT=5000 # default: 5000
# options: [desktop|server] default: `desktop`
MODE=
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=
# options: <http://localhost:3000 https://abc.com ...> space separated urls
WHITELIST=
# options: [http|https] default: http
PROTOCOL=
# default: 5000
PORT=
# optional
# for MODE: `desktop`, prompts user
@@ -74,11 +85,33 @@ ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
# SAS Options
# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
# Any options set here are automatically applied in the SAS session
# See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
SAS_OPTIONS= -NOXCMD
SASV9_OPTIONS= -NOXCMD
```
## Persisting the Session
Normally the server process will stop when your terminal dies. To keep it going you can use the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) as follows:
Normally the server process will stop when your terminal dies. To keep it going you can use the following suggested approaches:
1. Linux Background Job
2. NPM package `pm2`
### Background Job
Trigger the command using NOHUP, redirecting the output commands, eg `nohup ./api-linux > server.log 2>&1 &`.
You can now see the job running using the `jobs` command. To ensure that it will still run when your terminal is closed, execute the `disown` command. To kill it later, use the `kill -9 <pid>` command. You can see your sessions using `top -u <userid>`. Type `c` to see the commands being run against each pid.
### PM2
Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) and execute, eg as follows:
```bash
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
@@ -112,7 +145,7 @@ Instead of `app_name` you can pass:
## Server Version
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
The following credentials can be used for the initial connection to SASjs/server. It is highly recommended to change these on first use.
- CLIENTID: `clientID1`
- USERNAME: `secretuser`

52
api/package-lock.json generated
View File

@@ -8,8 +8,8 @@
"name": "api",
"version": "0.0.2",
"dependencies": {
"@sasjs/core": "4.9.0",
"@sasjs/utils": "2.36.2",
"@sasjs/core": "^4.19.0",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
@@ -1379,14 +1379,14 @@
}
},
"node_modules/@sasjs/core": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz",
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
"integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
},
"node_modules/@sasjs/utils": {
"version": "2.36.2",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz",
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
"hasInstallScript": true,
"dependencies": {
"@types/fs-extra": "9.0.13",
@@ -2448,9 +2448,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dependencies": {
"lodash": "^4.17.14"
}
@@ -7107,9 +7107,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"node_modules/mkdirp": {
"version": "1.0.4",
@@ -11127,14 +11127,14 @@
}
},
"@sasjs/core": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz",
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
"integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
},
"@sasjs/utils": {
"version": "2.36.2",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz",
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
"requires": {
"@types/fs-extra": "9.0.13",
"@types/prompts": "2.0.13",
@@ -12060,9 +12060,9 @@
}
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": {
"lodash": "^4.17.14"
}
@@ -15624,9 +15624,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"mkdirp": {
"version": "1.0.4",

View File

@@ -8,6 +8,7 @@
"prestart": "npm run initial",
"prebuild": "npm run initial",
"start": "nodemon ./src/server.ts",
"start:prod": "node ./build/src/server.js",
"build": "rimraf build && tsc",
"postbuild": "npm run copy:files",
"swagger": "tsoa spec",
@@ -46,8 +47,8 @@
},
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "4.9.0",
"@sasjs/utils": "2.36.2",
"@sasjs/core": "^4.19.0",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",

BIN
api/public/plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

View File

@@ -418,6 +418,25 @@ components:
example: /Public/somefolder/some.file
type: object
additionalProperties: false
InfoResponse:
properties:
mode:
type: string
cors:
type: string
whiteList:
items:
type: string
type: array
protocol:
type: string
required:
- mode
- cors
- whiteList
- protocol
type: object
additionalProperties: false
securitySchemes:
bearerAuth:
type: http
@@ -606,13 +625,62 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/DeployPayload'
/SASjsApi/drive/deploy/upload:
post:
operationId: DeployUpload
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'}
'400':
description: 'Invalid Format'
content:
application/json:
schema:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: failure, message: 'Provided not supported data format.'}
'500':
description: 'Execution Error'
content:
application/json:
schema:
$ref: '#/components/schemas/DeployResponse'
examples:
'Example 1':
value: {status: failure, message: 'Deployment failed!'}
summary: 'Creates/updates files within SASjs Drive using uploaded JSON file.'
tags:
- Drive
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
required:
- file
/SASjsApi/drive/file:
get:
operationId: GetFile
responses:
'204':
description: 'No content'
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Get file from SASjs Drive'
tags:
- Drive
@@ -623,19 +691,10 @@ paths:
-
in: query
name: _filePath
required: false
required: true
schema:
type: string
example: /Public/somefolder/some.file
requestBody:
required: false
content:
multipart/form-data:
schema:
type: object
properties:
filePath:
type: string
delete:
operationId: DeleteFile
responses:
@@ -649,7 +708,6 @@ paths:
required:
- status
type: object
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
summary: 'Delete file from SASjs Drive'
tags:
- Drive
@@ -660,19 +718,10 @@ paths:
-
in: query
name: _filePath
required: false
required: true
schema:
type: string
example: /Public/somefolder/some.file
requestBody:
required: false
content:
multipart/form-data:
schema:
type: object
properties:
filePath:
type: string
post:
operationId: SaveFile
responses:
@@ -775,6 +824,36 @@ paths:
type: string
required:
- file
/SASjsApi/drive/folder:
get:
operationId: GetFolder
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
folders: {items: {type: string}, type: array}
files: {items: {type: string}, type: array}
required:
- folders
- files
type: object
summary: 'Get folder contents from SASjs Drive'
tags:
- Drive
security:
-
bearerAuth: []
parameters:
-
in: query
name: _folderPath
required: false
schema:
type: string
example: /Public/somefolder
/SASjsApi/drive/filetree:
get:
operationId: GetFileTree
@@ -1180,10 +1259,31 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
/SASjsApi/info:
get:
operationId: Info
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/InfoResponse'
examples:
'Example 1':
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http}
summary: 'Get server info (mode, cors, whiteList, protocol).'
tags:
- Info
security: []
parameters: []
servers:
-
url: /
tags:
-
name: Info
description: 'Get Server Info'
-
name: Session
description: 'Get Session information'

View File

@@ -1,5 +1,6 @@
import path from 'path'
import {
CompileTree,
createFile,
loadDependenciesFile,
readFile,
@@ -18,7 +19,8 @@ const compiledSystemInit = async (systemInit: string) =>
macroFolders: [],
buildSourceFolder: '',
binaryFolders: [],
macroCorePath
macroCorePath,
compileTree: new CompileTree('') // dummy compileTree
}))
const createSysInitFile = async () => {

View File

@@ -5,23 +5,12 @@
_before_ any user-provided content.
A number of useful CORE macros are also compiled below, so that they can be
available "out of the box".
available by default for Stored Programs.
Note that the full CORE library is available to sessions in SASjs Studio.
<h4> SAS Macros </h4>
@li mcf_stpsrv_header.sas
@li mf_getuser.sas
@li mf_getvarlist.sas
@li mf_mkdir.sas
@li mf_nobs.sas
@li mf_uid.sas
@li mfs_httpheader.sas
@li mp_dirlist.sas
@li mp_ds2ddl.sas
@li mp_ds2md.sas
@li mp_getdbml.sas
@li mp_init.sas
@li mp_makedata.sas
@li mp_zip.sas
@li ms_webout.sas
**/

View File

@@ -10,8 +10,8 @@ import {
copySASjsCore,
getWebBuildFolderPath,
loadAppStreamConfig,
sasJSCoreMacros,
setProcessVariables
setProcessVariables,
setupFolders
} from './utils'
dotenv.config()
@@ -22,11 +22,13 @@ const { MODE, CORS, WHITELIST } = process.env
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
const whiteList: string[] = []
WHITELIST?.split(' ')?.forEach((url) => {
if (url.startsWith('http'))
// removing trailing slash of URLs listing for CORS
whiteList.push(url.replace(/\/$/, ''))
})
WHITELIST?.split(' ')
?.filter((url) => !!url)
.forEach((url) => {
if (url.startsWith('http'))
// removing trailing slash of URLs listing for CORS
whiteList.push(url.replace(/\/$/, ''))
})
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList }))
@@ -34,7 +36,7 @@ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
app.use(cookieParser())
app.use(morgan('tiny'))
app.use(express.json({ limit: '50mb' }))
app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public')))
const onError: ErrorRequestHandler = (err, req, res, next) => {
@@ -43,6 +45,7 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
}
export default setProcessVariables().then(async () => {
await setupFolders()
await copySASjsCore()
// loading these modules after setting up variables due to

View File

@@ -14,17 +14,24 @@ import {
Patch,
UploadedFile,
FormField,
Delete
Delete,
Hidden
} from 'tsoa'
import {
fileExists,
moveFile,
createFolder,
deleteFile as deleteFileOnSystem
deleteFile as deleteFileOnSystem,
folderExists,
listFilesInFolder,
listSubFoldersInFolder,
isFolder,
FileTree,
isFileTree
} from '@sasjs/utils'
import { createFileTree, ExecutionController, getTreeExample } from './internal'
import { FileTree, isFileTree, TreeNode } from '../types'
import { TreeNode } from '../types'
import { getTmpFilesFolderPath } from '../utils'
interface DeployPayload {
@@ -89,9 +96,21 @@ export class DriveController {
}
/**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
* @summary Creates/updates files within SASjs Drive using uploaded JSON file.
*
*/
@Example<DeployResponse>(successDeployResponse)
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
@Post('/deploy/upload')
public async deployUpload(
@UploadedFile() file: Express.Multer.File, // passing here for API docs
@Query() @Hidden() body?: DeployPayload // Hidden decorator has be optional
): Promise<DeployResponse> {
return deploy(body!)
}
/**
*
* @summary Get file from SASjs Drive
* @query _filePath Location of SAS program
@@ -100,28 +119,31 @@ export class DriveController {
@Get('/file')
public async getFile(
@Request() request: express.Request,
@Query() _filePath?: string,
@FormField() filePath?: string
@Query() _filePath: string
) {
return getFile(request, (_filePath ?? filePath)!)
return getFile(request, _filePath)
}
/**
*
* @summary Get folder contents from SASjs Drive
* @query _folderPath Location of SAS program
* @example _folderPath "/Public/somefolder"
*/
@Get('/folder')
public async getFolder(@Query() _folderPath?: string) {
return getFolder(_folderPath)
}
/**
* It's optional to either provide `_filePath` in url as query parameter
* Or provide `filePath` in body as form field.
* But it's required to provide else API will respond with Bad Request.
*
* @summary Delete file from SASjs Drive
* @query _filePath Location of SAS program
* @example _filePath "/Public/somefolder/some.file"
*/
@Delete('/file')
public async deleteFile(
@Query() _filePath?: string,
@FormField() filePath?: string
) {
return deleteFile((_filePath ?? filePath)!)
public async deleteFile(@Query() _filePath: string) {
return deleteFile(_filePath)
}
/**
@@ -227,7 +249,7 @@ const getFile = async (req: express.Request, filePath: string) => {
}
if (!(await fileExists(filePathFull))) {
throw new Error('File does not exist.')
throw new Error("File doesn't exist.")
}
const extension = path.extname(filePathFull).toLowerCase()
@@ -238,6 +260,36 @@ const getFile = async (req: express.Request, filePath: string) => {
req.res?.sendFile(path.resolve(filePathFull))
}
const getFolder = async (folderPath?: string) => {
const driveFilesPath = getTmpFilesFolderPath()
if (folderPath) {
const folderPathFull = path
.join(getTmpFilesFolderPath(), folderPath)
.replace(new RegExp('/', 'g'), path.sep)
if (!folderPathFull.includes(driveFilesPath)) {
throw new Error('Cannot get folder outside drive.')
}
if (!(await folderExists(folderPathFull))) {
throw new Error("Folder doesn't exist.")
}
if (!(await isFolder(folderPathFull))) {
throw new Error('Not a Folder.')
}
const files: string[] = await listFilesInFolder(folderPathFull)
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
return { files, folders }
}
const files: string[] = await listFilesInFolder(driveFilesPath)
const folders: string[] = await listSubFoldersInFolder(driveFilesPath)
return { files, folders }
}
const deleteFile = async (filePath: string) => {
const driveFilesPath = getTmpFilesFolderPath()
@@ -305,9 +357,3 @@ const updateFile = async (
return { status: 'success' }
}
const validateFilePath = async (filePath: string) => {
if (!(await fileExists(filePath))) {
throw 'DriveController: File does not exists.'
}
}

View File

@@ -6,3 +6,4 @@ export * from './group'
export * from './session'
export * from './stp'
export * from './user'
export * from './info'

View File

@@ -0,0 +1,37 @@
import { Route, Tags, Example, Get } from 'tsoa'
export interface InfoResponse {
mode: string
cors: string
whiteList: string[]
protocol: string
}
@Route('SASjsApi/info')
@Tags('Info')
export class InfoController {
/**
* @summary Get server info (mode, cors, whiteList, protocol).
*
*/
@Example<InfoResponse>({
mode: 'desktop',
cors: 'enable',
whiteList: ['http://example.com', 'http://example2.com'],
protocol: 'http'
})
@Get('/')
public info(): InfoResponse {
const response = {
mode: process.env.MODE ?? 'desktop',
cors:
process.env.CORS ?? process.env.MODE === 'server'
? 'disable'
: 'enable',
whiteList:
process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [],
protocol: process.env.PROTOCOL ?? 'http'
}
return response
}
}

View File

@@ -8,7 +8,7 @@ import {
moveFile,
readFileBinary
} from '@sasjs/utils'
import { PreProgramVars, TreeNode } from '../../types'
import { PreProgramVars, Session, TreeNode } from '../../types'
import {
extractHeaders,
generateFileUploadSasCode,
@@ -39,7 +39,8 @@ export class ExecutionController {
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
otherArgs?: any,
returnJson?: boolean
returnJson?: boolean,
session?: Session
) {
if (!(await fileExists(programPath)))
throw 'ExecutionController: SAS file does not exist.'
@@ -51,7 +52,8 @@ export class ExecutionController {
preProgramVariables,
vars,
otherArgs,
returnJson
returnJson,
session
)
}
@@ -60,11 +62,13 @@ export class ExecutionController {
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
otherArgs?: any,
returnJson?: boolean
returnJson?: boolean,
sessionByFileUpload?: Session
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController = getSessionController()
const session = await sessionController.getSession()
const session =
sessionByFileUpload ?? (await sessionController.getSession())
session.inUse = true
session.consumed = true
@@ -153,7 +157,9 @@ ${program}`
: ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
const fileResponse: boolean =
httpHeaders.hasOwnProperty('content-type') && !returnJson
httpHeaders.hasOwnProperty('content-type') &&
!returnJson && // not a POST Request
!isDebugOn(vars) // Debug is not enabled
const webout = (await fileExists(weboutPath))
? fileResponse
@@ -174,11 +180,10 @@ ${program}`
return {
httpHeaders,
result: fileResponse
? webout
: isDebugOn(vars) || session.crashed
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
: webout
result:
isDebugOn(vars) || session.crashed
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
: webout
}
}

View File

@@ -23,7 +23,9 @@ export class FileUploadController {
const sessionController = getSessionController()
session = await sessionController.getSession()
session.inUse = true
// marking consumed true, so that it's not available
// as readySession for any other request
session.consumed = true
req.sasSession = session

View File

@@ -12,8 +12,7 @@ import {
createFile,
fileExists,
generateTimestamp,
readFile,
moveFile
readFile
} from '@sasjs/utils'
const execFilePromise = promisify(execFile)
@@ -31,7 +30,7 @@ export class SessionController {
? readySessions[0]
: await this.createSession()
if (readySessions.length < 2) this.createSession()
if (readySessions.length < 3) this.createSession()
return session
}
@@ -41,6 +40,7 @@ export class SessionController {
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
@@ -87,6 +87,8 @@ ${autoExecContent}`
codePath,
'-LOG',
path.join(session.path, 'log.log'),
'-PRINT',
path.join(session.path, 'output.lst'),
'-WORK',
session.path,
'-AUTOEXEC',
@@ -138,7 +140,9 @@ ${autoExecContent}`
private scheduleSessionDestroy(session: Session) {
setTimeout(async () => {
if (session.inUse) {
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
// adding 10 more minutes
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session)
} else {

View File

@@ -1,13 +1,15 @@
import path from 'path'
import { getTmpFilesFolderPath } from '../../utils/file'
import {
MemberType,
createFolder,
createFile,
asyncForEach,
FolderMember,
ServiceMember,
FileTree,
FileMember
} from '../../types'
import { getTmpFilesFolderPath } from '../../utils/file'
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
FileMember,
MemberType,
FileTree
} from '@sasjs/utils'
// REFACTOR: export FileTreeCpntroller
export const createFileTree = async (

View File

@@ -143,9 +143,8 @@ const executeReturnRaw = async (
query
)) as ExecuteReturnRaw
// Should over-ride response header for
// debug on GET request to see entire log
// rendering on browser.
// Should over-ride response header for debug
// on GET request to see entire log rendering on browser.
if (isDebugOn(query)) {
httpHeaders['content-type'] = 'text/plain'
}
@@ -185,7 +184,8 @@ const executeReturnJson = async (
getPreProgramVariables(req),
{ ...req.query, ...req.body },
{ filesNamesMap: filesNamesMap },
true
true,
req.sasSession
)) as ExecuteReturnJson
let weboutRes: string | IRecordOfAny = webout

View File

@@ -4,7 +4,7 @@ import multer, { FileFilterCallback, Options } from 'multer'
import { blockFileRegex, getTmpUploadsPath } from '../utils'
const fieldNameSize = 300
const fileSize = 10485760 // 10 MB
const fileSize = 104857600 // 100 MB
const storage = multer.diskStorage({
destination: getTmpUploadsPath(),

View File

@@ -1,5 +1,5 @@
import express from 'express'
import { deleteFile } from '@sasjs/utils'
import { deleteFile, readFile } from '@sasjs/utils'
import { publishAppStream } from '../appStream'
@@ -8,7 +8,8 @@ import { DriveController } from '../../controllers/'
import {
deployValidation,
fileBodyValidation,
fileParamValidation
fileParamValidation,
folderParamValidation
} from '../../utils'
const controller = new DriveController()
@@ -42,14 +43,74 @@ driveRouter.post('/deploy', async (req, res) => {
}
})
driveRouter.post(
'/deploy/upload',
(...arg) => multerSingle('file', arg),
async (req, res) => {
if (!req.file) return res.status(400).send('"file" is not present.')
const fileContent = await readFile(req.file.path)
let jsonContent
try {
jsonContent = JSON.parse(fileContent)
} catch (err) {
deleteFile(req.file.path)
return res.status(400).send('File containing invalid JSON content.')
}
const { error, value: body } = deployValidation(jsonContent)
if (error) {
deleteFile(req.file.path)
return res.status(400).send(error.details[0].message)
}
try {
const response = await controller.deployUpload(req.file, body)
if (body.streamWebFolder) {
const { streamServiceName } = await publishAppStream(
body.appLoc,
body.streamWebFolder,
body.streamServiceName,
body.streamLogo
)
response.streamServiceName = streamServiceName
}
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
} finally {
deleteFile(req.file.path)
}
}
)
driveRouter.get('/file', async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) return res.status(400).send(errQ.details[0].message)
if (errQ) return res.status(400).send(errQ.details[0].message)
try {
await controller.getFile(req, query._filePath, body.filePath)
await controller.getFile(req, query._filePath)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
driveRouter.get('/folder', async (req, res) => {
const { error: errQ, value: query } = folderParamValidation(req.query)
if (errQ) return res.status(400).send(errQ.details[0].message)
try {
const response = await controller.getFolder(query._folderPath)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
@@ -57,12 +118,11 @@ driveRouter.get('/file', async (req, res) => {
driveRouter.delete('/file', async (req, res) => {
const { error: errQ, value: query } = fileParamValidation(req.query)
const { error: errB, value: body } = fileBodyValidation(req.body)
if (errQ && errB) return res.status(400).send(errQ.details[0].message)
if (errQ) return res.status(400).send(errQ.details[0].message)
try {
const response = await controller.deleteFile(query._filePath, body.filePath)
const response = await controller.deleteFile(query._filePath)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())

View File

@@ -9,6 +9,7 @@ import {
verifyAdmin
} from '../../middlewares'
import infoRouter from './info'
import driveRouter from './drive'
import stpRouter from './stp'
import codeRouter from './code'
@@ -20,6 +21,7 @@ import sessionRouter from './session'
const router = express.Router()
router.use('/info', infoRouter)
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
router.use('/auth', desktopRestrict, authRouter)
router.use(

View File

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

View File

@@ -18,11 +18,6 @@ import {
verifyTokenInDB
} from '../../../utils'
let app: Express
appPromise.then((_app) => {
app = _app
})
const clientId = 'someclientID'
const clientSecret = 'someclientSecret'
const user = {
@@ -35,12 +30,15 @@ const user = {
}
describe('auth', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const userController = new UserController()
const clientController = new ClientController()
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
await clientController.createClient({ clientId, clientSecret })

View File

@@ -6,11 +6,6 @@ import appPromise from '../../../app'
import { UserController, ClientController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
let app: Express
appPromise.then((_app) => {
app = _app
})
const client = {
clientId: 'someclientID',
clientSecret: 'someclientSecret'
@@ -28,12 +23,15 @@ const newClient = {
}
describe('client', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const userController = new UserController()
const clientController = new ClientController()
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
})

View File

@@ -10,7 +10,11 @@ import {
readFile,
deleteFolder,
generateTimestamp,
copy
copy,
createFolder,
createFile,
ServiceMember,
FolderMember
} from '@sasjs/utils'
import * as fileUtilModules from '../../../utils/file'
@@ -26,15 +30,9 @@ jest
import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import { getTreeExample } from '../../../controllers/internal'
import { FolderMember, ServiceMember } from '../../../types'
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
const { getTmpFilesFolderPath } = fileUtilModules
let app: Express
appPromise.then((_app) => {
app = _app
})
const clientId = 'someclientID'
const user = {
displayName: 'Test User',
@@ -44,7 +42,8 @@ const user = {
isActive: true
}
describe('files', () => {
describe('drive', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const controller = new UserController()
@@ -52,6 +51,8 @@ describe('files', () => {
let accessToken: string
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
@@ -69,6 +70,7 @@ describe('files', () => {
await mongoServer.stop()
await deleteFolder(tmpFolder)
})
describe('deploy', () => {
const shouldFailAssertion = async (payload: any) => {
const res = await request(app)
@@ -172,17 +174,126 @@ describe('files', () => {
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
await deleteFolder(getTmpFilesFolderPath())
await deleteFolder(path.join(getTmpFilesFolderPath(), 'public'))
})
})
describe('folder', () => {
describe('get', () => {
const getFolderApi = '/SASjsApi/drive/folder'
it('should get root SAS folder on drive', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({ files: [], folders: [] })
})
it('should get a SAS folder on drive having _folderPath as query param', async () => {
const pathToDrive = fileUtilModules.getTmpFilesFolderPath()
const dirLevel1 = 'level1'
const dirLevel2 = 'level2'
const fileLevel1 = 'file1'
const fileLevel2 = 'file2'
await createFolder(path.join(pathToDrive, dirLevel1, dirLevel2))
await createFile(
path.join(pathToDrive, dirLevel1, fileLevel1),
'some file content'
)
await createFile(
path.join(pathToDrive, dirLevel1, dirLevel2, fileLevel2),
'some file content'
)
const res1 = await request(app)
.get(getFolderApi)
.query({ _folderPath: '/' })
.auth(accessToken, { type: 'bearer' })
expect(res1.statusCode).toEqual(200)
expect(res1.body).toEqual({ files: [], folders: [dirLevel1] })
const res2 = await request(app)
.get(getFolderApi)
.query({ _folderPath: dirLevel1 })
.auth(accessToken, { type: 'bearer' })
expect(res2.statusCode).toEqual(200)
expect(res2.body).toEqual({ files: [fileLevel1], folders: [dirLevel2] })
const res3 = await request(app)
.get(getFolderApi)
.query({ _folderPath: `${dirLevel1}/${dirLevel2}` })
.auth(accessToken, { type: 'bearer' })
expect(res3.statusCode).toEqual(200)
expect(res3.body).toEqual({ files: [fileLevel2], folders: [] })
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get(getFolderApi).expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folder is not present', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
.expect(403)
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath outside Drive', async () => {
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: '/../path/code.sas' })
.expect(403)
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if folderPath is of a file', async () => {
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
const filePath = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
filePath
)
await copy(fileToCopyPath, pathToCopy)
const res = await request(app)
.get(getFolderApi)
.auth(accessToken, { type: 'bearer' })
.query({ _folderPath: filePath })
.expect(403)
expect(res.text).toEqual('Error: Not a Folder.')
expect(res.body).toEqual({})
})
})
})
describe('file', () => {
describe('create', () => {
it('should create a SAS file on drive having filePath as form field', async () => {
const pathToUpload = `/my/path/code-1.sas`
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', '/my/path/code.sas')
.field('filePath', pathToUpload)
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
expect(res.statusCode).toEqual(200)
@@ -192,10 +303,12 @@ describe('files', () => {
})
it('should create a SAS file on drive having _filePath as query param', async () => {
const pathToUpload = `/my/path/code-2.sas`
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/my/path/code1.sas' })
.query({ _filePath: pathToUpload })
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
expect(res.statusCode).toEqual(200)
@@ -217,7 +330,7 @@ describe('files', () => {
it('should respond with Forbidden if file is already present', async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.sas'
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
@@ -310,7 +423,7 @@ describe('files', () => {
it('should respond with Bad Request if attached file exceeds file limit', async () => {
const pathToUpload = '/my/path/code.sas'
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
const res = await request(app)
.post('/SASjsApi/drive/file')
@@ -320,7 +433,7 @@ describe('files', () => {
.expect(400)
expect(res.text).toEqual(
'File size is over limit. File limit is: 10 MB'
'File size is over limit. File limit is: 100 MB'
)
expect(res.body).toEqual({})
})
@@ -386,7 +499,7 @@ describe('files', () => {
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.field('filePath', `/my/path/code-${generateTimestamp()}.sas`)
.field('filePath', `/my/path/code-3.sas`)
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
.expect(403)
@@ -427,9 +540,9 @@ describe('files', () => {
const pathToUpload = '/my/path/code.exe'
const res = await request(app)
.patch(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
// .field('filePath', pathToUpload)
.query({ _filePath: pathToUpload })
.attach('file', fileToAttachPath)
.expect(400)
@@ -468,7 +581,7 @@ describe('files', () => {
it('should respond with Bad Request if attached file exceeds file limit', async () => {
const pathToUpload = '/my/path/code.sas'
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
const res = await request(app)
.patch('/SASjsApi/drive/file')
@@ -478,11 +591,84 @@ describe('files', () => {
.expect(400)
expect(res.text).toEqual(
'File size is over limit. File limit is: 10 MB'
'File size is over limit. File limit is: 100 MB'
)
expect(res.body).toEqual({})
})
})
describe('get', () => {
it('should get a SAS file on drive having _filePath as query param', async () => {
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
const fileToCopyContent = await readFile(fileToCopyPath)
const filePath = '/my/path/code.sas'
const pathToCopy = path.join(
fileUtilModules.getTmpFilesFolderPath(),
filePath
)
await copy(fileToCopyPath, pathToCopy)
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: filePath })
expect(res.statusCode).toEqual(200)
expect(res.body).toEqual({})
expect(res.text).toEqual(fileToCopyContent)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get('/SASjsApi/drive/file').expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if file is not present', async () => {
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: `/my/path/code-4.sas` })
.expect(403)
expect(res.text).toEqual(`Error: File doesn't exist.`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if filePath outside Drive', async () => {
const res = await request(app)
.get('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/../path/code.sas' })
.expect(403)
expect(res.text).toEqual('Error: Cannot get file outside drive.')
expect(res.body).toEqual({})
})
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const res = await request(app)
.patch('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.query({ _filePath: '/my/path/code.exe' })
.expect(400)
expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if filePath is missing', async () => {
const res = await request(app)
.post('/SASjsApi/drive/file')
.auth(accessToken, { type: 'bearer' })
.expect(400)
expect(res.text).toEqual(`"_filePath" is required`)
expect(res.body).toEqual({})
})
})
})
})

View File

@@ -6,11 +6,6 @@ import appPromise from '../../../app'
import { UserController, GroupController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
let app: Express
appPromise.then((_app) => {
app = _app
})
const clientId = 'someclientID'
const adminUser = {
displayName: 'Test Admin',
@@ -36,11 +31,14 @@ const userController = new UserController()
const groupController = new GroupController()
describe('group', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let adminAccessToken: string
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())

View File

@@ -0,0 +1,20 @@
import { Express } from 'express'
import request from 'supertest'
import appPromise from '../../../app'
describe('Info', () => {
let app: Express
beforeAll(async () => {
app = await appPromise
})
it('should should return configured information of the server instance', async () => {
const res = await request(app).get('/SASjsApi/info').expect(200)
expect(res.body.mode).toEqual('server')
expect(res.body.cors).toEqual('disable')
expect(res.body.whiteList).toEqual([])
expect(res.body.protocol).toEqual('http')
})
})

View File

@@ -6,11 +6,6 @@ import appPromise from '../../../app'
import { UserController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
let app: Express
appPromise.then((_app) => {
app = _app
})
const clientId = 'someclientID'
const adminUser = {
displayName: 'Test Admin',
@@ -30,10 +25,13 @@ const user = {
const controller = new UserController()
describe('user', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
})

View File

@@ -1,27 +1,6 @@
import { AppStreamConfig } from '../../types'
const style = `<style>
* {
font-family: 'Roboto', sans-serif;
}
.app-container {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
}
.app-container .app {
width: 150px;
margin: 10px;
overflow: hidden;
border-radius: 10px 10px 0 0;
text-align: center;
}
.app-container .app img{
width: 100%;
margin-bottom: 10px;
}
</style>`
import { script } from './script'
import { style } from './style'
const defaultAppLogo = '/sasjs-logo.svg'
@@ -31,7 +10,10 @@ const singleAppStreamHtml = (
logo?: string
) =>
` <a class="app" href="${streamServiceName}" title="${appLoc}">
<img src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}" />
<img
src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}"
onerror="this.src = '${defaultAppLogo}';"
/>
${streamServiceName}
</a>`
@@ -49,6 +31,14 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
)
.join('')}
<a class="app" title="Upload build.json">
<input id="fileId" type="file" hidden />
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
<img src="/plus.png" />
</button>
<span id="uploadMessage">Upload New App</span>
</a>
</div>
${script}
</body>
</html>`

View File

@@ -40,17 +40,6 @@ export const publishAppStream = async (
if (!streamServiceName) {
streamServiceName = `AppStreamName${appCount + 1}`
} else {
const alreadyDeployed = process.appStreamConfig[streamServiceName]
if (alreadyDeployed) {
if (alreadyDeployed.appLoc === appLoc) {
// redeploying to same streamServiceName
} else {
// trying to deploy to another existing streamServiceName
// assign new streamServiceName
streamServiceName = `${streamServiceName}-${appCount + 1}`
}
}
}
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
@@ -63,7 +52,7 @@ export const publishAppStream = async (
addEntryToFile
)
const sasJsPort = process.env.PORT ?? 5000
const sasJsPort = process.env.PORT || 5000
console.log(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`

View File

@@ -0,0 +1,58 @@
export const script = `<script>
const inputElement = document.getElementById('fileId')
document
.getElementById('uploadButton')
.addEventListener('click', function () {
inputElement.click()
})
inputElement.addEventListener(
'change',
function () {
const fileList = this.files /* now you can work with the file list */
updateFileUploadMessage('Requesting ...')
const file = fileList[0]
const formData = new FormData()
formData.append('file', file)
fetch('/SASjsApi/drive/deploy/upload', {
method: 'POST',
body: formData
})
.then(async (res) => {
const { status, ok } = res
if (status === 200 && ok) {
const data = await res.json()
return (
data.message +
'\\nstreamServiceName: ' +
data.streamServiceName +
'\\nrefreshing page once alert box closes.'
)
}
throw await res.text()
})
.then((message) => {
alert(message)
location.reload()
})
.catch((error) => {
alert(error)
resetFileUpload()
updateFileUploadMessage('Upload New App')
})
},
false
)
function updateFileUploadMessage(message) {
document.getElementById('uploadMessage').innerHTML = message
}
function resetFileUpload() {
inputElement.value = null
}
</script>`

View File

@@ -0,0 +1,22 @@
export const style = `<style>
* {
font-family: 'Roboto', sans-serif;
}
.app-container {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
}
.app-container .app {
width: 150px;
margin: 10px;
overflow: hidden;
text-align: center;
}
.app-container .app img{
width: 100%;
margin-bottom: 10px;
border-radius: 10px;
}
</style>`

View File

@@ -5,12 +5,17 @@ import { getWebBuildFolderPath } from '../../utils'
const webRouter = express.Router()
const codeToInject = `
const jsCodeForDesktopMode = `
<script>
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
</script>`
const jsCodeForServerMode = `
<script>
localStorage.setItem('CLIENT_ID', '${process.env.CLIENT_ID}')
</script>`
webRouter.get('/', async (_, res) => {
let content: string
try {
@@ -21,14 +26,12 @@ webRouter.get('/', async (_, res) => {
}
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
const codeToInject =
MODE?.trim() === 'server' ? jsCodeForServerMode : jsCodeForDesktopMode
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
res.setHeader('Content-Type', 'text/html')
return res.send(injectedContent)
}
return res.send(content)
res.setHeader('Content-Type', 'text/html')
return res.send(injectedContent)
})
export default webRouter

View File

@@ -4,8 +4,8 @@ import appPromise from './app'
import { getCertificates } from './utils'
appPromise.then(async (app) => {
const protocol = process.env.PROTOCOL ?? 'http'
const sasJsPort = process.env.PORT ?? 5000
const protocol = process.env.PROTOCOL || 'http'
const sasJsPort = process.env.PORT || 5000
console.log('PROTOCOL: ', protocol)

View File

@@ -1,62 +0,0 @@
export enum MemberType {
service = 'service',
file = 'file',
folder = 'folder'
}
export interface ServiceMember {
name: string
type: MemberType.service
code: string
}
export interface FileMember {
name: string
type: MemberType.file
code: string
}
export interface FolderMember {
name: string
type: MemberType.folder
members: (FolderMember | ServiceMember | FileMember)[]
}
export interface FileTree {
members: (FolderMember | ServiceMember | FileMember)[]
}
export const isFileTree = (arg: any): arg is FileTree =>
arg &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: ServiceMember | FileMember | FolderMember) =>
!isServiceMember(member, '-') &&
!isFileMember(member, '-') &&
!isFolderMember(member, '-')
).length === 0
const isServiceMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
typeof arg.code === 'string'
const isFileMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.file &&
typeof arg.code === 'string'
const isFolderMember = (arg: any, pre: string): arg is FolderMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.folder &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isServiceMember(member, pre + '-') &&
!isFileMember(member, pre + '-') &&
!isFolderMember(member, pre + '-')
).length === 0

View File

@@ -1,7 +1,6 @@
// TODO: uppercase types
export * from './AppStreamConfig'
export * from './Execution'
export * from './FileTree'
export * from './InfoJWT'
export * from './PreProgramVars'
export * from './Request'

View File

@@ -5,6 +5,8 @@ import { AppStreamConfig } from '../types'
import { getTmpAppStreamConfigPath } from './file'
export const loadAppStreamConfig = async () => {
if (process.env.NODE_ENV === 'test') return
const appStreamConfigPath = getTmpAppStreamConfigPath()
const content = (await fileExists(appStreamConfigPath))

View File

@@ -1,25 +1,28 @@
import mongoose from 'mongoose'
import { populateClients } from '../routes/api/auth'
import { seedDB } from './seedDB'
export const connectDB = async () => {
// NOTE: when exporting app.js as agent for supertest
// we should exclude connecting to the real database
if (process.env.NODE_ENV === 'test') {
return
} else {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
return
}
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err
console.log('Connected to db!')
await populateClients()
})
}
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
return
}
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err
console.log('Connected to db!')
await seedDB()
await populateClients()
})
}

View File

@@ -10,6 +10,8 @@ import {
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
export const copySASjsCore = async () => {
if (process.env.NODE_ENV === 'test') return
console.log('Copying Macros from container to drive(tmp).')
const macrosDrivePath = getTmpMacrosPath()

View File

@@ -9,7 +9,7 @@ export const sysInitCompiledPath = path.join(
)
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
export const sasJSCoreMacrosInfo = path.join(apiRoot, 'sasjscore', '.macrolist')
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
export const getWebBuildFolderPath = () =>
path.join(codebaseRoot, 'web', 'build')

View File

@@ -12,8 +12,9 @@ export * from './isDebugOn'
export * from './parseLogToArray'
export * from './removeTokensInDB'
export * from './saveTokensInDB'
export * from './seedDB'
export * from './setProcessVariables'
export * from './sleep'
export * from './setupFolders'
export * from './upload'
export * from './validation'
export * from './verifyTokenInDB'

35
api/src/utils/seedDB.ts Normal file
View File

@@ -0,0 +1,35 @@
import Client from '../model/Client'
import User from '../model/User'
const CLIENT = {
clientId: 'clientID1',
clientSecret: 'clientSecret'
}
const ADMIN_USER = {
id: 1,
displayName: 'Super Admin',
username: 'secretuser',
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
isAdmin: true,
isActive: true
}
export const seedDB = async () => {
// Checking if client is already in the database
const clientExist = await Client.findOne({ clientId: CLIENT.clientId })
if (!clientExist) {
const client = new Client(CLIENT)
await client.save()
console.log(`DB Seed - client created: ${CLIENT.clientId}`)
}
// Checking if user is already in the database
const usernameExist = await User.findOne({ username: ADMIN_USER.username })
if (!usernameExist) {
const user = new User(ADMIN_USER)
await user.save()
console.log(`DB Seed - admin account created: ${ADMIN_USER.username}`)
}
}

View File

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

View File

@@ -0,0 +1,7 @@
import { createFolder } from '@sasjs/utils'
import { getTmpFilesFolderPath } from './file'
export const setupFolders = async () => {
const drivePath = getTmpFilesFolderPath()
await createFolder(drivePath)
}

View File

@@ -1,3 +0,0 @@
export const sleep = async (delay: number) => {
await new Promise((resolve) => setTimeout(resolve, delay))
}

View File

@@ -1,6 +1,6 @@
import Joi from 'joi'
const usernameSchema = Joi.string().alphanum().min(6).max(20)
const usernameSchema = Joi.string().alphanum().min(3).max(16)
const passwordSchema = Joi.string().min(6).max(1024)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
@@ -98,6 +98,11 @@ export const fileParamValidation = (data: any): Joi.ValidationResult =>
_filePath: filePathSchema
}).validate(data)
export const folderParamValidation = (data: any): Joi.ValidationResult =>
Joi.object({
_folderPath: Joi.string()
}).validate(data)
export const runSASValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required()

View File

@@ -11,6 +11,10 @@
}
},
"tags": [
{
"name": "Info",
"description": "Get Server Info"
},
{
"name": "Session",
"description": "Get Session information"

16
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "server",
"version": "0.0.40",
"version": "0.0.58",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "server",
"version": "0.0.40",
"version": "0.0.58",
"devDependencies": {
"prettier": "^2.3.1",
"standard-version": "^9.3.2"
@@ -1350,9 +1350,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"node_modules/minimist-options": {
@@ -3158,9 +3158,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"minimist-options": {

View File

@@ -1,12 +1,12 @@
{
"name": "server",
"version": "0.0.40",
"version": "0.0.58",
"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",
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
"server:start": "cd api && npm run start:prod",
"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}\"",

View File

@@ -1,3 +1,6 @@
### Get contents of folder
GET http://localhost:5000/SASjsApi/drive/folder?_path=/Public/app/react-seed-app/services/web
###
POST http://localhost:5000/SASjsApi/drive/deploy
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I

47
web/package-lock.json generated
View File

@@ -22,6 +22,7 @@
"@types/node": "^12.20.28",
"@types/react": "^17.0.27",
"axios": "^0.24.0",
"jwt-decode": "3.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.3.0"
@@ -4191,9 +4192,9 @@
"dev": true
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
@@ -8161,6 +8162,11 @@
"node": ">=4.0"
}
},
"node_modules/jwt-decode": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -8472,9 +8478,9 @@
}
},
"node_modules/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"node_modules/mkdirp": {
"version": "0.5.5",
@@ -8581,9 +8587,9 @@
}
},
"node_modules/node-forge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
"dev": true,
"engines": {
"node": ">= 6.13.0"
@@ -14381,9 +14387,9 @@
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
@@ -17382,6 +17388,11 @@
"object.assign": "^4.1.2"
}
},
"jwt-decode": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -17622,9 +17633,9 @@
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"mkdirp": {
"version": "0.5.5",
@@ -17715,9 +17726,9 @@
}
},
"node-forge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
"dev": true
},
"node-releases": {

View File

@@ -21,6 +21,7 @@
"@types/node": "^12.20.28",
"@types/react": "^17.0.27",
"axios": "^0.24.0",
"jwt-decode": "3.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.3.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
web/public/running-sas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useContext } from 'react'
import { Route, HashRouter, Switch } from 'react-router-dom'
import { ThemeProvider } from '@mui/material/styles'
import { theme } from './theme'
@@ -9,12 +9,12 @@ import Home from './components/home'
import Drive from './containers/Drive'
import Studio from './containers/Studio'
import useTokens from './components/useTokens'
import { AppContext } from './context/appContext'
function App() {
const { tokens, setTokens } = useTokens()
const appContext = useContext(AppContext)
if (!tokens) {
if (!appContext.tokens) {
return (
<ThemeProvider theme={theme}>
<HashRouter>
@@ -24,7 +24,7 @@ function App() {
<Login getCodeOnly />
</Route>
<Route path="/">
<Login setTokens={setTokens} />
<Login />
</Route>
</Switch>
</HashRouter>

View File

@@ -1,13 +1,20 @@
import React, { useState } from 'react'
import React, { useState, useContext } from 'react'
import { Link, useHistory, useLocation } from 'react-router-dom'
import AppBar from '@mui/material/AppBar'
import Toolbar from '@mui/material/Toolbar'
import Tabs from '@mui/material/Tabs'
import Tab from '@mui/material/Tab'
import Button from '@mui/material/Button'
import {
AppBar,
Toolbar,
Tabs,
Tab,
Button,
Menu,
MenuItem
} from '@mui/material'
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import UserName from './userName'
import { AppContext } from '../context/appContext'
const NODE_ENV = process.env.NODE_ENV
const PORT_API = process.env.PORT_API
const baseUrl =
@@ -16,11 +23,29 @@ const baseUrl =
const Header = (props: any) => {
const history = useHistory()
const { pathname } = useLocation()
const appContext = useContext(AppContext)
const [tabValue, setTabValue] = useState(pathname)
const [anchorEl, setAnchorEl] = useState<
(EventTarget & HTMLButtonElement) | null
>(null)
const handleMenu = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
setTabValue(value)
}
const handleLogout = () => {
if (appContext.logout) appContext.logout()
}
return (
<AppBar
position="fixed"
@@ -28,10 +53,10 @@ const Header = (props: any) => {
>
<Toolbar variant="dense">
<img
src="logo-white.png"
src="logo.png"
alt="logo"
style={{
width: '50px',
width: '35px',
cursor: 'pointer',
marginRight: '25px'
}}
@@ -81,6 +106,39 @@ const Header = (props: any) => {
>
App Stream
</Button>
<div
style={{
display: 'flex',
flexGrow: 1,
justifyContent: 'flex-end'
}}
>
<UserName
userName={appContext.displayName}
onClickHandler={handleMenu}
/>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
open={!!anchorEl}
onClose={handleClose}
>
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
<Button variant="contained" color="primary">
Logout
</Button>
</MenuItem>
</Menu>
</div>
</Toolbar>
</AppBar>
)

View File

@@ -1,8 +1,9 @@
import React, { useState } from 'react'
import React, { useState, useContext } from 'react'
import { useLocation } from 'react-router-dom'
import PropTypes from 'prop-types'
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
import { AppContext } from '../context/appContext'
const headers = {
Accept: 'application/json',
@@ -33,8 +34,9 @@ const getTokens = async (payload: any) => {
}).then((data) => data.json())
}
const Login = ({ setTokens, getCodeOnly }: any) => {
const Login = ({ getCodeOnly }: any) => {
const location = useLocation()
const appContext = useContext(AppContext)
const [username, setUserName] = useState('')
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState('')
@@ -45,13 +47,12 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
error = false
setErrorMessage('')
e.preventDefault()
let clientId = process.env.CLIENT_ID
let clientId = process.env.CLIENT_ID ?? localStorage.getItem('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
if (responseType === 'code') clientId = params.get('client_id')
}
const { code } = await getAuthCode({
@@ -72,7 +73,8 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
code
})
setTokens(accessToken, refreshToken)
if (appContext.setTokens) appContext.setTokens(accessToken, refreshToken)
if (appContext.setUserName) appContext.setUserName(username)
}
}
@@ -127,7 +129,7 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
required
/>
{errorMessage && <span>{errorMessage}</span>}
<Button type="submit" variant="outlined">
<Button type="submit" variant="outlined" disabled={!appContext.setTokens}>
Submit
</Button>
</Box>
@@ -135,7 +137,6 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
}
Login.propTypes = {
setTokens: PropTypes.func,
getCodeOnly: PropTypes.bool
}

View File

@@ -0,0 +1,30 @@
import React from 'react'
import { Typography, IconButton } from '@mui/material'
import AccountCircle from '@mui/icons-material/AccountCircle'
const UserName = (props: any) => {
return (
<IconButton
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={props.onClickHandler}
color="inherit"
>
{props.avatarContent ? (
<img
src={props.avatarContent}
alt="user-avatar"
style={{ width: '25px' }}
/>
) : (
<AccountCircle></AccountCircle>
)}
<Typography variant="h6" sx={{ color: 'white', padding: '0 8px' }}>
{props.userName}
</Typography>
</IconButton>
)
}
export default UserName

View File

@@ -90,7 +90,11 @@ const Drive = () => {
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
<SideBar directoryData={directoryData} handleSelect={handleSelect} />
<SideBar
selectedFilePath={selectedFilePath}
directoryData={directoryData}
handleSelect={handleSelect}
/>
<Main
selectedFilePath={selectedFilePath}
removeFileFromTree={removeFileFromTree}

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useMemo } from 'react'
import { makeStyles } from '@mui/styles'
@@ -30,13 +30,27 @@ const useStyles = makeStyles(() => ({
const drawerWidth = 240
type Props = {
selectedFilePath: string
directoryData: TreeNode | null
handleSelect: (node: TreeNode) => void
}
const SideBar = ({ directoryData, handleSelect }: Props) => {
const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => {
const classes = useStyles()
const defaultExpanded = useMemo(() => {
const splittedPath = selectedFilePath.split('/')
const arr = ['']
let nodeId = ''
splittedPath.forEach((path) => {
if (path !== '') {
nodeId += '/' + path
arr.push(nodeId)
}
})
return arr
}, [selectedFilePath])
const renderTree = (nodes: TreeNode) => (
<TreeItem
classes={{ root: classes.root }}
@@ -72,7 +86,8 @@ const SideBar = ({ directoryData, handleSelect }: Props) => {
<TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
defaultExpanded={[directoryData.relativePath]}
defaultExpanded={defaultExpanded}
selected={defaultExpanded.slice(-1)}
>
{renderTree(directoryData)}
</TreeView>

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'
import axios from 'axios'
import Box from '@mui/material/Box'
import { Button, Paper, Stack, Tab } from '@mui/material'
import { Button, Paper, Stack, Tab, Tooltip } from '@mui/material'
import { makeStyles } from '@mui/styles'
import Editor, { OnMount } from '@monaco-editor/react'
import { useLocation } from 'react-router-dom'
@@ -15,6 +15,17 @@ const useStyles = makeStyles(() => ({
'&.Mui-selected': {
color: 'black'
}
},
subMenu: {
marginTop: '25px',
display: 'flex',
justifyContent: 'center'
},
runButton: {
display: 'flex',
alignItems: 'center',
padding: '5px 5px',
minWidth: 'unset'
}
}))
@@ -22,8 +33,10 @@ const Studio = () => {
const location = useLocation()
const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('')
const [ctrlPressed, setCtrlPressed] = useState(false)
const [webout, setWebout] = useState('')
const [tab, setTab] = React.useState('1')
const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue)
}
@@ -50,30 +63,32 @@ const Studio = () => {
.map((logLine: any) => logLine.line)
.join('\n')
setLog(`<div><h2>SAS Log</h2><pre>${parsedLog}</pre></div>`)
setLog(parsedLog)
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>`)
setWebout(`${res.data?._webout}`)
setTab('2')
// Scroll to bottom of log
window.scrollTo(0, document.body.scrollHeight)
})
.catch((err) => console.log(err))
}
const handleKeyDown = (event: any) => {
if (event.ctrlKey) {
if (event.key === 'v') {
setCtrlPressed(false)
}
if (event.key === 'Enter') runCode(getSelection() || fileContent)
if (!ctrlPressed) setCtrlPressed(true)
}
}
const handleKeyUp = (event: any) => {
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
}
useEffect(() => {
const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content)
@@ -97,73 +112,89 @@ const Studio = () => {
}, [location.search])
const classes = useStyles()
return (
<>
<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" />
<Box
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
>
<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" />
<Tooltip title="Displays content from the _webout fileref">
<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
</Tooltip>
</TabList>
</Box>
<TabPanel style={{ paddingBottom: 0 }} value="1">
<div className={classes.subMenu}>
<Tooltip title="CTRL+ENTER will also run SAS code">
<Button onClick={handleRunBtnClick} className={classes.runButton}>
<img
draggable="false"
style={{ width: '25px' }}
src="/running-sas.png"
></img>
<span style={{ fontSize: '12px' }}>RUN</span>
</Button>
</Stack>
</TabPanel>
<TabPanel value="2">
<div
id="sas_log"
style={{ marginTop: '50px' }}
dangerouslySetInnerHTML={{ __html: log }}
</Tooltip>
</div>
{/* <Toolbar /> */}
<Paper
sx={{
height: 'calc(100vh - 170px)',
padding: '10px',
overflow: 'auto',
position: 'relative'
}}
elevation={3}
>
<Editor
height="98%"
value={fileContent}
onMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => {
if (val) setFileContent(val)
}}
/>
</TabPanel>
<TabPanel value="3">
<div
style={{ marginTop: '50px' }}
dangerouslySetInnerHTML={{ __html: webout }}
/>
</TabPanel>
</TabContext>
</Box>
</>
<p
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: -10,
textAlign: 'center',
fontSize: '13px'
}}
>
Press CTRL + ENTER to run SAS code
</p>
</Paper>
</TabPanel>
<TabPanel value="2">
<div style={{ marginTop: '50px' }}>
<h2>SAS Log</h2>
<pre>{log}</pre>
</div>
</TabPanel>
<TabPanel value="3">
<div style={{ marginTop: '50px' }}>
<pre>{webout}</pre>
</div>
</TabPanel>
</TabContext>
</Box>
)
}

View File

@@ -0,0 +1,159 @@
import React, {
createContext,
Dispatch,
SetStateAction,
useState,
useEffect,
useCallback,
ReactNode
} from 'react'
import axios from 'axios'
import jwt_decode from 'jwt-decode'
const NODE_ENV = process.env.NODE_ENV
const PORT_API = process.env.PORT_API
const baseUrl =
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
const isAbsoluteURLRegex = /^(?:\w+:)\/\//
const setAxiosRequestHeader = (accessToken: string) => {
axios.interceptors.request.use(function (config) {
if (baseUrl && !isAbsoluteURLRegex.test(config.url as string)) {
config.url = baseUrl + config.url
}
console.log('axios.interceptors.request.use', accessToken)
config.headers!['Authorization'] = `Bearer ${accessToken}`
config.withCredentials = true
return config
})
}
const setAxiosResponse = (setTokens: Function) => {
// Add a response interceptor
axios.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
return response
},
async function (error) {
if (error.response?.status === 401) {
// refresh token
// const { accessToken, refreshToken: newRefresh } = await refreshMyToken(
// refreshToken
// )
// if (accessToken && newRefresh) {
// setTokens(accessToken, newRefresh)
// error.config.headers['Authorization'] = 'Bearer ' + accessToken
// error.config.baseURL = undefined
// return axios.request(error.config)
// }
console.log(53)
setTokens(undefined)
}
return Promise.reject(error)
}
)
}
const getTokens = () => {
const accessToken = localStorage.getItem('accessToken')
const refreshToken = localStorage.getItem('refreshToken')
if (accessToken && refreshToken) {
setAxiosRequestHeader(accessToken)
return { accessToken, refreshToken }
}
return undefined
}
interface AppContextProps {
userName: string
displayName: string
setUserName: Dispatch<SetStateAction<string>> | null
tokens?: { accessToken: string; refreshToken: string }
setTokens: ((accessToken: string, refreshToken: string) => void) | null
logout: (() => void) | null
}
export const AppContext = createContext<AppContextProps>({
userName: '',
displayName: '',
tokens: getTokens(),
setUserName: null,
setTokens: null,
logout: null
})
const AppContextProvider = (props: { children: ReactNode }) => {
const { children } = props
const [userName, setUserName] = useState('')
const [displayName, setDisplayName] = useState('')
const [tokens, setTokens] = useState(getTokens())
useEffect(() => {
setAxiosResponse(setTokens)
}, [])
useEffect(() => {
if (tokens === undefined) {
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
setUserName('')
setDisplayName('')
} else {
const decoded: any = jwt_decode(tokens.accessToken)
if (decoded.userId) {
axios
.get(`/SASjsApi/user/${decoded.userId}`)
.then((res: any) => {
if (res.data && res.data?.displayName) {
setDisplayName(res.data.displayName)
} else if (res.data && res.data?.username) {
setDisplayName(res.data.username)
}
})
.catch((err) => {
console.log(err)
})
}
}
}, [tokens])
const saveTokens = useCallback(
(accessToken: string, refreshToken: string) => {
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
setAxiosRequestHeader(accessToken)
setTokens({ accessToken, refreshToken })
},
[]
)
const logout = useCallback(() => {
setUserName('')
setTokens(undefined)
}, [])
return (
<AppContext.Provider
value={{
userName,
displayName,
setUserName,
tokens,
setTokens: saveTokens,
logout
}}
>
{children}
</AppContext.Provider>
)
}
export default AppContextProvider

View File

@@ -2,10 +2,13 @@ import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import AppContextProvider from './context/appContext'
ReactDOM.render(
<React.StrictMode>
<App />
<AppContextProvider>
<App />
</AppContextProvider>
</React.StrictMode>,
document.getElementById('root')
)