1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-12 20:04:36 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
cc6f8a64b5 fix(web-header): show users display name instead of username 2022-04-27 22:43:23 +05:00
62 changed files with 1137 additions and 1796 deletions

View File

@@ -54,7 +54,6 @@ jobs:
ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}} ACCESS_TOKEN_SECRET: ${{secrets.ACCESS_TOKEN_SECRET}}
REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}} REFRESH_TOKEN_SECRET: ${{secrets.REFRESH_TOKEN_SECRET}}
AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}} AUTH_CODE_SECRET: ${{secrets.AUTH_CODE_SECRET}}
SESSION_SECRET: ${{secrets.SESSION_SECRET}}
- name: Build Package - name: Build Package
working-directory: ./api working-directory: ./api

View File

@@ -8,20 +8,10 @@ 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,4 +11,3 @@ sasjscore/
certificates/ certificates/
executables/ executables/
.env .env
api/csp.config.json

View File

@@ -2,128 +2,6 @@
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.75](https://github.com/sasjs/server/compare/v0.0.69...v0.0.75) (2022-05-12)
### Features
* CSP_DISABLE env option ([dd3acce](https://github.com/sasjs/server/commit/dd3acce3935e7cfc0b2c44a401314306915a3a10))
### 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))
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
* helmet config on http mode ([b0fdaaa](https://github.com/sasjs/server/commit/b0fdaaaa79e3135699c51effac0388d8ec5ab23b))
* moved getAuthCode from api to web routes ([b40de8f](https://github.com/sasjs/server/commit/b40de8fa6a5aa763ed25a6fe6a381e483e0ab824))
* reqHeadrs.txt will contain headers to access APIs ([636301e](https://github.com/sasjs/server/commit/636301e664416fb085f704d83deb7f39ee0a91a7))
* **web:** seperate container for auth code ([5888f04](https://github.com/sasjs/server/commit/5888f04e08a32c6d2c7bcfcbc3a1d32425bff3b3))
### [0.0.74](https://github.com/sasjs/server/compare/v0.0.73...v0.0.74) (2022-05-12)
### Bug Fixes
* csp updates ([7cfa239](https://github.com/sasjs/server/commit/7cfa2398e12c5e515d27c896f36ff91604c2124d))
### [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) ### [0.0.58](https://github.com/sasjs/server/compare/v0.0.57...v0.0.58) (2022-04-24)

View File

@@ -48,22 +48,15 @@ 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=
# Path to SAS executable (sas.exe / sas.sh) # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
SAS_PATH=/path/to/sas/executable.exe # If enabled, be sure to also configure the WHITELIST of third party servers.
CORS=
# Path to working directory # options: <http://localhost:3000 https://abc.com ...> space separated urls
# This location is for SAS WORK, staged files, DRIVE, configuration etc WHITELIST=
DRIVE_PATH=/tmp
# options: [http|https] default: http # options: [http|https] default: http
PROTOCOL= PROTOCOL=
@@ -72,22 +65,16 @@ PROTOCOL=
PORT= PORT=
# # optional
## Additional SAS Options # for MODE: `desktop`, prompts user
# # for MODE: `server` gets value from api/package.json `configuration.sasPath`
SAS_PATH=/path/to/sas/executable.exe
# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS # optional
# Any options set here are automatically applied in the SAS session # for MODE: `desktop`, prompts user
# See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm # for MODE: `server` defaults to /tmp
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6 DRIVE_PATH=/tmp
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
@@ -97,33 +84,15 @@ 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` # SAS Options
# If enabled, be sure to also configure the WHITELIST of third party servers. # On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
CORS= # Any options set here are automatically applied in the SAS session
# See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm
# options: <http://localhost:3000 https://abc.com ...> space separated urls # And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
WHITELIST= SAS_OPTIONS= -NOXCMD
SASV9_OPTIONS= -NOXCMD
# 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'", "data:"],
# "script-src": ["'self'", "'unsafe-inline'"],
# "script-src-attr": ["'self'", "'unsafe-inline'"]
# }
HELMET_CSP_CONFIG_PATH=./csp.config.json
``` ```

View File

@@ -1,20 +1,13 @@
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

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

499
api/package-lock.json generated
View File

@@ -8,23 +8,19 @@
"name": "api", "name": "api",
"version": "0.0.2", "version": "0.0.2",
"dependencies": { "dependencies": {
"@sasjs/core": "^4.23.1", "@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",
"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.3.0" "swagger-ui-express": "^4.1.6"
}, },
"bin": { "bin": {
"api": "build/src/server.js" "api": "build/src/server.js"
@@ -33,9 +29,7 @@
"@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",
@@ -49,7 +43,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.6.0", "pkg": "5.5.2",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"supertest": "^6.1.3", "supertest": "^6.1.3",
@@ -1385,9 +1379,9 @@
} }
}, },
"node_modules/@sasjs/core": { "node_modules/@sasjs/core": {
"version": "4.23.1", "version": "4.19.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg==" "integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
}, },
"node_modules/@sasjs/utils": { "node_modules/@sasjs/utils": {
"version": "2.42.1", "version": "2.42.1",
@@ -1839,15 +1833,6 @@
"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",
@@ -1871,15 +1856,6 @@
"@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",
@@ -2471,17 +2447,6 @@
"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.4", "version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
@@ -2709,11 +2674,6 @@
"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",
@@ -2995,20 +2955,14 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001340", "version": "1.0.30001243",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
"integrity": "sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==", "integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
"dev": true, "dev": true,
"funding": [ "funding": {
{
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/browserslist" "url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
} }
]
}, },
"node_modules/chalk": { "node_modules/chalk": {
"version": "3.0.0", "version": "3.0.0",
@@ -3284,42 +3238,6 @@
"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",
@@ -3444,19 +3362,6 @@
"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",
@@ -3481,40 +3386,6 @@
"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",
@@ -4156,59 +4027,6 @@
"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",
@@ -4824,14 +4642,6 @@
"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",
@@ -7018,17 +6828,6 @@
"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",
@@ -7296,11 +7095,6 @@
"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",
@@ -7339,6 +7133,7 @@
"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",
@@ -8176,9 +7971,9 @@
} }
}, },
"node_modules/pkg": { "node_modules/pkg": {
"version": "5.6.0", "version": "5.5.2",
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.6.0.tgz", "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.5.2.tgz",
"integrity": "sha512-mHrAVSQWmHA41RnUmRpC7pK9lNnMfdA16CF3cqOI22a8LZxOQzF7M8YWtA2nfs+d7I0MTDXOtkDsAsFXeCpYjg==", "integrity": "sha512-pD0UB2ud01C6pVv2wpGsTYJrXI/bnvGRYvMLd44wFzA1p+A2jrlTGFPAYa7YEYzmitXhx23PqalaG1eUEnSwcA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "7.16.2", "@babel/parser": "7.16.2",
@@ -8190,7 +7985,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.3.0", "pkg-fetch": "3.2.6",
"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",
@@ -8222,9 +8017,9 @@
} }
}, },
"node_modules/pkg-fetch": { "node_modules/pkg-fetch": {
"version": "3.3.0", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.3.0.tgz", "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.2.6.tgz",
"integrity": "sha512-xJnIZ1KP+8rNN+VLafwu4tEeV4m8IkFBDdCFqmAJz9K1aiXEtbARmdbEe6HlXWGSVuShSHjFXpfkKRkDBQ5kiA==", "integrity": "sha512-Q8fx6SIT022g0cdSE4Axv/xpfHeltspo2gg1KsWRinLQZOTRRAtOOaEFghA1F3jJ8FVsh8hGrL/Pb6Ea5XHIFw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
@@ -8281,9 +8076,9 @@
} }
}, },
"node_modules/pkg-fetch/node_modules/semver": { "node_modules/pkg-fetch/node_modules/semver": {
"version": "7.3.7", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -8572,14 +8367,6 @@
} }
] ]
}, },
"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",
@@ -8760,11 +8547,6 @@
"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",
@@ -9440,11 +9222,11 @@
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ==" "integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
}, },
"node_modules/swagger-ui-express": { "node_modules/swagger-ui-express": {
"version": "4.3.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz",
"integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==", "integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==",
"dependencies": { "dependencies": {
"swagger-ui-dist": ">=4.1.3" "swagger-ui-dist": ">3.52.5"
}, },
"engines": { "engines": {
"node": ">= v0.10.32" "node": ">= v0.10.32"
@@ -9750,14 +9532,6 @@
"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",
@@ -9852,17 +9626,6 @@
"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",
@@ -11364,9 +11127,9 @@
} }
}, },
"@sasjs/core": { "@sasjs/core": {
"version": "4.23.1", "version": "4.19.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.23.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.19.0.tgz",
"integrity": "sha512-9d6yEPJRRvPLMUkpyaiQ62SXNMMyt2l815jxWgFjnVOxKeUQv9TPyZqZ0FpmWdVe6EY8dv8GLlyaBpOLDnY6Vg==" "integrity": "sha512-vG2YHJveQUQqN0YBhapXb8y+Qp4OniHzRedlqKRxyL0Pc+kwXx5co4Vo+dcOI5/MX0p+8oERP2aCR77s4FEUJg=="
}, },
"@sasjs/utils": { "@sasjs/utils": {
"version": "2.42.1", "version": "2.42.1",
@@ -11762,15 +11525,6 @@
"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",
@@ -11794,15 +11548,6 @@
"@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",
@@ -12314,17 +12059,6 @@
"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.4", "version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
@@ -12500,11 +12234,6 @@
} }
} }
}, },
"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",
@@ -12718,9 +12447,9 @@
"dev": true "dev": true
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001340", "version": "1.0.30001243",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001243.tgz",
"integrity": "sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw==", "integrity": "sha512-vNxw9mkTBtkmLFnJRv/2rhs1yufpDfCkBZexG3Y0xdOH2Z/eE/85E4Dl5j1YUN34nZVsSp6vVRFQRrez9wJMRA==",
"dev": true "dev": true
}, },
"chalk": { "chalk": {
@@ -12952,30 +12681,6 @@
"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",
@@ -13078,16 +12783,6 @@
"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",
@@ -13111,36 +12806,6 @@
} }
} }
}, },
"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",
@@ -13638,38 +13303,6 @@
"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",
@@ -14141,11 +13774,6 @@
"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",
@@ -15781,14 +15409,6 @@
"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",
@@ -15995,11 +15615,6 @@
"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",
@@ -16029,6 +15644,7 @@
"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",
@@ -16655,9 +16271,9 @@
} }
}, },
"pkg": { "pkg": {
"version": "5.6.0", "version": "5.5.2",
"resolved": "https://registry.npmjs.org/pkg/-/pkg-5.6.0.tgz", "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.5.2.tgz",
"integrity": "sha512-mHrAVSQWmHA41RnUmRpC7pK9lNnMfdA16CF3cqOI22a8LZxOQzF7M8YWtA2nfs+d7I0MTDXOtkDsAsFXeCpYjg==", "integrity": "sha512-pD0UB2ud01C6pVv2wpGsTYJrXI/bnvGRYvMLd44wFzA1p+A2jrlTGFPAYa7YEYzmitXhx23PqalaG1eUEnSwcA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/parser": "7.16.2", "@babel/parser": "7.16.2",
@@ -16669,7 +16285,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.3.0", "pkg-fetch": "3.2.6",
"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",
@@ -16726,9 +16342,9 @@
} }
}, },
"pkg-fetch": { "pkg-fetch": {
"version": "3.3.0", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.3.0.tgz", "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.2.6.tgz",
"integrity": "sha512-xJnIZ1KP+8rNN+VLafwu4tEeV4m8IkFBDdCFqmAJz9K1aiXEtbARmdbEe6HlXWGSVuShSHjFXpfkKRkDBQ5kiA==", "integrity": "sha512-Q8fx6SIT022g0cdSE4Axv/xpfHeltspo2gg1KsWRinLQZOTRRAtOOaEFghA1F3jJ8FVsh8hGrL/Pb6Ea5XHIFw==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
@@ -16770,9 +16386,9 @@
"dev": true "dev": true
}, },
"semver": { "semver": {
"version": "7.3.7", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
@@ -16939,11 +16555,6 @@
"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",
@@ -17081,11 +16692,6 @@
"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",
@@ -17607,11 +17213,11 @@
"integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ==" "integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
}, },
"swagger-ui-express": { "swagger-ui-express": {
"version": "4.3.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.2.0.tgz",
"integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==", "integrity": "sha512-znrHTwh9UpvsjqgWopA4noIet7mi7UGuIYZ465YfUDKQ5Dpas0jxnkfUKCo+0aB17YCBv26AhIjiQYDV4uvJFA==",
"requires": { "requires": {
"swagger-ui-dist": ">=4.1.3" "swagger-ui-dist": ">3.52.5"
} }
}, },
"symbol-tree": { "symbol-tree": {
@@ -17823,11 +17429,6 @@
"@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",
@@ -17894,14 +17495,6 @@
"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

@@ -47,31 +47,25 @@
}, },
"author": "4GL Ltd", "author": "4GL Ltd",
"dependencies": { "dependencies": {
"@sasjs/core": "^4.23.1", "@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",
"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.3.0" "swagger-ui-express": "^4.1.6"
}, },
"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",
@@ -85,7 +79,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.6.0", "pkg": "5.5.2",
"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

@@ -1,50 +0,0 @@
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

@@ -1,49 +0,0 @@
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
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,36 @@ components:
requestBodies: {} requestBodies: {}
responses: {} responses: {}
schemas: schemas:
AuthorizeResponse:
properties:
code:
type: string
description: 'Authorization code'
example: someRandomCryptoString
required:
- code
type: object
additionalProperties: false
AuthorizePayload:
properties:
username:
type: string
description: 'Username for user'
example: secretuser
password:
type: string
description: 'Password for user'
example: secretpassword
clientId:
type: string
description: 'Client ID'
example: clientID1
required:
- username
- password
- clientId
type: object
additionalProperties: false
TokenResponse: TokenResponse:
properties: properties:
accessToken: accessToken:
@@ -47,41 +77,6 @@ components:
- userId - userId
type: object type: object
additionalProperties: false additionalProperties: false
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:
properties:
code:
type: string
description: 'Authorization code'
example: someRandomCryptoString
required:
- code
type: object
additionalProperties: false
AuthorizePayload:
properties:
clientId:
type: string
description: 'Client ID'
example: clientID1
required:
- clientId
type: object
additionalProperties: false
ClientPayload: ClientPayload:
properties: properties:
clientId: clientId:
@@ -415,6 +410,14 @@ components:
- description - description
type: object type: object
additionalProperties: false additionalProperties: false
ExecuteReturnJsonPayload:
properties:
_program:
type: string
description: 'Location of SAS program'
example: /Public/somefolder/some.file
type: object
additionalProperties: false
InfoResponse: InfoResponse:
properties: properties:
mode: mode:
@@ -434,14 +437,6 @@ components:
- protocol - protocol
type: object type: object
additionalProperties: false additionalProperties: false
ExecuteReturnJsonPayload:
properties:
_program:
type: string
description: 'Location of SAS program'
example: /Public/somefolder/some.file
type: object
additionalProperties: false
securitySchemes: securitySchemes:
bearerAuth: bearerAuth:
type: http type: http
@@ -455,6 +450,30 @@ info:
name: '4GL Ltd' name: '4GL Ltd'
openapi: 3.0.0 openapi: 3.0.0
paths: paths:
/SASjsApi/auth/authorize:
post:
operationId: Authorize
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizeResponse'
examples:
'Example 1':
value: {code: someRandomCryptoString}
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
tags:
- Auth
security: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizePayload'
/SASjsApi/auth/token: /SASjsApi/auth/token:
post: post:
operationId: Token operationId: Token
@@ -512,86 +531,6 @@ paths:
- -
bearerAuth: [] bearerAuth: []
parameters: [] parameters: []
/:
get:
operationId: Home
responses:
'200':
description: Ok
content:
application/json:
schema:
type: string
summary: 'Render index.html'
tags:
- Web
security: []
parameters: []
/SASLogon/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'
/SASLogon/authorize:
post:
operationId: Authorize
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizeResponse'
examples:
'Example 1':
value: {code: someRandomCryptoString}
summary: 'Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE'
tags:
- Web
security: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizePayload'
/logout:
get:
operationId: Logout
responses:
'200':
description: Ok
content:
application/json:
schema: {}
summary: 'Accept a valid username/password'
tags:
- Web
security: []
parameters: []
/SASjsApi/client: /SASjsApi/client:
post: post:
operationId: CreateClient operationId: CreateClient
@@ -1238,24 +1177,6 @@ paths:
format: double format: double
type: number type: number
example: '6789' example: '6789'
/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: []
/SASjsApi/session: /SASjsApi/session:
get: get:
operationId: Session operationId: Session
@@ -1338,6 +1259,24 @@ 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: /
@@ -1369,6 +1308,3 @@ tags:
- -
name: CODE name: CODE
description: 'Operations on SAS code' description: 'Operations on SAS code'
-
name: Web
description: 'Operations on Web'

View File

@@ -1,13 +1,9 @@
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,
@@ -17,54 +13,13 @@ import {
setProcessVariables, setProcessVariables,
setupFolders setupFolders
} from './utils' } from './utils'
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
dotenv.config() dotenv.config()
const app = express() const app = express()
app.use(cookieParser()) const { MODE, CORS, WHITELIST } = process.env
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(' ') WHITELIST?.split(' ')
@@ -79,39 +34,12 @@ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
app.use(cors({ credentials: true, origin: whiteList })) app.use(cors({ credentials: true, origin: whiteList }))
} }
/*********************************** app.use(cookieParser())
* DB Connection & * app.use(morgan('tiny'))
* Express Sessions *
* With Mongo Store *
***********************************/
if (MODE?.trim() === 'server') {
let store: MongoStore | undefined
// 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)
store = MongoStore.create({ clientPromise, collectionName: 'sessions' })
}
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,
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!')
} }
@@ -133,5 +61,6 @@ export default setProcessVariables().then(async () => {
app.use(onError) app.use(onError)
await connectDB()
return app return app
}) })

View File

@@ -1,8 +1,10 @@
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 { InfoJWT } from '../types' import { InfoJWT } from '../types'
import { import {
generateAccessToken, generateAccessToken,
generateAuthCode,
generateRefreshToken, generateRefreshToken,
removeTokensInDB, removeTokensInDB,
saveTokensInDB saveTokensInDB
@@ -22,6 +24,20 @@ export class AuthController {
static deleteCode = (userId: number, clientId: string) => static deleteCode = (userId: number, clientId: string) =>
delete AuthController.authCodes[userId][clientId] delete AuthController.authCodes[userId][clientId]
/**
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
*
*/
@Example<AuthorizeResponse>({
code: 'someRandomCryptoString'
})
@Post('/authorize')
public async authorize(
@Body() body: AuthorizePayload
): Promise<AuthorizeResponse> {
return authorize(body)
}
/** /**
* @summary Accepts client/auth code and returns access/refresh tokens * @summary Accepts client/auth code and returns access/refresh tokens
* *
@@ -62,6 +78,30 @@ export class AuthController {
} }
} }
const authorize = async (data: any): Promise<AuthorizeResponse> => {
const { username, password, clientId } = data
// 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.')
// generate authorization code against clientId
const userInfo: InfoJWT = {
clientId,
userId: user.id
}
const code = AuthController.saveCode(
user.id,
clientId,
generateAuthCode(userInfo)
)
return { code }
}
const token = async (data: any): Promise<TokenResponse> => { const token = async (data: any): Promise<TokenResponse> => {
const { clientId, code } = data const { clientId, code } = data
@@ -99,6 +139,32 @@ const logout = async (userInfo: InfoJWT) => {
await removeTokensInDB(userInfo.userId, userInfo.clientId) await removeTokensInDB(userInfo.userId, userInfo.clientId)
} }
interface AuthorizePayload {
/**
* Username for user
* @example "secretuser"
*/
username: string
/**
* Password for user
* @example "secretpassword"
*/
password: string
/**
* Client ID
* @example "clientID1"
*/
clientId: string
}
interface AuthorizeResponse {
/**
* Authorization code
* @example "someRandomCryptoString"
*/
code: string
}
interface TokenPayload { interface TokenPayload {
/** /**
* Client ID * Client ID

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 { getPreProgramVariables, parseLogToArray } from '../utils' import { parseLogToArray } from '../utils'
interface ExecuteSASCodePayload { interface ExecuteSASCodePayload {
/** /**
@@ -56,3 +56,16 @@ 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

@@ -3,8 +3,7 @@ export * from './client'
export * from './code' export * from './code'
export * from './drive' export * from './drive'
export * from './group' export * from './group'
export * from './info'
export * from './session' export * from './session'
export * from './stp' export * from './stp'
export * from './user' export * from './user'
export * from './web' export * from './info'

View File

@@ -25,8 +25,9 @@ export class InfoController {
const response = { const response = {
mode: process.env.MODE ?? 'desktop', mode: process.env.MODE ?? 'desktop',
cors: cors:
process.env.CORS || process.env.CORS ?? process.env.MODE === 'server'
(process.env.MODE === 'server' ? 'disable' : 'enable'), ? 'disable'
: '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

@@ -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, 'reqHeaders.txt') const tokenFile = path.join(session.path, 'accessToken.txt')
await createFile(weboutPath, '') await createFile(weboutPath, '')
await createFile( await createFile(
tokenFile, tokenFile,
preProgramVariables?.httpHeaders.join('\n') ?? '' preProgramVariables?.accessToken ?? 'accessToken'
) )
const varStatments = Object.keys(vars).reduce( const varStatments = Object.keys(vars).reduce(

View File

@@ -24,7 +24,7 @@ export class SessionController {
} }
const session = (req: any) => ({ const session = (req: any) => ({
id: req.user.userId, id: req.user.id,
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,3 +210,16 @@ 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

@@ -1,156 +0,0 @@
import path from 'path'
import express from 'express'
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
import { readFile } from '@sasjs/utils'
import User from '../model/User'
import Client from '../model/Client'
import { getWebBuildFolderPath, generateAuthCode } from '../utils'
import { InfoJWT } from '../types'
import { AuthController } from './auth'
@Route('/')
@Tags('Web')
export class WebController {
/**
* @summary Render index.html
*
*/
@Get('/')
public async home() {
return home()
}
/**
* @summary Accept a valid username/password
*
*/
@Post('/SASLogon/login')
public async login(
@Request() req: express.Request,
@Body() body: LoginPayload
) {
return login(req, body)
}
/**
* @summary Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
*
*/
@Example<AuthorizeResponse>({
code: 'someRandomCryptoString'
})
@Post('/SASLogon/authorize')
public async authorize(
@Request() req: express.Request,
@Body() body: AuthorizePayload
): Promise<AuthorizeResponse> {
return authorize(req, body.clientId)
}
/**
* @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
}
}
}
const authorize = async (
req: express.Request,
clientId: string
): Promise<AuthorizeResponse> => {
const userId = req.session.user?.userId
if (!userId) throw new Error('Invalid userId.')
const client = await Client.findOne({ clientId })
if (!client) throw new Error('Invalid clientId.')
// generate authorization code against clientId
const userInfo: InfoJWT = {
clientId,
userId
}
const code = AuthController.saveCode(
userId,
clientId,
generateAuthCode(userInfo)
)
return { code }
}
interface LoginPayload {
/**
* Username for user
* @example "secretuser"
*/
username: string
/**
* Password for user
* @example "secretpassword"
*/
password: string
}
interface AuthorizePayload {
/**
* Client ID
* @example "clientID1"
*/
clientId: string
}
interface AuthorizeResponse {
/**
* Authorization code
* @example "someRandomCryptoString"
*/
code: string
}

View File

@@ -1,16 +1,7 @@
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,
@@ -52,7 +43,9 @@ const authenticateToken = (
} }
const authHeader = req.headers['authorization'] const authHeader = req.headers['authorization']
const token = authHeader?.split(' ')[1] const token =
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,26 +1,63 @@
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 { authorizeValidation, tokenValidation } from '../../utils' import {
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) => {
const { error, value: body } = authorizeValidation(req.body)
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 {
const response = await controller.authorize(body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
authRouter.post('/token', async (req, res) => { 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.send(response) res.cookie('accessToken', accessToken).send(response)
} catch (err: any) { } catch (err: any) {
res.status(403).send(err.toString()) res.status(403).send(err.toString())
} }
@@ -29,6 +66,7 @@ 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)
@@ -41,6 +79,7 @@ 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

@@ -36,22 +36,12 @@ 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

@@ -8,6 +8,7 @@ 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,
@@ -41,6 +42,7 @@ 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 () => {
@@ -49,6 +51,114 @@ describe('auth', () => {
await mongoServer.stop() await mongoServer.stop()
}) })
describe('authorize', () => {
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with authorization code', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: user.password,
clientId
})
.expect(200)
expect(res.body).toHaveProperty('code')
})
it('should respond with Bad Request if username is missing', async () => {
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
password: user.password,
clientId
})
.expect(400)
expect(res.text).toEqual(`"username" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if password is missing', async () => {
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
clientId
})
.expect(400)
expect(res.text).toEqual(`"password" is required`)
expect(res.body).toEqual({})
})
it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual(`"clientId" is required`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if username is incorrect', async () => {
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: user.password,
clientId
})
.expect(403)
expect(res.text).toEqual('Error: Username is not found.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if password is incorrect', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: 'WrongPassword',
clientId
})
.expect(403)
expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({})
})
it('should respond with Forbidden if clientId is incorrect', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASjsApi/auth/authorize')
.send({
username: user.username,
password: user.password,
clientId: 'WrongClientID'
})
.expect(403)
expect(res.text).toEqual('Invalid clientId.')
expect(res.body).toEqual({})
})
})
describe('token', () => { describe('token', () => {
const userInfo: InfoJWT = { const userInfo: InfoJWT = {
clientId, clientId,

View File

@@ -1,182 +0,0 @@
import { Express } from 'express'
import mongoose, { Mongoose } from 'mongoose'
import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest'
import appPromise from '../../../app'
import { UserController, ClientController } from '../../../controllers/'
const clientId = 'someclientID'
const clientSecret = 'someclientSecret'
const user = {
id: 1234,
displayName: 'Test User',
username: 'testUsername',
password: '87654321',
isAdmin: false,
isActive: true
}
describe('web', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
const userController = new UserController()
const clientController = new ClientController()
beforeAll(async () => {
app = await appPromise
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
await clientController.createClient({ clientId, clientSecret })
})
afterAll(async () => {
await con.connection.dropDatabase()
await con.connection.close()
await mongoServer.stop()
})
describe('home', () => {
it('should respond with CSRF Token', async () => {
await request(app)
.get('/')
.expect(
'set-cookie',
/_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\//
)
})
})
describe('SASLogon/login', () => {
let csrfToken: string
let cookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with successful login', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
password: user.password
})
.expect(200)
expect(res.body.loggedIn).toBeTruthy()
expect(res.body.user).toEqual({
username: user.username,
displayName: user.displayName
})
})
})
describe('SASLogon/authorize', () => {
let csrfToken: string
let cookies: string
let authCookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
await userController.createUser(user)
const credentials = {
username: user.username,
password: user.password
}
;({ cookies: authCookies } = await performLogin(
app,
credentials,
cookies,
csrfToken
))
})
afterAll(async () => {
const collections = mongoose.connection.collections
const collection = collections['users']
await collection.deleteMany({})
})
it('should respond with authorization code', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({ clientId })
expect(res.body).toHaveProperty('code')
})
it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({})
.expect(400)
expect(res.text).toEqual(`"clientId" is required`)
expect(res.body).toEqual({})
})
it('should respond with Forbidden if clientId is incorrect', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({
clientId: 'WrongClientID'
})
.expect(403)
expect(res.text).toEqual('Error: Invalid clientId.')
expect(res.body).toEqual({})
})
})
})
const getCSRF = async (app: Express) => {
// make request to get CSRF
const { header } = await request(app).get('/')
const cookies = header['set-cookie'].join()
console.log('cookies', cookies)
const csrfToken = extractCSRF(cookies)
return { csrfToken, cookies }
}
const performLogin = async (
app: Express,
credentials: { username: string; password: string },
cookies: string,
csrfToken: string
) => {
const { header } = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send(credentials)
const newCookies: string = header['set-cookie'].join()
return { cookies: newCookies }
}
const extractCSRF = (cookies: string) =>
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
cookies
)![2]

