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

Compare commits

...

121 Commits

Author SHA1 Message Date
Allan Bowe
45a2a01532 chore(release): 0.0.73 2022-05-10 11:23:59 +00:00
Allan Bowe
c61fec47c4 Merge pull request #165 from sasjs/issue-164
fix: helmet config on http mode
2022-05-10 14:01:40 +03:00
24d7f00c02 chore: type fix 2022-05-10 10:13:57 +00:00
b0fdaaaa79 fix: helmet config on http mode 2022-05-10 10:04:01 +00:00
Allan Bowe
2467616296 chore(release): 0.0.72 2022-05-09 12:33:32 +00:00
Allan Bowe
ceefbe48e9 chore(release): 0.0.71 2022-05-07 22:35:25 +00:00
Allan Bowe
426e90471e Merge pull request #163 from sasjs/issue159
fix: reqHeadrs.txt will contain headers to access APIs
2022-05-08 01:34:41 +03:00
Allan Bowe
c0b57b9e76 fix: bumping core 2022-05-07 22:31:44 +00:00
Saad Jutt
4a8e32dd20 fix: added more cookies to req 2022-05-08 03:18:04 +05:00
Saad Jutt
636301e664 fix: reqHeadrs.txt will contain headers to access APIs 2022-05-08 02:49:16 +05:00
Allan Bowe
25dc5dd215 chore(release): 0.0.70 2022-05-06 14:45:31 +00:00
Allan Bowe
503994dbd2 Merge pull request #161 from sasjs/csp-disable
Added additional options for HELMET
2022-05-06 17:44:18 +03:00
Saad Jutt
0dceb5c3c3 chore: web package-lock built with LTS 2022-05-06 19:41:02 +05:00
Mihajlo Medjedovic
1af04fa3b3 Merge branch 'csp-disable' of github.com:sasjs/server into csp-disable 2022-05-06 13:40:48 +00:00
Mihajlo Medjedovic
efa81fec77 chore: package-lock 2022-05-06 13:40:40 +00:00
Allan Bowe
10caf1918a chore: updating README 2022-05-06 12:13:45 +00:00
Mihajlo Medjedovic
4ed20a3b75 chore: readme update 2022-05-06 11:49:32 +00:00
Mihajlo Medjedovic
98b2c5fa25 chore: readme update 2022-05-06 11:46:40 +00:00
Mihajlo Medjedovic
3ad327b85f chore: helmet config cleanup 2022-05-06 11:40:12 +00:00
Mihajlo Medjedovic
dd3acce393 feat: CSP_DISABLE env option 2022-05-05 18:25:33 +00:00
Allan Bowe
8065727b9b chore(release): 0.0.69 2022-05-02 15:24:56 +00:00
Allan Bowe
e1223ec3f8 Merge pull request #158 from sasjs/update-csp-policy
fix(upload): appStream uses CSRF + Session authentication
2022-05-02 18:22:35 +03:00
Saad Jutt
1f89279264 fix(upload): appStream uses CSRF + Session authentication 2022-05-02 18:01:28 +05:00
Saad Jutt
a07f47a1ba chore(release): 0.0.68 2022-05-02 05:57:10 +05:00
Saad Jutt
2548c82dfe fix: using monaco editor locally 2022-05-02 05:57:03 +05:00
Saad Jutt
238aa1006f chore(release): 0.0.67 2022-05-02 03:41:07 +05:00
Saad Jutt
35cba97611 chore: commented helmet middleware 2022-05-02 03:40:14 +05:00
Saad Jutt
5f29dec16f chore(release): 0.0.66 2022-05-01 23:31:59 +05:00
Saad Jutt
e2a97fcb7c fix: added swagger ui init file manually 2022-05-01 23:31:48 +05:00
Allan Bowe
6adeeefcf5 chore(release): 0.0.65 2022-05-01 11:36:26 +00:00
Allan Bowe
c9d66b8576 Merge pull request #156 from sasjs/fix-swagger-api-with-csrf
fix: consume swagger api with CSRF
2022-05-01 14:35:23 +03:00
Saad Jutt
5aaac24080 fix: consume swagger api with CSRF 2022-05-01 06:07:17 +05:00
Saad Jutt
6d34206bbc chore(release): 0.0.64 2022-05-01 02:28:57 +05:00
Saad Jutt
7b39cc06d3 fix: removed fileExists for serving web 2022-05-01 02:28:50 +05:00
Saad Jutt
6e7f28a6f8 chore(release): 0.0.63 2022-05-01 02:10:24 +05:00
Saad Jutt
5689169ce4 chore: syntax fix for workflow 2022-05-01 02:10:17 +05:00
Saad Jutt
6139e7bff6 chore(release): 0.0.62 2022-05-01 02:08:03 +05:00
Saad Jutt
2c77317bb9 chore: release using node LTS 2022-05-01 02:07:55 +05:00
Saad Jutt
57b63db9cb chore(release): 0.0.61 2022-05-01 01:59:12 +05:00
Saad Jutt
60a2a4fe32 chore: bumped pkg version 2022-05-01 01:59:04 +05:00
Allan Bowe
09611cb416 chore(release): 0.0.60 2022-04-30 18:53:44 +00:00
Allan Bowe
2a9bb6e6b1 Merge pull request #155 from sasjs/api-access-via-session-authentication
fix: added CSRF check for granting access via session authentication
2022-04-30 21:44:35 +03:00
Saad Jutt
b4b60c69cf fix: setting CSRF Token for only rendering SPA 2022-04-30 06:32:24 +05:00
Saad Jutt
b060ad1b8e fix: added CSRF check for granting access via session authentication 2022-04-30 05:04:27 +05:00
munja
d47ed6d0e8 chore(release): 0.0.59 2022-04-29 13:28:34 +01:00
Muhammad Saad
a6993ef5ae Merge pull request #153 from sasjs/csrf-tokens-for-web
feat: enabled csrf tokens for web component
2022-04-28 17:26:05 -07:00
Saad Jutt
2571fc2ca8 chore: README.md updated 2022-04-29 05:09:11 +05:00
Saad Jutt
992f39b63a chore: lint fix 2022-04-29 04:11:29 +05:00
Saad Jutt
1ea3f6d8b3 chore: corrected order for web route 2022-04-29 03:11:08 +05:00
Saad Jutt
e462aebdc0 feat: enabled csrf tokens for web component 2022-04-29 02:59:48 +05:00
Muhammad Saad
13403517a4 Merge pull request #150 from sasjs/session-based-authentication
feat: enabled session based authentication for web
2022-04-28 07:11:45 -07:00
Saad Jutt
c3c2048e75 chore: temp 2022-04-28 07:15:36 +05:00
Saad Jutt
1d8acc36eb chore: temp 2022-04-28 07:15:09 +05:00
Saad Jutt
4c7ad56326 test: fixed specs 2022-04-28 07:07:56 +05:00
Saad Jutt
e57443f1ed fix(web): show display name instead of username 2022-04-28 07:00:49 +05:00
Saad Jutt
5da93f318a feat: enabled session based authentication for web 2022-04-28 06:44:25 +05:00
Muhammad Saad
a30fb1a241 Merge pull request #141 from sasjs/issue-135
fix: fetch client from DB for each request
2022-04-27 12:09:41 -07:00
Allan Bowe
4ae8f35e9a chore(release): 0.0.58 2022-04-24 20:25:08 +00:00
Saad Jutt
ebb46f51b6 chore: fix specs 2022-04-24 05:29:42 +05:00
Saad Jutt
fe24f51ca2 test: fix specs 2022-04-24 05:17:25 +05:00
Saad Jutt
fd15f3fb41 test: fix specs 2022-04-24 05:11:56 +05:00
Saad Jutt
7d31ee7696 chore: Merge branch 'main' into issue-135 2022-04-24 05:08:31 +05: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
Saad Jutt
4ad8c81e49 fix: fetch client from DB for each request 2022-04-24 04:16:13 +05: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
78 changed files with 2036 additions and 831 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. 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
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] ### Docker Development Mode
There is `.env.example` file present at `./api` of the project. [for Development]
There is `.env.example` file present at `./web` of the project. [for Development]
Remember to provide enviornment variables.
#### Development
Command to run docker for development: 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_ - `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: 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-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_ - `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` 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.
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. ### 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 install
npm start npm start
``` ```
##### Web #### Web Server
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.
``` ```
cd web
npm install npm install
npm start npm start
``` ```
#### Development (running only api server and have web build served): #### NodeJS Production Mode
##### API server also serving Web build files Update the `.env` file in the *api* folder. Then:
There is `.env.example` file present at `./api` directory. Remember to provide enviornment variables else default values will be used mentioned in `.env.example` files
Command to install and run api server.
```
cd ./web && npm i && npm build && cd ../
cd ./api && npm i && npm start
```
#### Production
##### API & WEB
``` ```
npm run server npm run server
@@ -105,7 +100,7 @@ This will install/build `web` and install `api`, then start prod server.
## Executables ## Executables
Command to generate executables In order to generate the final executables:
``` ```
cd ./web && npm i && npm build && cd ../ 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` 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

