mirror of
https://github.com/sasjs/server.git
synced 2025-12-20 07:11:20 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc6f8a64b5 |
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@@ -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
|
||||||
|
|||||||
27
.github/workflows/release.yml
vendored
27
.github/workflows/release.yml
vendored
@@ -2,26 +2,16 @@ name: SASjs Server Executable Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
tags:
|
||||||
- main
|
- 'v*.*.*'
|
||||||
|
|
||||||
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
|
||||||
@@ -49,11 +39,10 @@ jobs:
|
|||||||
zip macos.zip api-macos
|
zip macos.zip api-macos
|
||||||
zip windows.zip api-win.exe
|
zip windows.zip api-win.exe
|
||||||
|
|
||||||
- name: Install Semantic Release and plugins
|
|
||||||
run: |
|
|
||||||
npm i
|
|
||||||
npm i -g semantic-release
|
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
run: |
|
uses: softprops/action-gh-release@v1
|
||||||
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
|
with:
|
||||||
|
files: |
|
||||||
|
./executables/linux.zip
|
||||||
|
./executables/macos.zip
|
||||||
|
./executables/windows.zip
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,7 +4,6 @@ node_modules/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.env*
|
.env*
|
||||||
sas/
|
sas/
|
||||||
sasjs_root/
|
|
||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
@@ -12,4 +11,3 @@ sasjscore/
|
|||||||
certificates/
|
certificates/
|
||||||
executables/
|
executables/
|
||||||
.env
|
.env
|
||||||
api/csp.config.json
|
|
||||||
|
|||||||
43
.releaserc
43
.releaserc
@@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"branches": [
|
|
||||||
"main"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@semantic-release/commit-analyzer",
|
|
||||||
"@semantic-release/release-notes-generator",
|
|
||||||
"@semantic-release/changelog",
|
|
||||||
[
|
|
||||||
"@semantic-release/git",
|
|
||||||
{
|
|
||||||
"assets": [
|
|
||||||
"CHANGELOG.md"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/github",
|
|
||||||
{
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"path": "./executables/linux.zip",
|
|
||||||
"label": "Linux Executable Binary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./executables/macos.zip",
|
|
||||||
"label": "Macos Executable Binary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./executables/windows.zip",
|
|
||||||
"label": "Windows Executable Binary"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/exec",
|
|
||||||
{
|
|
||||||
"publishCmd": "echo 'publish command'"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
183
CHANGELOG.md
183
CHANGELOG.md
@@ -1,185 +1,6 @@
|
|||||||
## [0.3.2](https://github.com/sasjs/server/compare/v0.3.1...v0.3.2) (2022-05-27)
|
# Changelog
|
||||||
|
|
||||||
|
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.
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **web:** ability to use get/patch User API in desktop mode. ([2c259fe](https://github.com/sasjs/server/commit/2c259fe1de95d84e6929e311aaa6b895e66b42a3))
|
|
||||||
|
|
||||||
## [0.3.1](https://github.com/sasjs/server/compare/v0.3.0...v0.3.1) (2022-05-26)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **api:** username should be lowercase ([5ad6ee5](https://github.com/sasjs/server/commit/5ad6ee5e0f5d7d6faa45b72215f1d9d55cfc37db))
|
|
||||||
* **web:** reduced width for autoexec input ([7d11cc7](https://github.com/sasjs/server/commit/7d11cc79161e5a07f6c5392d742ef6b9d8658071))
|
|
||||||
|
|
||||||
# [0.3.0](https://github.com/sasjs/server/compare/v0.2.0...v0.3.0) (2022-05-25)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **web:** added profile + edit + autoexec changes ([c275db1](https://github.com/sasjs/server/commit/c275db184e874f0ee3a4f08f2592cfacf1e90742))
|
|
||||||
|
|
||||||
# [0.2.0](https://github.com/sasjs/server/compare/v0.1.0...v0.2.0) (2022-05-25)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **autoexec:** usage in case of desktop from file ([79dc2db](https://github.com/sasjs/server/commit/79dc2dba23dc48ec218a973119392a45cb3856b5))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **api:** added autoexec + major type setting changes ([2a7223a](https://github.com/sasjs/server/commit/2a7223ad7d6b8f3d4682447fd25d9426a7c79ac3))
|
|
||||||
|
|
||||||
# [0.1.0](https://github.com/sasjs/server/compare/v0.0.77...v0.1.0) (2022-05-23)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* issue174 + issue175 + issue146 ([80b33c7](https://github.com/sasjs/server/commit/80b33c7a18c1b7727316ffeca71658346733e935))
|
|
||||||
* **web:** click to copy + notification ([f37f8e9](https://github.com/sasjs/server/commit/f37f8e95d1a85e00ceca2413dbb5e1f3f3f72255))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **env:** added new env variable LOG_FORMAT_MORGAN ([53bf68a](https://github.com/sasjs/server/commit/53bf68a6aff44bb7b2f40d40d6554809253a01a8))
|
|
||||||
|
|
||||||
## [0.0.77](https://github.com/sasjs/server/compare/v0.0.76...v0.0.77) (2022-05-16)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **release:** Github workflow without npm token ([c017d13](https://github.com/sasjs/server/commit/c017d13061d21aeacd0690367992d12ca57a115b))
|
|
||||||
|
|
||||||
### [0.0.76](https://github.com/sasjs/server/compare/v0.0.75...v0.0.76) (2022-05-16)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* get csrf token from cookie if not present in header ([f89389b](https://github.com/sasjs/server/commit/f89389bbc6f1f8f7060db2bdeb89746cbd60f533))
|
|
||||||
|
|
||||||
### [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)
|
||||||
|
|
||||||
|
|||||||
79
README.md
79
README.md
@@ -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=
|
||||||
SASJS_ROOT=./sasjs_root
|
|
||||||
|
|
||||||
# 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,37 +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
|
|
||||||
|
|
||||||
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
|
|
||||||
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
|
|
||||||
LOG_FORMAT_MORGAN=
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -151,7 +116,7 @@ Install the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install p
|
|||||||
```bash
|
```bash
|
||||||
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
export SAS_PATH=/opt/sas9/SASHome/SASFoundation/9.4/sasexe/sas
|
||||||
export PORT=5001
|
export PORT=5001
|
||||||
export SASJS_ROOT=./sasjs_root
|
export DRIVE_PATH=./tmp
|
||||||
|
|
||||||
pm2 start api-linux
|
pm2 start api-linux
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
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
|
||||||
SASJS_ROOT=./sasjs_root
|
DRIVE_PATH=./tmp
|
||||||
|
|
||||||
LOG_FORMAT_MORGAN=common
|
|
||||||
|
|||||||
@@ -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
499
api/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -94,6 +88,9 @@
|
|||||||
"tsoa": "3.14.1",
|
"tsoa": "3.14.1",
|
||||||
"typescript": "^4.3.2"
|
"typescript": "^4.3.2"
|
||||||
},
|
},
|
||||||
|
"configuration": {
|
||||||
|
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
||||||
|
},
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"tmp/**/*"
|
"tmp/**/*"
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
3
api/public/axios.min.js
vendored
3
api/public/axios.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -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:
|
||||||
@@ -323,8 +318,6 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
isAdmin:
|
isAdmin:
|
||||||
type: boolean
|
type: boolean
|
||||||
autoExec:
|
|
||||||
type: string
|
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- displayName
|
- displayName
|
||||||
@@ -354,10 +347,6 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
description: 'Account should be active or not, defaults to true'
|
description: 'Account should be active or not, defaults to true'
|
||||||
example: 'true'
|
example: 'true'
|
||||||
autoExec:
|
|
||||||
type: string
|
|
||||||
description: 'User-specific auto-exec code'
|
|
||||||
example: ""
|
|
||||||
required:
|
required:
|
||||||
- displayName
|
- displayName
|
||||||
- username
|
- username
|
||||||
@@ -421,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:
|
||||||
@@ -440,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
|
||||||
@@ -461,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
|
||||||
@@ -518,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}, id: {type: number, format: double}}, required: [displayName, username, id], 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
|
||||||
@@ -995,7 +928,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/UserDetailsResponse'
|
$ref: '#/components/schemas/UserDetailsResponse'
|
||||||
description: 'Only Admin or user itself will get user autoExec code.'
|
|
||||||
summary: 'Get user properties - such as group memberships, userName, displayName.'
|
summary: 'Get user properties - such as group memberships, userName, displayName.'
|
||||||
tags:
|
tags:
|
||||||
- User
|
- User
|
||||||
@@ -1245,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
|
||||||
@@ -1345,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: /
|
||||||
@@ -1376,6 +1308,3 @@ tags:
|
|||||||
-
|
-
|
||||||
name: CODE
|
name: CODE
|
||||||
description: 'Operations on SAS code'
|
description: 'Operations on SAS code'
|
||||||
-
|
|
||||||
name: Web
|
|
||||||
description: 'Operations on Web'
|
|
||||||
|
|||||||
106
api/src/app.ts
106
api/src/app.ts
@@ -1,89 +1,26 @@
|
|||||||
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,
|
||||||
copySASjsCore,
|
copySASjsCore,
|
||||||
CorsType,
|
getWebBuildFolderPath,
|
||||||
getWebBuildFolder,
|
|
||||||
HelmetCoepType,
|
|
||||||
instantiateLogger,
|
|
||||||
loadAppStreamConfig,
|
loadAppStreamConfig,
|
||||||
ModeType,
|
|
||||||
ProtocolType,
|
|
||||||
ReturnCode,
|
|
||||||
setProcessVariables,
|
setProcessVariables,
|
||||||
setupFolders,
|
setupFolders
|
||||||
verifyEnvVariables
|
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { getEnvCSPDirectives } from './utils/parseHelmetConfig'
|
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
instantiateLogger()
|
|
||||||
|
|
||||||
if (verifyEnvVariables()) process.exit(ReturnCode.InvalidEnv)
|
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.use(cookieParser())
|
const { MODE, CORS, WHITELIST } = process.env
|
||||||
|
|
||||||
const {
|
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
||||||
MODE,
|
|
||||||
CORS,
|
|
||||||
WHITELIST,
|
|
||||||
PROTOCOL,
|
|
||||||
HELMET_CSP_CONFIG_PATH,
|
|
||||||
HELMET_COEP,
|
|
||||||
LOG_FORMAT_MORGAN
|
|
||||||
} = process.env
|
|
||||||
|
|
||||||
app.use(morgan(LOG_FORMAT_MORGAN as string))
|
|
||||||
|
|
||||||
export const cookieOptions = {
|
|
||||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
|
||||||
}
|
|
||||||
|
|
||||||
const cspConfigJson: { [key: string]: string[] | null } = getEnvCSPDirectives(
|
|
||||||
HELMET_CSP_CONFIG_PATH
|
|
||||||
)
|
|
||||||
if (PROTOCOL === ProtocolType.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: HELMET_COEP === HelmetCoepType.TRUE
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
/***********************************
|
|
||||||
* Enabling CORS *
|
|
||||||
***********************************/
|
|
||||||
if (CORS === CorsType.ENABLED) {
|
|
||||||
const whiteList: string[] = []
|
const whiteList: string[] = []
|
||||||
WHITELIST?.split(' ')
|
WHITELIST?.split(' ')
|
||||||
?.filter((url) => !!url)
|
?.filter((url) => !!url)
|
||||||
@@ -97,40 +34,12 @@ if (CORS === CorsType.ENABLED) {
|
|||||||
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 === ModeType.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!')
|
||||||
}
|
}
|
||||||
@@ -148,9 +57,10 @@ export default setProcessVariables().then(async () => {
|
|||||||
|
|
||||||
// should be served after setting up web route
|
// should be served after setting up web route
|
||||||
// index.html needs to be injected with some js script.
|
// index.html needs to be injected with some js script.
|
||||||
app.use(express.static(getWebBuildFolder()))
|
app.use(express.static(getWebBuildFolderPath()))
|
||||||
|
|
||||||
app.use(onError)
|
app.use(onError)
|
||||||
|
|
||||||
|
await connectDB()
|
||||||
return app
|
return app
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
import { ExecuteReturnJson, ExecutionController } from './internal'
|
||||||
|
import { PreProgramVars } from '../types'
|
||||||
import { ExecuteReturnJsonResponse } from '.'
|
import { ExecuteReturnJsonResponse } from '.'
|
||||||
import {
|
import { parseLogToArray } from '../utils'
|
||||||
getPreProgramVariables,
|
|
||||||
getUserAutoExec,
|
|
||||||
ModeType,
|
|
||||||
parseLogToArray
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
interface ExecuteSASCodePayload {
|
interface ExecuteSASCodePayload {
|
||||||
/**
|
/**
|
||||||
@@ -34,23 +30,14 @@ export class CodeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeSASCode = async (
|
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
|
||||||
req: express.Request,
|
|
||||||
{ code }: ExecuteSASCodePayload
|
|
||||||
) => {
|
|
||||||
const { user } = req
|
|
||||||
const userAutoExec =
|
|
||||||
process.env.MODE === ModeType.Server
|
|
||||||
? user?.autoExec
|
|
||||||
: await getUserAutoExec()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { webout, log, httpHeaders } =
|
||||||
(await new ExecutionController().executeProgram(
|
(await new ExecutionController().executeProgram(
|
||||||
code,
|
code,
|
||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
{ ...req.query, _debug: 131 },
|
{ ...req.query, _debug: 131 },
|
||||||
{ userAutoExec },
|
undefined,
|
||||||
true
|
true
|
||||||
)) as ExecuteReturnJson
|
)) as ExecuteReturnJson
|
||||||
|
|
||||||
@@ -69,3 +56,16 @@ const executeSASCode = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||||
|
|
||||||
import { TreeNode } from '../types'
|
import { TreeNode } from '../types'
|
||||||
import { getFilesFolder } from '../utils'
|
import { getTmpFilesFolderPath } from '../utils'
|
||||||
|
|
||||||
interface DeployPayload {
|
interface DeployPayload {
|
||||||
appLoc: string
|
appLoc: string
|
||||||
@@ -214,12 +214,12 @@ const getFileTree = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deploy = async (data: DeployPayload) => {
|
const deploy = async (data: DeployPayload) => {
|
||||||
const driveFilesPath = getFilesFolder()
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
|
|
||||||
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
||||||
|
|
||||||
const appLocPath = path
|
const appLocPath = path
|
||||||
.join(getFilesFolder(), ...appLocParts)
|
.join(getTmpFilesFolderPath(), ...appLocParts)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!appLocPath.includes(driveFilesPath)) {
|
if (!appLocPath.includes(driveFilesPath)) {
|
||||||
@@ -238,10 +238,10 @@ const deploy = async (data: DeployPayload) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFile = async (req: express.Request, filePath: string) => {
|
const getFile = async (req: express.Request, filePath: string) => {
|
||||||
const driveFilesPath = getFilesFolder()
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(getFilesFolder(), filePath)
|
.join(getTmpFilesFolderPath(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
@@ -261,11 +261,11 @@ const getFile = async (req: express.Request, filePath: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFolder = async (folderPath?: string) => {
|
const getFolder = async (folderPath?: string) => {
|
||||||
const driveFilesPath = getFilesFolder()
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
|
|
||||||
if (folderPath) {
|
if (folderPath) {
|
||||||
const folderPathFull = path
|
const folderPathFull = path
|
||||||
.join(getFilesFolder(), folderPath)
|
.join(getTmpFilesFolderPath(), folderPath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!folderPathFull.includes(driveFilesPath)) {
|
if (!folderPathFull.includes(driveFilesPath)) {
|
||||||
@@ -291,10 +291,10 @@ const getFolder = async (folderPath?: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteFile = async (filePath: string) => {
|
const deleteFile = async (filePath: string) => {
|
||||||
const driveFilesPath = getFilesFolder()
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(getFilesFolder(), filePath)
|
.join(getTmpFilesFolderPath(), filePath)
|
||||||
.replace(new RegExp('/', 'g'), path.sep)
|
.replace(new RegExp('/', 'g'), path.sep)
|
||||||
|
|
||||||
if (!filePathFull.includes(driveFilesPath)) {
|
if (!filePathFull.includes(driveFilesPath)) {
|
||||||
@@ -314,7 +314,7 @@ const saveFile = async (
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
): Promise<GetFileResponse> => {
|
): Promise<GetFileResponse> => {
|
||||||
const driveFilesPath = getFilesFolder()
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
@@ -339,7 +339,7 @@ const updateFile = async (
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
multerFile: Express.Multer.File
|
multerFile: Express.Multer.File
|
||||||
): Promise<GetFileResponse> => {
|
): Promise<GetFileResponse> => {
|
||||||
const driveFilesPath = getFilesFolder()
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
|
|
||||||
const filePathFull = path
|
const filePathFull = path
|
||||||
.join(driveFilesPath, filePath)
|
.join(driveFilesPath, filePath)
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { PreProgramVars, Session, TreeNode } from '../../types'
|
|||||||
import {
|
import {
|
||||||
extractHeaders,
|
extractHeaders,
|
||||||
generateFileUploadSasCode,
|
generateFileUploadSasCode,
|
||||||
getFilesFolder,
|
getTmpFilesFolderPath,
|
||||||
getMacrosFolder,
|
getTmpMacrosPath,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn
|
isDebugOn
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@@ -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(
|
||||||
@@ -110,7 +110,7 @@ export class ExecutionController {
|
|||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
options insert=(SASAUTOS="${getMacrosFolder()}");
|
options insert=(SASAUTOS="${getTmpMacrosPath()}");
|
||||||
|
|
||||||
/* runtime vars */
|
/* runtime vars */
|
||||||
${varStatments}
|
${varStatments}
|
||||||
@@ -119,10 +119,6 @@ filename _webout "${weboutPath}" mod;
|
|||||||
/* dynamic user-provided vars */
|
/* dynamic user-provided vars */
|
||||||
${preProgramVarStatments}
|
${preProgramVarStatments}
|
||||||
|
|
||||||
/* user autoexec starts */
|
|
||||||
${otherArgs?.userAutoExec ?? ''}
|
|
||||||
/* user autoexec ends */
|
|
||||||
|
|
||||||
/* actual job code */
|
/* actual job code */
|
||||||
${program}`
|
${program}`
|
||||||
|
|
||||||
@@ -195,7 +191,7 @@ ${program}`
|
|||||||
const root: TreeNode = {
|
const root: TreeNode = {
|
||||||
name: 'files',
|
name: 'files',
|
||||||
relativePath: '',
|
relativePath: '',
|
||||||
absolutePath: getFilesFolder(),
|
absolutePath: getTmpFilesFolderPath(),
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Request, RequestHandler } from 'express'
|
|
||||||
import multer from 'multer'
|
import multer from 'multer'
|
||||||
import { uuidv4 } from '@sasjs/utils'
|
import { uuidv4 } from '@sasjs/utils'
|
||||||
import { getSessionController } from '.'
|
import { getSessionController } from '.'
|
||||||
|
|
||||||
export class FileUploadController {
|
export class FileUploadController {
|
||||||
private storage = multer.diskStorage({
|
private storage = multer.diskStorage({
|
||||||
destination: function (req: Request, file: any, cb: any) {
|
destination: function (req: any, file: any, cb: any) {
|
||||||
//Sending the intercepted files to the sessions subfolder
|
//Sending the intercepted files to the sessions subfolder
|
||||||
cb(null, req.sasSession?.path)
|
cb(null, req.sasSession.path)
|
||||||
},
|
},
|
||||||
filename: function (req: Request, file: any, cb: any) {
|
filename: function (req: any, file: any, cb: any) {
|
||||||
//req_file prefix + unique hash added to sas request files
|
//req_file prefix + unique hash added to sas request files
|
||||||
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
|
cb(null, `req_file_${uuidv4().replace(/-/gm, '')}`)
|
||||||
}
|
}
|
||||||
@@ -19,7 +18,7 @@ export class FileUploadController {
|
|||||||
|
|
||||||
//It will intercept request and generate unique uuid to be used as a subfolder name
|
//It will intercept request and generate unique uuid to be used as a subfolder name
|
||||||
//that will store the files uploaded
|
//that will store the files uploaded
|
||||||
public preUploadMiddleware: RequestHandler = async (req, res, next) => {
|
public preUploadMiddleware = async (req: any, res: any, next: any) => {
|
||||||
let session
|
let session
|
||||||
|
|
||||||
const sessionController = getSessionController()
|
const sessionController = getSessionController()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Session } from '../../types'
|
|||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import {
|
import {
|
||||||
getSessionsFolder,
|
getTmpSessionsFolderPath,
|
||||||
generateUniqueFileName,
|
generateUniqueFileName,
|
||||||
sysInitCompiledPath
|
sysInitCompiledPath
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@@ -37,7 +37,7 @@ export class SessionController {
|
|||||||
|
|
||||||
private async createSession(): Promise<Session> {
|
private async createSession(): Promise<Session> {
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
// death time of session is 15 mins from creation
|
// death time of session is 15 mins from creation
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getFilesFolder } from '../../utils/file'
|
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||||
import {
|
import {
|
||||||
createFolder,
|
createFolder,
|
||||||
createFile,
|
createFile,
|
||||||
@@ -17,7 +17,7 @@ export const createFileTree = async (
|
|||||||
parentFolders: string[] = []
|
parentFolders: string[] = []
|
||||||
) => {
|
) => {
|
||||||
const destinationPath = path.join(
|
const destinationPath = path.join(
|
||||||
getFilesFolder(),
|
getTmpFilesFolderPath(),
|
||||||
path.join(...parentFolders)
|
path.join(...parentFolders)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export class SessionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = (req: express.Request) => ({
|
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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,16 +17,15 @@ import {
|
|||||||
ExecutionController,
|
ExecutionController,
|
||||||
ExecutionVars
|
ExecutionVars
|
||||||
} from './internal'
|
} from './internal'
|
||||||
|
import { PreProgramVars } from '../types'
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getTmpFilesFolderPath,
|
||||||
getFilesFolder,
|
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
isDebugOn,
|
||||||
LogLine,
|
LogLine,
|
||||||
makeFilesNamesMap,
|
makeFilesNamesMap,
|
||||||
parseLogToArray
|
parseLogToArray
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { MulterFile } from '../types/Upload'
|
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecuteReturnJsonPayload {
|
||||||
/**
|
/**
|
||||||
@@ -133,7 +132,7 @@ const executeReturnRaw = async (
|
|||||||
const query = req.query as ExecutionVars
|
const query = req.query as ExecutionVars
|
||||||
const sasCodePath =
|
const sasCodePath =
|
||||||
path
|
path
|
||||||
.join(getFilesFolder(), _program)
|
.join(getTmpFilesFolderPath(), _program)
|
||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -168,17 +167,15 @@ const executeReturnRaw = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnJson = async (
|
const executeReturnJson = async (
|
||||||
req: express.Request,
|
req: any,
|
||||||
_program: string
|
_program: string
|
||||||
): Promise<ExecuteReturnJsonResponse> => {
|
): Promise<ExecuteReturnJsonResponse> => {
|
||||||
const sasCodePath =
|
const sasCodePath =
|
||||||
path
|
path
|
||||||
.join(getFilesFolder(), _program)
|
.join(getTmpFilesFolderPath(), _program)
|
||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
const filesNamesMap = req.files?.length
|
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
||||||
? makeFilesNamesMap(req.files as MulterFile[])
|
|
||||||
: null
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { webout, log, httpHeaders } =
|
||||||
@@ -213,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import express from 'express'
|
|
||||||
import {
|
import {
|
||||||
Security,
|
Security,
|
||||||
Route,
|
Route,
|
||||||
@@ -11,13 +10,10 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Hidden,
|
Hidden
|
||||||
Request
|
|
||||||
} from 'tsoa'
|
} from 'tsoa'
|
||||||
import { desktopUser } from '../middlewares'
|
|
||||||
|
|
||||||
import User, { UserPayload } from '../model/User'
|
import User, { UserPayload } from '../model/User'
|
||||||
import { getUserAutoExec, updateUserAutoExec, ModeType } from '../utils'
|
|
||||||
|
|
||||||
export interface UserResponse {
|
export interface UserResponse {
|
||||||
id: number
|
id: number
|
||||||
@@ -31,7 +27,6 @@ interface UserDetailsResponse {
|
|||||||
username: string
|
username: string
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
autoExec?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@@ -78,23 +73,13 @@ export class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only Admin or user itself will get user autoExec code.
|
|
||||||
* @summary Get user properties - such as group memberships, userName, displayName.
|
* @summary Get user properties - such as group memberships, userName, displayName.
|
||||||
* @param userId The user's identifier
|
* @param userId The user's identifier
|
||||||
* @example userId 1234
|
* @example userId 1234
|
||||||
*/
|
*/
|
||||||
@Get('{userId}')
|
@Get('{userId}')
|
||||||
public async getUser(
|
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
|
||||||
@Request() req: express.Request,
|
return getUser(userId)
|
||||||
@Path() userId: number
|
|
||||||
): Promise<UserDetailsResponse> {
|
|
||||||
const { MODE } = process.env
|
|
||||||
|
|
||||||
if (MODE === ModeType.Desktop) return getDesktopAutoExec()
|
|
||||||
|
|
||||||
const { user } = req
|
|
||||||
const getAutoExec = user!.isAdmin || user!.userId == userId
|
|
||||||
return getUser(userId, getAutoExec)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,11 +99,6 @@ export class UserController {
|
|||||||
@Path() userId: number,
|
@Path() userId: number,
|
||||||
@Body() body: UserPayload
|
@Body() body: UserPayload
|
||||||
): Promise<UserDetailsResponse> {
|
): Promise<UserDetailsResponse> {
|
||||||
const { MODE } = process.env
|
|
||||||
|
|
||||||
if (MODE === ModeType.Desktop)
|
|
||||||
return updateDesktopAutoExec(body.autoExec ?? '')
|
|
||||||
|
|
||||||
return updateUser(userId, body)
|
return updateUser(userId, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +123,7 @@ const getAllUsers = async (): Promise<UserResponse[]> =>
|
|||||||
.exec()
|
.exec()
|
||||||
|
|
||||||
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
||||||
const { displayName, username, password, isAdmin, isActive, autoExec } = data
|
const { displayName, username, password, isAdmin, isActive } = data
|
||||||
|
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
const usernameExist = await User.findOne({ username })
|
const usernameExist = await User.findOne({ username })
|
||||||
@@ -158,8 +138,7 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
|||||||
username,
|
username,
|
||||||
password: hashPassword,
|
password: hashPassword,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isActive,
|
isActive
|
||||||
autoExec
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const savedUser = await user.save()
|
const savedUser = await user.save()
|
||||||
@@ -169,50 +148,38 @@ const createUser = async (data: UserPayload): Promise<UserDetailsResponse> => {
|
|||||||
displayName: savedUser.displayName,
|
displayName: savedUser.displayName,
|
||||||
username: savedUser.username,
|
username: savedUser.username,
|
||||||
isActive: savedUser.isActive,
|
isActive: savedUser.isActive,
|
||||||
isAdmin: savedUser.isAdmin,
|
isAdmin: savedUser.isAdmin
|
||||||
autoExec: savedUser.autoExec
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUser = async (
|
const getUser = async (id: number): Promise<UserDetailsResponse> => {
|
||||||
id: number,
|
|
||||||
getAutoExec: boolean
|
|
||||||
): Promise<UserDetailsResponse> => {
|
|
||||||
const user = await User.findOne({ id })
|
const user = await User.findOne({ id })
|
||||||
|
.select({
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
username: 1,
|
||||||
|
displayName: 1,
|
||||||
|
isAdmin: 1,
|
||||||
|
isActive: 1
|
||||||
|
})
|
||||||
|
.exec()
|
||||||
if (!user) throw new Error('User is not found.')
|
if (!user) throw new Error('User is not found.')
|
||||||
|
|
||||||
return {
|
return user
|
||||||
id: user.id,
|
|
||||||
displayName: user.displayName,
|
|
||||||
username: user.username,
|
|
||||||
isActive: user.isActive,
|
|
||||||
isAdmin: user.isAdmin,
|
|
||||||
autoExec: getAutoExec ? user.autoExec ?? '' : undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDesktopAutoExec = async () => {
|
|
||||||
return {
|
|
||||||
...desktopUser,
|
|
||||||
id: desktopUser.userId,
|
|
||||||
autoExec: await getUserAutoExec()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUser = async (
|
const updateUser = async (
|
||||||
id: number,
|
id: number,
|
||||||
data: Partial<UserPayload>
|
data: UserPayload
|
||||||
): Promise<UserDetailsResponse> => {
|
): Promise<UserDetailsResponse> => {
|
||||||
const { displayName, username, password, isAdmin, isActive, autoExec } = data
|
const { displayName, username, password, isAdmin, isActive } = data
|
||||||
|
|
||||||
const params: any = { displayName, isAdmin, isActive, autoExec }
|
const params: any = { displayName, isAdmin, isActive }
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
// Checking if user is already in the database
|
// Checking if user is already in the database
|
||||||
const usernameExist = await User.findOne({ username })
|
const usernameExist = await User.findOne({ username })
|
||||||
if (usernameExist && usernameExist.id != id)
|
if (usernameExist?.id != id) throw new Error('Username already exists.')
|
||||||
throw new Error('Username already exists.')
|
|
||||||
params.username = username
|
params.username = username
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,26 +189,18 @@ const updateUser = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true })
|
const updatedUser = await User.findOneAndUpdate({ id }, params, { new: true })
|
||||||
|
.select({
|
||||||
|
_id: 0,
|
||||||
|
id: 1,
|
||||||
|
username: 1,
|
||||||
|
displayName: 1,
|
||||||
|
isAdmin: 1,
|
||||||
|
isActive: 1
|
||||||
|
})
|
||||||
|
.exec()
|
||||||
|
if (!updatedUser) throw new Error('Unable to update user')
|
||||||
|
|
||||||
if (!updatedUser) throw new Error(`Unable to find user with id: ${id}`)
|
return updatedUser
|
||||||
|
|
||||||
return {
|
|
||||||
id: updatedUser.id,
|
|
||||||
username: updatedUser.username,
|
|
||||||
displayName: updatedUser.displayName,
|
|
||||||
isAdmin: updatedUser.isAdmin,
|
|
||||||
isActive: updatedUser.isActive,
|
|
||||||
autoExec: updatedUser.autoExec
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateDesktopAutoExec = async (autoExec: string) => {
|
|
||||||
await updateUserAutoExec(autoExec)
|
|
||||||
return {
|
|
||||||
...desktopUser,
|
|
||||||
id: desktopUser.userId,
|
|
||||||
autoExec
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteUser = async (
|
const deleteUser = async (
|
||||||
|
|||||||
@@ -1,158 +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 { getWebBuildFolder, 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(getWebBuildFolder(), '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,
|
|
||||||
autoExec: user.autoExec
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loggedIn: true,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,7 @@
|
|||||||
import { RequestHandler, Request, Response, NextFunction } from 'express'
|
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { csrfProtection } from '../app'
|
import { verifyTokenInDB } from '../utils'
|
||||||
import { fetchLatestAutoExec, ModeType, verifyTokenInDB } from '../utils'
|
|
||||||
import { desktopUser } from './desktop'
|
|
||||||
|
|
||||||
export const authenticateAccessToken: RequestHandler = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
next
|
|
||||||
) => {
|
|
||||||
const { MODE } = process.env
|
|
||||||
if (MODE === ModeType.Desktop) {
|
|
||||||
req.user = desktopUser
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if request is coming from web and has valid session
|
|
||||||
// it can be validated.
|
|
||||||
if (req.session?.loggedIn) {
|
|
||||||
if (req.session.user) {
|
|
||||||
const user = await fetchLatestAutoExec(req.session.user)
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
if (user.isActive) {
|
|
||||||
req.user = user
|
|
||||||
return csrfProtection(req, res, next)
|
|
||||||
} else return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res.sendStatus(401)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export const authenticateAccessToken = (req: any, res: any, next: any) => {
|
||||||
authenticateToken(
|
authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@@ -40,7 +11,7 @@ export const authenticateAccessToken: RequestHandler = async (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
export const authenticateRefreshToken = (req: any, res: any, next: any) => {
|
||||||
authenticateToken(
|
authenticateToken(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@@ -51,16 +22,16 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authenticateToken = (
|
const authenticateToken = (
|
||||||
req: Request,
|
req: any,
|
||||||
res: Response,
|
res: any,
|
||||||
next: NextFunction,
|
next: any,
|
||||||
key: string,
|
key: string,
|
||||||
tokenType: 'accessToken' | 'refreshToken'
|
tokenType: 'accessToken' | 'refreshToken'
|
||||||
) => {
|
) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') {
|
if (MODE?.trim() !== 'server') {
|
||||||
req.user = {
|
req.user = {
|
||||||
userId: 1234,
|
userId: '1234',
|
||||||
clientId: 'desktopModeClientId',
|
clientId: 'desktopModeClientId',
|
||||||
username: 'desktopModeUsername',
|
username: 'desktopModeUsername',
|
||||||
displayName: 'desktopModeDisplayName',
|
displayName: 'desktopModeDisplayName',
|
||||||
@@ -72,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) => {
|
||||||
|
|||||||
@@ -1,42 +1,18 @@
|
|||||||
import { RequestHandler, Request } from 'express'
|
export const desktopRestrict = (req: any, res: any, next: any) => {
|
||||||
import { RequestUser } from '../types'
|
|
||||||
import { ModeType } from '../utils'
|
|
||||||
|
|
||||||
const regexUser = /^\/SASjsApi\/user\/[0-9]*$/ // /SASjsApi/user/1
|
|
||||||
|
|
||||||
const allowedInDesktopMode: { [key: string]: RegExp[] } = {
|
|
||||||
GET: [regexUser],
|
|
||||||
PATCH: [regexUser]
|
|
||||||
}
|
|
||||||
|
|
||||||
const reqAllowedInDesktopMode = (request: Request): boolean => {
|
|
||||||
const { method, originalUrl: url } = request
|
|
||||||
|
|
||||||
return !!allowedInDesktopMode[method]?.find((urlRegex) => urlRegex.test(url))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const desktopRestrict: RequestHandler = (req, res, next) => {
|
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
|
if (MODE?.trim() !== 'server')
|
||||||
if (MODE === ModeType.Desktop) {
|
|
||||||
if (!reqAllowedInDesktopMode(req))
|
|
||||||
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
return res.status(403).send('Not Allowed while in Desktop Mode.')
|
||||||
}
|
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
export const desktopUsername: RequestHandler = (req, res, next) => {
|
export const desktopUsername = (req: any, res: any, next: any) => {
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE === ModeType.Desktop) return res.status(200).send(desktopUser)
|
if (MODE?.trim() !== 'server')
|
||||||
|
return res.status(200).send({
|
||||||
|
userId: 12345,
|
||||||
|
username: 'DESKTOPusername',
|
||||||
|
displayName: 'DESKTOP User'
|
||||||
|
})
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const desktopUser: RequestUser = {
|
|
||||||
userId: 12345,
|
|
||||||
clientId: 'desktop_app',
|
|
||||||
username: 'DESKTOPusername',
|
|
||||||
displayName: 'DESKTOP User',
|
|
||||||
isAdmin: true,
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { Request } from 'express'
|
import { Request } from 'express'
|
||||||
import multer, { FileFilterCallback, Options } from 'multer'
|
import multer, { FileFilterCallback, Options } from 'multer'
|
||||||
import { blockFileRegex, getUploadsFolder } from '../utils'
|
import { blockFileRegex, getTmpUploadsPath } from '../utils'
|
||||||
|
|
||||||
const fieldNameSize = 300
|
const fieldNameSize = 300
|
||||||
const fileSize = 104857600 // 100 MB
|
const fileSize = 104857600 // 100 MB
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: getUploadsFolder(),
|
destination: getTmpUploadsPath(),
|
||||||
filename: function (
|
filename: function (
|
||||||
_req: Request,
|
_req: Request,
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { RequestHandler } from 'express'
|
export const verifyAdmin = (req: any, res: any, next: any) => {
|
||||||
|
|
||||||
export const verifyAdmin: RequestHandler = (req, res, next) => {
|
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
if (MODE?.trim() !== 'server') return next()
|
if (MODE?.trim() !== 'server') return next()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { RequestHandler } from 'express'
|
export const verifyAdminIfNeeded = (req: any, res: any, next: any) => {
|
||||||
|
|
||||||
export const verifyAdminIfNeeded: RequestHandler = (req, res, next) => {
|
|
||||||
const { user } = req
|
const { user } = req
|
||||||
const userId = parseInt(req.params.userId)
|
const userId = parseInt(req.params.userId)
|
||||||
|
|
||||||
if (!user?.isAdmin && user?.userId !== userId) {
|
if (!user.isAdmin && user.userId !== userId) {
|
||||||
return res.status(401).send('Admin account required')
|
return res.status(401).send('Admin account required')
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
|
|||||||
@@ -27,18 +27,12 @@ export interface UserPayload {
|
|||||||
* @example "true"
|
* @example "true"
|
||||||
*/
|
*/
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
/**
|
|
||||||
* User-specific auto-exec code
|
|
||||||
* @example ""
|
|
||||||
*/
|
|
||||||
autoExec?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUserDocument extends UserPayload, Document {
|
interface IUserDocument extends UserPayload, Document {
|
||||||
id: number
|
id: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
autoExec: string
|
|
||||||
groups: Schema.Types.ObjectId[]
|
groups: Schema.Types.ObjectId[]
|
||||||
tokens: [{ [key: string]: string }]
|
tokens: [{ [key: string]: string }]
|
||||||
}
|
}
|
||||||
@@ -72,9 +66,6 @@ const userSchema = new Schema<IUserDocument>({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
autoExec: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
groups: [{ type: Schema.Types.ObjectId, ref: 'Group' }],
|
||||||
tokens: [
|
tokens: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,24 +1,46 @@
|
|||||||
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()
|
|
||||||
|
|
||||||
authRouter.post('/token', async (req, res) => {
|
const clientIDs = new Set()
|
||||||
const { error, value: body } = tokenValidation(req.body)
|
|
||||||
|
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)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
|
const { clientId } = body
|
||||||
|
|
||||||
|
// Verify client ID
|
||||||
|
if (!clientIDs.has(clientId)) {
|
||||||
|
return res.status(403).send('Invalid clientId.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AuthController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.token(body)
|
const response = await controller.authorize(body)
|
||||||
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -26,12 +48,25 @@ authRouter.post('/token', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
|
authRouter.post('/token', async (req, res) => {
|
||||||
const userInfo: InfoJWT = {
|
const { error, value: body } = tokenValidation(req.body)
|
||||||
userId: req.user!.userId!,
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
clientId: req.user!.clientId!
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const controller = new AuthController()
|
||||||
|
try {
|
||||||
|
const response = await controller.token(body)
|
||||||
|
const { accessToken } = response
|
||||||
|
|
||||||
|
res.cookie('accessToken', accessToken).send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
|
||||||
|
const userInfo: InfoJWT = req.user
|
||||||
|
|
||||||
|
const controller = new AuthController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.refresh(userInfo)
|
const response = await controller.refresh(userInfo)
|
||||||
|
|
||||||
@@ -41,12 +76,10 @@ authRouter.post('/refresh', authenticateRefreshToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
authRouter.delete('/logout', authenticateAccessToken, async (req, res) => {
|
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
|
||||||
const userInfo: InfoJWT = {
|
const userInfo: InfoJWT = req.user
|
||||||
userId: req.user!.userId!,
|
|
||||||
clientId: req.user!.clientId!
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const controller = new AuthController()
|
||||||
try {
|
try {
|
||||||
await controller.logout(userInfo)
|
await controller.logout(userInfo)
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ groupRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
groupRouter.get('/:groupId', authenticateAccessToken, async (req, res) => {
|
groupRouter.get('/:groupId', authenticateAccessToken, async (req: any, res) => {
|
||||||
const { groupId } = req.params
|
const { groupId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.getGroup(parseInt(groupId))
|
const response = await controller.getGroup(groupId)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -49,15 +49,12 @@ groupRouter.post(
|
|||||||
'/:groupId/:userId',
|
'/:groupId/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
async (req, res) => {
|
async (req: any, res) => {
|
||||||
const { groupId, userId } = req.params
|
const { groupId, userId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.addUserToGroup(
|
const response = await controller.addUserToGroup(groupId, userId)
|
||||||
parseInt(groupId),
|
|
||||||
parseInt(userId)
|
|
||||||
)
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -69,15 +66,12 @@ groupRouter.delete(
|
|||||||
'/:groupId/:userId',
|
'/:groupId/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
async (req, res) => {
|
async (req: any, res) => {
|
||||||
const { groupId, userId } = req.params
|
const { groupId, userId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.removeUserFromGroup(
|
const response = await controller.removeUserFromGroup(groupId, userId)
|
||||||
parseInt(groupId),
|
|
||||||
parseInt(userId)
|
|
||||||
)
|
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -89,12 +83,12 @@ groupRouter.delete(
|
|||||||
'/:groupId',
|
'/:groupId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdmin,
|
verifyAdmin,
|
||||||
async (req, res) => {
|
async (req: any, res) => {
|
||||||
const { groupId } = req.params
|
const { groupId } = req.params
|
||||||
|
|
||||||
const controller = new GroupController()
|
const controller = new GroupController()
|
||||||
try {
|
try {
|
||||||
await controller.deleteGroup(parseInt(groupId))
|
await controller.deleteGroup(groupId)
|
||||||
res.status(200).send('Group Deleted!')
|
res.status(200).send('Group Deleted!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -21,17 +21,17 @@ import * as fileUtilModules from '../../../utils/file'
|
|||||||
const timestamp = generateTimestamp()
|
const timestamp = generateTimestamp()
|
||||||
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
const tmpFolder = path.join(process.cwd(), `tmp-${timestamp}`)
|
||||||
jest
|
jest
|
||||||
.spyOn(fileUtilModules, 'getSasjsRootFolder')
|
.spyOn(fileUtilModules, 'getTmpFolderPath')
|
||||||
.mockImplementation(() => tmpFolder)
|
.mockImplementation(() => tmpFolder)
|
||||||
jest
|
jest
|
||||||
.spyOn(fileUtilModules, 'getUploadsFolder')
|
.spyOn(fileUtilModules, 'getTmpUploadsPath')
|
||||||
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
.mockImplementation(() => path.join(tmpFolder, 'uploads'))
|
||||||
|
|
||||||
import appPromise from '../../../app'
|
import appPromise from '../../../app'
|
||||||
import { UserController } from '../../../controllers/'
|
import { UserController } from '../../../controllers/'
|
||||||
import { getTreeExample } from '../../../controllers/internal'
|
import { getTreeExample } from '../../../controllers/internal'
|
||||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||||
const { getFilesFolder } = fileUtilModules
|
const { getTmpFilesFolderPath } = fileUtilModules
|
||||||
|
|
||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const user = {
|
const user = {
|
||||||
@@ -157,10 +157,10 @@ describe('drive', () => {
|
|||||||
expect(res.text).toEqual(
|
expect(res.text).toEqual(
|
||||||
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
'{"status":"success","message":"Files deployed successfully to @sasjs/server."}'
|
||||||
)
|
)
|
||||||
await expect(folderExists(getFilesFolder())).resolves.toEqual(true)
|
await expect(folderExists(getTmpFilesFolderPath())).resolves.toEqual(true)
|
||||||
|
|
||||||
const testJobFolder = path.join(
|
const testJobFolder = path.join(
|
||||||
getFilesFolder(),
|
getTmpFilesFolderPath(),
|
||||||
'public',
|
'public',
|
||||||
'jobs',
|
'jobs',
|
||||||
'extract'
|
'extract'
|
||||||
@@ -174,7 +174,7 @@ describe('drive', () => {
|
|||||||
|
|
||||||
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
||||||
|
|
||||||
await deleteFolder(path.join(getFilesFolder(), 'public'))
|
await deleteFolder(path.join(getTmpFilesFolderPath(), 'public'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ describe('drive', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should get a SAS folder on drive having _folderPath as query param', async () => {
|
it('should get a SAS folder on drive having _folderPath as query param', async () => {
|
||||||
const pathToDrive = fileUtilModules.getFilesFolder()
|
const pathToDrive = fileUtilModules.getTmpFilesFolderPath()
|
||||||
|
|
||||||
const dirLevel1 = 'level1'
|
const dirLevel1 = 'level1'
|
||||||
const dirLevel2 = 'level2'
|
const dirLevel2 = 'level2'
|
||||||
@@ -267,7 +267,10 @@ describe('drive', () => {
|
|||||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||||
const filePath = '/my/path/code.sas'
|
const filePath = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
|
const pathToCopy = path.join(
|
||||||
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
|
filePath
|
||||||
|
)
|
||||||
await copy(fileToCopyPath, pathToCopy)
|
await copy(fileToCopyPath, pathToCopy)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
@@ -330,7 +333,7 @@ describe('drive', () => {
|
|||||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(
|
||||||
fileUtilModules.getFilesFolder(),
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
pathToUpload
|
pathToUpload
|
||||||
)
|
)
|
||||||
await copy(fileToAttachPath, pathToCopy)
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
@@ -442,7 +445,7 @@ describe('drive', () => {
|
|||||||
const pathToUpload = '/my/path/code.sas'
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(
|
||||||
fileUtilModules.getFilesFolder(),
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
pathToUpload
|
pathToUpload
|
||||||
)
|
)
|
||||||
await copy(fileToAttachPath, pathToCopy)
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
@@ -464,7 +467,7 @@ describe('drive', () => {
|
|||||||
const pathToUpload = '/my/path/code.sas'
|
const pathToUpload = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(
|
const pathToCopy = path.join(
|
||||||
fileUtilModules.getFilesFolder(),
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
pathToUpload
|
pathToUpload
|
||||||
)
|
)
|
||||||
await copy(fileToAttachPath, pathToCopy)
|
await copy(fileToAttachPath, pathToCopy)
|
||||||
@@ -600,7 +603,10 @@ describe('drive', () => {
|
|||||||
const fileToCopyContent = await readFile(fileToCopyPath)
|
const fileToCopyContent = await readFile(fileToCopyPath)
|
||||||
const filePath = '/my/path/code.sas'
|
const filePath = '/my/path/code.sas'
|
||||||
|
|
||||||
const pathToCopy = path.join(fileUtilModules.getFilesFolder(), filePath)
|
const pathToCopy = path.join(
|
||||||
|
fileUtilModules.getTmpFilesFolderPath(),
|
||||||
|
filePath
|
||||||
|
)
|
||||||
await copy(fileToCopyPath, pathToCopy)
|
await copy(fileToCopyPath, pathToCopy)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
|
|||||||
@@ -9,18 +9,17 @@ import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
|||||||
const clientId = 'someclientID'
|
const clientId = 'someclientID'
|
||||||
const adminUser = {
|
const adminUser = {
|
||||||
displayName: 'Test Admin',
|
displayName: 'Test Admin',
|
||||||
username: 'testadminusername',
|
username: 'testAdminUsername',
|
||||||
password: '12345678',
|
password: '12345678',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
const user = {
|
const user = {
|
||||||
displayName: 'Test User',
|
displayName: 'Test User',
|
||||||
username: 'testusername',
|
username: 'testUsername',
|
||||||
password: '87654321',
|
password: '87654321',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isActive: true,
|
isActive: true
|
||||||
autoExec: 'some sas code for auto exec;'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
@@ -65,21 +64,6 @@ describe('user', () => {
|
|||||||
expect(res.body.displayName).toEqual(user.displayName)
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with new user having username as lowercase', async () => {
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/user')
|
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
|
||||||
.send({ ...user, username: user.username.toUpperCase() })
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.username).toEqual(user.username)
|
|
||||||
expect(res.body.displayName).toEqual(user.displayName)
|
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
|
||||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
@@ -258,7 +242,7 @@ describe('user', () => {
|
|||||||
const dbUser1 = await controller.createUser(user)
|
const dbUser1 = await controller.createUser(user)
|
||||||
const dbUser2 = await controller.createUser({
|
const dbUser2 = await controller.createUser({
|
||||||
...user,
|
...user,
|
||||||
username: 'randomuser'
|
username: 'randomUser'
|
||||||
})
|
})
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
@@ -376,25 +360,7 @@ describe('user', () => {
|
|||||||
await deleteAllUsers()
|
await deleteAllUsers()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with user autoExec when same user requests', async () => {
|
it('should respond with user', async () => {
|
||||||
const dbUser = await controller.createUser(user)
|
|
||||||
const userId = dbUser.id
|
|
||||||
const accessToken = await generateAndSaveToken(userId)
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.get(`/SASjsApi/user/${userId}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(res.body.username).toEqual(user.username)
|
|
||||||
expect(res.body.displayName).toEqual(user.displayName)
|
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
|
||||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with user autoExec when admin user requests', async () => {
|
|
||||||
const dbUser = await controller.createUser(user)
|
const dbUser = await controller.createUser(user)
|
||||||
const userId = dbUser.id
|
const userId = dbUser.id
|
||||||
|
|
||||||
@@ -408,7 +374,6 @@ describe('user', () => {
|
|||||||
expect(res.body.displayName).toEqual(user.displayName)
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
expect(res.body.autoExec).toEqual(user.autoExec)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with user when access token is not of an admin account', async () => {
|
it('should respond with user when access token is not of an admin account', async () => {
|
||||||
@@ -430,7 +395,6 @@ describe('user', () => {
|
|||||||
expect(res.body.displayName).toEqual(user.displayName)
|
expect(res.body.displayName).toEqual(user.displayName)
|
||||||
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
expect(res.body.isAdmin).toEqual(user.isAdmin)
|
||||||
expect(res.body.isActive).toEqual(user.isActive)
|
expect(res.body.isActive).toEqual(user.isActive)
|
||||||
expect(res.body.autoExec).toBeUndefined()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
|
|||||||
@@ -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({
|
|
||||||
id: expect.any(Number),
|
|
||||||
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()
|
|
||||||
|
|
||||||
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]
|
|
||||||
@@ -34,7 +34,7 @@ stpRouter.post(
|
|||||||
'/execute',
|
'/execute',
|
||||||
fileUploadController.preUploadMiddleware,
|
fileUploadController.preUploadMiddleware,
|
||||||
fileUploadController.getMulterUploadObject().any(),
|
fileUploadController.getMulterUploadObject().any(),
|
||||||
async (req, res: any) => {
|
async (req: any, res: any) => {
|
||||||
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
const { error: errQ, value: query } = executeProgramRawValidation(req.query)
|
||||||
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
const { error: errB, value: body } = executeProgramRawValidation(req.body)
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ userRouter.get('/', authenticateAccessToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
userRouter.get('/:userId', authenticateAccessToken, async (req, res) => {
|
userRouter.get('/:userId', authenticateAccessToken, async (req: any, res) => {
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
|
|
||||||
const controller = new UserController()
|
const controller = new UserController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.getUser(req, parseInt(userId))
|
const response = await controller.getUser(userId)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -52,17 +52,17 @@ userRouter.patch(
|
|||||||
'/:userId',
|
'/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdminIfNeeded,
|
verifyAdminIfNeeded,
|
||||||
async (req, res) => {
|
async (req: any, res) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
|
|
||||||
// only an admin can update `isActive` and `isAdmin` fields
|
// only an admin can update `isActive` and `isAdmin` fields
|
||||||
const { error, value: body } = updateUserValidation(req.body, user!.isAdmin)
|
const { error, value: body } = updateUserValidation(req.body, user.isAdmin)
|
||||||
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 UserController()
|
const controller = new UserController()
|
||||||
try {
|
try {
|
||||||
const response = await controller.updateUser(parseInt(userId), body)
|
const response = await controller.updateUser(userId, body)
|
||||||
res.send(response)
|
res.send(response)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
@@ -74,17 +74,17 @@ userRouter.delete(
|
|||||||
'/:userId',
|
'/:userId',
|
||||||
authenticateAccessToken,
|
authenticateAccessToken,
|
||||||
verifyAdminIfNeeded,
|
verifyAdminIfNeeded,
|
||||||
async (req, res) => {
|
async (req: any, res) => {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
|
|
||||||
// only an admin can delete user without providing password
|
// only an admin can delete user without providing password
|
||||||
const { error, value: data } = deleteUserValidation(req.body, user!.isAdmin)
|
const { error, value: data } = deleteUserValidation(req.body, user.isAdmin)
|
||||||
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 UserController()
|
const controller = new UserController()
|
||||||
try {
|
try {
|
||||||
await controller.deleteUser(parseInt(userId), data, user!.isAdmin)
|
await controller.deleteUser(userId, data, user.isAdmin)
|
||||||
res.status(200).send('Account Deleted!')
|
res.status(200).send('Account Deleted!')
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
|
|||||||
@@ -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>`
|
||||||
|
|||||||
@@ -2,16 +2,14 @@ import path from 'path'
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { folderExists } from '@sasjs/utils'
|
import { folderExists } from '@sasjs/utils'
|
||||||
|
|
||||||
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
|
import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
|
||||||
import { appStreamHtml } from './appStreamHtml'
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -22,7 +20,7 @@ export const publishAppStream = async (
|
|||||||
streamLogo?: string,
|
streamLogo?: string,
|
||||||
addEntryToFile: boolean = true
|
addEntryToFile: boolean = true
|
||||||
) => {
|
) => {
|
||||||
const driveFilesPath = getFilesFolder()
|
const driveFilesPath = getTmpFilesFolderPath()
|
||||||
|
|
||||||
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
||||||
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
||||||
|
|||||||
58
api/src/routes/appStream/script.ts
Normal file
58
api/src/routes/appStream/script.ts
Normal 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>`
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
8
api/src/types/Process.d.ts
vendored
Normal 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
17
api/src/types/Request.ts
Normal 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'
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export interface RequestUser {
|
|
||||||
userId: number
|
|
||||||
clientId: string
|
|
||||||
username: string
|
|
||||||
displayName: string
|
|
||||||
isAdmin: boolean
|
|
||||||
isActive: boolean
|
|
||||||
autoExec?: string
|
|
||||||
}
|
|
||||||
@@ -3,6 +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'
|
||||||
export * from './RequestUser'
|
|
||||||
|
|||||||
7
api/src/types/system/express-session.d.ts
vendored
7
api/src/types/system/express-session.d.ts
vendored
@@ -1,7 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
declare module 'express-session' {
|
|
||||||
interface SessionData {
|
|
||||||
loggedIn: boolean
|
|
||||||
user: import('../').RequestUser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
api/src/types/system/express.d.ts
vendored
7
api/src/types/system/express.d.ts
vendored
@@ -1,7 +0,0 @@
|
|||||||
declare namespace Express {
|
|
||||||
export interface Request {
|
|
||||||
accessToken?: string
|
|
||||||
user?: import('../').RequestUser
|
|
||||||
sasSession?: import('../').Session
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
api/src/types/system/global.d.ts
vendored
1
api/src/types/system/global.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
import 'jest-extended'
|
|
||||||
9
api/src/types/system/process.d.ts
vendored
9
api/src/types/system/process.d.ts
vendored
@@ -1,9 +0,0 @@
|
|||||||
declare namespace NodeJS {
|
|
||||||
export interface Process {
|
|
||||||
sasLoc: string
|
|
||||||
driveLoc: string
|
|
||||||
sessionController?: import('../../controllers/internal').SessionController
|
|
||||||
appStreamConfig: import('../').AppStreamConfig
|
|
||||||
logger: import('@sasjs/utils/logger').Logger
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,12 @@ import { createFile, fileExists, readFile } from '@sasjs/utils'
|
|||||||
import { publishAppStream } from '../routes/appStream'
|
import { publishAppStream } from '../routes/appStream'
|
||||||
import { AppStreamConfig } from '../types'
|
import { AppStreamConfig } from '../types'
|
||||||
|
|
||||||
import { getAppStreamConfigPath } from './file'
|
import { getTmpAppStreamConfigPath } from './file'
|
||||||
|
|
||||||
export const loadAppStreamConfig = async () => {
|
export const loadAppStreamConfig = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
const appStreamConfigPath = getAppStreamConfigPath()
|
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
||||||
|
|
||||||
const content = (await fileExists(appStreamConfigPath))
|
const content = (await fileExists(appStreamConfigPath))
|
||||||
? await readFile(appStreamConfigPath)
|
? await readFile(appStreamConfigPath)
|
||||||
@@ -63,7 +63,7 @@ export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saveAppStreamConfig = async () => {
|
const saveAppStreamConfig = async () => {
|
||||||
const appStreamConfigPath = getAppStreamConfigPath()
|
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createFile(
|
await createFile(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import {
|
|||||||
readFile
|
readFile
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||||
|
|
||||||
export const copySASjsCore = async () => {
|
export const copySASjsCore = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
console.log('Copying Macros from container to drive(tmp).')
|
console.log('Copying Macros from container to drive(tmp).')
|
||||||
|
|
||||||
const macrosDrivePath = getMacrosFolder()
|
const macrosDrivePath = getTmpMacrosPath()
|
||||||
|
|
||||||
await deleteFolder(macrosDrivePath)
|
await deleteFolder(macrosDrivePath)
|
||||||
await createFolder(macrosDrivePath)
|
await createFolder(macrosDrivePath)
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { createFile, readFile } from '@sasjs/utils'
|
|
||||||
import { getDesktopUserAutoExecPath } from './file'
|
|
||||||
|
|
||||||
export const getUserAutoExec = async (): Promise<string> =>
|
|
||||||
readFile(getDesktopUserAutoExecPath())
|
|
||||||
|
|
||||||
export const updateUserAutoExec = async (autoExecContent: string) =>
|
|
||||||
createFile(getDesktopUserAutoExecPath(), autoExecContent)
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { homedir } from 'os'
|
|
||||||
|
|
||||||
export const apiRoot = path.join(__dirname, '..', '..')
|
export const apiRoot = path.join(__dirname, '..', '..')
|
||||||
export const codebaseRoot = path.join(apiRoot, '..')
|
export const codebaseRoot = path.join(apiRoot, '..')
|
||||||
@@ -12,31 +11,28 @@ export const sysInitCompiledPath = path.join(
|
|||||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||||
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
export const sasJSCoreMacrosInfo = path.join(sasJSCoreMacros, '.macrolist')
|
||||||
|
|
||||||
export const getWebBuildFolder = () => path.join(codebaseRoot, 'web', 'build')
|
export const getWebBuildFolderPath = () =>
|
||||||
|
path.join(codebaseRoot, 'web', 'build')
|
||||||
|
|
||||||
export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
|
export const getTmpFolderPath = () => process.driveLoc
|
||||||
|
|
||||||
export const getDesktopUserAutoExecPath = () =>
|
export const getTmpAppStreamConfigPath = () =>
|
||||||
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
path.join(getTmpFolderPath(), 'appStreamConfig.json')
|
||||||
|
|
||||||
export const getSasjsRootFolder = () => process.driveLoc
|
export const getTmpMacrosPath = () => path.join(getTmpFolderPath(), 'sasjscore')
|
||||||
|
|
||||||
export const getAppStreamConfigPath = () =>
|
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
|
||||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
|
||||||
|
|
||||||
export const getMacrosFolder = () =>
|
export const getTmpFilesFolderPath = () =>
|
||||||
path.join(getSasjsRootFolder(), 'sasjscore')
|
path.join(getTmpFolderPath(), 'files')
|
||||||
|
|
||||||
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs')
|
||||||
|
|
||||||
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
export const getTmpWeboutFolderPath = () =>
|
||||||
|
path.join(getTmpFolderPath(), 'webouts')
|
||||||
|
|
||||||
export const getLogFolder = () => path.join(getSasjsRootFolder(), 'logs')
|
export const getTmpSessionsFolderPath = () =>
|
||||||
|
path.join(getTmpFolderPath(), 'sessions')
|
||||||
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
|
||||||
|
|
||||||
export const getSessionsFolder = () =>
|
|
||||||
path.join(getSasjsRootFolder(), 'sessions')
|
|
||||||
|
|
||||||
export const generateUniqueFileName = (fileName: string, extension = '') =>
|
export const generateUniqueFileName = (fileName: string, extension = '') =>
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
|
|||||||
const isWindows = () => process.platform === 'win32'
|
const isWindows = () => process.platform === 'win32'
|
||||||
|
|
||||||
export const getDesktopFields = async () => {
|
export const getDesktopFields = async () => {
|
||||||
const { SAS_PATH } = process.env
|
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||||
|
|
||||||
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
const sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||||
// const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
|
||||||
|
|
||||||
return { sasLoc }
|
return { sasLoc, driveLoc }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDriveLocation = async (): Promise<string> => {
|
const getDriveLocation = async (): Promise<string> => {
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Request } from 'express'
|
|
||||||
import { PreProgramVars } from '../types'
|
|
||||||
|
|
||||||
export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
|
||||||
const host = req.get('host')
|
|
||||||
const protocol = req.protocol + '://'
|
|
||||||
const { user, accessToken } = req
|
|
||||||
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
export * from './appStreamConfig'
|
export * from './appStreamConfig'
|
||||||
export * from './connectDB'
|
export * from './connectDB'
|
||||||
export * from './copySASjsCore'
|
export * from './copySASjsCore'
|
||||||
export * from './desktopAutoExec'
|
|
||||||
export * from './extractHeaders'
|
export * from './extractHeaders'
|
||||||
export * from './file'
|
export * from './file'
|
||||||
export * from './generateAccessToken'
|
export * from './generateAccessToken'
|
||||||
@@ -9,8 +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 './instantiateLogger'
|
|
||||||
export * from './isDebugOn'
|
export * from './isDebugOn'
|
||||||
export * from './parseLogToArray'
|
export * from './parseLogToArray'
|
||||||
export * from './removeTokensInDB'
|
export * from './removeTokensInDB'
|
||||||
@@ -20,5 +17,4 @@ export * from './setProcessVariables'
|
|||||||
export * from './setupFolders'
|
export * from './setupFolders'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
export * from './verifyEnvVariables'
|
|
||||||
export * from './verifyTokenInDB'
|
export * from './verifyTokenInDB'
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { LogLevel, Logger } from '@sasjs/utils/logger'
|
|
||||||
|
|
||||||
export const instantiateLogger = () => {
|
|
||||||
const logLevel = (process.env.LOG_LEVEL || LogLevel.Info) as LogLevel
|
|
||||||
const logger = new Logger(logLevel)
|
|
||||||
process.logger = logger
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,30 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
import { getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||||
|
|
||||||
import { getDesktopFields, ModeType } from '.'
|
import { configuration } from '../../package.json'
|
||||||
|
import { getDesktopFields } from '.'
|
||||||
|
|
||||||
export const setProcessVariables = async () => {
|
export const setProcessVariables = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
process.driveLoc = path.join(process.cwd(), 'tmp')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE?.trim() === 'server') {
|
||||||
process.sasLoc = process.env.SAS_PATH as string
|
const { SAS_PATH, DRIVE_PATH } = process.env
|
||||||
|
|
||||||
|
process.sasLoc = SAS_PATH ?? configuration.sasPath
|
||||||
|
const absPath = getAbsolutePath(DRIVE_PATH ?? 'tmp', process.cwd())
|
||||||
|
process.driveLoc = getRealPath(absPath)
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc } = await getDesktopFields()
|
const { sasLoc, driveLoc } = await getDesktopFields()
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
process.sasLoc = sasLoc
|
||||||
|
process.driveLoc = driveLoc
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SASJS_ROOT } = process.env
|
|
||||||
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
|
||||||
await createFolder(absPath)
|
|
||||||
process.driveLoc = getRealPath(absPath)
|
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
import { createFolder } from '@sasjs/utils'
|
||||||
import { getDesktopUserAutoExecPath, getFilesFolder } from './file'
|
import { getTmpFilesFolderPath } from './file'
|
||||||
import { ModeType } from './verifyEnvVariables'
|
|
||||||
|
|
||||||
export const setupFolders = async () => {
|
export const setupFolders = async () => {
|
||||||
const drivePath = getFilesFolder()
|
const drivePath = getTmpFilesFolderPath()
|
||||||
await createFolder(drivePath)
|
await createFolder(drivePath)
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Desktop) {
|
|
||||||
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
|
||||||
await createFile(getDesktopUserAutoExecPath(), '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import Joi from 'joi'
|
import Joi from 'joi'
|
||||||
|
|
||||||
const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16)
|
const usernameSchema = Joi.string().alphanum().min(3).max(16)
|
||||||
const passwordSchema = Joi.string().min(6).max(1024)
|
const passwordSchema = Joi.string().min(6).max(1024)
|
||||||
|
|
||||||
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
export const loginWebValidation = (data: any): Joi.ValidationResult =>
|
|
||||||
Joi.object({
|
|
||||||
username: usernameSchema.required(),
|
|
||||||
password: passwordSchema.required()
|
|
||||||
}).validate(data)
|
|
||||||
|
|
||||||
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
|
username: usernameSchema.required(),
|
||||||
|
password: passwordSchema.required(),
|
||||||
clientId: Joi.string().required()
|
clientId: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
@@ -35,8 +31,7 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
|
|||||||
username: usernameSchema.required(),
|
username: usernameSchema.required(),
|
||||||
password: passwordSchema.required(),
|
password: passwordSchema.required(),
|
||||||
isAdmin: Joi.boolean(),
|
isAdmin: Joi.boolean(),
|
||||||
isActive: Joi.boolean(),
|
isActive: Joi.boolean()
|
||||||
autoExec: Joi.string().allow('')
|
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const deleteUserValidation = (
|
export const deleteUserValidation = (
|
||||||
@@ -58,8 +53,7 @@ export const updateUserValidation = (
|
|||||||
const validationChecks: any = {
|
const validationChecks: any = {
|
||||||
displayName: Joi.string().min(6),
|
displayName: Joi.string().min(6),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
password: passwordSchema,
|
password: passwordSchema
|
||||||
autoExec: Joi.string().allow('')
|
|
||||||
}
|
}
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
validationChecks.isAdmin = Joi.boolean()
|
validationChecks.isAdmin = Joi.boolean()
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
export enum ModeType {
|
|
||||||
Server = 'server',
|
|
||||||
Desktop = 'desktop'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ProtocolType {
|
|
||||||
HTTP = 'http',
|
|
||||||
HTTPS = 'https'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum CorsType {
|
|
||||||
ENABLED = 'enable',
|
|
||||||
DISABLED = 'disable'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum HelmetCoepType {
|
|
||||||
TRUE = 'true',
|
|
||||||
FALSE = 'false'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LOG_FORMAT_MORGANType {
|
|
||||||
Combined = 'combined',
|
|
||||||
Common = 'common',
|
|
||||||
Dev = 'dev',
|
|
||||||
Short = 'short',
|
|
||||||
tiny = 'tiny'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ReturnCode {
|
|
||||||
Success,
|
|
||||||
InvalidEnv
|
|
||||||
}
|
|
||||||
|
|
||||||
export const verifyEnvVariables = (): ReturnCode => {
|
|
||||||
const errors: string[] = []
|
|
||||||
|
|
||||||
errors.push(...verifyMODE())
|
|
||||||
|
|
||||||
errors.push(...verifyPROTOCOL())
|
|
||||||
|
|
||||||
errors.push(...verifyPORT())
|
|
||||||
|
|
||||||
errors.push(...verifyCORS())
|
|
||||||
|
|
||||||
errors.push(...verifyHELMET_COEP())
|
|
||||||
|
|
||||||
errors.push(...verifyLOG_FORMAT_MORGAN())
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
process.logger?.error(
|
|
||||||
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
|
|
||||||
)
|
|
||||||
return ReturnCode.InvalidEnv
|
|
||||||
}
|
|
||||||
|
|
||||||
return ReturnCode.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyMODE = (): string[] => {
|
|
||||||
const errors: string[] = []
|
|
||||||
const { MODE } = process.env
|
|
||||||
|
|
||||||
if (MODE) {
|
|
||||||
const modeTypes = Object.values(ModeType)
|
|
||||||
if (!modeTypes.includes(MODE as ModeType))
|
|
||||||
errors.push(`- MODE '${MODE}'\n - valid options ${modeTypes}`)
|
|
||||||
} else {
|
|
||||||
process.env.MODE = DEFAULTS.MODE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Server) {
|
|
||||||
const {
|
|
||||||
ACCESS_TOKEN_SECRET,
|
|
||||||
REFRESH_TOKEN_SECRET,
|
|
||||||
AUTH_CODE_SECRET,
|
|
||||||
SESSION_SECRET,
|
|
||||||
DB_CONNECT
|
|
||||||
} = process.env
|
|
||||||
|
|
||||||
if (!ACCESS_TOKEN_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- ACCESS_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!REFRESH_TOKEN_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- REFRESH_TOKEN_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!AUTH_CODE_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- AUTH_CODE_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!SESSION_SECRET)
|
|
||||||
errors.push(
|
|
||||||
`- SESSION_SECRET is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test')
|
|
||||||
if (!DB_CONNECT)
|
|
||||||
errors.push(
|
|
||||||
`- DB_CONNECT is required for PROTOCOL '${ModeType.Server}'`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyPROTOCOL = (): string[] => {
|
|
||||||
const errors: string[] = []
|
|
||||||
const { PROTOCOL } = process.env
|
|
||||||
|
|
||||||
if (PROTOCOL) {
|
|
||||||
const protocolTypes = Object.values(ProtocolType)
|
|
||||||
if (!protocolTypes.includes(PROTOCOL as ProtocolType))
|
|
||||||
errors.push(`- PROTOCOL '${PROTOCOL}'\n - valid options ${protocolTypes}`)
|
|
||||||
} else {
|
|
||||||
process.env.PROTOCOL = DEFAULTS.PROTOCOL
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.PROTOCOL === ProtocolType.HTTPS) {
|
|
||||||
const { PRIVATE_KEY, FULL_CHAIN } = process.env
|
|
||||||
|
|
||||||
if (!PRIVATE_KEY)
|
|
||||||
errors.push(
|
|
||||||
`- PRIVATE_KEY is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!FULL_CHAIN)
|
|
||||||
errors.push(
|
|
||||||
`- FULL_CHAIN is required for PROTOCOL '${ProtocolType.HTTPS}'`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyCORS = (): string[] => {
|
|
||||||
const errors: string[] = []
|
|
||||||
const { CORS } = process.env
|
|
||||||
|
|
||||||
if (CORS) {
|
|
||||||
const corsTypes = Object.values(CorsType)
|
|
||||||
if (!corsTypes.includes(CORS as CorsType))
|
|
||||||
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
|
|
||||||
} else {
|
|
||||||
const { MODE } = process.env
|
|
||||||
process.env.CORS =
|
|
||||||
MODE === ModeType.Server ? CorsType.DISABLED : CorsType.ENABLED
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyPORT = (): string[] => {
|
|
||||||
const errors: string[] = []
|
|
||||||
const { PORT } = process.env
|
|
||||||
|
|
||||||
if (PORT) {
|
|
||||||
if (Number.isNaN(parseInt(PORT)))
|
|
||||||
errors.push(`- PORT '${PORT}'\n - should be a valid number`)
|
|
||||||
} else {
|
|
||||||
process.env.PORT = DEFAULTS.PORT
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyHELMET_COEP = (): string[] => {
|
|
||||||
const errors: string[] = []
|
|
||||||
const { HELMET_COEP } = process.env
|
|
||||||
|
|
||||||
if (HELMET_COEP) {
|
|
||||||
const helmetCoepTypes = Object.values(HelmetCoepType)
|
|
||||||
if (!helmetCoepTypes.includes(HELMET_COEP as HelmetCoepType))
|
|
||||||
errors.push(
|
|
||||||
`- HELMET_COEP '${HELMET_COEP}'\n - valid options ${helmetCoepTypes}`
|
|
||||||
)
|
|
||||||
HELMET_COEP
|
|
||||||
} else {
|
|
||||||
process.env.HELMET_COEP = DEFAULTS.HELMET_COEP
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifyLOG_FORMAT_MORGAN = (): string[] => {
|
|
||||||
const errors: string[] = []
|
|
||||||
const { LOG_FORMAT_MORGAN } = process.env
|
|
||||||
|
|
||||||
if (LOG_FORMAT_MORGAN) {
|
|
||||||
const logFormatMorganTypes = Object.values(LOG_FORMAT_MORGANType)
|
|
||||||
if (
|
|
||||||
!logFormatMorganTypes.includes(LOG_FORMAT_MORGAN as LOG_FORMAT_MORGANType)
|
|
||||||
)
|
|
||||||
errors.push(
|
|
||||||
`- LOG_FORMAT_MORGAN '${LOG_FORMAT_MORGAN}'\n - valid options ${logFormatMorganTypes}`
|
|
||||||
)
|
|
||||||
LOG_FORMAT_MORGAN
|
|
||||||
} else {
|
|
||||||
process.env.LOG_FORMAT_MORGAN = DEFAULTS.LOG_FORMAT_MORGAN
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULTS = {
|
|
||||||
MODE: ModeType.Desktop,
|
|
||||||
PROTOCOL: ProtocolType.HTTP,
|
|
||||||
PORT: '5000',
|
|
||||||
HELMET_COEP: HelmetCoepType.TRUE,
|
|
||||||
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,11 @@
|
|||||||
import User from '../model/User'
|
import User from '../model/User'
|
||||||
import { RequestUser } from '../types'
|
|
||||||
|
|
||||||
export const fetchLatestAutoExec = async (
|
|
||||||
reqUser: RequestUser
|
|
||||||
): Promise<RequestUser | undefined> => {
|
|
||||||
const dbUser = await User.findOne({ id: reqUser.userId })
|
|
||||||
|
|
||||||
if (!dbUser) return undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
userId: reqUser.userId,
|
|
||||||
clientId: reqUser.clientId,
|
|
||||||
username: dbUser.username,
|
|
||||||
displayName: dbUser.displayName,
|
|
||||||
isAdmin: dbUser.isAdmin,
|
|
||||||
isActive: dbUser.isActive,
|
|
||||||
autoExec: dbUser.autoExec
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const verifyTokenInDB = async (
|
export const verifyTokenInDB = async (
|
||||||
userId: number,
|
userId: number,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
token: string,
|
token: string,
|
||||||
tokenType: 'accessToken' | 'refreshToken'
|
tokenType: 'accessToken' | 'refreshToken'
|
||||||
): Promise<RequestUser | undefined> => {
|
) => {
|
||||||
const dbUser = await User.findOne({ id: userId })
|
const dbUser = await User.findOne({ id: userId })
|
||||||
|
|
||||||
if (!dbUser) return undefined
|
if (!dbUser) return undefined
|
||||||
@@ -40,8 +21,7 @@ export const verifyTokenInDB = async (
|
|||||||
username: dbUser.username,
|
username: dbUser.username,
|
||||||
displayName: dbUser.displayName,
|
displayName: dbUser.displayName,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isActive: dbUser.isActive,
|
isActive: dbUser.isActive
|
||||||
autoExec: dbUser.autoExec
|
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
10430
package-lock.json
generated
10430
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.76",
|
"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": {
|
||||||
"server": "npm run server:prepare && npm run server:start",
|
"server": "npm run server:prepare && npm run server:start",
|
||||||
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
|
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && npm run build && cd ..",
|
||||||
"server:start": "cd api && npm run start:prod",
|
"server:start": "cd api && npm run start:prod",
|
||||||
|
"release": "standard-version",
|
||||||
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-api:fix": "npx prettier --write \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-api": "npx prettier --check \"api/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
"lint-web:fix": "npx prettier --write \"web/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
"lint-web:fix": "npx prettier --write \"web/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||||
@@ -15,9 +16,7 @@
|
|||||||
"lint:fix": "npm run lint-api:fix && npm run lint-web:fix"
|
"lint:fix": "npm run lint-api:fix && npm run lint-web:fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@semantic-release/changelog": "^6.0.1",
|
"prettier": "^2.3.1",
|
||||||
"@semantic-release/exec": "^6.0.3",
|
"standard-version": "^9.3.2"
|
||||||
"@semantic-release/git": "^10.0.1",
|
|
||||||
"@semantic-release/github": "^8.0.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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>
|
||||||
474
web/package-lock.json
generated
474
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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,14 +21,10 @@
|
|||||||
"@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-copy-to-clipboard": "^5.1.0",
|
|
||||||
"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",
|
|
||||||
"react-toastify": "^9.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.16.0",
|
"@babel/core": "^7.16.0",
|
||||||
@@ -39,7 +36,6 @@
|
|||||||
"@types/dotenv-webpack": "^7.0.3",
|
"@types/dotenv-webpack": "^7.0.3",
|
||||||
"@types/prismjs": "^1.16.6",
|
"@types/prismjs": "^1.16.6",
|
||||||
"@types/react": "^17.0.37",
|
"@types/react": "^17.0.37",
|
||||||
"@types/react-copy-to-clipboard": "^5.0.2",
|
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router-dom": "^5.3.1",
|
"@types/react-router-dom": "^5.3.1",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ import Header from './components/header'
|
|||||||
import Home from './components/home'
|
import Home from './components/home'
|
||||||
import Drive from './containers/Drive'
|
import Drive from './containers/Drive'
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
import Settings from './containers/Settings'
|
|
||||||
|
|
||||||
import { AppContext } from './context/appContext'
|
import { AppContext } from './context/appContext'
|
||||||
import AuthCode from './containers/AuthCode'
|
|
||||||
import { ToastContainer } from 'react-toastify'
|
|
||||||
|
|
||||||
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>
|
||||||
@@ -46,14 +46,10 @@ function App() {
|
|||||||
<Route exact path="/SASjsStudio">
|
<Route exact path="/SASjsStudio">
|
||||||
<Studio />
|
<Studio />
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/SASjsSettings">
|
|
||||||
<Settings />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/SASjsLogon">
|
<Route exact path="/SASjsLogon">
|
||||||
<AuthCode />
|
<Login getCodeOnly />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
<ToastContainer />
|
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react'
|
import React, { useState, useContext } from 'react'
|
||||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,9 +11,8 @@ import {
|
|||||||
MenuItem
|
MenuItem
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
import SettingsIcon from '@mui/icons-material/Settings'
|
|
||||||
|
|
||||||
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
|
||||||
@@ -21,23 +20,15 @@ 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}` : ''
|
||||||
|
|
||||||
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
|
||||||
|
|
||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
const [tabValue, setTabValue] = useState(
|
const [tabValue, setTabValue] = useState(pathname)
|
||||||
validTabs.includes(pathname) ? pathname : '/'
|
|
||||||
)
|
|
||||||
const [anchorEl, setAnchorEl] = useState<
|
const [anchorEl, setAnchorEl] = useState<
|
||||||
(EventTarget & HTMLButtonElement) | null
|
(EventTarget & HTMLButtonElement) | null
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTabValue(validTabs.includes(pathname) ? pathname : '/')
|
|
||||||
}, [pathname])
|
|
||||||
|
|
||||||
const handleMenu = (
|
const handleMenu = (
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
) => {
|
) => {
|
||||||
@@ -53,10 +44,7 @@ const Header = (props: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
if (appContext.logout) {
|
if (appContext.logout) appContext.logout()
|
||||||
handleClose()
|
|
||||||
appContext.logout()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
@@ -125,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
|
||||||
@@ -144,18 +132,6 @@ const Header = (props: any) => {
|
|||||||
open={!!anchorEl}
|
open={!!anchorEl}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
>
|
>
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
to="/SASjsSettings"
|
|
||||||
onClick={handleClose}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<SettingsIcon />}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||||
<Button variant="contained" color="primary">
|
<Button variant="contained" color="primary">
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -1,39 +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.setUserId?.(user.id)
|
if (getCodeOnly) return setDisplayCode(code)
|
||||||
appContext.setUsername?.(user.username)
|
|
||||||
appContext.setDisplayName?.(user.displayName)
|
const { accessToken, refreshToken } = await getTokens({
|
||||||
appContext.setLoggedIn?.(loggedIn)
|
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"
|
||||||
@@ -46,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
|
||||||
@@ -63,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>
|
||||||
|
|||||||
94
web/src/components/useTokens.ts
Normal file
94
web/src/components/useTokens.ts
Normal 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())
|
||||||
|
// }
|
||||||
@@ -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
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import 'react-toastify/dist/ReactToastify.css'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { CssBaseline, Box, Typography, Button } 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('')
|
|
||||||
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 />
|
|
||||||
|
|
||||||
<CopyToClipboard
|
|
||||||
text={displayCode}
|
|
||||||
onCopy={() =>
|
|
||||||
toast.info('Code copied to ClipBoard', {
|
|
||||||
theme: 'dark',
|
|
||||||
position: toast.POSITION.BOTTOM_RIGHT
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button variant="contained">Copy to Clipboard</Button>
|
|
||||||
</CopyToClipboard>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AuthCode
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { Box, Paper, Tab, styled } from '@mui/material'
|
|
||||||
import TabContext from '@mui/lab/TabContext'
|
|
||||||
import TabList from '@mui/lab/TabList'
|
|
||||||
import TabPanel from '@mui/lab/TabPanel'
|
|
||||||
|
|
||||||
import Profile from './profile'
|
|
||||||
|
|
||||||
const StyledTab = styled(Tab)({
|
|
||||||
background: 'black',
|
|
||||||
margin: '0 5px 5px 0'
|
|
||||||
})
|
|
||||||
|
|
||||||
const StyledTabpanel = styled(TabPanel)({
|
|
||||||
flexGrow: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
const Settings = () => {
|
|
||||||
const [value, setValue] = React.useState('profile')
|
|
||||||
|
|
||||||
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
|
||||||
setValue(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
marginTop: '65px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TabContext value={value}>
|
|
||||||
<Box component={Paper} sx={{ margin: '0 5px', height: '92vh' }}>
|
|
||||||
<TabList
|
|
||||||
TabIndicatorProps={{
|
|
||||||
style: {
|
|
||||||
display: 'none'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
orientation="vertical"
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
<StyledTab label="Profile" value="profile" />
|
|
||||||
</TabList>
|
|
||||||
</Box>
|
|
||||||
<StyledTabpanel value="profile">
|
|
||||||
<Profile />
|
|
||||||
</StyledTabpanel>
|
|
||||||
</TabContext>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Settings
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react'
|
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
|
||||||
Grid,
|
|
||||||
CircularProgress,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
Divider,
|
|
||||||
CardContent,
|
|
||||||
TextField,
|
|
||||||
CardActions,
|
|
||||||
Button,
|
|
||||||
FormGroup,
|
|
||||||
FormControlLabel,
|
|
||||||
Checkbox
|
|
||||||
} from '@mui/material'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
|
|
||||||
import { AppContext } from '../../context/appContext'
|
|
||||||
|
|
||||||
const Profile = () => {
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const appContext = useContext(AppContext)
|
|
||||||
const [user, setUser] = useState({} as any)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.get(`/SASjsApi/user/${appContext.userId}`)
|
|
||||||
.then((res: any) => {
|
|
||||||
setUser(res.data)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleChange = (event: any) => {
|
|
||||||
const { name, value } = event.target
|
|
||||||
|
|
||||||
setUser({ ...user, [name]: value })
|
|
||||||
}
|
|
||||||
const handleSubmit = () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.patch(`/SASjsApi/user/${appContext.userId}`, {
|
|
||||||
username: user.username,
|
|
||||||
displayName: user.displayName,
|
|
||||||
autoExec: user.autoExec
|
|
||||||
})
|
|
||||||
.then((res: any) => {
|
|
||||||
toast.success('User information updated', {
|
|
||||||
theme: 'dark',
|
|
||||||
position: toast.POSITION.BOTTOM_RIGHT
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error('Failed: ' + err.response?.data || err.text, {
|
|
||||||
theme: 'dark',
|
|
||||||
position: toast.POSITION.BOTTOM_RIGHT
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return isLoading ? (
|
|
||||||
<CircularProgress
|
|
||||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<CardHeader title="Profile Information" />
|
|
||||||
<Divider />
|
|
||||||
<CardContent>
|
|
||||||
<Grid container spacing={4}>
|
|
||||||
<Grid item md={6} xs={12}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
error={user.displayName?.length === 0}
|
|
||||||
helperText="Please specify display name"
|
|
||||||
label="Display Name"
|
|
||||||
name="displayName"
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
value={user.displayName}
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item md={6} xs={12}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
error={user.username?.length === 0}
|
|
||||||
helperText="Please specify username"
|
|
||||||
label="Username"
|
|
||||||
name="username"
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
value={user.username}
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item lg={6} md={8} sm={12} xs={12}>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="autoExec"
|
|
||||||
name="autoExec"
|
|
||||||
onChange={handleChange}
|
|
||||||
multiline
|
|
||||||
rows="10"
|
|
||||||
value={user.autoExec}
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<FormGroup row>
|
|
||||||
<FormControlLabel
|
|
||||||
disabled
|
|
||||||
control={<Checkbox checked={user.isActive} />}
|
|
||||||
label="isActive"
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
disabled
|
|
||||||
control={<Checkbox checked={user.isAdmin} />}
|
|
||||||
label="isAdmin"
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</CardContent>
|
|
||||||
<Divider />
|
|
||||||
<CardActions>
|
|
||||||
<Button type="submit" variant="contained" onClick={handleSubmit}>
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
</CardActions>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Profile
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -7,81 +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
|
|
||||||
userId: number
|
|
||||||
setUserId: Dispatch<SetStateAction<number>> | 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,
|
|
||||||
userId: 0,
|
|
||||||
setUserId: 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 [userId, setUserId] = useState(0)
|
|
||||||
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)
|
|
||||||
setUserId(data.id)
|
|
||||||
setUsername(data.username)
|
|
||||||
setDisplayName(data.displayName)
|
|
||||||
setLoggedIn(true)
|
|
||||||
})
|
|
||||||
.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,
|
|
||||||
userId,
|
|
||||||
setUserId,
|
|
||||||
username,
|
|
||||||
setUsername,
|
|
||||||
displayName,
|
displayName,
|
||||||
setDisplayName,
|
setUserName,
|
||||||
|
tokens,
|
||||||
|
setTokens: saveTokens,
|
||||||
logout
|
logout
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user