View File

@@ -1,4 +1,5 @@
import { AppStreamConfig } from '../../types' import { AppStreamConfig } from '../../types'
import { script } from './script'
import { style } from './style' import { style } from './style'
const defaultAppLogo = '/sasjs-logo.svg' const defaultAppLogo = '/sasjs-logo.svg'
@@ -38,7 +39,6 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
<span id="uploadMessage">Upload New App</span> <span id="uploadMessage">Upload New App</span>
</a> </a>
</div> </div>
<script src="/axios.min.js"></script> ${script}
<script src="/app-streams-script.js"></script>
</body> </body>
</html>` </html>`

View File

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

View File

@@ -4,16 +4,13 @@ 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', csrfProtection, function (req, res, next) { app.use('/AppStream', 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,59 +1,37 @@
import { readFile } from '@sasjs/utils'
import express from 'express' import express from 'express'
import { WebController } from '../../controllers/web' import path from 'path'
import { authenticateAccessToken } from '../../middlewares' import { getWebBuildFolderPath } from '../../utils'
import { authorizeValidation, loginWebValidation } from '../../utils'
const webRouter = express.Router() const webRouter = express.Router()
const controller = new WebController()
webRouter.get('/', async (req, res) => { const jsCodeForDesktopMode = `
let response <script>
localStorage.setItem('accessToken', JSON.stringify('accessToken'))
localStorage.setItem('refreshToken', JSON.stringify('refreshToken'))
</script>`
const jsCodeForServerMode = `
<script>
localStorage.setItem('CLIENT_ID', '${process.env.CLIENT_ID}')
</script>`
webRouter.get('/', async (_, res) => {
let content: string
try { try {
response = await controller.home() const indexHtmlPath = path.join(getWebBuildFolderPath(), 'index.html')
content = await readFile(indexHtmlPath)
} catch (_) { } catch (_) {
response = 'Web Build is not present' return res.send('Web Build is not present')
} finally {
res.cookie('XSRF-TOKEN', req.csrfToken())
return res.send(response)
} }
})
webRouter.post('/SASLogon/login', async (req, res) => { const { MODE } = process.env
const { error, value: body } = loginWebValidation(req.body) const codeToInject =
if (error) return res.status(400).send(error.details[0].message) MODE?.trim() === 'server' ? jsCodeForServerMode : jsCodeForDesktopMode
const injectedContent = content.replace('</head>', `${codeToInject}</head>`)
try { res.setHeader('Content-Type', 'text/html')
const response = await controller.login(req, body) return res.send(injectedContent)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
webRouter.post(
'/SASLogon/authorize',
authenticateAccessToken,
async (req, res) => {
const { error, value: body } = authorizeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.authorize(req, body)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
webRouter.get('/logout', async (req, res) => {
try {
await controller.logout(req)
res.status(200).send('OK!')
} catch (err: any) {
res.status(403).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
httpHeaders: string[] accessToken: string
} }

8
api/src/types/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
}
}

17
api/src/types/Request.ts Normal file
View File

@@ -0,0 +1,17 @@
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,5 +3,6 @@ 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

@@ -1,14 +0,0 @@
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
}
}
}

View File

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

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,15 +1,28 @@
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 () => {
try { // NOTE: when exporting app.js as agent for supertest
await mongoose.connect(process.env.DB_CONNECT as string) // we should exclude connecting to the real database
} catch (err) { if (process.env.NODE_ENV === 'test') {
throw new Error('Unable to connect to DB!') return
} }
console.log('Connected to DB!') const { MODE } = process.env
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
return
}
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err
console.log('Connected to db!')
await seedDB() await seedDB()
return mongoose.connection await populateClients()
})
} }