@@ -8,10 +8,20 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node-version: [lts/*]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies WEB - name: Install Dependencies WEB
working-directory: ./web working-directory: ./web
run: npm ci run: npm ci

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ sasjscore/
certificates/ certificates/
executables/ executables/
.env .env
api/csp.config.json

View File

@@ -2,6 +2,194 @@
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. 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.73](https://github.com/sasjs/server/compare/v0.0.72...v0.0.73) (2022-05-10)
### Bug Fixes
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
### [0.0.72](https://github.com/sasjs/server/compare/v0.0.71...v0.0.72) (2022-05-09)
### [0.0.71](https://github.com/sasjs/server/compare/v0.0.70...v0.0.71) (2022-05-07)
### Bug Fixes
* added more cookies to req ([4a8e32d](https://github.com/sasjs/server/commit/4a8e32dd20b540b6dc92d749fad90d6c7fc69376))
* bumping core ([c0b57b9](https://github.com/sasjs/server/commit/c0b57b9e76d6db33fc64a68556a8be979dd69e40))
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
### [0.0.70](https://github.com/sasjs/server/compare/v0.0.69...v0.0.70) (2022-05-06)
### Features
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
### [0.0.69](https://github.com/sasjs/server/compare/v0.0.68...v0.0.69) (2022-05-02)
### Bug Fixes
* **upload:** appStream uses CSRF + Session authentication ([1f89279](https://github.com/sasjs/server/commit/1f8927926405887f3d134c0a1dd6452ffa33876e))
### [0.0.68](https://github.com/sasjs/server/compare/v0.0.67...v0.0.68) (2022-05-02)
### Bug Fixes
* using monaco editor locally ([2548c82](https://github.com/sasjs/server/commit/2548c82dfe1149e62a570a00546dddd9e30049b1))
### [0.0.67](https://github.com/sasjs/server/compare/v0.0.66...v0.0.67) (2022-05-01)
### [0.0.66](https://github.com/sasjs/server/compare/v0.0.64...v0.0.66) (2022-05-01)
### Bug Fixes
* added swagger ui init file manually ([e2a97fc](https://github.com/sasjs/server/commit/e2a97fcb7c54a57a7ca118677cfce93fe9430d8f))
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
### [0.0.65](https://github.com/sasjs/server/compare/v0.0.64...v0.0.65) (2022-05-01)
### Bug Fixes
* consume swagger api with CSRF ([5aaac24](https://github.com/sasjs/server/commit/5aaac24080362d6ce0c5d1157798a9343f40ae2a))
### [0.0.64](https://github.com/sasjs/server/compare/v0.0.63...v0.0.64) (2022-04-30)
### Bug Fixes
* removed fileExists for serving web ([7b39cc0](https://github.com/sasjs/server/commit/7b39cc06d358f5ffecb87955040c4eb0fcc7469e))
### [0.0.63](https://github.com/sasjs/server/compare/v0.0.62...v0.0.63) (2022-04-30)
### [0.0.62](https://github.com/sasjs/server/compare/v0.0.61...v0.0.62) (2022-04-30)
### [0.0.61](https://github.com/sasjs/server/compare/v0.0.59...v0.0.61) (2022-04-30)
### Bug Fixes
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
### [0.0.60](https://github.com/sasjs/server/compare/v0.0.59...v0.0.60) (2022-04-30)
### Bug Fixes
* added CSRF check for granting access via session authentication ([b060ad1](https://github.com/sasjs/server/commit/b060ad1b8e0bbc61c20dc25be553bba4cc4d2716))
* setting CSRF Token for only rendering SPA ([b4b60c6](https://github.com/sasjs/server/commit/b4b60c69cf67a42f4797f7f1afe68b7a5eec2998))
### [0.0.59](https://github.com/sasjs/server/compare/v0.0.58...v0.0.59) (2022-04-29)
### Features
* enabled csrf tokens for web component ([e462aeb](https://github.com/sasjs/server/commit/e462aebdc01f3c0068ed0074473a2063412dcf45))
* enabled session based authentication for web ([5da93f3](https://github.com/sasjs/server/commit/5da93f318aad10b1c67032a467191e4dbb99f411))
### Bug Fixes
* fetch client from DB for each request ([4ad8c81](https://github.com/sasjs/server/commit/4ad8c81e4927c1a82220ec015a781b095c8e859e))
* **web:** show display name instead of username ([e57443f](https://github.com/sasjs/server/commit/e57443f1ed662a022494bb93d79c3d2f10a2d082))
### [0.0.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)
### Bug Fixes
* bumping core library to get latest user management macros ([4862071](https://github.com/sasjs/server/commit/486207128da58fc4866bd0919c1bed2bd98097ea))
* missing dependency ([d09876c](https://github.com/sasjs/server/commit/d09876c05f89166eec20064f7aa7ed5b867be081))
### [0.0.57](https://github.com/sasjs/server/compare/v0.0.56...v0.0.57) (2022-04-21)
### Features
* create AppContext ([84ee743](https://github.com/sasjs/server/commit/84ee743eae16e87eaa91969393bebf01e2d15a44))
### [0.0.56](https://github.com/sasjs/server/compare/v0.0.55...v0.0.56) (2022-04-20)
### Bug Fixes
* shortening min length of username. Closes [#61](https://github.com/sasjs/server/issues/61) ([f02996f](https://github.com/sasjs/server/commit/f02996facf1019ec4022ccfbc99c1d0137074e1b))
### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)
### Bug Fixes
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
* drive path in server mode ([c4cea4a](https://github.com/sasjs/server/commit/c4cea4a12b7eda4daeed995f41c0b10bcea79871))
### [0.0.54](https://github.com/sasjs/server/compare/v0.0.53...v0.0.54) (2022-04-19)
### Bug Fixes
* added db seed at server startup ([2e63831](https://github.com/sasjs/server/commit/2e63831b90c7457e0e322719ebb1193fd6181cc3))
### [0.0.53](https://github.com/sasjs/server/compare/v0.0.49...v0.0.53) (2022-04-19)
### Features
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
### Bug Fixes
* provide clientId to web component ([db70b1c](https://github.com/sasjs/server/commit/db70b1ce555df6b29fb09c0c960d38b911c97b1b))
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
### [0.0.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
### Features
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
### Features
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
### Features
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
### Bug Fixes
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
### Bug Fixes
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02) ### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)

View File

@@ -48,14 +48,22 @@ When launching the app, it will make use of specific environment variables. Thes
Example contents of a `.env` file: Example contents of a `.env` file:
``` ```
# options: [desktop|server] default: `desktop` #
## Core Settings
#
# MODE options: [desktop|server] default: `desktop`
# Desktop mode is single user and designed for workstation use
# Server mode is multi-user and suitable for intranet / internet use
MODE= MODE=
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop` # Path to SAS executable (sas.exe / sas.sh)
CORS= SAS_PATH=/path/to/sas/executable.exe
# options: <http://localhost:3000 https://abc.com ...> space separated urls # Path to working directory
WHITELIST= # This location is for SAS WORK, staged files, DRIVE, configuration etc
DRIVE_PATH=/tmp
# options: [http|https] default: http # options: [http|https] default: http
PROTOCOL= PROTOCOL=
@@ -64,16 +72,22 @@ PROTOCOL=
PORT= PORT=
# optional #
# for MODE: `desktop`, prompts user ## Additional SAS Options
# for MODE: `server` gets value from api/package.json `configuration.sasPath` #
SAS_PATH=/path/to/sas/executable.exe
# optional # On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
# for MODE: `desktop`, prompts user # Any options set here are automatically applied in the SAS session
# for MODE: `server` defaults to /tmp # See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm
DRIVE_PATH=/tmp # And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
SAS_OPTIONS= -NOXCMD
SASV9_OPTIONS= -NOXCMD
#
## Additional Web Server Options
#
# ENV variables required for PROTOCOL: `https` # ENV variables required for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem PRIVATE_KEY=privkey.pem
@@ -83,12 +97,52 @@ FULL_CHAIN=fullchain.pem
ACCESS_TOKEN_SECRET=<secret> ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret> REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret> AUTH_CODE_SECRET=<secret>
SESSION_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
# If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=
# options: <http://localhost:3000 https://abc.com ...> space separated urls
WHITELIST=
# HELMET Cross Origin Embedder Policy
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
# options: [true|false] default: true
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
HELMET_COEP=
# HELMET Content Security Policy
# Path to a json file containing HELMET `contentSecurityPolicy` directives
# Docs: https://helmetjs.github.io/#reference
#
# Example config:
# {
# "img-src": ["'self'", "domain.com"],
# "script-src": ["'self'", "'unsafe-inline'"],
# "script-src-attr": ["'self'", "'unsafe-inline'"]
# }
HELMET_CSP_CONFIG_PATH=./csp.config.json
``` ```
## Persisting the Session ## 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 ```bash
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
@@ -122,7 +176,7 @@ Instead of `app_name` you can pass:
## Server Version ## 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` - CLIENTID: `clientID1`
- USERNAME: `secretuser` - USERNAME: `secretuser`

View File

@@ -1,13 +1,20 @@
MODE=[desktop|server] default considered as desktop MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
WHITELIST=<space separated urls, each starting with protocol `http` or `https`> WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http PROTOCOL=[http|https] default considered as http
PRIVATE_KEY=privkey.pem PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem FULL_CHAIN=fullchain.pem
PORT=[5000] default value is 5000 PORT=[5000] default value is 5000
HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used
HELMET_COEP=[true|false] if omitted HELMET default will be used
ACCESS_TOKEN_SECRET=<secret> ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret> REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret> AUTH_CODE_SECRET=<secret>
SESSION_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas

View File

@@ -0,0 +1,5 @@
{
"img-src": ["'self'", "domen.com"],
"script-src": ["'self'", "'unsafe-inline'"],
"script-src-attr": ["'self'", "'unsafe-inline'"]
}

491
api/package-lock.json generated
View File

@@ -8,19 +8,23 @@
"name": "api", "name": "api",
"version": "0.0.2", "version": "0.0.2",
"dependencies": { "dependencies": {
"@sasjs/core": "4.9.0", "@sasjs/core": "^4.23.1",
"@sasjs/utils": "2.42.1", "@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-session": "^1.17.2",
"helmet": "^5.0.2",
"joi": "^17.4.2", "joi": "^17.4.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"mongoose": "^6.0.12", "mongoose": "^6.0.12",
"mongoose-sequence": "^5.3.1", "mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.3", "multer": "^1.4.3",
"swagger-ui-express": "^4.1.6" "swagger-ui-express": "4.3.0"
}, },
"bin": { "bin": {
"api": "build/src/server.js" "api": "build/src/server.js"
@@ -29,7 +33,9 @@
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12", "@types/express": "^4.17.12",
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/jsonwebtoken": "^8.5.5", "@types/jsonwebtoken": "^8.5.5",
"@types/mongoose-sequence": "^3.0.6", "@types/mongoose-sequence": "^3.0.6",
@@ -43,7 +49,7 @@
"jest": "^27.0.6", "jest": "^27.0.6",
"mongodb-memory-server": "^8.0.0", "mongodb-memory-server": "^8.0.0",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"pkg": "5.5.2", "pkg": "5.6.0",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"supertest": "^6.1.3", "supertest": "^6.1.3",
@@ -1379,9 +1385,9 @@
} }
}, },
"node_modules/@sasjs/core": { "node_modules/@sasjs/core": {
"version": "4.9.0", "version": "4.23.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ==" "integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
}, },
"node_modules/@sasjs/utils": { "node_modules/@sasjs/utils": {
"version": "2.42.1", "version": "2.42.1",
@@ -1833,6 +1839,15 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true "dev": true
}, },
"node_modules/@types/csurf": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
"dev": true,
"dependencies": {
"@types/express-serve-static-core": "*"
}
},
"node_modules/@types/express": { "node_modules/@types/express": {
"version": "4.17.12", "version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
@@ -1856,6 +1871,15 @@
"@types/range-parser": "*" "@types/range-parser": "*"
} }
}, },
"node_modules/@types/express-session": {
"version": "1.17.4",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.4.tgz",
"integrity": "sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/fs-extra": { "node_modules/@types/fs-extra": {
"version": "9.0.13", "version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
@@ -2447,10 +2471,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/async": { "node_modules/async": {
"version": "2.6.3", "version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dependencies": { "dependencies": {
"lodash": "^4.17.14" "lodash": "^4.17.14"
} }
@@ -2674,6 +2709,11 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true "dev": true
}, },
"node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.19.0", "version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@@ -3238,6 +3278,42 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/connect-mongo": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-4.6.0.tgz",
"integrity": "sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==",
"dependencies": {
"debug": "^4.3.1",
"kruptein": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"mongodb": "^4.1.0"
}
},
"node_modules/connect-mongo/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/connect-mongo/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/consola": { "node_modules/consola": {
"version": "2.15.0", "version": "2.15.0",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
@@ -3362,6 +3438,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/csrf": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"dependencies": {
"rndm": "1.2.0",
"tsscmp": "1.0.6",
"uid-safe": "2.1.5"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/cssom": { "node_modules/cssom": {
"version": "0.4.4", "version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@@ -3386,6 +3475,40 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true "dev": true
}, },
"node_modules/csurf": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
"dependencies": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"csrf": "3.1.0",
"http-errors": "~1.7.3"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/csurf/node_modules/http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/csurf/node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/csv-stringify": { "node_modules/csv-stringify": {
"version": "5.6.5", "version": "5.6.5",
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
@@ -4027,6 +4150,59 @@
"node": ">= 0.10.0" "node": ">= 0.10.0"
} }
}, },
"node_modules/express-session": {
"version": "1.17.2",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz",
"integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==",
"dependencies": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express-session/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.2.11", "version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
@@ -4642,6 +4818,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/helmet": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz",
"integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/html-encoding-sniffer": { "node_modules/html-encoding-sniffer": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@@ -6828,6 +7012,17 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/kruptein": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.4.tgz",
"integrity": "sha512-614v+4fgOkcw98lI7rMO9HZ+Y2cK6MGYcR/NSVhRXcClUb72LTAf2NibAh8CKSjalY81rfrrjLQgb8TW9RP03Q==",
"dependencies": {
"asn1.js": "^5.4.1"
},
"engines": {
"node": ">8"
}
},
"node_modules/latest-version": { "node_modules/latest-version": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
@@ -7095,6 +7290,11 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -7133,7 +7333,6 @@
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==", "integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
"dev": true,
"dependencies": { "dependencies": {
"bson": "^4.5.4", "bson": "^4.5.4",
"denque": "^2.0.1", "denque": "^2.0.1",
@@ -7971,9 +8170,9 @@
} }
}, },
"node_modules/pkg": { "node_modules/pkg": {
"version": "5.5.2", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.5.2.tgz", "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.6.0.tgz",
"integrity": "sha512-pD0UB2ud01C6pVv2wpGsTYJrXI/bnvGRYvMLd44wFzA1p+A2jrlTGFPAYa7YEYzmitXhx23PqalaG1eUEnSwcA==", "integrity": "sha512-mHrAVSQWmHA41RnUmRpC7pK9lNnMfdA16CF3cqOI22a8LZxOQzF7M8YWtA2nfs+d7I0MTDXOtkDsAsFXeCpYjg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "7.16.2", "@babel/parser": "7.16.2",
@@ -7985,7 +8184,7 @@
"into-stream": "^6.0.0", "into-stream": "^6.0.0",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"multistream": "^4.1.0", "multistream": "^4.1.0",
"pkg-fetch": "3.2.6", "pkg-fetch": "3.3.0",
"prebuild-install": "6.1.4", "prebuild-install": "6.1.4",
"progress": "^2.0.3", "progress": "^2.0.3",
"resolve": "^1.20.0", "resolve": "^1.20.0",
@@ -8017,9 +8216,9 @@
} }
}, },
"node_modules/pkg-fetch": { "node_modules/pkg-fetch": {
"version": "3.2.6", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.2.6.tgz", "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.3.0.tgz",
"integrity": "sha512-Q8fx6SIT022g0cdSE4Axv/xpfHeltspo2gg1KsWRinLQZOTRRAtOOaEFghA1F3jJ8FVsh8hGrL/Pb6Ea5XHIFw==", "integrity": "sha512-xJnIZ1KP+8rNN+VLafwu4tEeV4m8IkFBDdCFqmAJz9K1aiXEtbARmdbEe6HlXWGSVuShSHjFXpfkKRkDBQ5kiA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
@@ -8076,9 +8275,9 @@
} }
}, },
"node_modules/pkg-fetch/node_modules/semver": { "node_modules/pkg-fetch/node_modules/semver": {
"version": "7.3.5", "version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -8367,6 +8566,14 @@
} }
] ]
}, },
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -8547,6 +8754,11 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/rndm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -9222,11 +9434,11 @@
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ==" "integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
}, },
"node_modules/swagger-ui-express": { "node_modules/swagger-ui-express": {
"version": "4.2.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz",
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==", "integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==",
"dependencies": { "dependencies": {
"swagger-ui-dist": ">3.52.5" "swagger-ui-dist": ">=4.1.3"
}, },
"engines": { "engines": {
"node": ">= v0.10.32" "node": ">= v0.10.32"
@@ -9532,6 +9744,14 @@
"yarn": ">=1.9.4" "yarn": ">=1.9.4"
} }
}, },
"node_modules/tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
"engines": {
"node": ">=0.6.x"
}
},
"node_modules/tunnel-agent": { "node_modules/tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -9626,6 +9846,17 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
@@ -11127,9 +11358,9 @@
} }
}, },
"@sasjs/core": { "@sasjs/core": {
"version": "4.9.0", "version": "4.23.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz",
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ==" "integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg=="
}, },
"@sasjs/utils": { "@sasjs/utils": {
"version": "2.42.1", "version": "2.42.1",
@@ -11525,6 +11756,15 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true "dev": true
}, },
"@types/csurf": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
"dev": true,
"requires": {
"@types/express-serve-static-core": "*"
}
},
"@types/express": { "@types/express": {
"version": "4.17.12", "version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
@@ -11548,6 +11788,15 @@
"@types/range-parser": "*" "@types/range-parser": "*"
} }
}, },
"@types/express-session": {
"version": "1.17.4",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.4.tgz",
"integrity": "sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/fs-extra": { "@types/fs-extra": {
"version": "9.0.13", "version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
@@ -12059,10 +12308,21 @@
"is-string": "^1.0.7" "is-string": "^1.0.7"
} }
}, },
"asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"requires": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"async": { "async": {
"version": "2.6.3", "version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"requires": { "requires": {
"lodash": "^4.17.14" "lodash": "^4.17.14"
} }
@@ -12234,6 +12494,11 @@
} }
} }
}, },
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"body-parser": { "body-parser": {
"version": "1.19.0", "version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@@ -12681,6 +12946,30 @@
"xdg-basedir": "^4.0.0" "xdg-basedir": "^4.0.0"
} }
}, },
"connect-mongo": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-4.6.0.tgz",
"integrity": "sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==",
"requires": {
"debug": "^4.3.1",
"kruptein": "^3.0.0"
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"consola": { "consola": {
"version": "2.15.0", "version": "2.15.0",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.0.tgz",
@@ -12783,6 +13072,16 @@
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
"dev": true "dev": true
}, },
"csrf": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"requires": {
"rndm": "1.2.0",
"tsscmp": "1.0.6",
"uid-safe": "2.1.5"
}
},
"cssom": { "cssom": {
"version": "0.4.4", "version": "0.4.4",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
@@ -12806,6 +13105,36 @@
} }
} }
}, },
"csurf": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
"requires": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"csrf": "3.1.0",
"http-errors": "~1.7.3"
},
"dependencies": {
"http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}
}
},
"csv-stringify": { "csv-stringify": {
"version": "5.6.5", "version": "5.6.5",
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
@@ -13303,6 +13632,38 @@
"vary": "~1.1.2" "vary": "~1.1.2"
} }
}, },
"express-session": {
"version": "1.17.2",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz",
"integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==",
"requires": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"dependencies": {
"cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"fast-glob": { "fast-glob": {
"version": "3.2.11", "version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
@@ -13774,6 +14135,11 @@
"integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
"dev": true "dev": true
}, },
"helmet": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-5.0.2.tgz",
"integrity": "sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg=="
},
"html-encoding-sniffer": { "html-encoding-sniffer": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
@@ -15409,6 +15775,14 @@
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="
}, },
"kruptein": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.4.tgz",
"integrity": "sha512-614v+4fgOkcw98lI7rMO9HZ+Y2cK6MGYcR/NSVhRXcClUb72LTAf2NibAh8CKSjalY81rfrrjLQgb8TW9RP03Q==",
"requires": {
"asn1.js": "^5.4.1"
}
},
"latest-version": { "latest-version": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
@@ -15615,6 +15989,11 @@
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
"dev": true "dev": true
}, },
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -15644,7 +16023,6 @@
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==", "integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
"dev": true,
"requires": { "requires": {
"bson": "^4.5.4", "bson": "^4.5.4",
"denque": "^2.0.1", "denque": "^2.0.1",
@@ -16271,9 +16649,9 @@
} }
}, },
"pkg": { "pkg": {
"version": "5.5.2", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.5.2.tgz", "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.6.0.tgz",
"integrity": "sha512-pD0UB2ud01C6pVv2wpGsTYJrXI/bnvGRYvMLd44wFzA1p+A2jrlTGFPAYa7YEYzmitXhx23PqalaG1eUEnSwcA==", "integrity": "sha512-mHrAVSQWmHA41RnUmRpC7pK9lNnMfdA16CF3cqOI22a8LZxOQzF7M8YWtA2nfs+d7I0MTDXOtkDsAsFXeCpYjg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/parser": "7.16.2", "@babel/parser": "7.16.2",
@@ -16285,7 +16663,7 @@
"into-stream": "^6.0.0", "into-stream": "^6.0.0",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"multistream": "^4.1.0", "multistream": "^4.1.0",
"pkg-fetch": "3.2.6", "pkg-fetch": "3.3.0",
"prebuild-install": "6.1.4", "prebuild-install": "6.1.4",
"progress": "^2.0.3", "progress": "^2.0.3",
"resolve": "^1.20.0", "resolve": "^1.20.0",
@@ -16342,9 +16720,9 @@
} }
}, },
"pkg-fetch": { "pkg-fetch": {
"version": "3.2.6", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.2.6.tgz", "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.3.0.tgz",
"integrity": "sha512-Q8fx6SIT022g0cdSE4Axv/xpfHeltspo2gg1KsWRinLQZOTRRAtOOaEFghA1F3jJ8FVsh8hGrL/Pb6Ea5XHIFw==", "integrity": "sha512-xJnIZ1KP+8rNN+VLafwu4tEeV4m8IkFBDdCFqmAJz9K1aiXEtbARmdbEe6HlXWGSVuShSHjFXpfkKRkDBQ5kiA==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
@@ -16386,9 +16764,9 @@
"dev": true "dev": true
}, },
"semver": { "semver": {
"version": "7.3.5", "version": "7.3.7",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -16555,6 +16933,11 @@
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true "dev": true
}, },
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
},
"range-parser": { "range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -16692,6 +17075,11 @@
"glob": "^7.1.3" "glob": "^7.1.3"
} }
}, },
"rndm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
},
"run-parallel": { "run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -17213,11 +17601,11 @@
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ==" "integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
}, },
"swagger-ui-express": { "swagger-ui-express": {
"version": "4.2.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz",
"integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==", "integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==",
"requires": { "requires": {
"swagger-ui-dist": ">3.52.5" "swagger-ui-dist": ">=4.1.3"
} }
}, },
"symbol-tree": { "symbol-tree": {
@@ -17429,6 +17817,11 @@
"@tsoa/runtime": "^3.13.0" "@tsoa/runtime": "^3.13.0"
} }
}, },
"tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="
},
"tunnel-agent": { "tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -17495,6 +17888,14 @@
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"unbox-primitive": { "unbox-primitive": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",

View File

@@ -8,6 +8,7 @@
"prestart": "npm run initial", "prestart": "npm run initial",
"prebuild": "npm run initial", "prebuild": "npm run initial",
"start": "nodemon ./src/server.ts", "start": "nodemon ./src/server.ts",
"start:prod": "node ./build/src/server.js",
"build": "rimraf build && tsc", "build": "rimraf build && tsc",
"postbuild": "npm run copy:files", "postbuild": "npm run copy:files",
"swagger": "tsoa spec", "swagger": "tsoa spec",
@@ -46,25 +47,31 @@
}, },
"author": "4GL Ltd", "author": "4GL Ltd",
"dependencies": { "dependencies": {
"@sasjs/core": "4.9.0", "@sasjs/core": "^4.23.1",
"@sasjs/utils": "2.42.1", "@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-session": "^1.17.2",
"helmet": "^5.0.2",
"joi": "^17.4.2", "joi": "^17.4.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"mongoose": "^6.0.12", "mongoose": "^6.0.12",
"mongoose-sequence": "^5.3.1", "mongoose-sequence": "^5.3.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.3", "multer": "^1.4.3",
"swagger-ui-express": "^4.1.6" "swagger-ui-express": "4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12", "@types/express": "^4.17.12",
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/jsonwebtoken": "^8.5.5", "@types/jsonwebtoken": "^8.5.5",
"@types/mongoose-sequence": "^3.0.6", "@types/mongoose-sequence": "^3.0.6",
@@ -78,7 +85,7 @@
"jest": "^27.0.6", "jest": "^27.0.6",
"mongodb-memory-server": "^8.0.0", "mongodb-memory-server": "^8.0.0",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"pkg": "5.5.2", "pkg": "5.6.0",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"supertest": "^6.1.3", "supertest": "^6.1.3",

View File

@@ -0,0 +1,50 @@
window.onload = function () {
// Build a system
var url = window.location.search.match(/url=([^&]+)/)
if (url && url.length > 1) {
url = decodeURIComponent(url[1])
} else {
url = window.location.origin
}
var options = {
customOptions: {
url: '/swagger.yaml',
requestInterceptor: function (request) {
request.credentials = 'include'
var cookie = document.cookie
var startIndex = cookie.indexOf('XSRF-TOKEN')
var csrf = cookie.slice(startIndex + 11).split('; ')[0]
request.headers['X-XSRF-TOKEN'] = csrf
return request
}
}
}
url = options.swaggerUrl || url
var urls = options.swaggerUrls
var customOptions = options.customOptions
var spec1 = options.swaggerDoc
var swaggerOptions = {
spec: spec1,
url: url,
urls: urls,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: 'StandaloneLayout'
}
for (var attrname in customOptions) {
swaggerOptions[attrname] = customOptions[attrname]
}
var ui = SwaggerUIBundle(swaggerOptions)
if (customOptions.oauth) {
ui.initOAuth(customOptions.oauth)
}
if (customOptions.authAction) {
ui.authActions.authorize(customOptions.authAction)
}
window.ui = ui
}

View File

@@ -0,0 +1,49 @@
const inputElement = document.getElementById('fileId')
document.getElementById('uploadButton').addEventListener('click', function () {
inputElement.click()
})
inputElement.addEventListener(
'change',
function () {
const fileList = this.files /* now you can work with the file list */
updateFileUploadMessage('Requesting ...')
const file = fileList[0]
const formData = new FormData()
formData.append('file', file)
axios
.post('/SASjsApi/drive/deploy/upload', formData)
.then((res) => res.data)
.then((data) => {
return (
data.message +
'\nstreamServiceName: ' +
data.streamServiceName +
'\nrefreshing page once alert box closes.'
)
})
.then((message) => {
alert(message)
location.reload()
})
.catch((error) => {
alert(error.response.data)
resetFileUpload()
updateFileUploadMessage('Upload New App')
})
},
false
)
function updateFileUploadMessage(message) {
document.getElementById('uploadMessage').innerHTML = message
}
function resetFileUpload() {
inputElement.value = null
}

