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

Compare commits

..

49 Commits

Author SHA1 Message Date
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
40 changed files with 1113 additions and 375 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 ../

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

View File

@@ -2,6 +2,69 @@
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.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) ### [0.0.55](https://github.com/sasjs/server/compare/v0.0.53...v0.0.55) (2022-04-20)

View File

@@ -84,6 +84,7 @@ 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
# SAS Options # SAS Options
@@ -98,7 +99,20 @@ SASV9_OPTIONS= -NOXCMD
## 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
@@ -132,7 +146,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,17 @@
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
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

473
api/package-lock.json generated
View File

@@ -8,12 +8,16 @@
"name": "api", "name": "api",
"version": "0.0.2", "version": "0.0.2",
"dependencies": { "dependencies": {
"@sasjs/core": "4.9.0", "@sasjs/core": "^4.19.0",
"@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",
@@ -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.19.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ==" "integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
}, },
"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",
@@ -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.19.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ==" "integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
}, },
"@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",
@@ -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,12 +47,16 @@
}, },
"author": "4GL Ltd", "author": "4GL Ltd",
"dependencies": { "dependencies": {
"@sasjs/core": "4.9.0", "@sasjs/core": "^4.19.0",
"@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",
@@ -64,7 +69,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",
@@ -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

@@ -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:
@@ -450,6 +465,47 @@ info:
name: '4GL Ltd' name: '4GL Ltd'
openapi: 3.0.0 openapi: 3.0.0
paths: paths:
/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
@@ -1308,3 +1364,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,
@@ -18,8 +22,30 @@ 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 } = process.env
export const cookieOptions = {
secure: PROTOCOL === 'https',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
/***********************************
* CSRF Protection *
***********************************/
export const csrfProtection = csrf({ cookie: cookieOptions })
/***********************************
* Handle security and origin *
***********************************/
app.use(helmet())
/***********************************
* Enabling CORS *
***********************************/
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') { if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
const whiteList: string[] = [] const whiteList: string[] = []
WHITELIST?.split(' ') WHITELIST?.split(' ')
@@ -34,12 +60,35 @@ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
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!')
} }
@@ -61,6 +110,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

@@ -25,9 +25,8 @@ export class InfoController {
const response = { const response = {
mode: process.env.MODE ?? 'desktop', mode: process.env.MODE ?? 'desktop',
cors: cors:
process.env.CORS ?? process.env.MODE === 'server' process.env.CORS ||
? 'disable' (process.env.MODE === 'server' ? 'disable' : 'enable'),
: 'enable',
whiteList: whiteList:
process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [], process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [],
protocol: process.env.PROTOCOL ?? 'http' protocol: process.env.PROTOCOL ?? 'http'

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

@@ -0,0 +1,75 @@
import express from 'express'
import { Request, Route, Tags, Post, Body, Get } from 'tsoa'
import User from '../model/User'
@Route('/')
@Tags('Web')
export class WebController {
/**
* @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 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

@@ -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,
@@ -42,7 +41,6 @@ describe('auth', () => {
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 () => {
@@ -154,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

@@ -5,7 +5,6 @@ import apiRouter from './api'
import appStreamRouter from './appStream' import appStreamRouter from './appStream'
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', function (req, res, next) {
@@ -13,4 +12,6 @@ export const setupRoutes = (app: Express) => {
// whatever the current router is // whatever the current router is
appStreamRouter(req, res, next) appStreamRouter(req, res, next)
}) })
app.use('/', webRouter)
} }

View File

@@ -1,8 +1,11 @@
import express from 'express' import express from 'express'
import { csrfProtection } from '../../app'
import webRouter from './web' import webRouter from './web'
const router = express.Router() const router = express.Router()
router.use(csrfProtection)
router.use('/', webRouter) router.use('/', webRouter)
export default router export default router

View File

@@ -1,37 +1,47 @@
import { readFile } from '@sasjs/utils'
import express from 'express'
import path from 'path' import path from 'path'
import { getWebBuildFolderPath } from '../../utils' import express from 'express'
import { readFile } from '@sasjs/utils'
import { WebController } from '../../controllers/web'
import { getWebBuildFolderPath, loginWebValidation } from '../../utils'
const webRouter = express.Router() const webRouter = express.Router()
const jsCodeForDesktopMode = ` webRouter.get('/', async (req, res) => {
<script> const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
</script>`
const jsCodeForServerMode = `
<script>
localStorage.setItem('CLIENT_ID', '${process.env.CLIENT_ID}')
</script>`
webRouter.get('/', async (_, res) => {
let content: string
try { try {
const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html') // Attention! Cannot use fileExists here, due to limitation after building executable
content = await readFile(indexHtmlPath) const content = await readFile(indexHtmlPath)
res.cookie('XSRF-TOKEN', req.csrfToken())
res.setHeader('Content-Type', 'text/html')
return res.send(content)
} 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) => {
const codeToInject = const { error, value: body } = loginWebValidation(req.body)
MODE?.trim() === 'server' ? jsCodeForServerMode : jsCodeForDesktopMode if (error) return res.status(400).send(error.details[0].message)
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
res.setHeader('Content-Type', 'text/html') const controller = new WebController()
return res.send(injectedContent) try {
const response = await controller.login(req, body)
res.send(response)
} catch (err: any) {
res.status(400).send(err.toString())
}
})
webRouter.get('/logout', async (req, res) => {
const controller = new WebController()
try {
await controller.logout(req)
res.status(200).send()
} catch (err: any) {
res.status(400).send(err.toString())
}
}) })
export default webRouter export default webRouter

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,28 +1,15 @@
import mongoose from 'mongoose' import mongoose from 'mongoose'
import { populateClients } from '../routes/api/auth'
import { seedDB } from './seedDB' 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!')
} }
const { MODE } = process.env console.log('Connected to DB!')
await seedDB()
if (MODE?.trim() !== 'server') { return mongoose.connection
console.log('Running in Destop Mode, no DB to connect.')
return
}
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err
console.log('Connected to db!')
await seedDB()
await populateClients()
})
} }

View File

@@ -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

@@ -46,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.55", "version": "0.0.64",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "server", "name": "server",
"version": "0.0.55", "version": "0.0.64",
"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.55", "version": "0.0.64",
"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 @@
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>

12
web/package-lock.json generated
View File

@@ -4191,9 +4191,9 @@
"dev": true "dev": true
}, },
"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==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lodash": "^4.17.14" "lodash": "^4.17.14"
@@ -14381,9 +14381,9 @@
"dev": true "dev": true
}, },
"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==",
"dev": true, "dev": true,
"requires": { "requires": {
"lodash": "^4.17.14" "lodash": "^4.17.14"

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"
@@ -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,33 +25,40 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
error = false error = false
setErrorMessage('') setErrorMessage('')
e.preventDefault() e.preventDefault()
let clientId = process.env.CLIENT_ID ?? localStorage.getItem('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') clientId = params.get('client_id') if (responseType === 'code') {
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)
} }
} }
@@ -114,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
@@ -126,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>
@@ -134,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

@@ -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')
) )