mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70c3834022 | ||
|
|
dbf6c7de08 | ||
|
|
d49ea47bd7 | ||
|
|
be4951d112 | ||
|
|
c116b263d9 | ||
|
|
b4436bad0d | ||
|
|
5e325522f4 | ||
|
|
e576fad8f4 | ||
| eda8e56bb0 | |||
|
|
bee4f215d2 | ||
|
|
100f138f98 | ||
| 6ffaa7e9e2 | |||
|
|
a433786011 | ||
|
|
1adff9a783 | ||
| 1435e380be | |||
| e099f2e678 | |||
| ddd155ba01 | |||
| 9936241815 | |||
| 570995e572 | |||
| 462829fd9a | |||
| c1c0554de2 | |||
| bd3aff9a7b | |||
| a1e255e0c7 | |||
| 0dae034f17 | |||
| 89048ce943 | |||
| a82cabb001 | |||
| c4066d32a0 | |||
|
|
6a44cd69d9 | ||
|
|
e607115995 | ||
| edab51c519 | |||
|
|
081cc3102c | ||
|
|
b19aa1eba4 | ||
| 2c31922f58 | |||
|
|
4d7a571a6e | ||
|
|
a373a4eb5f | ||
| 5e3ce8a98f | |||
|
|
737b34567e | ||
|
|
6373442f83 | ||
|
|
3de59ac4f8 | ||
|
|
941988cd7c | ||
| 158f044363 | |||
|
|
02ae041a81 | ||
|
|
c4c84b1537 | ||
| b3402ea80a |
75
CHANGELOG.md
75
CHANGELOG.md
@@ -1,3 +1,78 @@
|
|||||||
|
## [0.33.2](https://github.com/sasjs/server/compare/v0.33.1...v0.33.2) (2023-04-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing print redirection pending full [#274](https://github.com/sasjs/server/issues/274) fix ([d49ea47](https://github.com/sasjs/server/commit/d49ea47bd7a2add42bdb9a717082201f29e16597))
|
||||||
|
|
||||||
|
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* applying nologo only for sas.exe ([b4436ba](https://github.com/sasjs/server/commit/b4436bad0d24d5b5a402272632db1739b1018c90)), closes [#352](https://github.com/sasjs/server/issues/352)
|
||||||
|
|
||||||
|
# [0.33.0](https://github.com/sasjs/server/compare/v0.32.0...v0.33.0) (2023-04-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* option to reset admin password on startup ([eda8e56](https://github.com/sasjs/server/commit/eda8e56bb0ea20fdaacabbbe7dcf1e3ea7bd215a))
|
||||||
|
|
||||||
|
# [0.32.0](https://github.com/sasjs/server/compare/v0.31.0...v0.32.0) (2023-04-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add an api endpoint for admin to get list of client ids ([6ffaa7e](https://github.com/sasjs/server/commit/6ffaa7e9e2a62c083bb9fcc3398dcbed10cebdb1))
|
||||||
|
|
||||||
|
# [0.31.0](https://github.com/sasjs/server/compare/v0.30.3...v0.31.0) (2023-03-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* prevent brute force attack by rate limiting login endpoint ([a82cabb](https://github.com/sasjs/server/commit/a82cabb00134c79c5ee77afd1b1628a1f768e050))
|
||||||
|
|
||||||
|
## [0.30.3](https://github.com/sasjs/server/compare/v0.30.2...v0.30.3) (2023-03-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add location.pathname to location.origin conditionally ([edab51c](https://github.com/sasjs/server/commit/edab51c51997f17553e037dc7c2b5e5fa6ea8ffe))
|
||||||
|
|
||||||
|
## [0.30.2](https://github.com/sasjs/server/compare/v0.30.1...v0.30.2) (2023-03-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** add path to base in launch program url ([2c31922](https://github.com/sasjs/server/commit/2c31922f58a8aa20d7fa6bfc95b53a350f90c798))
|
||||||
|
|
||||||
|
## [0.30.1](https://github.com/sasjs/server/compare/v0.30.0...v0.30.1) (2023-03-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** add proper base url in axios.defaults ([5e3ce8a](https://github.com/sasjs/server/commit/5e3ce8a98f1825e14c1d26d8da0c9821beeff7b3))
|
||||||
|
|
||||||
|
# [0.30.0](https://github.com/sasjs/server/compare/v0.29.0...v0.30.0) (2023-02-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* lint + remove default settings ([3de59ac](https://github.com/sasjs/server/commit/3de59ac4f8e3d95cad31f09e6963bd04c4811f26))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add new env config DB_TYPE ([158f044](https://github.com/sasjs/server/commit/158f044363abf2576c8248f0ca9da4bc9cb7e9d8))
|
||||||
|
|
||||||
|
# [0.29.0](https://github.com/sasjs/server/compare/v0.28.7...v0.29.0) (2023-02-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add /SASjsApi endpoint in permissions ([b3402ea](https://github.com/sasjs/server/commit/b3402ea80afb8802eee8b8b6cbbbcc29903424bc))
|
||||||
|
|
||||||
## [0.28.7](https://github.com/sasjs/server/compare/v0.28.6...v0.28.7) (2023-02-03)
|
## [0.28.7](https://github.com/sasjs/server/compare/v0.28.6...v0.28.7) (2023-02-03)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -137,6 +137,9 @@ CA_ROOT=fullchain.pem (optional)
|
|||||||
## ENV variables required for MODE: `server`
|
## ENV variables required for MODE: `server`
|
||||||
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: [mongodb|cosmos_mongodb] default: mongodb
|
||||||
|
DB_TYPE=
|
||||||
|
|
||||||
# AUTH_PROVIDERS options: [ldap] default: ``
|
# AUTH_PROVIDERS options: [ldap] default: ``
|
||||||
AUTH_PROVIDERS=
|
AUTH_PROVIDERS=
|
||||||
|
|
||||||
@@ -172,6 +175,32 @@ HELMET_COEP=
|
|||||||
# }
|
# }
|
||||||
HELMET_CSP_CONFIG_PATH=./csp.config.json
|
HELMET_CSP_CONFIG_PATH=./csp.config.json
|
||||||
|
|
||||||
|
# To prevent brute force attack on login route we have implemented rate limiter
|
||||||
|
# Only valid for MODE: server
|
||||||
|
# Following are configurable env variable rate limiter
|
||||||
|
|
||||||
|
# After this, access is blocked for 1 day
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
|
||||||
|
|
||||||
|
|
||||||
|
# After this, access is blocked for an hour
|
||||||
|
# Store number for 90 days since first fail
|
||||||
|
# Once a successful login is attempted, it resets
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
|
||||||
|
|
||||||
|
# Name of the admin user that will be created on startup if not exists already
|
||||||
|
# Default is `secretuser`
|
||||||
|
ADMIN_USERNAME=secretuser
|
||||||
|
|
||||||
|
# Temporary password for the ADMIN_USERNAME, which is in place until the first login
|
||||||
|
# Default is `secretpassword`
|
||||||
|
ADMIN_PASSWORD_INITIAL=secretpassword
|
||||||
|
|
||||||
|
# Specify whether app has to reset the ADMIN_USERNAME's password or not
|
||||||
|
# Default is NO. Possible options are YES and NO
|
||||||
|
# If ADMIN_PASSWORD_RESET is YES then the ADMIN_USERNAME will be prompted to change the password from ADMIN_PASSWORD_INITIAL on their next login. This will repeat on every server restart, unless the option is removed / set to NO.
|
||||||
|
ADMIN_PASSWORD_RESET=NO
|
||||||
|
|
||||||
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
||||||
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
||||||
LOG_FORMAT_MORGAN=
|
LOG_FORMAT_MORGAN=
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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
|
HELMET_COEP=[true|false] if omitted HELMET default will be used
|
||||||
|
|
||||||
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
|
||||||
|
DB_TYPE=[mongodb|cosmos_mongodb] default considered as mongodb
|
||||||
|
|
||||||
AUTH_PROVIDERS=[ldap]
|
AUTH_PROVIDERS=[ldap]
|
||||||
|
|
||||||
@@ -23,6 +24,16 @@ LDAP_BIND_PASSWORD = <password>
|
|||||||
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
LDAP_USERS_BASE_DN = <ou=users,dc=cloudron>
|
||||||
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
LDAP_GROUPS_BASE_DN = <ou=groups,dc=cloudron>
|
||||||
|
|
||||||
|
#default value is 100
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
|
||||||
|
|
||||||
|
#default value is 10
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
|
||||||
|
|
||||||
|
ADMIN_USERNAME=secretuser
|
||||||
|
ADMIN_PASSWORD_INITIAL=secretpassword
|
||||||
|
ADMIN_PASSWORD_RESET=NO
|
||||||
|
|
||||||
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
|
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
|
|||||||
129
api/package-lock.json
generated
129
api/package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^4.40.1",
|
"@sasjs/core": "^4.40.1",
|
||||||
"@sasjs/utils": "2.48.1",
|
"@sasjs/utils": "3.2.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"connect-mongo": "^4.6.0",
|
"connect-mongo": "^4.6.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"rate-limiter-flexible": "2.4.1",
|
||||||
"rotating-file-stream": "^3.0.4",
|
"rotating-file-stream": "^3.0.4",
|
||||||
"swagger-ui-express": "4.3.0",
|
"swagger-ui-express": "4.3.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
@@ -2027,6 +2028,24 @@
|
|||||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@fast-csv/format": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^14.0.1",
|
||||||
|
"lodash.escaperegexp": "^4.1.2",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.isfunction": "^3.0.9",
|
||||||
|
"lodash.isnil": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fast-csv/format/node_modules/@types/node": {
|
||||||
|
"version": "14.18.42",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz",
|
||||||
|
"integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg=="
|
||||||
|
},
|
||||||
"node_modules/@hapi/hoek": {
|
"node_modules/@hapi/hoek": {
|
||||||
"version": "9.2.1",
|
"version": "9.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
|
||||||
@@ -2543,17 +2562,17 @@
|
|||||||
"integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
|
"integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@sasjs/utils": {
|
"node_modules/@sasjs/utils": {
|
||||||
"version": "2.48.1",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.2.0.tgz",
|
||||||
"integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==",
|
"integrity": "sha512-Hdt4t/ErAy9JeJAyH7sJ+tA3ipKYUwRAAWN1CGMG0+BK2/TUVjpPtP9xYCtKculzfHFadthNXTnFVTfe4D4MLw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fast-csv/format": "4.3.5",
|
||||||
"@types/fs-extra": "9.0.13",
|
"@types/fs-extra": "9.0.13",
|
||||||
"@types/prompts": "2.0.13",
|
"@types/prompts": "2.0.13",
|
||||||
"chalk": "4.1.1",
|
"chalk": "4.1.1",
|
||||||
"cli-table": "0.3.6",
|
"cli-table": "0.3.6",
|
||||||
"consola": "2.15.0",
|
"consola": "2.15.0",
|
||||||
"csv-stringify": "5.6.5",
|
|
||||||
"find": "0.3.0",
|
"find": "0.3.0",
|
||||||
"fs-extra": "10.0.0",
|
"fs-extra": "10.0.0",
|
||||||
"jwt-decode": "3.1.2",
|
"jwt-decode": "3.1.2",
|
||||||
@@ -2605,9 +2624,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sideway/formula": {
|
"node_modules/@sideway/formula": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||||
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
|
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
|
||||||
},
|
},
|
||||||
"node_modules/@sideway/pinpoint": {
|
"node_modules/@sideway/pinpoint": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -4606,11 +4625,6 @@
|
|||||||
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/csv-stringify": {
|
|
||||||
"version": "5.6.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
|
||||||
"integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A=="
|
|
||||||
},
|
|
||||||
"node_modules/data-urls": {
|
"node_modules/data-urls": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
|
||||||
@@ -8113,6 +8127,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.escaperegexp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@@ -8123,11 +8142,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isfunction": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="
|
||||||
|
},
|
||||||
"node_modules/lodash.isinteger": {
|
"node_modules/lodash.isinteger": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.isnil": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng=="
|
||||||
|
},
|
||||||
"node_modules/lodash.isnumber": {
|
"node_modules/lodash.isnumber": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
@@ -9594,6 +9628,11 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rate-limiter-flexible": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-dgH4T44TzKVO9CLArNto62hJOwlWJMLUjVVr/ii0uUzZXEXthDNr7/yefW5z/1vvHAfycc1tnuiYyNJ8CTRB3g=="
|
||||||
|
},
|
||||||
"node_modules/raw-body": {
|
"node_modules/raw-body": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||||
@@ -12977,6 +13016,26 @@
|
|||||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@fast-csv/format": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==",
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "^14.0.1",
|
||||||
|
"lodash.escaperegexp": "^4.1.2",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"lodash.isfunction": "^3.0.9",
|
||||||
|
"lodash.isnil": "^4.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": {
|
||||||
|
"version": "14.18.42",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz",
|
||||||
|
"integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@hapi/hoek": {
|
"@hapi/hoek": {
|
||||||
"version": "9.2.1",
|
"version": "9.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
|
||||||
@@ -13376,16 +13435,16 @@
|
|||||||
"integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
|
"integrity": "sha512-hVEVnH8tej57Cran/X/iUoDms7EoL+2fwAPvjQMgHBHh8ynsF8aqYBreiRCwbrvdrjBsnmayOVh2RiQLtfHhoQ=="
|
||||||
},
|
},
|
||||||
"@sasjs/utils": {
|
"@sasjs/utils": {
|
||||||
"version": "2.48.1",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.2.0.tgz",
|
||||||
"integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==",
|
"integrity": "sha512-Hdt4t/ErAy9JeJAyH7sJ+tA3ipKYUwRAAWN1CGMG0+BK2/TUVjpPtP9xYCtKculzfHFadthNXTnFVTfe4D4MLw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"@fast-csv/format": "4.3.5",
|
||||||
"@types/fs-extra": "9.0.13",
|
"@types/fs-extra": "9.0.13",
|
||||||
"@types/prompts": "2.0.13",
|
"@types/prompts": "2.0.13",
|
||||||
"chalk": "4.1.1",
|
"chalk": "4.1.1",
|
||||||
"cli-table": "0.3.6",
|
"cli-table": "0.3.6",
|
||||||
"consola": "2.15.0",
|
"consola": "2.15.0",
|
||||||
"csv-stringify": "5.6.5",
|
|
||||||
"find": "0.3.0",
|
"find": "0.3.0",
|
||||||
"fs-extra": "10.0.0",
|
"fs-extra": "10.0.0",
|
||||||
"jwt-decode": "3.1.2",
|
"jwt-decode": "3.1.2",
|
||||||
@@ -13427,9 +13486,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sideway/formula": {
|
"@sideway/formula": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||||
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
|
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
|
||||||
},
|
},
|
||||||
"@sideway/pinpoint": {
|
"@sideway/pinpoint": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -15082,11 +15141,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"csv-stringify": {
|
|
||||||
"version": "5.6.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
|
|
||||||
"integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A=="
|
|
||||||
},
|
|
||||||
"data-urls": {
|
"data-urls": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
|
||||||
@@ -17708,6 +17762,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
},
|
},
|
||||||
|
"lodash.escaperegexp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="
|
||||||
|
},
|
||||||
"lodash.includes": {
|
"lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@@ -17718,11 +17777,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
||||||
},
|
},
|
||||||
|
"lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||||
|
},
|
||||||
|
"lodash.isfunction": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="
|
||||||
|
},
|
||||||
"lodash.isinteger": {
|
"lodash.isinteger": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
||||||
},
|
},
|
||||||
|
"lodash.isnil": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng=="
|
||||||
|
},
|
||||||
"lodash.isnumber": {
|
"lodash.isnumber": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
@@ -18811,6 +18885,11 @@
|
|||||||
"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",
|
||||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||||
},
|
},
|
||||||
|
"rate-limiter-flexible": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-dgH4T44TzKVO9CLArNto62hJOwlWJMLUjVVr/ii0uUzZXEXthDNr7/yefW5z/1vvHAfycc1tnuiYyNJ8CTRB3g=="
|
||||||
|
},
|
||||||
"raw-body": {
|
"raw-body": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"author": "4GL Ltd",
|
"author": "4GL Ltd",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/core": "^4.40.1",
|
"@sasjs/core": "^4.40.1",
|
||||||
"@sasjs/utils": "2.48.1",
|
"@sasjs/utils": "3.2.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"connect-mongo": "^4.6.0",
|
"connect-mongo": "^4.6.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"rate-limiter-flexible": "2.4.1",
|
||||||
"rotating-file-stream": "^3.0.4",
|
"rotating-file-stream": "^3.0.4",
|
||||||
"swagger-ui-express": "4.3.0",
|
"swagger-ui-express": "4.3.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
|
|||||||
@@ -758,6 +758,27 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ClientPayload'
|
$ref: '#/components/schemas/ClientPayload'
|
||||||
|
get:
|
||||||
|
operationId: GetAllClients
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ClientPayload'
|
||||||
|
type: array
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: [{clientId: someClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}, {clientId: someOtherClientID, clientSecret: someOtherRandomCryptoString, accessTokenExpiration: 86400}]
|
||||||
|
summary: 'Admin only task. Returns the list of all the clients *'
|
||||||
|
tags:
|
||||||
|
- Client
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
parameters: []
|
||||||
/SASjsApi/code/execute:
|
/SASjsApi/code/execute:
|
||||||
post:
|
post:
|
||||||
operationId: ExecuteCode
|
operationId: ExecuteCode
|
||||||
|
|||||||
@@ -3,19 +3,27 @@ import mongoose from 'mongoose'
|
|||||||
import session from 'express-session'
|
import session from 'express-session'
|
||||||
import MongoStore from 'connect-mongo'
|
import MongoStore from 'connect-mongo'
|
||||||
|
|
||||||
import { ModeType, ProtocolType } from '../utils'
|
import { DatabaseType, ModeType, ProtocolType } from '../utils'
|
||||||
|
|
||||||
export const configureExpressSession = (app: Express) => {
|
export const configureExpressSession = (app: Express) => {
|
||||||
const { MODE } = process.env
|
const { MODE, DB_TYPE } = process.env
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
let store: MongoStore | undefined
|
let store: MongoStore | undefined
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
store = MongoStore.create({
|
if (DB_TYPE === DatabaseType.COSMOS_MONGODB) {
|
||||||
client: mongoose.connection!.getClient() as any,
|
// COSMOS DB requires specific connection options (compatibility mode)
|
||||||
collectionName: 'sessions'
|
// See: https://www.npmjs.com/package/connect-mongo#set-the-compatibility-mode
|
||||||
})
|
store = MongoStore.create({
|
||||||
|
client: mongoose.connection!.getClient() as any,
|
||||||
|
autoRemove: 'interval'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
store = MongoStore.create({
|
||||||
|
client: mongoose.connection!.getClient() as any
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { PROTOCOL, ALLOWED_DOMAIN } = process.env
|
const { PROTOCOL, ALLOWED_DOMAIN } = process.env
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
|
import { Security, Route, Tags, Example, Post, Body, Get } from 'tsoa'
|
||||||
|
|
||||||
import Client, {
|
import Client, {
|
||||||
ClientPayload,
|
ClientPayload,
|
||||||
@@ -29,6 +29,28 @@ export class ClientController {
|
|||||||
): Promise<ClientPayload> {
|
): Promise<ClientPayload> {
|
||||||
return createClient(body)
|
return createClient(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Admin only task. Returns the list of all the clients
|
||||||
|
*/
|
||||||
|
@Example<ClientPayload[]>([
|
||||||
|
{
|
||||||
|
clientId: 'someClientID1234',
|
||||||
|
clientSecret: 'someRandomCryptoString',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clientId: 'someOtherClientID',
|
||||||
|
clientSecret: 'someOtherRandomCryptoString',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
|
}
|
||||||
|
])
|
||||||
|
@Get('/')
|
||||||
|
public async getAllClients(): Promise<ClientPayload[]> {
|
||||||
|
return getAllClients()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
|
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
|
||||||
@@ -60,3 +82,13 @@ const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
|
|||||||
refreshTokenExpiration: savedClient.refreshTokenExpiration
|
refreshTokenExpiration: savedClient.refreshTokenExpiration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAllClients = async (): Promise<ClientPayload[]> => {
|
||||||
|
return Client.find({}).select({
|
||||||
|
_id: 0,
|
||||||
|
clientId: 1,
|
||||||
|
clientSecret: 1,
|
||||||
|
accessTokenExpiration: 1,
|
||||||
|
refreshTokenExpiration: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
isWindows() ? '-nologo' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nologo' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ export const createSASProgram = async (
|
|||||||
%mend;
|
%mend;
|
||||||
%_sasjs_server_init()
|
%_sasjs_server_init()
|
||||||
|
|
||||||
proc printto print="%sysfunc(getoption(log))";
|
|
||||||
run;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
|
import { Request, Route, Tags, Post, Body, Get, Example } from 'tsoa'
|
||||||
import { readFile } from '@sasjs/utils'
|
import { readFile, convertSecondsToHms } from '@sasjs/utils'
|
||||||
|
|
||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
import {
|
import {
|
||||||
getWebBuildFolder,
|
getWebBuildFolder,
|
||||||
generateAuthCode,
|
generateAuthCode,
|
||||||
|
RateLimiter,
|
||||||
AuthProviderType,
|
AuthProviderType,
|
||||||
LDAPClient
|
LDAPClient
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
@@ -83,19 +84,38 @@ const login = async (
|
|||||||
) => {
|
) => {
|
||||||
// Authenticate User
|
// Authenticate User
|
||||||
const user = await User.findOne({ username })
|
const user = await User.findOne({ username })
|
||||||
if (!user) throw new Error('Username is not found.')
|
|
||||||
|
|
||||||
if (
|
let validPass = false
|
||||||
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
|
|
||||||
user.authProvider === AuthProviderType.LDAP
|
if (user) {
|
||||||
) {
|
if (
|
||||||
const ldapClient = await LDAPClient.init()
|
process.env.AUTH_PROVIDERS === AuthProviderType.LDAP &&
|
||||||
await ldapClient.verifyUser(username, password)
|
user.authProvider === AuthProviderType.LDAP
|
||||||
} else {
|
) {
|
||||||
const validPass = user.comparePassword(password)
|
const ldapClient = await LDAPClient.init()
|
||||||
if (!validPass) throw new Error('Invalid password.')
|
validPass = await ldapClient
|
||||||
|
.verifyUser(username, password)
|
||||||
|
.catch(() => false)
|
||||||
|
} else {
|
||||||
|
validPass = user.comparePassword(password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// code to prevent brute force attack
|
||||||
|
|
||||||
|
const rateLimiter = RateLimiter.getInstance()
|
||||||
|
|
||||||
|
if (!validPass) {
|
||||||
|
const retrySecs = await rateLimiter.consume(req.ip, user?.username)
|
||||||
|
if (retrySecs > 0) throw errors.tooManyRequests(retrySecs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) throw errors.userNotFound
|
||||||
|
if (!validPass) throw errors.invalidPassword
|
||||||
|
|
||||||
|
// Reset on successful authorization
|
||||||
|
rateLimiter.resetOnSuccess(req.ip, user.username)
|
||||||
|
|
||||||
req.session.loggedIn = true
|
req.session.loggedIn = true
|
||||||
req.session.user = {
|
req.session.user = {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -172,3 +192,18 @@ interface AuthorizeResponse {
|
|||||||
*/
|
*/
|
||||||
code: string
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errors = {
|
||||||
|
invalidPassword: {
|
||||||
|
code: 401,
|
||||||
|
message: 'Invalid Password.'
|
||||||
|
},
|
||||||
|
userNotFound: {
|
||||||
|
code: 401,
|
||||||
|
message: 'Username is not found.'
|
||||||
|
},
|
||||||
|
tooManyRequests: (seconds: number) => ({
|
||||||
|
code: 429,
|
||||||
|
message: `Too Many Requests! Retry after ${convertSecondsToHms(seconds)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
PermissionSettingForRoute,
|
PermissionSettingForRoute,
|
||||||
PermissionType
|
PermissionType
|
||||||
} from '../controllers/permission'
|
} from '../controllers/permission'
|
||||||
import { getPath, isPublicRoute } from '../utils'
|
import { getPath, isPublicRoute, TopLevelRoutes } from '../utils'
|
||||||
|
|
||||||
export const authorize: RequestHandler = async (req, res, next) => {
|
export const authorize: RequestHandler = async (req, res, next) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
@@ -22,6 +22,9 @@ export const authorize: RequestHandler = async (req, res, next) => {
|
|||||||
if (!dbUser) return res.sendStatus(401)
|
if (!dbUser) return res.sendStatus(401)
|
||||||
|
|
||||||
const path = getPath(req)
|
const path = getPath(req)
|
||||||
|
const { baseUrl } = req
|
||||||
|
const topLevelRoute =
|
||||||
|
TopLevelRoutes.find((route) => baseUrl.startsWith(route)) || baseUrl
|
||||||
|
|
||||||
// find permission w.r.t user
|
// find permission w.r.t user
|
||||||
const permission = await Permission.findOne({
|
const permission = await Permission.findOne({
|
||||||
@@ -35,6 +38,21 @@ export const authorize: RequestHandler = async (req, res, next) => {
|
|||||||
else return res.sendStatus(401)
|
else return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find permission w.r.t user on top level
|
||||||
|
const topLevelPermission = await Permission.findOne({
|
||||||
|
path: topLevelRoute,
|
||||||
|
type: PermissionType.route,
|
||||||
|
user: dbUser._id
|
||||||
|
})
|
||||||
|
|
||||||
|
if (topLevelPermission) {
|
||||||
|
if (topLevelPermission.setting === PermissionSettingForRoute.grant)
|
||||||
|
return next()
|
||||||
|
else return res.sendStatus(401)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isPermissionDenied = false
|
||||||
|
|
||||||
// find permission w.r.t user's groups
|
// find permission w.r.t user's groups
|
||||||
for (const group of dbUser.groups) {
|
for (const group of dbUser.groups) {
|
||||||
const groupPermission = await Permission.findOne({
|
const groupPermission = await Permission.findOne({
|
||||||
@@ -42,8 +60,28 @@ export const authorize: RequestHandler = async (req, res, next) => {
|
|||||||
type: PermissionType.route,
|
type: PermissionType.route,
|
||||||
group
|
group
|
||||||
})
|
})
|
||||||
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
|
||||||
return next()
|
if (groupPermission) {
|
||||||
|
if (groupPermission.setting === PermissionSettingForRoute.grant) {
|
||||||
|
return next()
|
||||||
|
} else {
|
||||||
|
isPermissionDenied = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isPermissionDenied) {
|
||||||
|
// find permission w.r.t user's groups on top level
|
||||||
|
for (const group of dbUser.groups) {
|
||||||
|
const groupPermission = await Permission.findOne({
|
||||||
|
path: topLevelRoute,
|
||||||
|
type: PermissionType.route,
|
||||||
|
group
|
||||||
|
})
|
||||||
|
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
|
|||||||
22
api/src/middlewares/bruteForceProtection.ts
Normal file
22
api/src/middlewares/bruteForceProtection.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { RequestHandler } from 'express'
|
||||||
|
import { convertSecondsToHms } from '@sasjs/utils'
|
||||||
|
import { RateLimiter } from '../utils'
|
||||||
|
|
||||||
|
export const bruteForceProtection: RequestHandler = async (req, res, next) => {
|
||||||
|
const ip = req.ip
|
||||||
|
const username = req.body.username
|
||||||
|
|
||||||
|
const rateLimiter = RateLimiter.getInstance()
|
||||||
|
|
||||||
|
const retrySecs = await rateLimiter.check(ip, username)
|
||||||
|
|
||||||
|
if (retrySecs > 0) {
|
||||||
|
res
|
||||||
|
.status(429)
|
||||||
|
.send(`Too Many Requests! Retry after ${convertSecondsToHms(retrySecs)}`)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ export * from './csrfProtection'
|
|||||||
export * from './desktop'
|
export * from './desktop'
|
||||||
export * from './verifyAdmin'
|
export * from './verifyAdmin'
|
||||||
export * from './verifyAdminIfNeeded'
|
export * from './verifyAdminIfNeeded'
|
||||||
|
export * from './bruteForceProtection'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { ClientController } from '../../controllers'
|
import { ClientController } from '../../controllers'
|
||||||
import { registerClientValidation } from '../../utils'
|
import { registerClientValidation } from '../../utils'
|
||||||
|
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
|
||||||
|
|
||||||
const clientRouter = express.Router()
|
const clientRouter = express.Router()
|
||||||
|
|
||||||
@@ -17,4 +18,19 @@ clientRouter.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
clientRouter.get(
|
||||||
|
'/',
|
||||||
|
authenticateAccessToken,
|
||||||
|
verifyAdmin,
|
||||||
|
async (req, res) => {
|
||||||
|
const controller = new ClientController()
|
||||||
|
try {
|
||||||
|
const response = await controller.getAllClients()
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export default clientRouter
|
export default clientRouter
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import request from 'supertest'
|
|||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController, ClientController } from '../../../controllers/'
|
import { UserController, ClientController } from '../../../controllers/'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||||
|
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../../../model/Client'
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
clientId: 'someclientID',
|
clientId: 'someclientID',
|
||||||
@@ -26,6 +27,7 @@ describe('client', () => {
|
|||||||
let app: Express
|
let app: Express
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
|
let adminAccessToken: string
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const clientController = new ClientController()
|
const clientController = new ClientController()
|
||||||
|
|
||||||
@@ -34,6 +36,18 @@ describe('client', () => {
|
|||||||
|
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
|
||||||
|
const dbUser = await userController.createUser(adminUser)
|
||||||
|
adminAccessToken = generateAccessToken({
|
||||||
|
clientId: client.clientId,
|
||||||
|
userId: dbUser.id
|
||||||
|
})
|
||||||
|
await saveTokensInDB(
|
||||||
|
dbUser.id,
|
||||||
|
client.clientId,
|
||||||
|
adminAccessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -43,22 +57,6 @@ describe('client', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
let adminAccessToken: string
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const dbUser = await userController.createUser(adminUser)
|
|
||||||
adminAccessToken = generateAccessToken({
|
|
||||||
clientId: client.clientId,
|
|
||||||
userId: dbUser.id
|
|
||||||
})
|
|
||||||
await saveTokensInDB(
|
|
||||||
dbUser.id,
|
|
||||||
client.clientId,
|
|
||||||
adminAccessToken,
|
|
||||||
'refreshToken'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
const collections = mongoose.connection.collections
|
const collections = mongoose.connection.collections
|
||||||
const collection = collections['clients']
|
const collection = collections['clients']
|
||||||
@@ -157,4 +155,80 @@ describe('client', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
const collections = mongoose.connection.collections
|
||||||
|
const collection = collections['clients']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with an array of all clients', async () => {
|
||||||
|
await clientController.createClient(newClient)
|
||||||
|
await clientController.createClient({
|
||||||
|
clientId: 'clientID',
|
||||||
|
clientSecret: 'clientSecret'
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/SASjsApi/client')
|
||||||
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
clientId: 'newClientID',
|
||||||
|
clientSecret: 'newClientSecret',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
clientId: 'clientID',
|
||||||
|
clientSecret: 'clientSecret',
|
||||||
|
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
|
||||||
|
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(res.body).toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
const res = await request(app).get('/SASjsApi/client').send().expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Unauthorized')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with Forbideen if access token is not of an admin account', async () => {
|
||||||
|
const user = {
|
||||||
|
displayName: 'User 2',
|
||||||
|
username: 'username2',
|
||||||
|
password: '12345678',
|
||||||
|
isAdmin: false,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
const dbUser = await userController.createUser(user)
|
||||||
|
const accessToken = generateAccessToken({
|
||||||
|
clientId: client.clientId,
|
||||||
|
userId: dbUser.id
|
||||||
|
})
|
||||||
|
await saveTokensInDB(
|
||||||
|
dbUser.id,
|
||||||
|
client.clientId,
|
||||||
|
accessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/SASjsApi/client')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(401)
|
||||||
|
|
||||||
|
expect(res.text).toEqual('Admin account required')
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,6 +82,80 @@ describe('web', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should respond with too many requests when attempting with invalid password for a same user too many times', async () => {
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
const promises: request.Test[] = []
|
||||||
|
|
||||||
|
const maxConsecutiveFailsByUsernameAndIp = Number(
|
||||||
|
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
)
|
||||||
|
|
||||||
|
Array(maxConsecutiveFailsByUsernameAndIp + 1)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => {
|
||||||
|
promises.push(
|
||||||
|
request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: 'invalid-password'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
.expect(429)
|
||||||
|
|
||||||
|
expect(res.text).toContain('Too Many Requests!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond with too many requests when attempting with invalid credentials for different users but with same ip too many times', async () => {
|
||||||
|
await userController.createUser(user)
|
||||||
|
|
||||||
|
const promises: request.Test[] = []
|
||||||
|
|
||||||
|
const maxWrongAttemptsByIpPerDay = Number(
|
||||||
|
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
|
||||||
|
)
|
||||||
|
|
||||||
|
Array(maxWrongAttemptsByIpPerDay + 1)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => {
|
||||||
|
promises.push(
|
||||||
|
request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
username: `user${i}`,
|
||||||
|
password: 'invalid-password'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/SASLogon/login')
|
||||||
|
.set('x-xsrf-token', csrfToken)
|
||||||
|
.send({
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
})
|
||||||
|
.expect(429)
|
||||||
|
|
||||||
|
expect(res.text).toContain('Too Many Requests!')
|
||||||
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if CSRF Token is not present', async () => {
|
it('should respond with Bad Request if CSRF Token is not present', async () => {
|
||||||
await userController.createUser(user)
|
await userController.createUser(user)
|
||||||
|
|
||||||
@@ -119,6 +193,7 @@ describe('web', () => {
|
|||||||
let authCookies: string
|
let authCookies: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
await deleteDocumentsFromLimitersCollections()
|
||||||
;({ csrfToken } = await getCSRF(app))
|
;({ csrfToken } = await getCSRF(app))
|
||||||
|
|
||||||
await userController.createUser(user)
|
await userController.createUser(user)
|
||||||
@@ -210,3 +285,12 @@ const extractCSRF = (text: string) =>
|
|||||||
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
||||||
text
|
text
|
||||||
)![1]
|
)![1]
|
||||||
|
|
||||||
|
const deleteDocumentsFromLimitersCollections = async () => {
|
||||||
|
const { collections } = mongoose.connection
|
||||||
|
const login_fail_ip_per_day_collection = collections['login_fail_ip_per_day']
|
||||||
|
await login_fail_ip_per_day_collection.deleteMany({})
|
||||||
|
const login_fail_consecutive_username_and_ip_collection =
|
||||||
|
collections['login_fail_consecutive_username_and_ip']
|
||||||
|
await login_fail_consecutive_username_and_ip_collection.deleteMany({})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { generateCSRFToken } from '../../middlewares'
|
import { generateCSRFToken } from '../../middlewares'
|
||||||
import { WebController } from '../../controllers/web'
|
import { WebController } from '../../controllers/web'
|
||||||
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
|
import {
|
||||||
|
authenticateAccessToken,
|
||||||
|
bruteForceProtection,
|
||||||
|
desktopRestrict
|
||||||
|
} from '../../middlewares'
|
||||||
import { authorizeValidation, loginWebValidation } from '../../utils'
|
import { authorizeValidation, loginWebValidation } from '../../utils'
|
||||||
|
|
||||||
const webRouter = express.Router()
|
const webRouter = express.Router()
|
||||||
@@ -27,17 +31,26 @@ webRouter.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
webRouter.post('/SASLogon/login', desktopRestrict, async (req, res) => {
|
webRouter.post(
|
||||||
const { error, value: body } = loginWebValidation(req.body)
|
'/SASLogon/login',
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
desktopRestrict,
|
||||||
|
bruteForceProtection,
|
||||||
|
async (req, res) => {
|
||||||
|
const { error, value: body } = loginWebValidation(req.body)
|
||||||
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.login(req, body)
|
const response = await controller.login(req, body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
if (err instanceof Error) {
|
||||||
|
res.status(500).send(err.toString())
|
||||||
|
} else {
|
||||||
|
res.status(err.code).send(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
webRouter.post(
|
webRouter.post(
|
||||||
'/SASLogon/authorize',
|
'/SASLogon/authorize',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Request } from 'express'
|
import { Request } from 'express'
|
||||||
|
|
||||||
|
export const TopLevelRoutes = ['/AppStream', '/SASjsApi']
|
||||||
|
|
||||||
const StaticAuthorizedRoutes = [
|
const StaticAuthorizedRoutes = [
|
||||||
'/AppStream',
|
|
||||||
'/SASjsApi/code/execute',
|
'/SASjsApi/code/execute',
|
||||||
'/SASjsApi/stp/execute',
|
'/SASjsApi/stp/execute',
|
||||||
'/SASjsApi/drive/deploy',
|
'/SASjsApi/drive/deploy',
|
||||||
@@ -15,7 +16,7 @@ const StaticAuthorizedRoutes = [
|
|||||||
export const getAuthorizedRoutes = () => {
|
export const getAuthorizedRoutes = () => {
|
||||||
const streamingApps = Object.keys(process.appStreamConfig)
|
const streamingApps = Object.keys(process.appStreamConfig)
|
||||||
const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`)
|
const streamingAppsRoutes = streamingApps.map((app) => `/AppStream/${app}`)
|
||||||
return [...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
return [...TopLevelRoutes, ...StaticAuthorizedRoutes, ...streamingAppsRoutes]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPath = (req: Request) => {
|
export const getPath = (req: Request) => {
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export * from './instantiateLogger'
|
|||||||
export * from './isDebugOn'
|
export * from './isDebugOn'
|
||||||
export * from './isPublicRoute'
|
export * from './isPublicRoute'
|
||||||
export * from './ldapClient'
|
export * from './ldapClient'
|
||||||
export * from './zipped'
|
|
||||||
export * from './parseLogToArray'
|
export * from './parseLogToArray'
|
||||||
|
export * from './rateLimiter'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
export * from './saveTokensInDB'
|
export * from './saveTokensInDB'
|
||||||
export * from './seedDB'
|
export * from './seedDB'
|
||||||
@@ -32,3 +32,4 @@ export * from './upload'
|
|||||||
export * from './validation'
|
export * from './validation'
|
||||||
export * from './verifyEnvVariables'
|
export * from './verifyEnvVariables'
|
||||||
export * from './verifyTokenInDB'
|
export * from './verifyTokenInDB'
|
||||||
|
export * from './zipped'
|
||||||
|
|||||||
126
api/src/utils/rateLimiter.ts
Normal file
126
api/src/utils/rateLimiter.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
import { RateLimiterMongo } from 'rate-limiter-flexible'
|
||||||
|
|
||||||
|
export class RateLimiter {
|
||||||
|
private static instance: RateLimiter
|
||||||
|
private limiterSlowBruteByIP: RateLimiterMongo
|
||||||
|
private limiterConsecutiveFailsByUsernameAndIP: RateLimiterMongo
|
||||||
|
private maxWrongAttemptsByIpPerDay: number
|
||||||
|
private maxConsecutiveFailsByUsernameAndIp: number
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
const {
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
} = process.env
|
||||||
|
|
||||||
|
this.maxWrongAttemptsByIpPerDay = Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY)
|
||||||
|
this.maxConsecutiveFailsByUsernameAndIp = Number(
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
)
|
||||||
|
|
||||||
|
this.limiterSlowBruteByIP = new RateLimiterMongo({
|
||||||
|
storeClient: mongoose.connection,
|
||||||
|
keyPrefix: 'login_fail_ip_per_day',
|
||||||
|
points: this.maxWrongAttemptsByIpPerDay,
|
||||||
|
duration: 60 * 60 * 24,
|
||||||
|
blockDuration: 60 * 60 * 24 // Block for 1 day
|
||||||
|
})
|
||||||
|
|
||||||
|
this.limiterConsecutiveFailsByUsernameAndIP = new RateLimiterMongo({
|
||||||
|
storeClient: mongoose.connection,
|
||||||
|
keyPrefix: 'login_fail_consecutive_username_and_ip',
|
||||||
|
points: this.maxConsecutiveFailsByUsernameAndIp,
|
||||||
|
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
|
||||||
|
blockDuration: 60 * 60 // Block for 1 hour
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance() {
|
||||||
|
if (!RateLimiter.instance) {
|
||||||
|
RateLimiter.instance = new RateLimiter()
|
||||||
|
}
|
||||||
|
return RateLimiter.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUsernameIPKey(ip: string, username: string) {
|
||||||
|
return `${username}_${ip}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method checks for brute force attack
|
||||||
|
* If attack is detected then returns the number of seconds after which user can make another request
|
||||||
|
* Else returns 0
|
||||||
|
*/
|
||||||
|
public async check(ip: string, username: string) {
|
||||||
|
const usernameIPkey = this.getUsernameIPKey(ip, username)
|
||||||
|
|
||||||
|
const [resSlowByIP, resUsernameAndIP] = await Promise.all([
|
||||||
|
this.limiterSlowBruteByIP.get(ip),
|
||||||
|
this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
|
||||||
|
])
|
||||||
|
|
||||||
|
// NOTE: To make use of blockDuration option from RateLimiterMongo
|
||||||
|
// comparison in both following if statements should have greater than symbol
|
||||||
|
// otherwise, blockDuration option will not work
|
||||||
|
// For more info see: https://github.com/animir/node-rate-limiter-flexible/wiki/Options#blockduration
|
||||||
|
|
||||||
|
// Check if IP or Username + IP is already blocked
|
||||||
|
if (
|
||||||
|
resSlowByIP !== null &&
|
||||||
|
resSlowByIP.consumedPoints > this.maxWrongAttemptsByIpPerDay
|
||||||
|
) {
|
||||||
|
return Math.ceil(resSlowByIP.msBeforeNext / 1000)
|
||||||
|
} else if (
|
||||||
|
resUsernameAndIP !== null &&
|
||||||
|
resUsernameAndIP.consumedPoints > this.maxConsecutiveFailsByUsernameAndIp
|
||||||
|
) {
|
||||||
|
return Math.ceil(resUsernameAndIP.msBeforeNext / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume 1 point from limiters on wrong attempt and block if limits reached
|
||||||
|
* If limit is reached, return the number of seconds after which user can make another request
|
||||||
|
* Else return 0
|
||||||
|
*/
|
||||||
|
public async consume(ip: string, username?: string) {
|
||||||
|
try {
|
||||||
|
const promises = [this.limiterSlowBruteByIP.consume(ip)]
|
||||||
|
if (username) {
|
||||||
|
const usernameIPkey = this.getUsernameIPKey(ip, username)
|
||||||
|
|
||||||
|
// Count failed attempts by Username + IP only for registered users
|
||||||
|
promises.push(
|
||||||
|
this.limiterConsecutiveFailsByUsernameAndIP.consume(usernameIPkey)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
} catch (rlRejected: any) {
|
||||||
|
if (rlRejected instanceof Error) {
|
||||||
|
throw rlRejected
|
||||||
|
} else {
|
||||||
|
// based upon the implementation of consume method of RateLimiterMongo
|
||||||
|
// we are sure that rlRejected will contain msBeforeNext
|
||||||
|
// for further reference,
|
||||||
|
// see https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#login-endpoint-protection
|
||||||
|
return Math.ceil(rlRejected.msBeforeNext / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resetOnSuccess(ip: string, username: string) {
|
||||||
|
const usernameIPkey = this.getUsernameIPKey(ip, username)
|
||||||
|
const resUsernameAndIP =
|
||||||
|
await this.limiterConsecutiveFailsByUsernameAndIP.get(usernameIPkey)
|
||||||
|
|
||||||
|
if (resUsernameAndIP !== null && resUsernameAndIP.consumedPoints > 0) {
|
||||||
|
await this.limiterConsecutiveFailsByUsernameAndIP.delete(usernameIPkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import bcrypt from 'bcryptjs'
|
||||||
import Client from '../model/Client'
|
import Client from '../model/Client'
|
||||||
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||||
import User from '../model/User'
|
import User, { IUser } from '../model/User'
|
||||||
import Configuration, { ConfigurationType } from '../model/Configuration'
|
import Configuration, { ConfigurationType } from '../model/Configuration'
|
||||||
|
import { ResetAdminPasswordType } from './verifyEnvVariables'
|
||||||
|
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
@@ -40,9 +42,13 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
process.logger.success(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
|
process.logger.success(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ADMIN_USER = getAdminUser()
|
||||||
|
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||||
if (!usernameExist) {
|
if (usernameExist) {
|
||||||
|
usernameExist = await resetAdminPassword(usernameExist, ADMIN_USER.password)
|
||||||
|
} else {
|
||||||
const user = new User(ADMIN_USER)
|
const user = new User(ADMIN_USER)
|
||||||
usernameExist = await user.save()
|
usernameExist = await user.save()
|
||||||
|
|
||||||
@@ -51,7 +57,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupExist.hasUser(usernameExist)) {
|
if (usernameExist.isAdmin && !groupExist.hasUser(usernameExist)) {
|
||||||
groupExist.addUser(usernameExist)
|
groupExist.addUser(usernameExist)
|
||||||
process.logger.success(
|
process.logger.success(
|
||||||
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${ALL_USERS_GROUP.name}'`
|
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${ALL_USERS_GROUP.name}'`
|
||||||
@@ -90,11 +96,52 @@ const CLIENT = {
|
|||||||
clientId: 'clientID1',
|
clientId: 'clientID1',
|
||||||
clientSecret: 'clientSecret'
|
clientSecret: 'clientSecret'
|
||||||
}
|
}
|
||||||
const ADMIN_USER = {
|
|
||||||
id: 1,
|
const getAdminUser = () => {
|
||||||
displayName: 'Super Admin',
|
const { ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL } = process.env
|
||||||
username: 'secretuser',
|
|
||||||
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
|
const salt = bcrypt.genSaltSync(10)
|
||||||
isAdmin: true,
|
const hashedPassword = bcrypt.hashSync(ADMIN_PASSWORD_INITIAL as string, salt)
|
||||||
isActive: true
|
|
||||||
|
return {
|
||||||
|
displayName: 'Super Admin',
|
||||||
|
username: ADMIN_USERNAME,
|
||||||
|
password: hashedPassword,
|
||||||
|
isAdmin: true,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetAdminPassword = async (user: IUser, password: string) => {
|
||||||
|
const { ADMIN_PASSWORD_RESET } = process.env
|
||||||
|
|
||||||
|
if (ADMIN_PASSWORD_RESET === ResetAdminPasswordType.YES) {
|
||||||
|
if (!user.isAdmin) {
|
||||||
|
process.logger.error(
|
||||||
|
`Can not reset the password of non-admin user (${user.username}) on startup.`
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.authProvider) {
|
||||||
|
process.logger.error(
|
||||||
|
`Can not reset the password of admin (${user.username}) with ${user.authProvider} as authentication mechanism.`
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
process.logger.info(
|
||||||
|
`DB Seed - resetting password for admin user: ${user.username}`
|
||||||
|
)
|
||||||
|
|
||||||
|
user.password = password
|
||||||
|
user.needsToUpdatePassword = true
|
||||||
|
user = await user.save()
|
||||||
|
|
||||||
|
process.logger.success(`DB Seed - successfully reset the password`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ export enum ReturnCode {
|
|||||||
InvalidEnv
|
InvalidEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DatabaseType {
|
||||||
|
MONGO = 'mongodb',
|
||||||
|
COSMOS_MONGODB = 'cosmos_mongodb'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ResetAdminPasswordType {
|
||||||
|
YES = 'YES',
|
||||||
|
NO = 'NO'
|
||||||
|
}
|
||||||
|
|
||||||
export const verifyEnvVariables = (): ReturnCode => {
|
export const verifyEnvVariables = (): ReturnCode => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
|
|
||||||
@@ -70,6 +80,12 @@ export const verifyEnvVariables = (): ReturnCode => {
|
|||||||
|
|
||||||
errors.push(...verifyLDAPVariables())
|
errors.push(...verifyLDAPVariables())
|
||||||
|
|
||||||
|
errors.push(...verifyDbType())
|
||||||
|
|
||||||
|
errors.push(...verifyRateLimiter())
|
||||||
|
|
||||||
|
errors.push(...verifyAdminUserConfig())
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
process.logger?.error(
|
process.logger?.error(
|
||||||
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
||||||
@@ -342,11 +358,111 @@ const verifyLDAPVariables = () => {
|
|||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verifyDbType = () => {
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
const { MODE, DB_TYPE } = process.env
|
||||||
|
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
if (DB_TYPE) {
|
||||||
|
const dbTypes = Object.values(DatabaseType)
|
||||||
|
if (!dbTypes.includes(DB_TYPE as DatabaseType))
|
||||||
|
errors.push(`- DB_TYPE '${DB_TYPE}'\n - valid options ${dbTypes}`)
|
||||||
|
} else {
|
||||||
|
process.env.DB_TYPE = DEFAULTS.DB_TYPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyRateLimiter = () => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const {
|
||||||
|
MODE,
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY,
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
} = process.env
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
if (MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) {
|
||||||
|
if (
|
||||||
|
!isNumeric(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) ||
|
||||||
|
Number(MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY) < 1
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`- Invalid value for 'MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY' - Only positive number is acceptable`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.env.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY =
|
||||||
|
DEFAULTS.MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) {
|
||||||
|
if (
|
||||||
|
!isNumeric(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) ||
|
||||||
|
Number(MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP) < 1
|
||||||
|
) {
|
||||||
|
errors.push(
|
||||||
|
`- Invalid value for 'MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP' - Only positive number is acceptable`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.env.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP =
|
||||||
|
DEFAULTS.MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyAdminUserConfig = () => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { MODE, ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL, ADMIN_PASSWORD_RESET } =
|
||||||
|
process.env
|
||||||
|
if (MODE === ModeType.Server) {
|
||||||
|
if (ADMIN_USERNAME) {
|
||||||
|
process.env.ADMIN_USERNAME = ADMIN_USERNAME.toLowerCase()
|
||||||
|
} else {
|
||||||
|
process.env.ADMIN_USERNAME = DEFAULTS.ADMIN_USERNAME
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ADMIN_PASSWORD_INITIAL)
|
||||||
|
process.env.ADMIN_PASSWORD_INITIAL = DEFAULTS.ADMIN_PASSWORD_INITIAL
|
||||||
|
|
||||||
|
if (ADMIN_PASSWORD_RESET) {
|
||||||
|
const resetPasswordTypes = Object.values(ResetAdminPasswordType)
|
||||||
|
if (
|
||||||
|
!resetPasswordTypes.includes(
|
||||||
|
ADMIN_PASSWORD_RESET as ResetAdminPasswordType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
errors.push(
|
||||||
|
`- ADMIN_PASSWORD_RESET '${ADMIN_PASSWORD_RESET}'\n - valid options ${resetPasswordTypes}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
process.env.ADMIN_PASSWORD_RESET = DEFAULTS.ADMIN_PASSWORD_RESET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumeric = (val: string): boolean => {
|
||||||
|
return !isNaN(Number(val))
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
MODE: ModeType.Desktop,
|
MODE: ModeType.Desktop,
|
||||||
PROTOCOL: ProtocolType.HTTP,
|
PROTOCOL: ProtocolType.HTTP,
|
||||||
PORT: '5000',
|
PORT: '5000',
|
||||||
HELMET_COEP: HelmetCoepType.TRUE,
|
HELMET_COEP: HelmetCoepType.TRUE,
|
||||||
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
|
||||||
RUN_TIMES: RunTimeType.SAS
|
RUN_TIMES: RunTimeType.SAS,
|
||||||
|
DB_TYPE: DatabaseType.MONGO,
|
||||||
|
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY: '100',
|
||||||
|
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP: '10',
|
||||||
|
ADMIN_USERNAME: 'secretuser',
|
||||||
|
ADMIN_PASSWORD_INITIAL: 'secretpassword',
|
||||||
|
ADMIN_PASSWORD_RESET: ResetAdminPasswordType.NO
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ const RunMenu = ({
|
|||||||
handleRunBtnClick
|
handleRunBtnClick
|
||||||
}: RunMenuProps) => {
|
}: RunMenuProps) => {
|
||||||
const launchProgram = () => {
|
const launchProgram = () => {
|
||||||
const baseUrl = window.location.origin
|
const pathName =
|
||||||
|
window.location.pathname === '/' ? '' : window.location.pathname
|
||||||
|
const baseUrl = window.location.origin + pathName
|
||||||
|
|
||||||
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import axios from 'axios'
|
|||||||
const NODE_ENV = process.env.NODE_ENV
|
const NODE_ENV = process.env.NODE_ENV
|
||||||
const PORT_API = process.env.PORT_API
|
const PORT_API = process.env.PORT_API
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
NODE_ENV === 'development'
|
||||||
|
? `http://localhost:${PORT_API ?? 5000}`
|
||||||
|
: window.location.origin + window.location.pathname
|
||||||
|
|
||||||
axios.defaults = Object.assign(axios.defaults, {
|
axios.defaults = Object.assign(axios.defaults, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user