3
api/public/axios.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
api/public/plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

View File

@@ -5,6 +5,21 @@ components:
requestBodies: {} requestBodies: {}
responses: {} responses: {}
schemas: schemas:
LoginPayload:
properties:
username:
type: string
description: 'Username for user'
example: secretuser
password:
type: string
description: 'Password for user'
example: secretpassword
required:
- username
- password
type: object
additionalProperties: false
AuthorizeResponse: AuthorizeResponse:
properties: properties:
code: code:
@@ -418,6 +433,25 @@ components:
example: /Public/somefolder/some.file example: /Public/somefolder/some.file
type: object type: object
additionalProperties: false 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: securitySchemes:
bearerAuth: bearerAuth:
type: http type: http
@@ -431,6 +465,62 @@ info:
name: '4GL Ltd' name: '4GL Ltd'
openapi: 3.0.0 openapi: 3.0.0
paths: paths:
/:
get:
operationId: Home
responses:
'200':
description: Ok
content:
application/json:
schema:
type: string
summary: 'Render index.html'
tags:
- Web
security: []
parameters: []
/login:
post:
operationId: Login
responses:
'200':
description: Ok
content:
application/json:
schema:
properties:
user: {properties: {displayName: {type: string}, username: {type: string}}, required: [displayName, username], type: object}
loggedIn: {type: boolean}
required:
- user
- loggedIn
type: object
summary: 'Accept a valid username/password'
tags:
- Web
security: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginPayload'
/logout:
get:
operationId: Logout
responses:
'200':
description: Ok
content:
application/json:
schema: {}
summary: 'Accept a valid username/password'
tags:
- Web
security: []
parameters: []
/SASjsApi/auth/authorize: /SASjsApi/auth/authorize:
post: post:
operationId: Authorize operationId: Authorize
@@ -1240,10 +1330,31 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecuteReturnJsonPayload' $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: servers:
- -
url: / url: /
tags: tags:
-
name: Info
description: 'Get Server Info'
- -
name: Session name: Session
description: 'Get Session information' description: 'Get Session information'
@@ -1268,3 +1379,6 @@ tags:
- -
name: CODE name: CODE
description: 'Operations on SAS code' description: 'Operations on SAS code'
-
name: Web
description: 'Operations on Web'