View File

@@ -1,29 +0,0 @@
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,7 +8,6 @@ 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'

View File

@@ -1,35 +0,0 @@
import path from 'path'
import fs from 'fs'
export const getEnvCSPDirectives = (
HELMET_CSP_CONFIG_PATH: string | undefined
) => {
let cspConfigJson = {
'img-src': ["'self'", 'data:'],
'script-src': ["'self'", "'unsafe-inline'"],
'script-src-attr': ["'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
}

View File

@@ -5,14 +5,10 @@ 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(),
password: passwordSchema.required(),
clientId: Joi.string().required() clientId: Joi.string().required()
}).validate(data) }).validate(data)

View File

@@ -46,10 +46,6 @@
{ {
"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.75", "version": "0.0.58",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "server", "name": "server",
"version": "0.0.75", "version": "0.0.58",
"devDependencies": { "devDependencies": {
"prettier": "^2.3.1", "prettier": "^2.3.1",
"standard-version": "^9.3.2" "standard-version": "^9.3.2"

View File

@@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.0.75", "version": "0.0.58",
"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": {

View File

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

View File

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

384
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
"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",
@@ -20,11 +21,9 @@
"@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", "jwt-decode": "3.1.2",
"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": {

View File

@@ -10,17 +10,19 @@ import Drive from './containers/Drive'
import Studio from './containers/Studio' import Studio from './containers/Studio'
import { AppContext } from './context/appContext' import { AppContext } from './context/appContext'
import AuthCode from './containers/AuthCode'
function App() { function App() {
const appContext = useContext(AppContext) const appContext = useContext(AppContext)
if (!appContext.loggedIn) { if (!appContext.tokens) {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<HashRouter> <HashRouter>
<Header /> <Header />
<Switch> <Switch>
<Route exact path="/SASjsLogon">
<Login getCodeOnly />
</Route>
<Route path="/"> <Route path="/">
<Login /> <Login />
</Route> </Route>
@@ -45,7 +47,7 @@ function App() {
<Studio /> <Studio />
</Route> </Route>
<Route exact path="/SASjsLogon"> <Route exact path="/SASjsLogon">
<AuthCode /> <Login getCodeOnly />
</Route> </Route>
</Switch> </Switch>
</HashRouter> </HashRouter>

View File

@@ -12,7 +12,7 @@ import {
} from '@mui/material' } from '@mui/material'
import OpenInNewIcon from '@mui/icons-material/OpenInNew' import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import Username from './username' import UserName from './userName'
import { AppContext } from '../context/appContext' import { AppContext } from '../context/appContext'
const NODE_ENV = process.env.NODE_ENV const NODE_ENV = process.env.NODE_ENV
@@ -113,8 +113,8 @@ const Header = (props: any) => {
justifyContent: 'flex-end' justifyContent: 'flex-end'
}} }}
> >
<Username <UserName
username={appContext.displayName || appContext.username} userName={appContext.displayName}
onClickHandler={handleMenu} onClickHandler={handleMenu}
/> />
<Menu <Menu

View File

@@ -1,38 +1,98 @@
import axios from 'axios'
import React, { useState, useContext } from 'react' import React, { useState, useContext } from 'react'
import { useLocation } from 'react-router-dom'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { CssBaseline, Box, TextField, Button } from '@mui/material' import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
import { AppContext } from '../context/appContext' import { AppContext } from '../context/appContext'
const login = async (payload: { username: string; password: string }) => const headers = {
axios.post('/SASLogon/login', payload).then((res) => res.data) Accept: 'application/json',
'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 Login = () => { const getAuthCode = async (credentials: any) => {
return fetch(`${baseUrl}/SASjsApi/auth/authorize`, {
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 getTokens = async (payload: any) => {
return fetch(`${baseUrl}/SASjsApi/auth/token`, {
method: 'POST',
headers,
body: JSON.stringify(payload)
}).then((data) => data.json())
}
const Login = ({ getCodeOnly }: any) => {
const location = useLocation()
const appContext = useContext(AppContext) const appContext = useContext(AppContext)
const [username, setUsername] = useState('') const [username, setUserName] = useState('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState('') const [errorMessage, setErrorMessage] = useState('')
let error: boolean
const [displayCode, setDisplayCode] = useState(null)
const handleSubmit = async (e: any) => { const handleSubmit = async (e: any) => {
error = false
setErrorMessage('') setErrorMessage('')
e.preventDefault() e.preventDefault()
let clientId = process.env.CLIENT_ID ?? localStorage.getItem('CLIENT_ID')
const { loggedIn, user } = await login({ if (getCodeOnly) {
const params = new URLSearchParams(location.search)
const responseType = params.get('response_type')
if (responseType === 'code') clientId = params.get('client_id')
}
const { code } = await getAuthCode({
clientId,
username, username,
password password
}).catch((err: any) => { }).catch((err: string) => {
setErrorMessage(err.response.data) error = true
setErrorMessage(err)
return {} return {}
}) })
if (loggedIn) { if (!error) {
appContext.setLoggedIn?.(loggedIn) if (getCodeOnly) return setDisplayCode(code)
appContext.setUsername?.(user.username)
appContext.setDisplayName?.(user.displayName) const { accessToken, refreshToken } = await getTokens({
clientId,
code
})
if (appContext.setTokens) appContext.setTokens(accessToken, refreshToken)
if (appContext.setUserName) appContext.setUserName(username)
} }
} }
if (displayCode) {
return (
<Box className="main">
<CssBaseline />
<br />
<h2>Authorization Code</h2>
<Typography m={2} p={3} style={{ overflowWrap: 'anywhere' }}>
{displayCode}
</Typography>
<br />
</Box>
)
}
return ( return (
<Box <Box
className="main" className="main"
@@ -45,12 +105,19 @@ const Login = () => {
<CssBaseline /> <CssBaseline />
<br /> <br />
<h2 style={{ width: 'auto' }}>Welcome to SASjs Server!</h2> <h2 style={{ width: 'auto' }}>Welcome to SASjs Server!</h2>
{getCodeOnly && (
<p style={{ width: 'auto' }}>
Provide credentials to get authorization code.
</p>
)}
<br />
<TextField <TextField
id="username" id="username"
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
@@ -62,11 +129,7 @@ const Login = () => {
required required
/> />
{errorMessage && <span>{errorMessage}</span>} {errorMessage && <span>{errorMessage}</span>}
<Button <Button type="submit" variant="outlined" disabled={!appContext.setTokens}>
type="submit"
variant="outlined"
disabled={!appContext.setLoggedIn}
>
Submit Submit
</Button> </Button>
</Box> </Box>

View File

@@ -0,0 +1,94 @@
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

@@ -2,7 +2,7 @@ import React from 'react'
import { Typography, IconButton } from '@mui/material' import { Typography, IconButton } from '@mui/material'
import AccountCircle from '@mui/icons-material/AccountCircle' import AccountCircle from '@mui/icons-material/AccountCircle'
const Username = (props: any) => { const UserName = (props: any) => {
return ( return (
<IconButton <IconButton
aria-label="account of current user" aria-label="account of current user"
@@ -21,10 +21,10 @@ const Username = (props: any) => {
<AccountCircle></AccountCircle> <AccountCircle></AccountCircle>
)} )}
<Typography variant="h6" sx={{ color: 'white', padding: '0 8px' }}> <Typography variant="h6" sx={{ color: 'white', padding: '0 8px' }}>
{props.username} {props.userName}
</Typography> </Typography>
</IconButton> </IconButton>
) )
} }
export default Username export default UserName

View File

@@ -1,63 +0,0 @@
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { CssBaseline, Box, Typography } from '@mui/material'
const getAuthCode = async (credentials: any) =>
axios.post('/SASLogon/authorize', credentials).then((res) => res.data)
const AuthCode = () => {
const location = useLocation()
const [displayCode, setDisplayCode] = useState(null)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
requestAuthCode()
}, [])
const requestAuthCode = async () => {
setErrorMessage('')
const params = new URLSearchParams(location.search)
const responseType = params.get('response_type')
if (responseType !== 'code')
return setErrorMessage('response type is not support')
const clientId = params.get('client_id')
if (!clientId) return setErrorMessage('clientId is not provided')
setErrorMessage('Fetching auth code... ')
const { code } = await getAuthCode({
clientId
})
.then((res) => {
setErrorMessage('')
return res
})
.catch((err: any) => {
setErrorMessage(err.response.data)
return { code: null }
})
return setDisplayCode(code)
}
return (
<Box className="main">
<CssBaseline />
<br />
<h2>Authorization Code</h2>
{displayCode && (
<Typography m={2} p={3} style={{ overflowWrap: 'anywhere' }}>
{displayCode}
</Typography>
)}
{errorMessage && <Typography>{errorMessage}</Typography>}
<br />
</Box>
)
}
export default AuthCode

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 'react-monaco-editor' import Editor from '@monaco-editor/react'
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,7 +125,6 @@ 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

@@ -4,7 +4,7 @@ import axios from 'axios'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import { Button, Paper, Stack, Tab, Tooltip } from '@mui/material' import { Button, Paper, Stack, Tab, Tooltip } from '@mui/material'
import { makeStyles } from '@mui/styles' import { makeStyles } from '@mui/styles'
import Editor, { EditorDidMount } from 'react-monaco-editor' import Editor, { OnMount } from '@monaco-editor/react'
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'
@@ -42,7 +42,7 @@ const Studio = () => {
} }
const editorRef = useRef(null as any) const editorRef = useRef(null as any)
const handleEditorDidMount: EditorDidMount = (editor) => { const handleEditorDidMount: OnMount = (editor) => {
editor.focus() editor.focus()
editorRef.current = editor editorRef.current = editor
} }
@@ -141,7 +141,6 @@ const Studio = () => {
<Tooltip title="CTRL+ENTER will also run SAS code"> <Tooltip title="CTRL+ENTER will also run SAS code">
<Button onClick={handleRunBtnClick} className={classes.runButton}> <Button onClick={handleRunBtnClick} className={classes.runButton}>
<img <img
alt=""
draggable="false" draggable="false"
style={{ width: '25px' }} style={{ width: '25px' }}
src="/running-sas.png" src="/running-sas.png"
@@ -162,9 +161,8 @@ const Studio = () => {
> >
<Editor <Editor
height="98%" height="98%"
language="sas"
value={fileContent} value={fileContent}
editorDidMount={handleEditorDidMount} onMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }} options={{ readOnly: ctrlPressed }}
onChange={(val) => { onChange={(val) => {
if (val) setFileContent(val) if (val) setFileContent(val)

View File

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

View File

@@ -4,18 +4,6 @@ import './index.css'
import App from './App' import App from './App'
import AppContextProvider from './context/appContext' 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>
<AppContextProvider> <AppContextProvider>

View File

@@ -1,5 +1,4 @@
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'
@@ -54,8 +53,7 @@ const config: Configuration = {
new CopyPlugin({ new CopyPlugin({
patterns: [{ from: 'public' }] patterns: [{ from: 'public' }]
}), }),
new dotenv(), new dotenv()
new MonacoWebpackPlugin()
] ]
} }