View File

@@ -5,23 +5,12 @@
_before_ any user-provided content. _before_ any user-provided content.
A number of useful CORE macros are also compiled below, so that they can be 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> <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 mfs_httpheader.sas
@li mp_dirlist.sas @li ms_webout.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
**/ **/

View File

@@ -1,9 +1,13 @@
import path from 'path' import path from 'path'
import express, { ErrorRequestHandler } from 'express' import express, { ErrorRequestHandler } from 'express'
import csrf from 'csurf'
import session from 'express-session'
import MongoStore from 'connect-mongo'
import morgan from 'morgan' import morgan from 'morgan'
import cookieParser from 'cookie-parser' import cookieParser from 'cookie-parser'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import cors from 'cors' import cors from 'cors'
import helmet from 'helmet'
import { import {
connectDB, connectDB,
@@ -13,31 +17,97 @@ import {
setProcessVariables, setProcessVariables,
setupFolders setupFolders
} from './utils' } from './utils'
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
dotenv.config() dotenv.config()
const app = express() const app = express()
const { MODE, CORS, WHITELIST } = process.env app.use(cookieParser())
app.use(morgan('tiny'))
const { MODE, CORS, WHITELIST, PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } =
process.env
export const cookieOptions = {
secure: PROTOCOL === 'https',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
HELMET_CSP_CONFIG_PATH
)
const coepFlag =
HELMET_COEP === 'true' || HELMET_COEP === undefined ? true : false
if (PROTOCOL === 'http') cspConfigJson['upgrade-insecure-requests'] = null
/***********************************
* CSRF Protection *
***********************************/
export const csrfProtection = csrf({ cookie: cookieOptions })
/***********************************
* Handle security and origin *
***********************************/
app.use(
helmet({
contentSecurityPolicy: {
directives: {
...helmet.contentSecurityPolicy.getDefaultDirectives(),
...cspConfigJson
}
},
crossOriginEmbedderPolicy: coepFlag
})
)
/***********************************
* Enabling CORS *
***********************************/
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') { if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
const whiteList: string[] = [] const whiteList: string[] = []
WHITELIST?.split(' ')?.forEach((url) => { WHITELIST?.split(' ')
if (url.startsWith('http')) ?.filter((url) => !!url)
// removing trailing slash of URLs listing for CORS .forEach((url) => {
whiteList.push(url.replace(/\/$/, '')) 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) console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList })) app.use(cors({ credentials: true, origin: whiteList }))
} }
app.use(cookieParser()) /***********************************
app.use(morgan('tiny')) * DB Connection & *
* Express Sessions *
* With Mongo Store *
***********************************/
if (MODE?.trim() === 'server') {
// NOTE: when exporting app.js as agent for supertest
// we should exclude connecting to the real database
if (process.env.NODE_ENV !== 'test') {
const clientPromise = connectDB().then((conn) => conn!.getClient() as any)
app.use(
session({
secret: process.env.SESSION_SECRET as string,
saveUninitialized: false, // don't create session until something stored
resave: false, //don't save session if unmodified
store: MongoStore.create({ clientPromise, collectionName: 'sessions' }),
cookie: cookieOptions
})
)
}
}
app.use(express.json({ limit: '100mb' })) app.use(express.json({ limit: '100mb' }))
app.use(express.static(path.join(__dirname, '../public'))) app.use(express.static(path.join(__dirname, '../public')))
const onError: ErrorRequestHandler = (err, req, res, next) => { const onError: ErrorRequestHandler = (err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!')
console.error(err.stack) console.error(err.stack)
res.status(500).send('Something broke!') res.status(500).send('Something broke!')
} }
@@ -59,6 +129,5 @@ export default setProcessVariables().then(async () => {
app.use(onError) app.use(onError)
await connectDB()
return app return app
}) })

View File

@@ -1,6 +1,7 @@
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa' import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import User from '../model/User' import User from '../model/User'
import Client from '../model/Client'
import { InfoJWT } from '../types' import { InfoJWT } from '../types'
import { import {
generateAccessToken, generateAccessToken,
@@ -81,6 +82,9 @@ export class AuthController {
const authorize = async (data: any): Promise<AuthorizeResponse> => { const authorize = async (data: any): Promise<AuthorizeResponse> => {
const { username, password, clientId } = data const { username, password, clientId } = data
const client = await Client.findOne({ clientId })
if (!client) throw new Error('Invalid clientId.')
// Authenticate User // Authenticate User
const user = await User.findOne({ username }) const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.') if (!user) throw new Error('Username is not found.')

View File

@@ -3,7 +3,7 @@ import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal' import { ExecuteReturnJson, ExecutionController } from './internal'
import { PreProgramVars } from '../types' import { PreProgramVars } from '../types'
import { ExecuteReturnJsonResponse } from '.' import { ExecuteReturnJsonResponse } from '.'
import { parseLogToArray } from '../utils' import { getPreProgramVariables, parseLogToArray } from '../utils'
interface ExecuteSASCodePayload { interface ExecuteSASCodePayload {
/** /**
@@ -56,16 +56,3 @@ const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
} }
} }
} }
const getPreProgramVariables = (req: any): PreProgramVars => {
const host = req.get('host')
const protocol = req.protocol + '://'
const { user, accessToken } = req
return {
username: user.username,
userId: user.userId,
displayName: user.displayName,
serverUrl: protocol + host,
accessToken
}
}

View File

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

View File

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

View File

@@ -75,12 +75,12 @@ export class ExecutionController {
const logPath = path.join(session.path, 'log.log') const logPath = path.join(session.path, 'log.log')
const headersPath = path.join(session.path, 'stpsrv_header.txt') const headersPath = path.join(session.path, 'stpsrv_header.txt')
const weboutPath = path.join(session.path, 'webout.txt') const weboutPath = path.join(session.path, 'webout.txt')
const tokenFile = path.join(session.path, 'accessToken.txt') const tokenFile = path.join(session.path, 'reqHeaders.txt')
await createFile(weboutPath, '') await createFile(weboutPath, '')
await createFile( await createFile(
tokenFile, tokenFile,
preProgramVariables?.accessToken ?? 'accessToken' preProgramVariables?.httpHeaders.join('\n') ?? ''
) )
const varStatments = Object.keys(vars).reduce( const varStatments = Object.keys(vars).reduce(
@@ -157,7 +157,9 @@ ${program}`
: '' : ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent) const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
const fileResponse: boolean = 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)) const webout = (await fileExists(weboutPath))
? fileResponse ? fileResponse

View File

@@ -12,8 +12,7 @@ import {
createFile, createFile,
fileExists, fileExists,
generateTimestamp, generateTimestamp,
readFile, readFile
moveFile
} from '@sasjs/utils' } from '@sasjs/utils'
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
@@ -41,6 +40,7 @@ export class SessionController {
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId) const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
const creationTimeStamp = sessionId.split('-').pop() as string const creationTimeStamp = sessionId.split('-').pop() as string
// death time of session is 15 mins from creation
const deathTimeStamp = ( const deathTimeStamp = (
parseInt(creationTimeStamp) + parseInt(creationTimeStamp) +
15 * 60 * 1000 - 15 * 60 * 1000 -
@@ -140,7 +140,9 @@ ${autoExecContent}`
private scheduleSessionDestroy(session: Session) { private scheduleSessionDestroy(session: Session) {
setTimeout(async () => { setTimeout(async () => {
if (session.inUse) { 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) this.scheduleSessionDestroy(session)
} else { } else {

View File

@@ -24,7 +24,7 @@ export class SessionController {
} }
const session = (req: any) => ({ const session = (req: any) => ({
id: req.user.id, id: req.user.userId,
username: req.user.username, username: req.user.username,
displayName: req.user.displayName displayName: req.user.displayName
}) })

View File

@@ -17,8 +17,8 @@ import {
ExecutionController, ExecutionController,
ExecutionVars ExecutionVars
} from './internal' } from './internal'
import { PreProgramVars } from '../types'
import { import {
getPreProgramVariables,
getTmpFilesFolderPath, getTmpFilesFolderPath,
HTTPHeaders, HTTPHeaders,
isDebugOn, isDebugOn,
@@ -210,16 +210,3 @@ const executeReturnJson = async (
} }
} }
} }
const getPreProgramVariables = (req: any): PreProgramVars => {
const host = req.get('host')
const protocol = req.protocol + '://'
const { user, accessToken } = req
return {
username: user.username,
userId: user.userId,
displayName: user.displayName,
serverUrl: protocol + host,
accessToken
}
}

View File

@@ -0,0 +1,98 @@
import path from 'path'
import express from 'express'
import { Request, Route, Tags, Post, Body, Get } from 'tsoa'
import { readFile } from '@sasjs/utils'
import User from '../model/User'
import { getWebBuildFolderPath } from '../utils'
@Route('/')
@Tags('Web')
export class WebController {
/**
* @summary Render index.html
*
*/
@Get('/')
public async home() {
return home()
}
/**
* @summary Accept a valid username/password
*
*/
@Post('/login')
public async login(
@Request() req: express.Request,
@Body() body: LoginPayload
) {
return login(req, body)
}
/**
* @summary Accept a valid username/password
*
*/
@Get('/logout')
public async logout(@Request() req: express.Request) {
return new Promise((resolve) => {
req.session.destroy(() => {
resolve(true)
})
})
}
}
const home = async () => {
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
// Attention! Cannot use fileExists here,
// due to limitation after building executable
const content = await readFile(indexHtmlPath)
return content
}
const login = async (
req: express.Request,
{ username, password }: LoginPayload
) => {
// Authenticate User
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
const validPass = user.comparePassword(password)
if (!validPass) throw new Error('Invalid password.')
req.session.loggedIn = true
req.session.user = {
userId: user.id,
clientId: 'web_app',
username: user.username,
displayName: user.displayName,
isAdmin: user.isAdmin,
isActive: user.isActive
}
return {
loggedIn: true,
user: {
username: user.username,
displayName: user.displayName
}
}
}
interface LoginPayload {
/**
* Username for user
* @example "secretuser"
*/
username: string
/**
* Password for user
* @example "secretpassword"
*/
password: string
}

View File

@@ -1,7 +1,16 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app'
import { verifyTokenInDB } from '../utils' import { verifyTokenInDB } from '../utils'
export const authenticateAccessToken = (req: any, res: any, next: any) => { export const authenticateAccessToken = (req: any, res: any, next: any) => {
// if request is coming from web and has valid session
// we can validate the request and check for CSRF Token
if (req.session?.loggedIn) {
req.user = req.session.user
return csrfProtection(req, res, next)
}
authenticateToken( authenticateToken(
req, req,
res, res,
@@ -43,9 +52,7 @@ const authenticateToken = (
} }
const authHeader = req.headers['authorization'] const authHeader = req.headers['authorization']
const token = const token = authHeader?.split(' ')[1]
authHeader?.split(' ')[1] ??
(tokenType === 'accessToken' ? req.cookies.accessToken : '')
if (!token) return res.sendStatus(401) if (!token) return res.sendStatus(401)
jwt.verify(token, key, async (err: any, data: any) => { jwt.verify(token, key, async (err: any, data: any) => {

View File

@@ -1,44 +1,22 @@
import express from 'express' import express from 'express'
import { AuthController } from '../../controllers/' import { AuthController } from '../../controllers/'
import Client from '../../model/Client'
import { import {
authenticateAccessToken, authenticateAccessToken,
authenticateRefreshToken authenticateRefreshToken
} from '../../middlewares' } from '../../middlewares'
import { import { authorizeValidation, tokenValidation } from '../../utils'
authorizeValidation,
getDesktopFields,
tokenValidation
} from '../../utils'
import { InfoJWT } from '../../types' import { InfoJWT } from '../../types'
const authRouter = express.Router() const authRouter = express.Router()
const controller = new AuthController()
const clientIDs = new Set()
export const populateClients = async () => {
const result = await Client.find()
clientIDs.clear()
result.forEach((r) => {
clientIDs.add(r.clientId)
})
}
authRouter.post('/authorize', async (req, res) => { authRouter.post('/authorize', async (req, res) => {
const { error, value: body } = authorizeValidation(req.body) const { error, value: body } = authorizeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message) if (error) return res.status(400).send(error.details[0].message)
const { clientId } = body
// Verify client ID
if (!clientIDs.has(clientId)) {
return res.status(403).send('Invalid clientId.')
}
const controller = new AuthController()
try { try {
const response = await controller.authorize(body) const response = await controller.authorize(body)
@@ -52,12 +30,10 @@ authRouter.post('/token', async (req, res) => {
const { error, value: body } = tokenValidation(req.body) const { error, value: body } = tokenValidation(req.body)
if (error) return res.status(400).send(error.details[0].message) if (error) return res.status(400).send(error.details[0].message)
const controller = new AuthController()
try { try {
const response = await controller.token(body) const response = await controller.token(body)
const { accessToken } = response
res.cookie('accessToken', accessToken).send(response) res.send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
} }
@@ -66,7 +42,6 @@ authRouter.post('/token', async (req, res) => {
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => { authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
const userInfo: InfoJWT = req.user const userInfo: InfoJWT = req.user
const controller = new AuthController()
try { try {
const response = await controller.refresh(userInfo) const response = await controller.refresh(userInfo)
@@ -79,7 +54,6 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => { authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
const userInfo: InfoJWT = req.user const userInfo: InfoJWT = req.user
const controller = new AuthController()
try { try {
await controller.logout(userInfo) await controller.logout(userInfo)
} catch (e) {} } catch (e) {}

View File

@@ -9,6 +9,7 @@ import {
verifyAdmin verifyAdmin
} from '../../middlewares' } from '../../middlewares'
import infoRouter from './info'
import driveRouter from './drive' import driveRouter from './drive'
import stpRouter from './stp' import stpRouter from './stp'
import codeRouter from './code' import codeRouter from './code'
@@ -20,6 +21,7 @@ import sessionRouter from './session'
const router = express.Router() const router = express.Router()
router.use('/info', infoRouter)
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter) router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
router.use('/auth', desktopRestrict, authRouter) router.use('/auth', desktopRestrict, authRouter)
router.use( router.use(
@@ -34,12 +36,22 @@ router.use('/group', desktopRestrict, groupRouter)
router.use('/stp', authenticateAccessToken, stpRouter) router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/code', authenticateAccessToken, codeRouter) router.use('/code', authenticateAccessToken, codeRouter)
router.use('/user', desktopRestrict, userRouter) router.use('/user', desktopRestrict, userRouter)
router.use( router.use(
'/', '/',
swaggerUi.serve, swaggerUi.serve,
swaggerUi.setup(undefined, { swaggerUi.setup(undefined, {
swaggerOptions: { swaggerOptions: {
url: '/swagger.yaml' url: '/swagger.yaml',
requestInterceptor: (request: any) => {
request.credentials = 'include'
const cookie = document.cookie
const startIndex = cookie.indexOf('XSRF-TOKEN')
const csrf = cookie.slice(startIndex + 11).split('; ')[0]
request.headers['X-XSRF-TOKEN'] = csrf
return request
}
} }
}) })
) )

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

@@ -8,7 +8,6 @@ import {
ClientController, ClientController,
AuthController AuthController
} from '../../../controllers/' } from '../../../controllers/'
import { populateClients } from '../auth'
import { InfoJWT } from '../../../types' import { InfoJWT } from '../../../types'
import { import {
generateAccessToken, generateAccessToken,
@@ -18,11 +17,6 @@ import {
verifyTokenInDB verifyTokenInDB
} from '../../../utils' } from '../../../utils'
let app: Express
appPromise.then((_app) => {
app = _app
})
const clientId = 'someclientID' const clientId = 'someclientID'
const clientSecret = 'someclientSecret' const clientSecret = 'someclientSecret'
const user = { const user = {
@@ -35,16 +29,18 @@ const user = {
} }
describe('auth', () => { describe('auth', () => {
let app: Express
let con: Mongoose let con: Mongoose
let mongoServer: MongoMemoryServer let mongoServer: MongoMemoryServer
const userController = new UserController() const userController = new UserController()
const clientController = new ClientController() const clientController = new ClientController()
beforeAll(async () => { beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create() mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri()) con = await mongoose.connect(mongoServer.getUri())
await clientController.createClient({ clientId, clientSecret }) await clientController.createClient({ clientId, clientSecret })
await populateClients()
}) })
afterAll(async () => { afterAll(async () => {
@@ -156,7 +152,7 @@ describe('auth', () => {
}) })
.expect(403) .expect(403)
expect(res.text).toEqual('Invalid clientId.') expect(res.text).toEqual('Error: Invalid clientId.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
}) })

View File

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

View File

@@ -33,11 +33,6 @@ import { getTreeExample } from '../../../controllers/internal'
import { generateAccessToken, saveTokensInDB } from '../../../utils/' import { generateAccessToken, saveTokensInDB } from '../../../utils/'
const { getTmpFilesFolderPath } = fileUtilModules const { getTmpFilesFolderPath } = fileUtilModules
let app: Express
appPromise.then((_app) => {
app = _app
})
const clientId = 'someclientID' const clientId = 'someclientID'
const user = { const user = {
displayName: 'Test User', displayName: 'Test User',
@@ -48,6 +43,7 @@ const user = {
} }
describe('drive', () => { describe('drive', () => {
let app: Express
let con: Mongoose let con: Mongoose
let mongoServer: MongoMemoryServer let mongoServer: MongoMemoryServer
const controller = new UserController() const controller = new UserController()
@@ -55,6 +51,8 @@ describe('drive', () => {
let accessToken: string let accessToken: string
beforeAll(async () => { beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create() mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri()) con = await mongoose.connect(mongoServer.getUri())

View File

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

View File

@@ -1,27 +1,5 @@
import { AppStreamConfig } from '../../types' import { AppStreamConfig } from '../../types'
import { style } from './style'
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>`
const defaultAppLogo = '/sasjs-logo.svg' const defaultAppLogo = '/sasjs-logo.svg'
@@ -52,6 +30,15 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo) singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
) )
.join('')} .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> </div>
<script src="/axios.min.js"></script>
<script src="/app-streams-script.js"></script>
</body> </body>
</html>` </html>`

View File

@@ -7,9 +7,11 @@ import { appStreamHtml } from './appStreamHtml'
const router = express.Router() const router = express.Router()
router.get('/', async (_, res) => { router.get('/', async (req, res) => {
const content = appStreamHtml(process.appStreamConfig) const content = appStreamHtml(process.appStreamConfig)
res.cookie('XSRF-TOKEN', req.csrfToken())
return res.send(content) return res.send(content)
}) })

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

@@ -4,13 +4,16 @@ import webRouter from './web'
import apiRouter from './api' import apiRouter from './api'
import appStreamRouter from './appStream' import appStreamRouter from './appStream'
import { csrfProtection } from '../app'
export const setupRoutes = (app: Express) => { export const setupRoutes = (app: Express) => {
app.use('/', webRouter)
app.use('/SASjsApi', apiRouter) app.use('/SASjsApi', apiRouter)
app.use('/AppStream', function (req, res, next) { app.use('/AppStream', csrfProtection, function (req, res, next) {
// this needs to be a function to hook on // this needs to be a function to hook on
// whatever the current router is // whatever the current router is
appStreamRouter(req, res, next) appStreamRouter(req, res, next)
}) })
app.use('/', csrfProtection, webRouter)
} }

View File

@@ -1,34 +1,41 @@
import { readFile } from '@sasjs/utils'
import express from 'express' import express from 'express'
import path from 'path' import { WebController } from '../../controllers/web'
import { getWebBuildFolderPath } from '../../utils' import { loginWebValidation } from '../../utils'
const webRouter = express.Router() const webRouter = express.Router()
const controller = new WebController()
const codeToInject = ` webRouter.get('/', async (req, res) => {
<script>
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
</script>`
webRouter.get('/', async (_, res) => {
let content: string
try { try {
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html') const response = await controller.home()
content = await readFile(indexHtmlPath)
res.cookie('XSRF-TOKEN', req.csrfToken())
return res.send(response)
} catch (_) { } catch (_) {
return res.send('Web Build is not present') return res.send('Web Build is not present')
} }
})
const { MODE } = process.env webRouter.post('/login', async (req, res) => {
if (MODE?.trim() !== 'server') { const { error, value: body } = loginWebValidation(req.body)
const injectedContent = content.replace('</head>', `${codeToInject}</head>`) if (error) return res.status(400).send(error.details[0].message)
res.setHeader('Content-Type', 'text/html') try {
return res.send(injectedContent) const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
res.status(400).send(err.toString())
} }
})
return res.send(content) webRouter.get('/logout', async (req, res) => {
try {
await controller.logout(req)
res.status(200).send('OK!')
} catch (err: any) {
res.status(400).send(err.toString())
}
}) })
export default webRouter export default webRouter

View File

@@ -3,5 +3,5 @@ export interface PreProgramVars {
userId: number userId: number
displayName: string displayName: string
serverUrl: string serverUrl: string
accessToken: string httpHeaders: string[]
} }

View File

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

View File

@@ -1,17 +0,0 @@
import { MacroVars } from '@sasjs/utils'
export interface ExecutionQuery {
_program: string
macroVars?: MacroVars
_debug?: number
}
export interface FileQuery {
filePath: string
}
export const isExecutionQuery = (arg: any): arg is ExecutionQuery =>
arg && !Array.isArray(arg) && typeof arg._program === 'string'
export const isFileQuery = (arg: any): arg is FileQuery =>
arg && !Array.isArray(arg) && typeof arg.filePath === 'string'

View File

@@ -3,6 +3,5 @@ export * from './AppStreamConfig'
export * from './Execution' export * from './Execution'
export * from './InfoJWT' export * from './InfoJWT'
export * from './PreProgramVars' export * from './PreProgramVars'
export * from './Request'
export * from './Session' export * from './Session'
export * from './TreeNode' export * from './TreeNode'

View File

@@ -0,0 +1,14 @@
import express from 'express'
declare module 'express-session' {
interface SessionData {
loggedIn: boolean
user: {
userId: number
clientId: string
username: string
displayName: string
isAdmin: boolean
isActive: boolean
}
}
}

1
api/src/types/system/global.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import 'jest-extended'

8
api/src/types/system/process.d.ts vendored Normal file
View File

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

View File

@@ -1,25 +1,15 @@
import mongoose from 'mongoose' import mongoose from 'mongoose'
import { populateClients } from '../routes/api/auth' import { seedDB } from './seedDB'
export const connectDB = async () => { export const connectDB = async () => {
// NOTE: when exporting app.js as agent for supertest try {
// we should exclude connecting to the real database await mongoose.connect(process.env.DB_CONNECT as string)
if (process.env.NODE_ENV === 'test') { } catch (err) {
return throw new Error('Unable to connect to DB!')
} 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()
})
} }
console.log('Connected to DB!')
await seedDB()
return mongoose.connection
} }

View File

@@ -9,7 +9,7 @@ export const sysInitCompiledPath = path.join(
) )
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore') 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 = () => export const getWebBuildFolderPath = () =>
path.join(codebaseRoot, 'web', 'build') path.join(codebaseRoot, 'web', 'build')

View File

@@ -0,0 +1,29 @@
import { PreProgramVars } from '../types'
export const getPreProgramVariables = (req: any): PreProgramVars => {
const host = req.get('host')
const protocol = req.protocol + '://'
const { user, accessToken } = req
const csrfToken = req.headers['x-xsrf-token']
const sessionId = req.cookies['connect.sid']
const { _csrf } = req.cookies
const httpHeaders: string[] = []
if (accessToken) httpHeaders.push(`Authorization: Bearer ${accessToken}`)
if (csrfToken) httpHeaders.push(`x-xsrf-token: ${csrfToken}`)
const cookies: string[] = []
if (sessionId) cookies.push(`connect.sid=${sessionId}`)
if (_csrf) cookies.push(`_csrf=${_csrf}`)
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
return {
username: user.username,
userId: user.userId,
displayName: user.displayName,
serverUrl: protocol + host,
httpHeaders
}
}

View File

@@ -8,13 +8,14 @@ export * from './generateAuthCode'
export * from './generateRefreshToken' export * from './generateRefreshToken'
export * from './getCertificates' export * from './getCertificates'
export * from './getDesktopFields' export * from './getDesktopFields'
export * from './getPreProgramVariables'
export * from './isDebugOn' export * from './isDebugOn'
export * from './parseLogToArray' export * from './parseLogToArray'
export * from './removeTokensInDB' export * from './removeTokensInDB'
export * from './saveTokensInDB' export * from './saveTokensInDB'
export * from './seedDB'
export * from './setProcessVariables' export * from './setProcessVariables'
export * from './setupFolders' export * from './setupFolders'
export * from './sleep'
export * from './upload' export * from './upload'
export * from './validation' export * from './validation'
export * from './verifyTokenInDB' export * from './verifyTokenInDB'

View File

@@ -0,0 +1,33 @@
import path from 'path'
import fs from 'fs'
export const getEnvCSPDirectives = (
HELMET_CSP_CONFIG_PATH: string | undefined
) => {
let cspConfigJson = {
'script-src': ["'self'", "'unsafe-inline'"]
}
if (
typeof HELMET_CSP_CONFIG_PATH === 'string' &&
HELMET_CSP_CONFIG_PATH.length > 0
) {
const cspConfigPath = path.join(process.cwd(), HELMET_CSP_CONFIG_PATH)
try {
let file = fs.readFileSync(cspConfigPath).toString()
try {
cspConfigJson = JSON.parse(file)
} catch (e) {
console.error(
'Parsing Content Security Policy JSON config failed. Make sure it is valid json'
)
}
} catch (e) {
console.error('Error reading HELMET CSP config file', e)
}
}
return cspConfigJson
}

35
api/src/utils/seedDB.ts Normal file
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 path from 'path'
import { getRealPath } from '@sasjs/utils' import { getAbsolutePath, getRealPath } from '@sasjs/utils'
import { configuration } from '../../package.json' import { configuration } from '../../package.json'
import { getDesktopFields } from '.' import { getDesktopFields } from '.'
@@ -12,18 +12,17 @@ export const setProcessVariables = async () => {
const { MODE } = process.env 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() const { sasLoc, driveLoc } = await getDesktopFields()
process.sasLoc = sasLoc process.sasLoc = sasLoc
process.driveLoc = driveLoc process.driveLoc = driveLoc
} else {
const { SAS_PATH, DRIVE_PATH } = process.env
process.sasLoc = SAS_PATH ?? configuration.sasPath
process.driveLoc = getRealPath(
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
)
} }
console.log('sasLoc: ', process.sasLoc) console.log('sasLoc: ', process.sasLoc)

View File

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

View File

@@ -1,10 +1,16 @@
import Joi from 'joi' 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) const passwordSchema = Joi.string().min(6).max(1024)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i export const blockFileRegex = /\.(exe|sh|htaccess)$/i
export const loginWebValidation = (data: any): Joi.ValidationResult =>
Joi.object({
username: usernameSchema.required(),
password: passwordSchema.required()
}).validate(data)
export const authorizeValidation = (data: any): Joi.ValidationResult => export const authorizeValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
username: usernameSchema.required(), username: usernameSchema.required(),

View File

@@ -11,6 +11,10 @@
} }
}, },
"tags": [ "tags": [
{
"name": "Info",
"description": "Get Server Info"
},
{ {
"name": "Session", "name": "Session",
"description": "Get Session information" "description": "Get Session information"
@@ -42,6 +46,10 @@
{ {
"name": "CODE", "name": "CODE",
"description": "Operations on SAS code" "description": "Operations on SAS code"
},
{
"name": "Web",
"description": "Operations on Web"
} }
], ],
"yaml": true, "yaml": true,

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "server", "name": "server",
"version": "0.0.48", "version": "0.0.73",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "server", "name": "server",
"version": "0.0.48", "version": "0.0.73",
"devDependencies": { "devDependencies": {
"prettier": "^2.3.1", "prettier": "^2.3.1",
"standard-version": "^9.3.2" "standard-version": "^9.3.2"

View File

@@ -1,12 +1,12 @@
{ {
"name": "server", "name": "server",
"version": "0.0.48", "version": "0.0.73",
"description": "NodeJS wrapper for calling the SAS binary executable", "description": "NodeJS wrapper for calling the SAS binary executable",
"repository": "https://github.com/sasjs/server", "repository": "https://github.com/sasjs/server",
"scripts": { "scripts": {
"server": "npm run server:prepare && npm run server:start", "server": "npm run server:prepare && npm run server:start",
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..", "server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
"server:start": "cd api && npm run start", "server:start": "cd api && npm run start:prod",
"release": "standard-version", "release": "standard-version",
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", "lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", "lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",

View File

@@ -1,2 +1,3 @@
### Get current user's info via access token ### Get current user's info via session ID
GET http://localhost:5000/SASjsApi/session GET http://localhost:5000/SASjsApi/session
cookie: connect.sid=s:G2DeFdKuWhnmTOsTHmTWrxAXPx2P6TLD.JyNLxfACC1w3NlFQFfL5chyxtrqbPYmS6iButRc1goE

View File

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

385
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",
"@monaco-editor/react": "^4.3.1",
"@mui/icons-material": "^5.0.3", "@mui/icons-material": "^5.0.3",
"@mui/lab": "^5.0.0-alpha.50", "@mui/lab": "^5.0.0-alpha.50",
"@mui/material": "^5.0.3", "@mui/material": "^5.0.3",
@@ -21,8 +20,11 @@
"@types/node": "^12.20.28", "@types/node": "^12.20.28",
"@types/react": "^17.0.27", "@types/react": "^17.0.27",
"axios": "^0.24.0", "axios": "^0.24.0",
"monaco-editor": "^0.33.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-monaco-editor": "^0.48.0",
"react-router-dom": "^5.3.0" "react-router-dom": "^5.3.0"
}, },
"devDependencies": { "devDependencies": {

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 { Route, HashRouter, Switch } from 'react-router-dom'
import { ThemeProvider } from '@mui/material/styles' import { ThemeProvider } from '@mui/material/styles'
import { theme } from './theme' import { theme } from './theme'
@@ -9,12 +9,12 @@ import Home from './components/home'
import Drive from './containers/Drive' import Drive from './containers/Drive'
import Studio from './containers/Studio' import Studio from './containers/Studio'
import useTokens from './components/useTokens' import { AppContext } from './context/appContext'
function App() { function App() {
const { tokens, setTokens } = useTokens() const appContext = useContext(AppContext)
if (!tokens) { if (!appContext.loggedIn) {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<HashRouter> <HashRouter>
@@ -24,7 +24,7 @@ function App() {
<Login getCodeOnly /> <Login getCodeOnly />
</Route> </Route>
<Route path="/"> <Route path="/">
<Login setTokens={setTokens} /> <Login />
</Route> </Route>
</Switch> </Switch>
</HashRouter> </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 { Link, useHistory, useLocation } from 'react-router-dom'
import AppBar from '@mui/material/AppBar' import {
import Toolbar from '@mui/material/Toolbar' AppBar,
import Tabs from '@mui/material/Tabs' Toolbar,
import Tab from '@mui/material/Tab' Tabs,
import Button from '@mui/material/Button' Tab,
Button,
Menu,
MenuItem
} from '@mui/material'
import OpenInNewIcon from '@mui/icons-material/OpenInNew' import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import Username from './username'
import { AppContext } from '../context/appContext'
const NODE_ENV = process.env.NODE_ENV const NODE_ENV = process.env.NODE_ENV
const PORT_API = process.env.PORT_API const PORT_API = process.env.PORT_API
const baseUrl = const baseUrl =
@@ -16,11 +23,29 @@ const baseUrl =
const Header = (props: any) => { const Header = (props: any) => {
const history = useHistory() const history = useHistory()
const { pathname } = useLocation() const { pathname } = useLocation()
const appContext = useContext(AppContext)
const [tabValue, setTabValue] = useState(pathname) 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) => { const handleTabChange = (event: React.SyntheticEvent, value: string) => {
setTabValue(value) setTabValue(value)
} }
const handleLogout = () => {
if (appContext.logout) appContext.logout()
}
return ( return (
<AppBar <AppBar
position="fixed" position="fixed"
@@ -28,10 +53,10 @@ const Header = (props: any) => {
> >
<Toolbar variant="dense"> <Toolbar variant="dense">
<img <img
src="logo-white.png" src="logo.png"
alt="logo" alt="logo"
style={{ style={{
width: '50px', width: '35px',
cursor: 'pointer', cursor: 'pointer',
marginRight: '25px' marginRight: '25px'
}} }}
@@ -81,6 +106,39 @@ const Header = (props: any) => {
> >
App Stream App Stream
</Button> </Button>
<div
style={{
display: 'flex',
flexGrow: 1,
justifyContent: 'flex-end'
}}
>
<Username
username={appContext.displayName || appContext.username}
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> </Toolbar>
</AppBar> </AppBar>
) )

View File

@@ -1,41 +1,21 @@
import React, { useState } from 'react' import axios from 'axios'
import React, { useState, useContext } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material' import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
import { AppContext } from '../context/appContext'
const headers = { const getAuthCode = async (credentials: any) =>
Accept: 'application/json', axios.post('/SASjsApi/auth/authorize', credentials).then((res) => res.data)
'Content-Type': 'application/json'
}
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 getAuthCode = async (credentials: any) => { const login = async (payload: { username: string; password: string }) =>
return fetch(`${baseUrl}/SASjsApi/auth/authorize`, { axios.post('/login', payload).then((res) => res.data)
method: 'POST',
headers,
body: JSON.stringify(credentials)
}).then(async (response) => {
const resText = await response.text()
if (response.status !== 200) throw resText
return JSON.parse(resText) const Login = ({ getCodeOnly }: any) => {
})
}
const getTokens = async (payload: any) => {
return fetch(`${baseUrl}/SASjsApi/auth/token`, {
method: 'POST',
headers,
body: JSON.stringify(payload)
}).then((data) => data.json())
}
const Login = ({ setTokens, getCodeOnly }: any) => {
const location = useLocation() const location = useLocation()
const [username, setUserName] = useState('') const appContext = useContext(AppContext)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState('') const [errorMessage, setErrorMessage] = useState('')
let error: boolean let error: boolean
@@ -45,34 +25,40 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
error = false error = false
setErrorMessage('') setErrorMessage('')
e.preventDefault() e.preventDefault()
let clientId = process.env.CLIENT_ID
if (getCodeOnly) { if (getCodeOnly) {
const params = new URLSearchParams(location.search) const params = new URLSearchParams(location.search)
const responseType = params.get('response_type') const responseType = params.get('response_type')
if (responseType === 'code') if (responseType === 'code') {
clientId = params.get('client_id') ?? undefined const clientId = params.get('client_id')
const { code } = await getAuthCode({
clientId,
username,
password
}).catch((err: any) => {
error = true
setErrorMessage(err.response.data)
return {}
})
if (!error) return setDisplayCode(code)
return
}
} }
const { code } = await getAuthCode({ const { loggedIn, user } = await login({
clientId,
username, username,
password password
}).catch((err: string) => { }).catch((err: any) => {
error = true error = true
setErrorMessage(err) setErrorMessage(err.response.data)
return {} return {}
}) })
if (!error) { if (loggedIn) {
if (getCodeOnly) return setDisplayCode(code) appContext.setLoggedIn?.(loggedIn)
appContext.setUsername?.(user.username)
const { accessToken, refreshToken } = await getTokens({ appContext.setDisplayName?.(user.displayName)
clientId,
code
})
setTokens(accessToken, refreshToken)
} }
} }
@@ -115,7 +101,7 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
label="Username" label="Username"
type="text" type="text"
variant="outlined" variant="outlined"
onChange={(e: any) => setUserName(e.target.value)} onChange={(e: any) => setUsername(e.target.value)}
required required
/> />
<TextField <TextField
@@ -127,7 +113,11 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
required required
/> />
{errorMessage && <span>{errorMessage}</span>} {errorMessage && <span>{errorMessage}</span>}
<Button type="submit" variant="outlined"> <Button
type="submit"
variant="outlined"
disabled={!appContext.setLoggedIn}
>
Submit Submit
</Button> </Button>
</Box> </Box>
@@ -135,7 +125,6 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
} }
Login.propTypes = { Login.propTypes = {
setTokens: PropTypes.func,
getCodeOnly: PropTypes.bool getCodeOnly: PropTypes.bool
} }

View File

@@ -1,94 +0,0 @@
import axios from 'axios'
import { useEffect, useState } from 'react'
export default function useTokens() {
const getTokens = () => {
const accessToken = localStorage.getItem('accessToken')
const refreshToken = localStorage.getItem('refreshToken')
if (accessToken && refreshToken) {
setAxiosRequestHeader(accessToken)
return { accessToken, refreshToken }
}
return undefined
}
const [tokens, setTokens] = useState(getTokens())
useEffect(() => {
if (tokens === undefined) {
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')
}
}, [tokens])
setAxiosResponse(setTokens)
const saveTokens = (accessToken: string, refreshToken: string) => {
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
setAxiosRequestHeader(accessToken)
setTokens({ accessToken, refreshToken })
}
return {
setTokens: saveTokens,
tokens
}
}
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
}
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)
// }
setTokens(undefined)
}
return Promise.reject(error)
}
)
}
// const refreshMyToken = async (refreshToken: string) => {
// return fetch('http://localhost:5000/SASjsApi/auth/refresh', {
// method: 'POST',
// headers: {
// Authorization: `Bearer ${refreshToken}`
// }
// }).then((data) => data.json())
// }

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 ( return (
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<CssBaseline /> <CssBaseline />
<SideBar directoryData={directoryData} handleSelect={handleSelect} /> <SideBar
selectedFilePath={selectedFilePath}
directoryData={directoryData}
handleSelect={handleSelect}
/>
<Main <Main
selectedFilePath={selectedFilePath} selectedFilePath={selectedFilePath}
removeFileFromTree={removeFileFromTree} removeFileFromTree={removeFileFromTree}

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import axios from 'axios' import axios from 'axios'
import Editor from '@monaco-editor/react' import Editor from 'react-monaco-editor'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Paper from '@mui/material/Paper' import Paper from '@mui/material/Paper'
@@ -125,6 +125,7 @@ const Main = (props: Props) => {
{!isLoading && props?.selectedFilePath && editMode && ( {!isLoading && props?.selectedFilePath && editMode && (
<Editor <Editor
height="95%" height="95%"
language="sas"
value={fileContent} value={fileContent}
onChange={(val) => { onChange={(val) => {
if (val) setFileContent(val) if (val) setFileContent(val)

View File

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

View File

@@ -2,9 +2,9 @@ import React, { useEffect, useRef, useState } from 'react'
import axios from 'axios' import axios from 'axios'
import Box from '@mui/material/Box' 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 { makeStyles } from '@mui/styles'
import Editor, { OnMount } from '@monaco-editor/react' import Editor, { EditorDidMount } from 'react-monaco-editor'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { TabContext, TabList, TabPanel } from '@mui/lab' import { TabContext, TabList, TabPanel } from '@mui/lab'
@@ -15,6 +15,17 @@ const useStyles = makeStyles(() => ({
'&.Mui-selected': { '&.Mui-selected': {
color: 'black' color: 'black'
} }
},
subMenu: {
marginTop: '25px',
display: 'flex',
justifyContent: 'center'
},
runButton: {
display: 'flex',
alignItems: 'center',
padding: '5px 5px',
minWidth: 'unset'
} }
})) }))
@@ -22,14 +33,16 @@ const Studio = () => {
const location = useLocation() const location = useLocation()
const [fileContent, setFileContent] = useState('') const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('') const [log, setLog] = useState('')
const [ctrlPressed, setCtrlPressed] = useState(false)
const [webout, setWebout] = useState('') const [webout, setWebout] = useState('')
const [tab, setTab] = React.useState('1') const [tab, setTab] = React.useState('1')
const handleTabChange = (_e: any, newValue: string) => { const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue) setTab(newValue)
} }
const editorRef = useRef(null as any) const editorRef = useRef(null as any)
const handleEditorDidMount: OnMount = (editor) => { const handleEditorDidMount: EditorDidMount = (editor) => {
editor.focus() editor.focus()
editorRef.current = editor editorRef.current = editor
} }
@@ -61,6 +74,21 @@ const Studio = () => {
.catch((err) => console.log(err)) .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(() => { useEffect(() => {
const content = localStorage.getItem('fileContent') ?? '' const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content) setFileContent(content)
@@ -86,70 +114,89 @@ const Studio = () => {
const classes = useStyles() const classes = useStyles()
return ( return (
<> <Box
<br /> onKeyUp={handleKeyUp}
<br /> onKeyDown={handleKeyDown}
<br /> sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
<Box sx={{ width: '100%', typography: 'body1' }}> >
<TabContext value={tab}> <TabContext value={tab}>
<Box <Box
sx={{ sx={{
borderBottom: 1, borderBottom: 1,
borderColor: 'divider' borderColor: 'divider'
}} }}
style={{ position: 'fixed', background: 'white', width: '100%' }} style={{ position: 'fixed', background: 'white', width: '100%' }}
> >
<TabList onChange={handleTabChange} centered> <TabList onChange={handleTabChange} centered>
<Tab className={classes.root} label="Code" value="1" /> <Tab className={classes.root} label="Code" value="1" />
<Tab className={classes.root} label="Log" value="2" /> <Tab className={classes.root} label="Log" value="2" />
<Tooltip title="Displays content from the _webout fileref">
<Tab className={classes.root} label="Webout" value="3" /> <Tab className={classes.root} label="Webout" value="3" />
</TabList> </Tooltip>
</Box> </TabList>
<TabPanel value="1"> </Box>
{/* <Toolbar /> */}
<Paper <TabPanel style={{ paddingBottom: 0 }} value="1">
sx={{ <div className={classes.subMenu}>
height: '70vh', <Tooltip title="CTRL+ENTER will also run SAS code">
marginTop: '50px', <Button onClick={handleRunBtnClick} className={classes.runButton}>
padding: '10px', <img
overflow: 'auto', alt=""
position: 'relative' draggable="false"
}} style={{ width: '25px' }}
elevation={3} src="/running-sas.png"
> ></img>
<Editor <span style={{ fontSize: '12px' }}>RUN</span>
height="95%"
value={fileContent}
onMount={handleEditorDidMount}
onChange={(val) => {
if (val) setFileContent(val)
}}
/>
</Paper>
<Stack
spacing={3}
direction="row"
sx={{ justifyContent: 'center', marginTop: '20px' }}
>
<Button variant="contained" onClick={handleRunBtnClick}>
Run SAS Code
</Button> </Button>
</Stack> </Tooltip>
</TabPanel> </div>
<TabPanel value="2"> {/* <Toolbar /> */}
<div style={{ marginTop: '50px' }}> <Paper
<h2>SAS Log</h2> sx={{
<pre>{log}</pre> height: 'calc(100vh - 170px)',
</div> padding: '10px',
</TabPanel> overflow: 'auto',
<TabPanel value="3"> position: 'relative'
<div style={{ marginTop: '50px' }}> }}
<pre>{webout}</pre> elevation={3}
</div> >
</TabPanel> <Editor
</TabContext> height="98%"
</Box> language="sas"
</> value={fileContent}
editorDidMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => {
if (val) setFileContent(val)
}}
/>
<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,85 @@
import React, {
createContext,
Dispatch,
SetStateAction,
useState,
useEffect,
useCallback,
ReactNode
} from 'react'
import axios from 'axios'
interface AppContextProps {
checkingSession: boolean
loggedIn: boolean
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
username: string
setUsername: Dispatch<SetStateAction<string>> | null
displayName: string
setDisplayName: Dispatch<SetStateAction<string>> | null
logout: (() => void) | null
}
export const AppContext = createContext<AppContextProps>({
checkingSession: false,
loggedIn: false,
setLoggedIn: null,
username: '',
setUsername: null,
displayName: '',
setDisplayName: null,
logout: null
})
const AppContextProvider = (props: { children: ReactNode }) => {
const { children } = props
const [checkingSession, setCheckingSession] = useState(false)
const [loggedIn, setLoggedIn] = useState(false)
const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('')
useEffect(() => {
setCheckingSession(true)
axios
.get('/SASjsApi/session')
.then((res) => res.data)
.then((data: any) => {
setCheckingSession(false)
setLoggedIn(true)
setUsername(data.username)
setDisplayName(data.displayName)
})
.catch(() => {
setLoggedIn(false)
axios.get('/') // get CSRF TOKEN
})
}, [])
const logout = useCallback(() => {
axios.get('/logout').then(() => {
setLoggedIn(false)
setUsername('')
setDisplayName('')
})
}, [])
return (
<AppContext.Provider
value={{
checkingSession,
loggedIn,
setLoggedIn,
username,
setUsername,
displayName,
setDisplayName,
logout
}}
>
{children}
</AppContext.Provider>
)
}
export default AppContextProvider

View File

@@ -2,10 +2,25 @@ import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import './index.css' import './index.css'
import App from './App' import App from './App'
import AppContextProvider from './context/appContext'
import axios from 'axios'
const NODE_ENV = process.env.NODE_ENV
const PORT_API = process.env.PORT_API
const baseUrl =
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
axios.defaults = Object.assign(axios.defaults, {
withCredentials: true,
baseURL: baseUrl
})
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App /> <AppContextProvider>
<App />
</AppContextProvider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
) )

View File

@@ -1,4 +1,5 @@
import path from 'path' import path from 'path'
import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'
import { Configuration } from 'webpack' import { Configuration } from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin' import HtmlWebpackPlugin from 'html-webpack-plugin'
import CopyPlugin from 'copy-webpack-plugin' import CopyPlugin from 'copy-webpack-plugin'
@@ -53,7 +54,8 @@ const config: Configuration = {
new CopyPlugin({ new CopyPlugin({
patterns: [{ from: 'public' }] patterns: [{ from: 'public' }]
}), }),
new dotenv() new dotenv(),
new MonacoWebpackPlugin()
] ]
} }